using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Microsoft.Extensions.DependencyInjection;
namespace Flaro.Api.Tests;
/// Das Sehprotokoll und was sich daraus ablesen lässt.
[TestFixture]
public sealed class StatistikTests
{
private string _bestand = "";
[SetUp]
public void Anlegen() => _bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-stat-" + Guid.CreateVersion7().ToString("N"))).FullName;
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { /* Kür */ }
}
private void Datei(string relativ)
{
var voll = Path.Combine(_bestand, relativ);
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
File.WriteAllBytes(voll, new byte[2048]);
}
/// Die Uhr des Servers — stellbar, damit Zeit vergehen kann.
private Stelluhr _uhr = new(DateTimeOffset.UtcNow);
/// Ein Server, dessen Uhr dieser Test stellt.
private FlaroServer MitUhr()
{
_uhr = new Stelluhr(DateTimeOffset.UtcNow);
return new FlaroServer(dienste => dienste.AddSingleton(_uhr));
}
private async Task Eingelesen(FlaroServer server)
{
var admin = await server.SetUpAndSignIn();
var client = server.AlsAngemeldeter(admin.Token);
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand));
angelegt.EnsureSuccessStatusCode();
var bibliothek = (await angelegt.Content.ReadFromJsonAsync())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var versuch = 0; versuch < 100; versuch++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek.Id}/scan");
if (stand!.State is "finished") return client;
if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}");
await Task.Delay(50);
}
Assert.Fail("Der Scan wurde nicht fertig.");
return client;
}
private static async Task ErsterFilm(HttpClient client)
{
var seite = await client.GetFromJsonAsync>("/api/v1/movies");
return seite!.Items[0].Id;
}
private static Task Melden(HttpClient client, Guid id, double stelle, double? laenge) =>
client.PostAsJsonAsync($"/api/v1/items/{id}/progress",
new ProgressRequest(stelle, laenge, null));
private static Task Statistik(HttpClient client) =>
client.GetFromJsonAsync("/api/v1/users/me/statistics");
/// Geschaute Zeit landet im Protokoll.
[Test]
public async Task Was_laeuft_wird_mitgeschrieben()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var leer = await Statistik(client);
Assert.That(leer!.LoggedSeconds, Is.Zero, "vor der ersten Wiedergabe steht nichts drin");
// Zweimal eine Minute schauen — mit einer Uhr, die mitläuft.
await Melden(client, film, 0, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(1));
await Melden(client, film, 60, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(1));
await Melden(client, film, 120, 7200);
var danach = await Statistik(client);
Assert.That(danach!.LoggedSeconds, Is.EqualTo(120).Within(1),
"zwei Minuten gesehen, zwei Minuten im Protokoll");
}
/// Vorspulen ist kein Schauen.
///
///
/// Der Grund für den Deckel. Zwischen zwei Meldungen liegen zehn
/// Sekunden. Wer in dieser Zeit von Minute 5 auf Minute 90 springt, hat
/// nicht 85 Minuten gesehen — ohne Deckel stünde ein einmal
/// durchgespulter Film in der Statistik wie zweimal angeschaut.
///
[Test]
public async Task Vorspulen_zaehlt_nicht_als_Sehzeit()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 300, 7200);
// Ein Sprung um 85 Minuten, zehn Sekunden später.
_uhr.Weiter(TimeSpan.FromSeconds(10));
await Melden(client, film, 5700, 7200);
var stand = await Statistik(client);
Assert.That(stand!.LoggedSeconds, Is.LessThan(60),
"der Sprung darf nicht als anderthalb Stunden zählen");
}
/// Zurückspringen zieht nichts ab.
[Test]
public async Task Zuruecksprigen_zaehlt_weder_vor_noch_zurueck()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 0, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(2));
await Melden(client, film, 100, 7200);
var vorher = (await Statistik(client))!.LoggedSeconds;
_uhr.Weiter(TimeSpan.FromMinutes(1));
await Melden(client, film, 20, 7200);
var nachher = (await Statistik(client))!.LoggedSeconds;
Assert.That(nachher, Is.EqualTo(vorher).Within(0.001));
}
/// Die Wochenkurve hat so viele Punkte wie verlangt.
///
///
/// Auch leere Wochen stehen darin. Eine Kurve, die nur die Wochen zeigt,
/// in denen etwas lief, behauptet eine Gleichmässigkeit, die es nicht gab.
///
[Test]
public async Task Die_Kurve_hat_auch_leere_Wochen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 0, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(2));
await Melden(client, film, 90, 7200);
var stand = await client.GetFromJsonAsync(
"/api/v1/users/me/statistics?wochen=6");
Assert.Multiple(() =>
{
Assert.That(stand!.Weeks, Has.Count.EqualTo(6));
Assert.That(stand.Weeks.Select(w => w.Start.DayOfWeek), Is.All.EqualTo(DayOfWeek.Monday),
"Wochen beginnen am Montag, sonst lässt sich die Kurve nicht vergleichen");
Assert.That(stand.Weeks[^1].Seconds, Is.GreaterThan(0), "die laufende Woche ist die letzte");
Assert.That(stand.Weeks.Sum(w => w.Seconds), Is.EqualTo(stand.LoggedSeconds).Within(0.001));
});
}
/// Was ohne Protokoll vollständig ist, steht auch da.
[Test]
public async Task Gesehene_Titel_zaehlen_auch_ohne_Protokoll()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await client.PutAsJsonAsync($"/api/v1/items/{film}/watched", new WatchedRequest(true));
var stand = await Statistik(client);
Assert.Multiple(() =>
{
Assert.That(stand!.WatchedTitles, Is.EqualTo(1));
Assert.That(stand.Recent, Has.Count.EqualTo(1));
Assert.That(stand.Recent[0].Title, Is.EqualTo("Matrix"));
Assert.That(stand.Recent[0].Watched, Is.True);
Assert.That(stand.LoggedSeconds, Is.Zero,
"abhaken ist kein Schauen — das Protokoll bleibt leer");
});
}
/// Jeder sieht nur seine eigenen Zahlen.
///
///
/// Auch der Administrator. Wer wann welchen Film anschaut, ist eine
/// Auskunft über Menschen und nicht über den Server — es gibt deshalb gar
/// keinen Weg, ein fremdes Konto zu meinen.
///
[Test]
public async Task Die_Zahlen_des_einen_sind_nicht_die_des_anderen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = MitUhr();
var alsAdmin = await Eingelesen(server);
var film = await ErsterFilm(alsAdmin);
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswortlang");
await Melden(alsAdmin, film, 0, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(3));
await Melden(alsAdmin, film, 120, 7200);
var meine = await Statistik(alsAdmin);
var fremde = await Statistik(gast);
Assert.Multiple(() =>
{
Assert.That(meine!.LoggedSeconds, Is.GreaterThan(0));
Assert.That(fremde!.LoggedSeconds, Is.Zero);
Assert.That(fremde.Recent, Is.Empty);
});
}
/// Zwei Fortschrittsmeldungen gleichzeitig stürzen nicht ab.
///
///
/// Aus dem Protokoll eines laufenden Servers. Der Spieler meldet
/// alle zehn Sekunden, ein Sprung meldet sofort — zwei Meldungen können
/// sich überholen. Vorher fanden dann beide keine Zeile im Sehprotokoll,
/// beide legten eine an, und die zweite lief in „UNIQUE constraint failed:
/// Sehtage.UserId, Sehtage.Tag, Sehtage.MovieId". Der Zuschauer merkte
/// nichts, im Protokoll stand ein Stapelabzug.
///
[Test]
public async Task Zwei_Meldungen_gleichzeitig_zaehlen_beide()
{
Datei("Gleichzeitig (2024)/Gleichzeitig (2024).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
// Den Nullpunkt setzen, dann Zeit vergehen lassen — sonst greift der
// Deckel und es gäbe ohnehin nichts gutzuschreiben.
await Melden(client, film, 0, 7200);
_uhr.Weiter(TimeSpan.FromMinutes(5));
var beide = await Task.WhenAll(
client.PostAsJsonAsync($"/api/v1/items/{film}/progress", new ProgressRequest(60, 7200, null)),
client.PostAsJsonAsync($"/api/v1/items/{film}/progress", new ProgressRequest(90, 7200, null)));
Assert.That(beide.Select(a => (int)a.StatusCode), Has.All.LessThan(400),
"Eine Fortschrittsmeldung darf nie an einem Wettlauf scheitern.");
Assert.That((await Statistik(client))!.LoggedSeconds, Is.GreaterThan(0),
"und mitgezählt werden muss sie auch.");
}
/// Ein Stummel als Laufzeit nimmt den Titel nicht dauerhaft aus „Fortsetzen".
///
///
///
/// Der gemeldete Fall. Ein Film stand bei 1:32, und in
/// „Fortsetzen" tauchte er trotzdem nicht auf — bis er einmal neu
/// gestartet wurde. Die Reihe fragt nicht die Regel, sondern deren
/// festgehaltenes Ergebnis: die Spalte InProgress. Berechnet
/// wird sie nur, wenn eine Meldung eintrifft.
///
///
/// Hier wird geprüft, dass eine Meldung mit zu kurzer Laufzeit den
/// Eintrag gar nicht erst herauswirft — und dass er danach weiter in
/// „Fortsetzen" steht, ohne dass jemand den Film anfassen muss.
///
///
[Test]
public async Task Eine_zu_kurze_Laufzeit_wirft_nicht_aus_Fortsetzen()
{
Datei("Spinnenmann (2026)/Spinnenmann (2026).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 5520, 11460);
var davor = await client.GetFromJsonAsync>("/api/v1/home/continue");
Assert.That(davor!.Items.Select(e => e.Id), Has.Member(film),
"bei 1:32 von 3:11 gehört er dort hin");
// Die Meldung, die früher alles zerstörte: ein Client, der an einer
// wachsenden Playlist misst und nur einen Stummel sieht.
await Melden(client, film, 5520, 60);
var danach = await client.GetFromJsonAsync>("/api/v1/home/continue");
Assert.That(danach!.Items.Select(e => e.Id), Has.Member(film),
"und er bleibt dort, auch nach einer unsinnig kurzen Laufzeit");
}
/// „Entfernen" nimmt ihn heraus — das soll weiter gehen.
///
///
/// Die Reparatur darf nicht so weit gehen, dass sie eine Entscheidung
/// des Zuschauers überschreibt.
///
[Test]
public async Task Wer_entfernt_wird_bleibt_draussen()
{
Datei("Spinnenmann (2026)/Spinnenmann (2026).mkv");
using var server = MitUhr();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 5520, 11460);
(await client.PostAsync($"/api/v1/items/{film}/dismiss", null)).EnsureSuccessStatusCode();
var danach = await client.GetFromJsonAsync>("/api/v1/home/continue");
Assert.That(danach!.Items, Is.Empty);
}
}