using Microsoft.Extensions.Logging.Abstractions; using SameBeat.Application.DTO; using SameBeat.Application.Interfejsy; using SameBeat.Application.Serwisy; using SameBeat.Domain.Encje; using SameBeat.Domain.Enumy; using Xunit; namespace SameBeat.Tests.Unit; /// /// Testy SerwisMikrofonu (ETAP 5) -- NAJWAZNIEJSZA funkcja produktu. Zaleznosci od bazy danych sa /// zastapione prostymi fejkami w pamieci (projekt nie uzywa biblioteki do mockowania), tak samo jak /// SerwisScenyTests/SerwisMuzykiTests. Test autorytatywnego timera (SprawdzIOdbierzWygasleAsync_...) /// uzywa krotkiego, PRAWDZIWEGO oplyniecia czasu (Thread.Sleep + DateTime.UtcNow) -- dokladnie tak, /// jak WydarzenieMuzykaTests/SerwisMuzykiTests testuja zegar muzyki w ETAPIE 4, zamiast fejkowac zegar. /// SerwisTimeraMikrofonu (BackgroundService) sam w sobie jest tylko petla co 1s wywolujaca dokladnie /// ta metode -- to ona niesie cala logike biznesowa i to ja tutaj testujemy. /// public class SerwisMikrofonuTests { 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 FejkRepozytoriumPoczekalni : IRepozytoriumPoczekalni { private readonly List _uczestnicy = new(); public Task ZnajdzPoIdAsync(Guid id, CancellationToken token = default) => Task.FromResult(_uczestnicy.SingleOrDefault(u => u.Id == id)); public Task> PobierzOczekujacychDlaWydarzeniaAsync(Guid wydarzenieId, CancellationToken token = default) => Task.FromResult(_uczestnicy.Where(u => u.WydarzenieId == wydarzenieId && u.Status == StatusUczestnikaOczekujacego.Oczekuje).ToList()); public void Dodaj(UczestnikOczekujacy uczestnik) => _uczestnicy.Add(uczestnik); } private sealed class FejkRepozytoriumSceny : IRepozytoriumSceny { private readonly List _sloty = new(); private readonly FejkRepozytoriumPoczekalni _repozytoriumPoczekalni; public FejkRepozytoriumSceny(FejkRepozytoriumPoczekalni repozytoriumPoczekalni) { _repozytoriumPoczekalni = repozytoriumPoczekalni; } public Task ZnajdzPoNumerzeAsync(Guid wydarzenieId, int numerSlotu, CancellationToken token = default) => Task.FromResult(_sloty.SingleOrDefault(s => s.WydarzenieId == wydarzenieId && s.NumerSlotu == numerSlotu)); public Task ZnajdzPoUczestnikuAsync(Guid wydarzenieId, Guid uczestnikOczekujacyId, CancellationToken token = default) => Task.FromResult(_sloty.SingleOrDefault(s => s.WydarzenieId == wydarzenieId && s.UczestnikOczekujacyId == uczestnikOczekujacyId)); public Task> PobierzZajeteDlaWydarzeniaAsync(Guid wydarzenieId, CancellationToken token = default) => Task.FromResult(_sloty.Where(s => s.WydarzenieId == wydarzenieId && s.UczestnikOczekujacyId != null).ToList()); public async Task> PobierzZajeteZNickamiAsync(Guid wydarzenieId, CancellationToken token = default) { var wynik = new List(); foreach (var slot in _sloty.Where(s => s.WydarzenieId == wydarzenieId && s.UczestnikOczekujacyId != null).OrderBy(s => s.NumerSlotu)) { var uczestnik = await _repozytoriumPoczekalni.ZnajdzPoIdAsync(slot.UczestnikOczekujacyId!.Value, token); wynik.Add(new SlotScenyDto(slot.NumerSlotu, uczestnik!.Nick)); } return wynik; } public void Dodaj(SlotSceny slot) => _sloty.Add(slot); } private sealed class FejkRepozytoriumHistoriiMikrofonu : IRepozytoriumHistoriiMikrofonu { private readonly List _wpisy = new(); public IReadOnlyList Wpisy => _wpisy; public void Dodaj(HistoriaMikrofonu wpis) => _wpisy.Add(wpis); public Task ZnajdzOtwartyAsync(Guid wydarzenieId, Guid uczestnikOczekujacyId, CancellationToken token = default) => Task.FromResult(_wpisy .Where(h => h.WydarzenieId == wydarzenieId && h.UczestnikOczekujacyId == uczestnikOczekujacyId && h.KoniecFaktycznyUtc == null) .OrderByDescending(h => h.StartUtc) .FirstOrDefault()); } private sealed class FejkJednostkaPracy : IJednostkaPracy { public Task ZapiszZmianyAsync(CancellationToken token = default) => Task.FromResult(0); } private sealed class FejkSerwisAdministracjiLiveKit : ISerwisAdministracjiLiveKit { public int LiczbaWywolan { get; private set; } public string? OstatniaTozsamosc { get; private set; } public string? OstatniPokoj { get; private set; } public bool? OstatniMikrofonWlaczony { get; private set; } public Task UstawUprawnieniaMikrofonuAsync(string identity, string nazwaPokoju, bool mikrofonWlaczony, CancellationToken token = default) { LiczbaWywolan++; OstatniaTozsamosc = identity; OstatniPokoj = nazwaPokoju; OstatniMikrofonWlaczony = mikrofonWlaczony; return Task.CompletedTask; } public Task OdbierzUprawnieniaScenyAsync(string identity, string nazwaPokoju, CancellationToken token = default) => Task.CompletedTask; } private const string PokojGlowny = "impreza-glowna"; private readonly FejkRepozytoriumWydarzen _repozytoriumWydarzen = new(); private readonly FejkRepozytoriumPoczekalni _repozytoriumPoczekalni = new(); private readonly FejkRepozytoriumSceny _repozytoriumSceny; private readonly FejkRepozytoriumHistoriiMikrofonu _repozytoriumHistoriiMikrofonu = new(); private readonly FejkSerwisAdministracjiLiveKit _serwisAdministracjiLiveKit = new(); private readonly SerwisMikrofonu _serwis; public SerwisMikrofonuTests() { _repozytoriumSceny = new FejkRepozytoriumSceny(_repozytoriumPoczekalni); _serwis = new SerwisMikrofonu( _repozytoriumWydarzen, _repozytoriumPoczekalni, _repozytoriumSceny, _repozytoriumHistoriiMikrofonu, _serwisAdministracjiLiveKit, new FejkJednostkaPracy(), PokojGlowny, NullLogger.Instance); } private Wydarzenie DodajWydarzenie() { var wydarzenie = Wydarzenie.Utworz("Testowa impreza"); _repozytoriumWydarzen.Dodaj(wydarzenie); return wydarzenie; } private UczestnikOczekujacy DodajUczestnikaNaScenie(Guid wydarzenieId, int numerSlotu, string nick = "Gosc") { var uczestnik = UczestnikOczekujacy.Dolacz(wydarzenieId, nick); uczestnik.WpuscNaScene(); _repozytoriumPoczekalni.Dodaj(uczestnik); var slot = SlotSceny.Utworz(wydarzenieId, numerSlotu); slot.Przypisz(uczestnik.Id); _repozytoriumSceny.Dodaj(slot); return uczestnik; } // ---- PrzyznajAsync -- walidacja ---- [Fact] public async Task PrzyznajAsync_BrakWydarzenia_ZwracaNiepowodzenie() { var wynik = await _serwis.PrzyznajAsync(Guid.NewGuid(), 5, "Host"); Assert.False(wynik.Sukces); } [Theory] [InlineData(0)] [InlineData(-1)] public async Task PrzyznajAsync_NiedodatniCzas_ZwracaNiepowodzenie(int czasSekundy) { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 1); var wynik = await _serwis.PrzyznajAsync(uczestnik.Id, czasSekundy, "Host"); Assert.False(wynik.Sukces); } [Fact] public async Task PrzyznajAsync_NieistniejacyUczestnik_ZwracaNiepowodzenie() { DodajWydarzenie(); var wynik = await _serwis.PrzyznajAsync(Guid.NewGuid(), 5, "Host"); Assert.False(wynik.Sukces); } [Fact] public async Task PrzyznajAsync_UczestnikNieNaScenie_ZwracaNiepowodzenie() { var wydarzenie = DodajWydarzenie(); var uczestnik = UczestnikOczekujacy.Dolacz(wydarzenie.Id, "WPoczekalni"); _repozytoriumPoczekalni.Dodaj(uczestnik); var wynik = await _serwis.PrzyznajAsync(uczestnik.Id, 5, "Host"); Assert.False(wynik.Sukces); Assert.Equal(0, _serwisAdministracjiLiveKit.LiczbaWywolan); } [Fact] public async Task PrzyznajAsync_PoprawneDane_PrzyznajeIWolaLiveKitZKamera_I_Mikrofonem() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 3, "Ala"); var wynik = await _serwis.PrzyznajAsync(uczestnik.Id, 5, "PiotrHost"); Assert.True(wynik.Sukces); Assert.Equal(3, wynik.Dane!.NumerSlotu); Assert.Equal("Ala", wynik.Dane.Nick); Assert.NotNull(wynik.Dane.KoniecUtc); Assert.Equal(uczestnik.Id, wydarzenie.AktywnyMikrofonUczestnikId); Assert.Equal(1, _serwisAdministracjiLiveKit.LiczbaWywolan); Assert.Equal(uczestnik.LiveKitIdentity, _serwisAdministracjiLiveKit.OstatniaTozsamosc); Assert.Equal(PokojGlowny, _serwisAdministracjiLiveKit.OstatniPokoj); Assert.True(_serwisAdministracjiLiveKit.OstatniMikrofonWlaczony); var wpisHistorii = Assert.Single(_repozytoriumHistoriiMikrofonu.Wpisy); Assert.Equal(uczestnik.Id, wpisHistorii.UczestnikOczekujacyId); Assert.Equal(5, wpisHistorii.CzasSekundy); Assert.Equal("PiotrHost", wpisHistorii.PrzyznalHostNick); Assert.Null(wpisHistorii.KoniecFaktycznyUtc); } [Fact] public async Task PrzyznajAsync_GdyJuzKtosMaMikrofon_ZwracaNiepowodzenie() { var wydarzenie = DodajWydarzenie(); var pierwszy = DodajUczestnikaNaScenie(wydarzenie.Id, 1, "Pierwszy"); var drugi = DodajUczestnikaNaScenie(wydarzenie.Id, 2, "Drugi"); await _serwis.PrzyznajAsync(pierwszy.Id, 5, "Host"); var wynik = await _serwis.PrzyznajAsync(drugi.Id, 5, "Host"); Assert.False(wynik.Sukces); Assert.Equal(pierwszy.Id, wydarzenie.AktywnyMikrofonUczestnikId); } // ---- PrzerwijAsync ---- [Fact] public async Task PrzerwijAsync_AktywnyMikrofon_OdbieraIWolaLiveKitBezMikrofonu() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 1, "Ala"); await _serwis.PrzyznajAsync(uczestnik.Id, 5, "Host"); var wynik = await _serwis.PrzerwijAsync(uczestnik.Id); Assert.True(wynik.Sukces); Assert.Null(wynik.Dane!.KoniecUtc); Assert.Null(wydarzenie.AktywnyMikrofonUczestnikId); Assert.Equal(2, _serwisAdministracjiLiveKit.LiczbaWywolan); // 1x przyznaj, 1x przerwij Assert.False(_serwisAdministracjiLiveKit.OstatniMikrofonWlaczony); var wpisHistorii = Assert.Single(_repozytoriumHistoriiMikrofonu.Wpisy); Assert.NotNull(wpisHistorii.KoniecFaktycznyUtc); } [Fact] public async Task PrzerwijAsync_NiktNieMaAktywnegoMikrofonu_JestIdempotentne() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 1); var wynik = await _serwis.PrzerwijAsync(uczestnik.Id); Assert.True(wynik.Sukces); Assert.Null(wynik.Dane!.NumerSlotu); Assert.Equal(0, _serwisAdministracjiLiveKit.LiczbaWywolan); } [Fact] public async Task PrzerwijAsync_InnyUczestnikNizAktywny_NieRuszaAktywnegoMikrofonu() { var wydarzenie = DodajWydarzenie(); var pierwszy = DodajUczestnikaNaScenie(wydarzenie.Id, 1, "Pierwszy"); var drugi = DodajUczestnikaNaScenie(wydarzenie.Id, 2, "Drugi"); await _serwis.PrzyznajAsync(pierwszy.Id, 5, "Host"); var wynik = await _serwis.PrzerwijAsync(drugi.Id); Assert.True(wynik.Sukces); Assert.Equal(pierwszy.Id, wydarzenie.AktywnyMikrofonUczestnikId); } // ---- SprawdzIOdbierzWygasleAsync -- autorytatywny timer, PRAWDZIWY zegar ---- [Fact] public async Task SprawdzIOdbierzWygasleAsync_BrakAktywnegoMikrofonu_ZwracaNull() { DodajWydarzenie(); var wynik = await _serwis.SprawdzIOdbierzWygasleAsync(); Assert.Null(wynik); } [Fact] public async Task SprawdzIOdbierzWygasleAsync_JeszczeNieWygasl_ZwracaNullINieRuszaStanu() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 1); await _serwis.PrzyznajAsync(uczestnik.Id, 5, "Host"); // 5 sekund, nie zdazy wygasnac var wynik = await _serwis.SprawdzIOdbierzWygasleAsync(); Assert.Null(wynik); Assert.Equal(uczestnik.Id, wydarzenie.AktywnyMikrofonUczestnikId); } /// /// SEDNO ETAPU 5: serwer, nie przegladarka, egzekwuje czas. Przyznajemy mikrofon na 1 sekunde, /// realnie czekamy troche dluzej (Thread.Sleep, prawdziwy DateTime.UtcNow -- tak samo jak /// WydarzenieMuzykaTests/SerwisMuzykiTests testuja zegar muzyki), po czym wywolujemy DOKLADNIE ta /// metode, ktora SerwisTimeraMikrofonu (BackgroundService) wola co 1s w petli -- i sprawdzamy, ze /// mikrofon faktycznie zostal odebrany: w bazie (Wydarzenie, HistoriaMikrofonu) I przez LiveKit. /// [Fact] public async Task SprawdzIOdbierzWygasleAsync_PoUplywieCzasu_OdbieraMikrofonRealnie() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 2, "Bartek"); await _serwis.PrzyznajAsync(uczestnik.Id, 1, "Host"); // 1 sekunda Thread.Sleep(1300); // realne opoznienie -- nie fejkujemy zegara var wynik = await _serwis.SprawdzIOdbierzWygasleAsync(); Assert.NotNull(wynik); Assert.Equal(2, wynik!.NumerSlotu); Assert.Equal("Bartek", wynik.Nick); Assert.Null(wynik.KoniecUtc); // Zywy stan w bazie faktycznie wyczyszczony -- to jest "serwer egzekwuje", nie tylko event. Assert.Null(wydarzenie.AktywnyMikrofonUczestnikId); Assert.Null(wydarzenie.AktywnyMikrofonKoniecUtc); // LiveKit faktycznie wywolany, zeby fizycznie odebrac prawo publikacji audio. Assert.Equal(2, _serwisAdministracjiLiveKit.LiczbaWywolan); // 1x przyznaj, 1x wygasniecie Assert.False(_serwisAdministracjiLiveKit.OstatniMikrofonWlaczony); // Audyt zamkniety. var wpisHistorii = Assert.Single(_repozytoriumHistoriiMikrofonu.Wpisy); Assert.NotNull(wpisHistorii.KoniecFaktycznyUtc); // Kolejne tiki timera po odebraniu juz nic nie robia (idempotentne, brak aktywnego mikrofonu). var kolejnyWynik = await _serwis.SprawdzIOdbierzWygasleAsync(); Assert.Null(kolejnyWynik); } // ---- PobierzStanAsync ---- [Fact] public async Task PobierzStanAsync_BrakWydarzenia_ZwracaPustyStanNieBlad() { var wynik = await _serwis.PobierzStanAsync(); Assert.True(wynik.Sukces); Assert.Null(wynik.Dane!.NumerSlotu); Assert.Null(wynik.Dane.Nick); Assert.Null(wynik.Dane.KoniecUtc); } [Fact] public async Task PobierzStanAsync_AktywnyMikrofon_ZwracaNumerSlotuNickIKoniec() { var wydarzenie = DodajWydarzenie(); var uczestnik = DodajUczestnikaNaScenie(wydarzenie.Id, 4, "Celina"); await _serwis.PrzyznajAsync(uczestnik.Id, 10, "Host"); var wynik = await _serwis.PobierzStanAsync(); Assert.True(wynik.Sukces); Assert.Equal(4, wynik.Dane!.NumerSlotu); Assert.Equal("Celina", wynik.Dane.Nick); Assert.NotNull(wynik.Dane.KoniecUtc); } }