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);
}
}