using System.Net.Http.Json; using Flaro.Api.Endpoints; namespace Flaro.Api.Tests; /// Was eine Fortschrittsmeldung am gemerkten Stand ändern darf. /// /// /// /// Der Anlass. Ein Film stand bei 46 Minuten, und nach einem Neuladen /// bot die Detailseite nur noch „Abspielen" an. Gemessen, woran es lag: eine /// einzige Meldung „Stelle 500 s, Laufzeit 60 s" machte aus der Stelle 500 /// die Stelle 60, aus der Laufzeit 600 die Laufzeit 60, setzte das Häkchen /// „gesehen" und nahm den Titel aus „Weiterschauen". /// /// /// Woher so eine Meldung kommt: wer einen gerechneten Strom abspielt, misst /// an einer Playlist, die noch wächst, und sieht nur das bisher Erzeugte. /// Scheitert die Wiedergabe gleich darauf — auf dem iPad tat sie das —, /// bleibt dieser Stummel als „Laufzeit" stehen. Dazu meldet jeder Spieler /// beim Öffnen einmal die Zeit null, bevor er überhaupt gesprungen ist. /// /// /// Die Regel. Ein Client darf einen Stand fortschreiben, aber nicht /// zerstören. Die Laufzeit, die der Server kennt, schlägt die gemeldete, und /// eine gemeldete Null löscht nichts. /// /// [TestFixture] public sealed class FortschrittTests { private string _bestand = ""; [SetUp] public void Anlegen() => _bestand = Directory.CreateDirectory( Path.Combine(Path.GetTempPath(), "flaro-fort-" + Guid.CreateVersion7().ToString("N"))).FullName; [TearDown] public void Aufraeumen() { try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { /* Kür */ } } /// Ein Server mit einem eingelesenen Film, angemeldet als Administrator. private async Task<(HttpClient Client, Guid Film)> MitEinemFilm(FlaroServer server) { var voll = Path.Combine(_bestand, "Langprobe (2026)", "Langprobe (2026).mkv"); Directory.CreateDirectory(Path.GetDirectoryName(voll)!); await File.WriteAllBytesAsync(voll, new byte[2048]); 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") break; if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}"); await Task.Delay(50); } var seite = await client.GetFromJsonAsync>("/api/v1/movies"); return (client, seite!.Items[0].Id); } private static async Task Melden( HttpClient client, Guid film, double stelle, double? laufzeit) { var antwort = await client.PostAsJsonAsync($"/api/v1/items/{film}/progress", new ProgressRequest(stelle, laufzeit, null)); antwort.EnsureSuccessStatusCode(); return (await antwort.Content.ReadFromJsonAsync())!; } /// Eine zu kurz gemeldete Laufzeit nimmt dem Film nicht die Stelle. /// /// /// Das ist der gemeldete Fehler, Zahl für Zahl. Vorher kam hier Stelle 60 /// heraus, Laufzeit 60 und „gesehen". /// [Test] public async Task Eine_zu_kurze_Laufzeit_zerstoert_den_Stand_nicht() { using var server = new FlaroServer(); var (client, film) = await MitEinemFilm(server); await Melden(client, film, 500, 600); var danach = await Melden(client, film, 500, 60); Assert.Multiple(() => { Assert.That(danach.PositionSeconds, Is.EqualTo(500).Within(0.001), "die Stelle darf nicht auf den Stummel zusammengestaucht werden"); Assert.That(danach.RuntimeSeconds, Is.EqualTo(600).Within(0.001), "und die bekannte Laufzeit nicht durch ihn ersetzt"); Assert.That(danach.Watched, Is.False, "bei 500 von 600 Sekunden ist nichts zu Ende gesehen"); Assert.That(danach.InProgress, Is.True, "der Film gehört weiter in die Reihe Weiterschauen"); }); } /// Eine gemeldete Null löscht keine gemerkte Stelle. /// /// /// Beide Clients tun das. Die App meldet beim Öffnen ausdrücklich /// die Zeit null — dort legt erst diese Meldung die Sitzung an. Der /// Browser tut es im Fenster zwischen „Datei geöffnet" und „Sprung /// ausgeführt", und wenn er die Datei nicht entschlüsseln kann, kommt er /// aus diesem Fenster nie heraus. /// [Test] public async Task Eine_gemeldete_Null_loescht_die_Stelle_nicht() { using var server = new FlaroServer(); var (client, film) = await MitEinemFilm(server); await Melden(client, film, 2771, 11468); var danach = await Melden(client, film, 0, 11468); Assert.That(danach.PositionSeconds, Is.EqualTo(2771).Within(0.001)); } /// Wer wirklich von vorn anfängt, setzt die Stelle trotzdem zurück. /// /// /// Das ist der Preis der Regel darüber, und er ist klein: zehn Sekunden /// später meldet derselbe Spieler die 10, und die zählt. „Von vorn" /// bleibt damit „von vorn", nur eben ab der ersten gesehenen Sekunde. /// [Test] public async Task Von_vorn_setzt_die_Stelle_mit_der_ersten_echten_Meldung_zurueck() { using var server = new FlaroServer(); var (client, film) = await MitEinemFilm(server); await Melden(client, film, 2771, 11468); await Melden(client, film, 0, 11468); var danach = await Melden(client, film, 10, 11468); Assert.That(danach.PositionSeconds, Is.EqualTo(10).Within(0.001)); } /// Zurückspulen bleibt möglich. /// /// /// Die Regel gilt der Null, nicht dem Rückwärtsgehen. Wer eine Szene /// noch einmal sehen will, soll danach auch dort weitermachen. /// [Test] public async Task Zurueckspulen_setzt_die_Stelle_weiter_zurueck() { using var server = new FlaroServer(); var (client, film) = await MitEinemFilm(server); await Melden(client, film, 500, 600); var danach = await Melden(client, film, 120, 600); Assert.That(danach.PositionSeconds, Is.EqualTo(120).Within(0.001)); } /// Ohne jede bekannte Laufzeit zählt weiter, was der Client sagt. /// /// /// Sonst gäbe es für einen frisch angefangenen Titel keinen Anteil und /// damit nie ein „gesehen". Die neue Rangfolge nimmt dem Client nur den /// Vorrang, nicht das Wort. /// [Test] public async Task Ohne_bekannte_Laufzeit_gilt_die_gemeldete() { using var server = new FlaroServer(); var (client, film) = await MitEinemFilm(server); var danach = await Melden(client, film, 100, 1000); Assert.That(danach.RuntimeSeconds, Is.EqualTo(1000).Within(0.001)); } }