using Microsoft.Extensions.Logging.Abstractions; using SameBeat.Application.Interfejsy; using SameBeat.Application.Komendy; using SameBeat.Application.Serwisy; using SameBeat.Domain.Encje; using Xunit; namespace SameBeat.Tests.Unit; /// /// Testy SerwisCzatu (ETAP 6). Zaleznosci od bazy danych sa zastapione prostymi fejkami w pamieci -- /// ten sam wzorzec co SerwisPoczekalniTests/SerwisMikrofonuTests, projekt nie uzywa biblioteki do /// mockowania. Test rate limitu uzywa krotkiego, PRAWDZIWEGO oplyniecia czasu (Thread.Sleep + /// DateTime.UtcNow), dokladnie jak SprawdzIOdbierzWygasleAsync_PoUplywieCzasu w SerwisMikrofonuTests, /// zamiast fejkowac zegar. /// public class SerwisCzatuTests { private sealed class FejkRepozytoriumWydarzen : IRepozytoriumWydarzen { private readonly List _wydarzenia = new(); public Task PobierzOstatnieAsync(CancellationToken token = default) => Task.FromResult(_wydarzenia.LastOrDefault()); public Task ZnajdzPoIdAsync(Guid id, CancellationToken token = default) => Task.FromResult(_wydarzenia.SingleOrDefault(w => w.Id == id)); public void Dodaj(Wydarzenie wydarzenie) => _wydarzenia.Add(wydarzenie); } private sealed class FejkRepozytoriumCzatu : IRepozytoriumCzatu { public readonly List Uczestnicy = new(); public readonly List Wiadomosci = new(); public Task ZnajdzUczestnikaPoIdAsync(Guid id, CancellationToken token = default) => Task.FromResult(Uczestnicy.SingleOrDefault(u => u.Id == id)); public void DodajUczestnika(UczestnikCzatu uczestnik) => Uczestnicy.Add(uczestnik); public void DodajWiadomosc(WiadomoscCzatu wiadomosc) => Wiadomosci.Add(wiadomosc); public Task PobierzOstatniaWiadomoscUczestnikaAsync(Guid nadawcaTozsamoscId, CancellationToken token = default) => Task.FromResult(Wiadomosci .Where(w => w.NadawcaTozsamoscId == nadawcaTozsamoscId) .OrderByDescending(w => w.WyslanoUtc) .FirstOrDefault()); public Task> PobierzHistorieAsync(Guid wydarzenieId, int liczbaWiadomosci, CancellationToken token = default) { var ostatnieOdNajnowszej = Wiadomosci .Where(w => w.WydarzenieId == wydarzenieId && !w.Usunieta) .OrderByDescending(w => w.WyslanoUtc) .Take(liczbaWiadomosci) .ToList(); ostatnieOdNajnowszej.Reverse(); return Task.FromResult(ostatnieOdNajnowszej); } public Task ZnajdzWiadomoscPoIdAsync(Guid id, CancellationToken token = default) => Task.FromResult(Wiadomosci.SingleOrDefault(w => w.Id == id)); } private sealed class FejkJednostkaPracy : IJednostkaPracy { public Task ZapiszZmianyAsync(CancellationToken token = default) => Task.FromResult(0); } private readonly FejkRepozytoriumWydarzen _repozytoriumWydarzen = new(); private readonly FejkRepozytoriumCzatu _repozytoriumCzatu = new(); private readonly SerwisCzatu _serwis; public SerwisCzatuTests() { _serwis = new SerwisCzatu( _repozytoriumWydarzen, _repozytoriumCzatu, new FejkJednostkaPracy(), NullLogger.Instance); } private Wydarzenie DodajWydarzenie() { var wydarzenie = Wydarzenie.Utworz("Testowa impreza"); _repozytoriumWydarzen.Dodaj(wydarzenie); return wydarzenie; } private async Task DolaczUczestnikaAsync(string nick = "Gosc") { var wynik = await _serwis.DolaczAsync(new DolaczDoCzatuKomenda(nick)); Assert.True(wynik.Sukces); return _repozytoriumCzatu.Uczestnicy.Single(u => u.Id == wynik.Dane!.UczestnikCzatuId); } // ---- DolaczAsync ---- [Fact] public async Task DolaczAsync_BrakWydarzenia_ZwracaNiepowodzenie() { var wynik = await _serwis.DolaczAsync(new DolaczDoCzatuKomenda("Ala")); Assert.False(wynik.Sukces); } [Fact] public async Task DolaczAsync_PoprawnyNick_TworzyUczestnikaCzatu() { DodajWydarzenie(); var wynik = await _serwis.DolaczAsync(new DolaczDoCzatuKomenda("Ala")); Assert.True(wynik.Sukces); Assert.NotEqual(Guid.Empty, wynik.Dane!.UczestnikCzatuId); Assert.Single(_repozytoriumCzatu.Uczestnicy); } // ---- WyslijWiadomoscAsync -- walidacja tresci ---- [Fact] public async Task WyslijWiadomoscAsync_PustaTresc_ZwracaNiepowodzenie() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, " ")); Assert.False(wynik.Sukces); Assert.Empty(_repozytoriumCzatu.Wiadomosci); } [Fact] public async Task WyslijWiadomoscAsync_ZaDlugaTresc_ZwracaNiepowodzenie() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); var trescZaDluga = new string('a', WiadomoscCzatu.MaksymalnaDlugoscTresci + 1); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, trescZaDluga)); Assert.False(wynik.Sukces); Assert.Empty(_repozytoriumCzatu.Wiadomosci); } [Theory] [InlineData("cwiczenie ")] [InlineData("polowa znaku >")] [InlineData("polowa znaku <")] public async Task WyslijWiadomoscAsync_TrescZeZnakamiKatowymi_ZwracaNiepowodzenie(string tresc) { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, tresc)); Assert.False(wynik.Sukces); Assert.Empty(_repozytoriumCzatu.Wiadomosci); } [Fact] public async Task WyslijWiadomoscAsync_PoprawnaTresc_ZapisujeIZwracaWiadomosc() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync("Bartek"); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc wszystkim!")); Assert.True(wynik.Sukces); Assert.Equal("Bartek", wynik.Dane!.Nick); Assert.Equal("Czesc wszystkim!", wynik.Dane.Tresc); Assert.Single(_repozytoriumCzatu.Wiadomosci); } // Regresja: bez tego pola przyciski moderacji hosta (wycisz/odcisz/zbanuj) nie maja jak wskazac // nadawcy -- byl to krytyczny brak wykryty w audycie ETAPU 6. [Fact] public async Task WyslijWiadomoscAsync_ZwracaUczestnikCzatuIdNadawcy() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync("Bartek"); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc!")); Assert.True(wynik.Sukces); Assert.Equal(uczestnik.Id, wynik.Dane!.UczestnikCzatuId); } [Fact] public async Task PobierzHistorieDlaHostaAsync_ZwracaUczestnikCzatuIdKazdejWiadomosci() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync("Bartek"); await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc!")); var wynik = await _serwis.PobierzHistorieDlaHostaAsync(); Assert.True(wynik.Sukces); Assert.Single(wynik.Dane!); Assert.Equal(uczestnik.Id, wynik.Dane![0].UczestnikCzatuId); } // ---- WyslijWiadomoscAsync -- wyciszenie / ban ---- [Fact] public async Task WyslijWiadomoscAsync_UczestnikWyciszony_ZwracaNiepowodzenie() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); await _serwis.WyciszAsync(uczestnik.Id); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc")); Assert.False(wynik.Sukces); Assert.Empty(_repozytoriumCzatu.Wiadomosci); } [Fact] public async Task WyslijWiadomoscAsync_UczestnikOdciszony_MozePisacPonownie() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); await _serwis.WyciszAsync(uczestnik.Id); await _serwis.OdciszAsync(uczestnik.Id); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc")); Assert.True(wynik.Sukces); } [Fact] public async Task WyslijWiadomoscAsync_UczestnikZbanowany_ZwracaNiepowodzenie() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); await _serwis.ZbanujAsync(uczestnik.Id); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Czesc")); Assert.False(wynik.Sukces); Assert.Empty(_repozytoriumCzatu.Wiadomosci); } [Fact] public async Task WyslijWiadomoscAsync_NieznanyUczestnik_ZwracaNiepowodzenie() { DodajWydarzenie(); var wynik = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(Guid.NewGuid(), "Czesc")); Assert.False(wynik.Sukces); } // ---- WyslijWiadomoscAsync -- rate limit, PRAWDZIWY zegar ---- /// /// SEDNO ochrony przed spamem: druga wiadomosc wyslana od razu po pierwszej ma zostac odrzucona, /// a po realnym odczekaniu odstepu -- przejsc. Rate limit jest liczony z ostatniej wiadomosci w /// "bazie" (fejku), nie ze stanu w pamieci procesu SerwisCzatu -- patrz komentarz w klasie. /// [Fact] public async Task WyslijWiadomoscAsync_DrugaWiadomoscZaSzybko_ZostajeOdrzucona_APoOdczekaniuPrzechodzi() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); var pierwsza = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Pierwsza")); Assert.True(pierwsza.Sukces); var druga = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Druga za szybko")); Assert.False(druga.Sukces); Assert.Single(_repozytoriumCzatu.Wiadomosci); Thread.Sleep(2100); // realne opoznienie -- nie fejkujemy zegara, limit to 2 sekundy var trzecia = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Po odczekaniu")); Assert.True(trzecia.Sukces); Assert.Equal(2, _repozytoriumCzatu.Wiadomosci.Count); } [Fact] public async Task WyslijWiadomoscAsync_RateLimitJestPerUczestnik_InnyUczestnikPiszeBezOgraniczenia() { DodajWydarzenie(); var pierwszy = await DolaczUczestnikaAsync("Pierwszy"); var drugi = await DolaczUczestnikaAsync("Drugi"); var wynikPierwszego = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(pierwszy.Id, "Czesc")); Assert.True(wynikPierwszego.Sukces); var wynikDrugiego = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(drugi.Id, "Czesc")); Assert.True(wynikDrugiego.Sukces); } // ---- PobierzHistorieAsync ---- [Fact] public async Task PobierzHistorieAsync_BrakWydarzenia_ZwracaPustaListe() { var wynik = await _serwis.PobierzHistorieAsync(); Assert.True(wynik.Sukces); Assert.Empty(wynik.Dane!); } [Fact] public async Task PobierzHistorieAsync_UsunietaWiadomoscNieWystepujeWHistorii() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); var wyslana = await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Do usuniecia")); Assert.True(wyslana.Sukces); await _serwis.UsunWiadomoscAsync(wyslana.Dane!.Id); var historia = await _serwis.PobierzHistorieAsync(); Assert.True(historia.Sukces); Assert.Empty(historia.Dane!); } [Fact] public async Task PobierzHistorieAsync_NieusunieteWiadomosciWracajaWKolejnosciChronologicznej() { DodajWydarzenie(); var uczestnik = await DolaczUczestnikaAsync(); await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Pierwsza")); Thread.Sleep(2100); await _serwis.WyslijWiadomoscAsync(new WyslijWiadomoscKomenda(uczestnik.Id, "Druga")); var historia = await _serwis.PobierzHistorieAsync(); Assert.True(historia.Sukces); Assert.Equal(2, historia.Dane!.Count); Assert.Equal("Pierwsza", historia.Dane[0].Tresc); Assert.Equal("Druga", historia.Dane[1].Tresc); } // ---- Moderacja ---- [Fact] public async Task UsunWiadomoscAsync_NieznanaWiadomosc_ZwracaNiepowodzenie() { var wynik = await _serwis.UsunWiadomoscAsync(Guid.NewGuid()); Assert.False(wynik.Sukces); } [Fact] public async Task ZbanujAsync_NieznanyUczestnik_ZwracaNiepowodzenie() { var wynik = await _serwis.ZbanujAsync(Guid.NewGuid()); Assert.False(wynik.Sukces); } }