using System.Net;
using System.Net.Http.Json;
using Flaro.Api;
using Flaro.Api.Endpoints;
using Flaro.Core.Delivery;
namespace Flaro.Api.Tests;
/// Was der Server auf dem Kratzplatz wegräumt — und was nicht.
///
///
///
/// Der Anlass: 39 Gigabyte. Für jede Mitnahme baut der Server eine
/// MP4 in Quellgrösse und löschte sie nie. Stromordner verschwanden nur
/// beim geordneten Herunterfahren, Untertitel gar nicht.
///
///
/// Hier wird die Verdrahtung geprüft, nicht die Regel — die steht
/// ohne Platte und ohne Server in
/// Flaro.Core.Tests/KratzplatzregelTests. Was hier zählt: findet der
/// Aufräumer die Dateien, erkennt er, was zur Mediathek gehört, und lässt
/// er das Gültige stehen.
///
///
[TestFixture]
public sealed class KratzplatzTests
{
private string _bestand = "";
/// Ein fester Änderungszeitpunkt, damit der Name vorhersagbar ist.
private static readonly DateTimeOffset Gestempelt =
new(2026, 3, 1, 10, 0, 0, TimeSpan.Zero);
[SetUp]
public void Anlegen() => _bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-kratz-" + 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 und bekanntem Kratzplatz.
private async Task<(HttpClient Client, string Kratzplatz, Guid DateiId)> Aufgesetzt(FlaroServer server)
{
var voll = Path.Combine(_bestand, "Probe (2026)", "Probe (2026).mkv");
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
await File.WriteAllBytesAsync(voll, new byte[4096]);
// **Der Zeitstempel wird gesetzt, nicht abgelesen.** Der Name einer
// umgepackten Datei trägt ihn; ohne festen Wert liesse sich kein
// gültiger Name bauen, und der Test prüfte nur das Löschen.
File.SetLastWriteTimeUtc(voll, Gestempelt.UtcDateTime);
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 orte = await client.GetFromJsonAsync>("/api/v1/system/storage");
var kratz = orte!.First(o => o.Variable == "FLARO_TRANSCODE").Pfad;
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
var film = await client.GetFromJsonAsync($"/api/v1/movies/{filme!.Items[0].Id}");
return (client, kratz, film!.Files[0].Id);
}
private static string Legen(string ordner, string name, int kilobyte = 4)
{
Directory.CreateDirectory(ordner);
var pfad = Path.Combine(ordner, name);
File.WriteAllBytes(pfad, new byte[kilobyte * 1024]);
return pfad;
}
private static Task Aufraeumen(HttpClient client) =>
client.PostAsync("/api/v1/system/storage/transcode/cleanup", null)
.ContinueWith(a => a.Result.Content.ReadFromJsonAsync()).Unwrap();
/// Ein Paket, zu dem es keinen Eintrag mehr gibt, fliegt.
[Test]
public async Task Ein_verwaistes_Paket_ist_nach_dem_Aufraeumen_weg()
{
using var server = new FlaroServer();
var (client, kratz, _) = await Aufgesetzt(server);
var fremd = Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(Guid.CreateVersion7(), Gestempelt, PackageCoordinator.Bauart));
var bericht = await Aufraeumen(client);
Assert.Multiple(() =>
{
Assert.That(File.Exists(fremd), Is.False);
Assert.That(bericht!.Frei, Is.GreaterThan(0), "und es wird gemeldet, was freigeworden ist");
});
}
/// Ein Paket, das noch gilt, bleibt liegen.
///
///
/// Die wichtigere Hälfte. Ein Aufräumer, der zu viel wegwirft,
/// kostet bei jedem zweiten Abspielen die volle Umpackzeit — und das
/// merkt man erst im Betrieb.
///
[Test]
public async Task Ein_gueltiges_Paket_bleibt_liegen()
{
using var server = new FlaroServer();
var (client, kratz, dateiId) = await Aufgesetzt(server);
var gueltig = Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart));
await Aufraeumen(client);
Assert.That(File.Exists(gueltig), Is.True);
}
/// Ein Paket zum alten Stand derselben Datei fliegt.
[Test]
public async Task Ein_Paket_mit_altem_Zeitstempel_ist_weg()
{
using var server = new FlaroServer();
var (client, kratz, dateiId) = await Aufgesetzt(server);
var veraltet = Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(dateiId, Gestempelt.AddDays(-5), PackageCoordinator.Bauart));
await Aufraeumen(client);
Assert.That(File.Exists(veraltet), Is.False);
}
/// Was eine frühere Fassung des Servers gebaut hat, fliegt.
[Test]
public async Task Ein_Paket_fremder_Bauart_ist_weg()
{
using var server = new FlaroServer();
var (client, kratz, dateiId) = await Aufgesetzt(server);
var alteBauart = Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart - 1));
await Aufraeumen(client);
Assert.That(File.Exists(alteBauart), Is.False);
}
/// Ein liegengebliebenes Teilstück fliegt bedingungslos.
///
///
/// Es entsteht nur beim Bauen; wer gerade baut, hält die Datei fest. Ein
/// liegengebliebenes ist das Überbleibsel eines harten Abbruchs — und
/// genau dafür gibt es die Endung.
///
[Test]
public async Task Ein_Teilstueck_ist_weg()
{
using var server = new FlaroServer();
var (client, kratz, dateiId) = await Aufgesetzt(server);
var teil = Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart) + ".teil");
await Aufraeumen(client);
Assert.That(File.Exists(teil), Is.False);
}
/// Ein verwaister Stromordner ist nach dem Aufräumen weg.
[Test]
public async Task Ein_verwaister_Stromordner_ist_weg()
{
using var server = new FlaroServer();
var (client, kratz, _) = await Aufgesetzt(server);
var ordner = Path.Combine(kratz, "stroeme", Guid.CreateVersion7().ToString());
Legen(ordner, "000.m4s");
// Älter als die Geduld, sonst gilt er als eben erst beschrieben.
Directory.SetLastWriteTimeUtc(ordner, DateTime.UtcNow.AddHours(-1));
await Aufraeumen(client);
Assert.That(Directory.Exists(ordner), Is.False);
}
/// Ein eben beschriebener Stromordner bleibt.
///
///
/// Die Sitzung lebt im Arbeitsspeicher; dass ein Ordner gerade Segmente
/// bekommt, sieht man nur an seiner Änderungszeit. Ohne diese Prüfung
/// risse ein Aufräumlauf einer laufenden Wiedergabe den Boden weg.
///
[Test]
public async Task Ein_frischer_Stromordner_bleibt()
{
using var server = new FlaroServer();
var (client, kratz, _) = await Aufgesetzt(server);
var ordner = Path.Combine(kratz, "stroeme", Guid.CreateVersion7().ToString());
Legen(ordner, "000.m4s");
await Aufraeumen(client);
Assert.That(Directory.Exists(ordner), Is.True);
}
/// Eine Untertiteldatei zu einem gelöschten Titel fliegt.
[Test]
public async Task Eine_verwaiste_Untertiteldatei_ist_weg()
{
using var server = new FlaroServer();
var (client, kratz, _) = await Aufgesetzt(server);
var weg = Legen(Path.Combine(kratz, "untertitel"),
$"{Guid.CreateVersion7():N}-{Gestempelt.ToUnixTimeMilliseconds()}-2.vtt");
await Aufraeumen(client);
Assert.That(File.Exists(weg), Is.False);
}
/// Die Übersicht nennt die drei Unterordner mit Grösse und Anzahl.
[Test]
public async Task Die_Belegung_nennt_die_drei_Unterordner()
{
using var server = new FlaroServer();
var (client, kratz, dateiId) = await Aufgesetzt(server);
Legen(Path.Combine(kratz, "packages"),
Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart), kilobyte: 8);
var orte = await client.GetFromJsonAsync>("/api/v1/system/storage");
var kratzplatz = orte!.First(o => o.Variable == "FLARO_TRANSCODE");
Assert.Multiple(() =>
{
Assert.That(kratzplatz.Teile, Is.Not.Null);
Assert.That(kratzplatz.Teile!.Select(t => t.Name),
Is.EquivalentTo(new[] { "stroeme", "packages", "untertitel" }));
var pakete = kratzplatz.Teile!.First(t => t.Name == "packages");
Assert.That(pakete.Dateien, Is.EqualTo(1));
Assert.That(pakete.Frei, Is.EqualTo(8 * 1024), "das Feld Frei trägt hier die Belegung des Ordners");
Assert.That(orte!.First(o => o.Variable == "FLARO_DATA").Teile, Is.Null,
"nur der Kratzplatz wird aufgeschlüsselt");
});
}
[Test]
public async Task Aufraeumen_verlangt_Administratorrechte()
{
using var server = new FlaroServer();
var (admin, _, _) = await Aufgesetzt(server);
var gast = await server.NeuerBenutzer(admin, "gast", "gastpasswortlang");
var antwort = await gast.PostAsync("/api/v1/system/storage/transcode/cleanup", null);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}
/* ---------- Die Einstellungen ---------- */
[Test]
public async Task Die_Vorgaben_stehen_ohne_dass_jemand_etwas_gesetzt_hat()
{
using var server = new FlaroServer();
var (client, _, _) = await Aufgesetzt(server);
var stand = await client.GetFromJsonAsync("/api/v1/settings/maintenance");
Assert.Multiple(() =>
{
Assert.That(stand!.CapGb, Is.EqualTo(20).Within(0.01));
Assert.That(stand.UnusedDays, Is.EqualTo(7));
Assert.That(stand.GraceMinutes, Is.EqualTo(30));
});
}
[Test]
public async Task Eine_gesetzte_Obergrenze_kommt_zurueck()
{
using var server = new FlaroServer();
var (client, _, _) = await Aufgesetzt(server);
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(5, null, null));
var stand = await client.GetFromJsonAsync("/api/v1/settings/maintenance");
Assert.Multiple(() =>
{
Assert.That(stand!.CapGb, Is.EqualTo(5).Within(0.01));
Assert.That(stand.UnusedDays, Is.EqualTo(7), "was nicht geschickt wurde, bleibt");
});
}
[Test]
public async Task Eine_unsinnige_Schonfrist_wird_zurechtgebogen()
{
using var server = new FlaroServer();
var (client, _, _) = await Aufgesetzt(server);
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(null, null, 1));
var stand = await client.GetFromJsonAsync("/api/v1/settings/maintenance");
Assert.That(stand!.GraceMinutes, Is.EqualTo(5),
"eine Minute löschte die Datei mitten im Herunterladen weg");
}
}