Files
Flaro/server/tests/Flaro.Api.Tests/FortschrittTests.cs
T
ScarriffleandClaude Opus 5 339a6c07d0 Ein Client darf einen Stand fortschreiben, nicht zerstören
Ein Film stand bei 46 Minuten; nach einem Neuladen bot die Detailseite nur
noch „Abspielen" an. Gemessen am laufenden Server, was eine einzige Meldung
„Stelle 500 s, Laufzeit 60 s" anrichtet:

    Stelle        500 s  ->   60 s
    Laufzeit      600 s  ->   60 s
    gesehen        nein  ->   ja
    Weiterschauen    ja  ->   nein

Der Server beschnitt die Stelle auf die gemeldete Laufzeit, ersetzte die
richtige Laufzeit durch sie und setzte das Häkchen. Ein Film war damit weg.

Und 60 ist keine erfundene Zahl. Wer einen gerechneten Strom abspielt, misst
an einer Playlist, die noch wächst, und sieht anfangs nur einen Stummel.
Genau den meldete die App als Laufzeit — und sie meldet beim Öffnen
ausserdem die Zeit null, bevor sie überhaupt gesprungen ist.

Drei Stellen, dieselbe Regel:

- Server: die gemessene Laufzeit schlägt die gemeldete. Die Datei zuerst,
  nicht der gemerkte Wert — stünde dort schon ein Stummel, bliebe er sonst
  für immer stehen. So heilt sich ein verdorbener Eintrag von selbst.
- Server: eine gemeldete Null löscht keine gemerkte Stelle. Wer wirklich
  von vorn anfängt, verliert nichts: zehn Sekunden später meldet derselbe
  Spieler die 10, und die zählt. Zurückspulen bleibt unberührt.
- App: dieselbe Rangfolge bei der Laufzeit, und beim Öffnen wird die
  Stelle gemeldet, an der der Spieler einsteigen will, statt der Null.

Im Webspieler kam eine eigene Null dazu: bei Direktwiedergabe gibt der
Server `startSeconds: 0` zurück und der Browser springt selbst, sobald die
Metadaten da sind. Kommen sie nie, weil er mit der Datei nicht zurechtkommt,
blieb `filmzeit()` auf null — und der Rückfall aufs Umkodieren, der
ausdrücklich „an der Stelle weiter, an der es hing" meint, fing von vorn an.
Der ausstehende Sprung zählt jetzt mit.

Fünf neue Prüfungen halten die gemessenen Zahlen fest, damit die Regel nicht
wieder umkippt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 21:36:35 +02:00

186 lines
7.4 KiB
C#

using System.Net.Http.Json;
using Flaro.Api.Endpoints;
namespace Flaro.Api.Tests;
/// <summary>Was eine Fortschrittsmeldung am gemerkten Stand ändern darf.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Anlass.</b> 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".
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// <b>Die Regel.</b> 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.
/// </para>
/// </remarks>
[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 */ }
}
/// <summary>Ein Server mit einem eingelesenen Film, angemeldet als Administrator.</summary>
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<LibraryResponse>())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var versuch = 0; versuch < 100; versuch++)
{
var stand = await client.GetFromJsonAsync<ScanStatus>($"/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<Page<MovieResponse>>("/api/v1/movies");
return (client, seite!.Items[0].Id);
}
private static async Task<ItemStateResponse> 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<ItemStateResponse>())!;
}
/// <summary>Eine zu kurz gemeldete Laufzeit nimmt dem Film nicht die Stelle.</summary>
///
/// <remarks>
/// Das ist der gemeldete Fehler, Zahl für Zahl. Vorher kam hier Stelle 60
/// heraus, Laufzeit 60 und „gesehen".
/// </remarks>
[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");
});
}
/// <summary>Eine gemeldete Null löscht keine gemerkte Stelle.</summary>
///
/// <remarks>
/// <b>Beide Clients tun das.</b> 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.
/// </remarks>
[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));
}
/// <summary>Wer wirklich von vorn anfängt, setzt die Stelle trotzdem zurück.</summary>
///
/// <remarks>
/// 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.
/// </remarks>
[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));
}
/// <summary>Zurückspulen bleibt möglich.</summary>
///
/// <remarks>
/// Die Regel gilt der Null, nicht dem Rückwärtsgehen. Wer eine Szene
/// noch einmal sehen will, soll danach auch dort weitermachen.
/// </remarks>
[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));
}
/// <summary>Ohne jede bekannte Laufzeit zählt weiter, was der Client sagt.</summary>
///
/// <remarks>
/// 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.
/// </remarks>
[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));
}
}