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