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