Files
ScarriffleandClaude Opus 5 343b0cdcfb Ein Knopf, der nichts tut, ist schlimmer als keiner
Der geplante Film zeigte weiter „Abspielen" und „Herunterladen", und wer
drückte, bekam „Dieser Titel ist ab dem 6.10. um 18 Uhr verfügbar". Die
Sperre im Server war richtig; die Oberfläche wusste nur nichts davon.

Gemessen: die Liste führte `PublishAt` seit jeher mit, die Einzelabfrage
`GET /api/v1/movies/{id}` nicht — und die Detailseite entscheidet genau
daran. `publishAt` kam also immer als `null` an, die Prüfung lief ins
Leere. Dasselbe fehlte bei `GET /api/v1/series/{id}` und bei den Folgen.

Jetzt steht dort, wo sonst der Knopf stünde: „Bald ist es soweit", mit
dem Termin darunter. Kein Versprechen, das beim Klicken zurückgenommen
wird.

Der Test prüfte bisher nur die Liste — darum fiel es nicht auf. Er fragt
jetzt die Detailseite und schlägt ohne die Korrektur fehl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 15:10:30 +02:00

238 lines
10 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
namespace Flaro.Api.Tests;
/// <summary>Was auf Prüfung wartet, sieht niemand ausser Administratoren.</summary>
///
/// <remarks>
/// <para>
/// <b>Die Route-Aufzählung unten ist der eigentliche Test.</b> Auf Filme und
/// Folgen wird an rund zwanzig Stellen zugegriffen — Mediathek, Suche,
/// Vorschläge, Weiterschauen, Ähnliches. Eine davon ohne Filter, und
/// unveröffentlichter Inhalt erscheint trotzdem; auffallen würde es nicht
/// beim Programmieren, sondern wenn jemand im Haushalt einen Titel sieht,
/// den er nicht sehen sollte.
/// </para>
/// <para>
/// Deshalb prüft dieser Test nicht eine Abfrage, sondern <b>jede Route, die
/// Inhalt ausliefert</b> — und zwar auf die Abwesenheit eines Titels. Eine
/// neue Route, die den Filter umgeht, fällt hier durch.
/// </para>
/// </remarks>
public class FreigabeTests
{
private string _bestand = "";
[SetUp]
public void Anlegen() => _bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-freigabe-" + Guid.CreateVersion7().ToString("N"))).FullName;
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
}
private void Datei(string relativ)
{
var voll = Path.Combine(_bestand, relativ);
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
File.WriteAllBytes(voll, new byte[2048]);
}
/// <summary>Jede Route, die Titel ausliefert.</summary>
private static readonly string[] Inhaltsrouten =
[
"/api/v1/movies",
"/api/v1/movies?search=Geheim",
"/api/v1/movies?sort=added",
"/api/v1/series",
"/api/v1/home/continue",
"/api/v1/home/next",
"/api/v1/home/suggestions",
];
[Test]
public async Task Ungepruefte_Titel_sieht_nur_der_Administrator()
{
Datei("Geheimsache (2024).mkv");
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var angelegt = await alsAdmin.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand, ReviewNewItems: true));
angelegt.EnsureSuccessStatusCode();
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
await Scannen(alsAdmin, bibliothek.Id);
// Der Administrator sieht ihn — sonst könnte er ihn nicht prüfen.
var wartend = await alsAdmin.GetFromJsonAsync<List<Pruefeintrag>>("/api/v1/review");
Assert.That(wartend, Has.Count.EqualTo(1));
Assert.That(wartend![0].Title, Is.EqualTo("Geheimsache"));
// Ein gewöhnlicher Benutzer nirgends.
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswort");
foreach (var route in Inhaltsrouten)
{
var antwort = await gast.GetAsync(route);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK), route);
var text = await antwort.Content.ReadAsStringAsync();
Assert.That(text, Does.Not.Contain("Geheimsache"),
$"„Geheimsache“ steht in {route} — dort fehlt der Freigabefilter.");
}
}
[Test]
public async Task Veroeffentlichen_macht_ihn_fuer_alle_sichtbar()
{
Datei("Geheimsache (2024).mkv");
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var angelegt = await alsAdmin.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand, ReviewNewItems: true));
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
await Scannen(alsAdmin, bibliothek.Id);
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswort");
Assert.That(await Titel(gast), Is.Empty);
var frei = await alsAdmin.PostAsJsonAsync("/api/v1/review/publish",
new FreigabeRequest(null, null));
frei.EnsureSuccessStatusCode();
Assert.That(await Titel(gast), Is.EqualTo(new[] { "Geheimsache" }));
}
/// <summary>Ein geplanter Titel ist sichtbar, aber noch nicht abspielbar.</summary>
///
/// <remarks>
/// <para>
/// <b>Zwei Zustände, nicht einer.</b> Ohne Zeitpunkt wartet ein Titel auf
/// eine Entscheidung — den soll niemand sehen. Mit einem Zeitpunkt in der
/// Zukunft ist die Entscheidung gefallen: er kommt, nur noch nicht. Ihn
/// dann zu verstecken hiesse, die Vorfreude zu unterschlagen, die der Sinn
/// des Planens ist.
/// </para>
/// <para>
/// <b>Sichtbar heisst aber nicht abspielbar</b>, und das ist der Teil, der
/// wirklich zählt: ohne den Riegel bei der Auslieferung wäre „Verfügbar ab
/// …" eine Zierde, und die Datei ginge trotzdem heraus.
/// </para>
/// </remarks>
[Test]
public async Task Ein_geplanter_Titel_ist_sichtbar_aber_nicht_abspielbar()
{
Datei("Geheimsache (2024).mkv");
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var angelegt = await alsAdmin.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand, ReviewNewItems: true));
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
await Scannen(alsAdmin, bibliothek.Id);
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswort");
// Auf morgen gelegt.
await alsAdmin.PostAsJsonAsync("/api/v1/review/publish",
new FreigabeRequest(null, DateTimeOffset.UtcNow.AddDays(1)));
Assert.That(await Titel(gast), Is.EqualTo(new[] { "Geheimsache" }),
"Ein geplanter Titel steht in der Mediathek — dafür plant man ihn.");
// **Aber die Datei geht nicht heraus.** Das ist der Teil, der zählt.
var filme = await gast.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var datei = filme!.Items.Single().Files.Single();
var geladen = await gast.GetAsync($"/api/v1/files/{datei.Id}/download");
var gestroemt = await gast.PostAsJsonAsync($"/api/v1/files/{datei.Id}/stream",
new StreamRequest(["mp4"], ["h264"], ["aac"]));
Assert.Multiple(() =>
{
Assert.That(geladen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
Assert.That(gestroemt.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
});
// **Und die Detailseite muss es wissen.** Sie entscheidet daran, ob
// sie „Abspielen" zeigt oder den Termin. Die Liste führte `PublishAt`
// von Anfang an mit, die Einzelabfrage nicht — der Knopf stand also
// da, und wer ihn drückte, bekam eine Absage. Ein Knopf, der nichts
// tut, ist schlimmer als keiner.
var einzeln = await gast.GetFromJsonAsync<MovieResponse>(
$"/api/v1/movies/{filme.Items.Single().Id}");
Assert.That(einzeln!.PublishAt, Is.Not.Null,
"die Detailseite braucht den Termin, sonst zeigt sie Abspielen an");
// **Auch für Administratoren.** Eine Hintertür für eine Rolle wäre
// eine Regel, die manchmal gilt; wer vorziehen will, hat den Knopf.
var alsAdminGeladen = await alsAdmin.GetAsync($"/api/v1/files/{datei.Id}/download");
Assert.That(alsAdminGeladen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
// Und er steht weiterhin in der Prüfliste — mit seinem Termin.
var wartend = await alsAdmin.GetFromJsonAsync<List<Pruefeintrag>>("/api/v1/review");
Assert.That(wartend, Has.Count.EqualTo(1));
Assert.That(wartend![0].PublishAt, Is.Not.Null);
// Auf gestern gelegt: sofort da, ohne dass irgendetwas umgeschaltet wurde.
await alsAdmin.PostAsJsonAsync("/api/v1/review/publish",
new FreigabeRequest(null, DateTimeOffset.UtcNow.AddDays(-1)));
Assert.That(await Titel(gast), Is.EqualTo(new[] { "Geheimsache" }));
var jetztGeladen = await gast.GetAsync($"/api/v1/files/{datei.Id}/download");
Assert.That(jetztGeladen.StatusCode, Is.Not.EqualTo(HttpStatusCode.Forbidden),
"ab dem Termin geht die Datei heraus");
}
/// <summary>Ohne Prüfung erscheint alles sofort — das ist die Vorgabe.</summary>
[Test]
public async Task Ohne_Pruefung_erscheint_alles_sofort()
{
Datei("Offen (2024).mkv");
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var angelegt = await alsAdmin.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand));
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
await Scannen(alsAdmin, bibliothek.Id);
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswort");
Assert.That(await Titel(gast), Is.EqualTo(new[] { "Offen" }));
Assert.That(await alsAdmin.GetFromJsonAsync<List<Pruefeintrag>>("/api/v1/review"), Is.Empty);
}
private static async Task<string[]> Titel(HttpClient client) =>
(await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!
.Items.Select(m => m.Title).ToArray();
private static async Task Scannen(HttpClient client, Guid bibliothek)
{
(await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null))
.EnsureSuccessStatusCode();
for (var versuch = 0; versuch < 600; versuch++)
{
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State is "finished") return;
if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}");
await Task.Delay(50);
}
Assert.Fail("Der Scan wurde nicht fertig.");
}
}