Neue Funde erst prüfen, dann veröffentlichen
Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie freigibt — oder ihnen einen Termin gibt. Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst umgeschaltet; damit stimmt sie nach einem Neustart, nach einer Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan. Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien. Schriebe jede ihren eigenen Filter, reichte eine vergessene — und auffallen würde es nicht beim Programmieren, sondern wenn jemand im Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt, und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die Middleware. Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter umgeht, fällt dort durch. Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und der gesamte Bestand wäre für alle ausser Administratoren verschwunden, sofort beim Start im Container und ohne Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eb63271e92
commit
632d3efa35
@@ -0,0 +1,187 @@
|
||||
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 Zeitpunkt in der Zukunft hält ihn zurück — ohne Hintergrunddienst.</summary>
|
||||
[Test]
|
||||
public async Task Ein_geplanter_Titel_erscheint_erst_zu_seiner_Zeit()
|
||||
{
|
||||
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.Empty, "Ein geplanter Titel ist noch nicht sichtbar.");
|
||||
|
||||
// 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" }));
|
||||
}
|
||||
|
||||
/// <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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user