using Microsoft.Extensions.Logging.Abstractions; using SameBeat.Application.Interfejsy; using SameBeat.Application.Komendy; using SameBeat.Application.Serwisy; using SameBeat.Domain.Encje; using SameBeat.Domain.Enumy; using SameBeat.Infrastructure.Autoryzacja; using Xunit; namespace SameBeat.Tests.Unit; /// /// Testy blokady konta po zbyt wielu nieudanych probach logowania (ETAP 7 -- ochrona per-konto, /// niezalezna od rate limitingu per-IP na /api/auth/login, patrz Program.cs). Zaleznosci od bazy sa /// fejkami w pamieci (jak w pozostalych testach serwisow) -- hashowanie hasel jest jednak PRAWDZIWE /// (Infrastructure.UslugaHaszowaniaHasel), zeby test pilnowal calego mechanizmu, nie atrapy. /// /// Test odblokowania po uplywie czasu uzywa krotkiego, PRAWDZIWEGO oplyniecia czasu (Thread.Sleep + /// DateTime.UtcNow) -- dokladnie ten sam wzorzec co SerwisMikrofonuTests/SerwisTimeraMikrofonu, zamiast /// fejkowania zegara. /// public class SerwisLogowaniaTests { private sealed class FejkRepozytoriumUzytkownikow : IRepozytoriumUzytkownikow { private readonly List _uzytkownicy = new(); public Task ZnajdzPoNickuAsync(string nick, CancellationToken token = default) => Task.FromResult(_uzytkownicy.SingleOrDefault(u => u.Nick == nick)); public Task ZnajdzPoIdAsync(Guid id, CancellationToken token = default) => Task.FromResult(_uzytkownicy.SingleOrDefault(u => u.Id == id)); public Task IstniejeJakikolwiekUzytkownikAsync(CancellationToken token = default) => Task.FromResult(_uzytkownicy.Count > 0); public void Dodaj(Uzytkownik uzytkownik) => _uzytkownicy.Add(uzytkownik); } private sealed class FejkJednostkaPracy : IJednostkaPracy { public Task ZapiszZmianyAsync(CancellationToken token = default) => Task.FromResult(0); } private sealed class FejkGeneratorTokenowJwt : IGeneratorTokenowJwt { public (string Token, DateTime WygasaUtc) Wygeneruj(Uzytkownik uzytkownik) => ("token-testowy", DateTime.UtcNow.AddHours(1)); } private const string Nick = "piotr"; private const string PoprawneHaslo = "TajneHaslo123!"; private const int MaksymalnaLiczbaProb = 3; private readonly FejkRepozytoriumUzytkownikow _repozytoriumUzytkownikow = new(); private readonly IUslugaHaszowaniaHasel _uslugaHaszowaniaHasel = new UslugaHaszowaniaHasel(); private SerwisLogowania UtworzSerwis(TimeSpan czasBlokady) => new(_repozytoriumUzytkownikow, _uslugaHaszowaniaHasel, new FejkGeneratorTokenowJwt(), new FejkJednostkaPracy(), MaksymalnaLiczbaProb, czasBlokady, NullLogger.Instance); private Uzytkownik DodajUzytkownika() { var uzytkownik = Uzytkownik.Utworz(Nick, RolaUzytkownika.Host); uzytkownik.UstawHasloHash(_uslugaHaszowaniaHasel.Haszuj(uzytkownik, PoprawneHaslo)); _repozytoriumUzytkownikow.Dodaj(uzytkownik); return uzytkownik; } [Fact] public async Task ZalogujAsync_PoprawneDane_ZerujeLicznikNieudanychProb() { var uzytkownik = DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo-1")); await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo-2")); var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, PoprawneHaslo)); Assert.True(wynik.Sukces); Assert.Equal(0, uzytkownik.NieudaneProbyLogowania); Assert.Null(uzytkownik.ZablokowanyDoUtc); } [Fact] public async Task ZalogujAsync_NiepoprawneHaslo_ZwiekszaLicznikNieudanychProb() { var uzytkownik = DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo")); Assert.False(wynik.Sukces); Assert.Equal(1, uzytkownik.NieudaneProbyLogowania); Assert.False(uzytkownik.CzyZablokowany(DateTime.UtcNow)); } [Fact] public async Task ZalogujAsync_PoNPrzekroczonychProbach_BlokujeKonto() { var uzytkownik = DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); for (var i = 0; i < MaksymalnaLiczbaProb; i++) { await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo")); } Assert.True(uzytkownik.CzyZablokowany(DateTime.UtcNow)); } [Fact] public async Task ZalogujAsync_KontoZablokowane_OdrzucaNawetPoprawneHaslo() { var uzytkownik = DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); for (var i = 0; i < MaksymalnaLiczbaProb; i++) { await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo")); } // Konto juz zablokowane -- nawet PRAWIDLOWE haslo ma zostac odrzucone, dopoki blokada trwa. var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, PoprawneHaslo)); Assert.False(wynik.Sukces); Assert.Contains("Zbyt wiele", wynik.Blad); Assert.NotNull(uzytkownik.ZablokowanyDoUtc); } [Fact] public async Task ZalogujAsync_KomunikatBlokady_JestCzytelnyPoPolsku() { DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); for (var i = 0; i < MaksymalnaLiczbaProb; i++) { await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo")); } var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, PoprawneHaslo)); Assert.False(wynik.Sukces); Assert.Contains("Sprobuj ponownie za", wynik.Blad); } /// /// SEDNO testu: blokada NIE jest trwala -- po realnym uplywie czasu blokady kolejne poprawne /// logowanie ma sie udac. Krotki, prawdziwy czas blokady (1s) + Thread.Sleep, zamiast fejkowania /// zegara -- ten sam wzorzec co SerwisMikrofonuTests.SprawdzIOdbierzWygasleAsync_PoUplywieCzasu_.... /// [Fact] public async Task ZalogujAsync_PoUplywieCzasuBlokady_PozwalaZalogowacSiePonownie() { var uzytkownik = DodajUzytkownika(); var serwis = UtworzSerwis(TimeSpan.FromSeconds(1)); for (var i = 0; i < MaksymalnaLiczbaProb; i++) { await serwis.ZalogujAsync(new ZalogujKomenda(Nick, "zle-haslo")); } Assert.True(uzytkownik.CzyZablokowany(DateTime.UtcNow)); Thread.Sleep(1300); // realne opoznienie -- nie fejkujemy zegara var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, PoprawneHaslo)); Assert.True(wynik.Sukces); Assert.Equal(0, uzytkownik.NieudaneProbyLogowania); Assert.Null(uzytkownik.ZablokowanyDoUtc); } [Fact] public async Task ZalogujAsync_NieistniejacyNick_NieRzucaINieZapisujeStanu() { var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); var wynik = await serwis.ZalogujAsync(new ZalogujKomenda("nieistniejacy", "cokolwiek")); Assert.False(wynik.Sukces); } [Fact] public async Task ZalogujAsync_ZbanowanyUzytkownik_OdrzucaNiezaleznieOdLicznikaProb() { var uzytkownik = DodajUzytkownika(); uzytkownik.Zbanuj(); var serwis = UtworzSerwis(TimeSpan.FromMinutes(15)); var wynik = await serwis.ZalogujAsync(new ZalogujKomenda(Nick, PoprawneHaslo)); Assert.False(wynik.Sukces); Assert.Equal("Konto jest zablokowane.", wynik.Blad); } }