Files
Flaro/server/tests/Flaro.Api.Tests/WartungsplanTests.cs
T
ScarriffleandClaude Opus 5 97edb49564 Eine zweite Bildquelle, weil TMDB nicht alles hat
„365 Tage" stand mit polnischem Plakat da. Nachgesehen: TMDB hat zu diesem
Film 26 Plakate und kein einziges deutsches. Die Auswahl von gestern nimmt
seither das sprachlose statt des polnischen — erfinden kann sie keines.

fanart.tv ist die Quelle, bei der jedes Plakat seine Sprache mitführt, und
die Gemeinschaft dort pflegt landessprachliche Fassungen. OMDb wäre keine
Hilfe gewesen: es liefert das Plakat von IMDb, und das ist in aller Regel
das amerikanische.

**Ausdrücklich keine zweite Metadatenquelle.** `IBildquelle` liefert
Bilder, sonst nichts. Titel, Beschreibung und Besetzung kommen weiterhin
von einer Stelle — zwei Anbieter für dieselbe Angabe wären zwei Wahrheiten.

**Gefragt wird nur, wenn es lohnt.** `MovieMetadata` trägt jetzt die
Sprache des gewählten Plakats mit. Hat TMDB schon eines in der
eingestellten Sprache, geht keine Anfrage hinaus — bei achthundert Titeln
ist das der Unterschied zwischen „läuft nebenher" und „hängt am fremden
Dienst". Und nur ein landessprachliches sticht: ein sprachloses von
fanart.tv ist nicht besser als ein sprachloses von TMDB.

**Die Rangfolge liegt jetzt im Kern**, nicht im TMDB-Client. Sie gilt für
alle Quellen, sonst kämen die Bilder zweier Anbieter in zwei Reihenfolgen
zusammen. Im Bilderdialog stehen beide Quellen gemeinsam geordnet: ein
deutsches Plakat von fanart.tv vor einem sprachlosen von TMDB.

**Nur Filme.** Der Serienweg von fanart.tv verlangt eine TVDB-Kennung, und
die führt Flaro nicht. Eine halbe Lösung, die bei Serien stillschweigend
nichts tut, wäre schlechter als eine, die sagt, worauf sie sich beschränkt.

**Kein mitgelieferter Schlüssel.** Bei TMDB gibt es einen als Rückfall;
fanart.tv verlangt einen persönlichen, und einen fremden mitzuliefern
hiesse, ihn fremd zu benutzen. Ohne eigenen Schlüssel geht keine Anfrage
hinaus — geprüft. Einzutragen unter Einstellungen → Übersicht → Metadaten.

Dass „00" dort „ohne Sprache" heisst, wird beim Lesen übersetzt: sonst
führte die gemeinsame Rangfolge ein sprachloses Plakat als Sprache „00"
und setzte es hinter Englisch statt davor.

732 Tests grün, davon sieben neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:18:27 +02:00

268 lines
11 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Flaro.Api;
using Flaro.Api.Endpoints;
using Microsoft.Data.Sqlite;
namespace Flaro.Api.Tests;
/// <summary>Sichern, aufräumen, Bilder wegwerfen — und was dabei herauskommt.</summary>
///
/// <remarks>
/// <para>
/// <b>Die Sicherung ist das Stück, das im Ernstfall zählt.</b> Eine Kopie,
/// die sich nicht öffnen lässt, ist schlimmer als keine: man verlässt sich
/// darauf und merkt es erst, wenn es zu spät ist. Deshalb prüft der Test
/// nicht, dass eine Datei entstanden ist, sondern dass sie sich als
/// Datenbank öffnen lässt und die Zeilen enthält.
/// </para>
/// <para>
/// <b>Warum nicht einfach die Datei kopieren.</b> Die Datenbank läuft im
/// WAL-Modus: neben <c>flaro.db</c> stehen <c>-wal</c> und <c>-shm</c>, und
/// wer nur die erste kopiert, bekommt einen Stand von vor Stunden oder
/// einen halben. <c>VACUUM INTO</c> schreibt eine vollständige Kopie im
/// laufenden Betrieb — genau das hält dieser Test fest.
/// </para>
/// </remarks>
[TestFixture]
public sealed class WartungsplanTests
{
private static async Task<HttpClient> AlsAdmin(FlaroServer server)
{
var admin = await server.SetUpAndSignIn();
return server.AlsAngemeldeter(admin.Token);
}
private static Task<HttpResponseMessage> Ausfuehren(HttpClient client, string aufgabe) =>
client.PostAsync($"/api/v1/system/maintenance/{aufgabe}", null);
private static async Task<string> Datenordner(HttpClient client)
{
var orte = await client.GetFromJsonAsync<List<Speicherort>>("/api/v1/system/storage");
return orte!.First(o => o.Variable == "FLARO_DATA").Pfad;
}
/// <summary>Die Sicherung ergibt eine Datenbank, die sich öffnen lässt.</summary>
[Test]
public async Task Die_Sicherung_ist_eine_lesbare_Datenbank()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var antwort = await Ausfuehren(client, "sicherung");
antwort.EnsureSuccessStatusCode();
var ordner = Path.Combine(await Datenordner(client), "sicherungen");
var kopien = Directory.GetFiles(ordner, "flaro-*.db");
Assert.That(kopien, Has.Length.EqualTo(1));
// Die Probe aufs Exempel: öffnen und zählen.
await using var verbindung = new SqliteConnection($"Data Source={kopien[0]};Mode=ReadOnly");
await verbindung.OpenAsync();
await using var befehl = verbindung.CreateCommand();
befehl.CommandText = "SELECT COUNT(*) FROM Users";
Assert.That(Convert.ToInt32(await befehl.ExecuteScalarAsync()), Is.EqualTo(1),
"der eingerichtete Administrator muss in der Kopie stehen");
}
/// <summary>Es bleiben so viele Sicherungen liegen, wie eingestellt ist.</summary>
///
/// <remarks>
/// Ohne Grenze füllt eine Sicherung alle drei Tage über ein Jahr
/// hundertzwanzig Kopien derselben Datenbank an — auf demselben Volume,
/// das sie schützen soll.
/// </remarks>
[Test]
public async Task Alte_Sicherungen_werden_weggeraeumt()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(null, null, null, BackupKeep: 2));
var ordner = Path.Combine(await Datenordner(client), "sicherungen");
Directory.CreateDirectory(ordner);
// Drei ältere Stände, die der Lauf überzählig macht.
foreach (var tag in new[] { "2020-01-01-0000", "2020-01-02-0000", "2020-01-03-0000" })
await File.WriteAllTextAsync(Path.Combine(ordner, $"flaro-{tag}.db"), "alt");
(await Ausfuehren(client, "sicherung")).EnsureSuccessStatusCode();
Assert.That(Directory.GetFiles(ordner, "flaro-*.db"), Has.Length.EqualTo(2),
"die neue und die neueste alte");
}
/// <summary>Die Sicherung landet im eingestellten Ordner.</summary>
[Test]
public async Task Ein_eigener_Ordner_wird_benutzt()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var eigen = Path.Combine(Path.GetTempPath(), "flaro-sicherung-" + Guid.CreateVersion7().ToString("N"));
try
{
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(null, null, null, BackupFolder: eigen));
(await Ausfuehren(client, "sicherung")).EnsureSuccessStatusCode();
Assert.That(Directory.GetFiles(eigen, "flaro-*.db"), Has.Length.EqualTo(1));
}
finally
{
try { Directory.Delete(eigen, recursive: true); } catch (IOException) { /* Kür */ }
}
}
/// <summary>Das Aufräumen der Datenbank läuft durch und meldet etwas.</summary>
[Test]
public async Task Die_Datenbank_laesst_sich_optimieren()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var antwort = await Ausfuehren(client, "optimieren");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
/// <summary>Ein Bild, auf das nichts zeigt, fliegt — ein benutztes nicht.</summary>
///
/// <remarks>
/// <b>Die Schonfrist ist der Grund für die Zeitstempel.</b> Ein Bild
/// wird abgelegt, bevor der Verweis darauf gespeichert ist; ohne Frist
/// räumte ein Lauf mitten im Scan genau das weg, was gerade geholt wurde.
/// </remarks>
[Test]
public async Task Verwaiste_Bilder_fliegen_frische_bleiben()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var orte = await client.GetFromJsonAsync<List<Speicherort>>("/api/v1/system/storage");
var bilder = Path.Combine(orte!.First(o => o.Variable == "FLARO_CACHE").Pfad, "images", "ab");
Directory.CreateDirectory(bilder);
var alt = Path.Combine(bilder, "abverwaistesbild.img");
var frisch = Path.Combine(bilder, "abfrischesbild.img");
await File.WriteAllBytesAsync(alt, new byte[256]);
await File.WriteAllBytesAsync(frisch, new byte[256]);
File.SetLastWriteTimeUtc(alt, DateTime.UtcNow.AddDays(-1));
(await Ausfuehren(client, "bilder")).EnsureSuccessStatusCode();
Assert.Multiple(() =>
{
Assert.That(File.Exists(alt), Is.False, "auf dieses Bild zeigt nichts mehr");
Assert.That(File.Exists(frisch), Is.True,
"das könnte gerade erst abgelegt und noch nicht verknüpft sein");
});
}
/// <summary>Der Stand nennt jede Aufgabe mit ihrem Abstand.</summary>
[Test]
public async Task Der_Stand_nennt_alle_drei_Aufgaben()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var stand = await client.GetFromJsonAsync<List<Wartungsstand>>("/api/v1/system/maintenance");
Assert.Multiple(() =>
{
Assert.That(stand!.Select(s => s.Aufgabe),
Is.EquivalentTo(new[] { "Sicherung", "Optimieren", "Bilder" }));
Assert.That(stand!.First(s => s.Aufgabe == "Sicherung").Tage, Is.EqualTo(3));
Assert.That(stand!.All(s => s.Zuletzt is null), Is.True, "noch lief keine");
});
}
/// <summary>Nach einem Lauf steht da, wann er war und was herauskam.</summary>
[Test]
public async Task Nach_einem_Lauf_steht_der_Zeitpunkt_fest()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
await Ausfuehren(client, "sicherung");
var stand = await client.GetFromJsonAsync<List<Wartungsstand>>("/api/v1/system/maintenance");
var sicherung = stand!.First(s => s.Aufgabe == "Sicherung");
Assert.Multiple(() =>
{
Assert.That(sicherung.Zuletzt, Is.Not.Null);
Assert.That(sicherung.Faellig, Is.Not.Null, "und wann die nächste ansteht");
Assert.That(sicherung.Ergebnis, Does.Contain("MB"));
});
}
[Test]
public async Task Eine_unbekannte_Aufgabe_wird_abgewiesen()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var antwort = await Ausfuehren(client, "kaffeekochen");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task Wartung_verlangt_Administratorrechte()
{
using var server = new FlaroServer();
var admin = await AlsAdmin(server);
var gast = await server.NeuerBenutzer(admin, "gast", "gastpasswortlang");
Assert.Multiple(async () =>
{
Assert.That((await Ausfuehren(gast, "sicherung")).StatusCode,
Is.EqualTo(HttpStatusCode.Forbidden));
Assert.That((await gast.GetAsync("/api/v1/system/maintenance")).StatusCode,
Is.EqualTo(HttpStatusCode.Forbidden));
});
}
/// <summary>Ein Abstand von null heisst: diese Aufgabe läuft nicht von selbst.</summary>
[Test]
public async Task Abgeschaltet_bedeutet_kein_Faelligkeitstermin()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(null, null, null, BackupDays: 0));
var stand = await client.GetFromJsonAsync<List<Wartungsstand>>("/api/v1/system/maintenance");
Assert.That(stand!.First(s => s.Aufgabe == "Sicherung").Faellig, Is.Null);
}
/// <summary>Die Einstellungen des Plans kommen zurück, wie sie gesetzt wurden.</summary>
[Test]
public async Task Die_Vorgaben_des_Plans_stehen_und_lassen_sich_aendern()
{
using var server = new FlaroServer();
var client = await AlsAdmin(server);
var vorher = await client.GetFromJsonAsync<MaintenanceSettingsResponse>("/api/v1/settings/maintenance");
await client.PutAsJsonAsync("/api/v1/settings/maintenance",
new MaintenanceSettingsRequest(null, null, null, BackupDays: 1, BackupKeep: 10));
var nachher = await client.GetFromJsonAsync<MaintenanceSettingsResponse>("/api/v1/settings/maintenance");
Assert.Multiple(() =>
{
Assert.That(vorher!.BackupDays, Is.EqualTo(3));
Assert.That(vorher.OptimizeDays, Is.EqualTo(7));
Assert.That(nachher!.BackupDays, Is.EqualTo(1));
Assert.That(nachher.BackupKeep, Is.EqualTo(10));
Assert.That(nachher.OptimizeDays, Is.EqualTo(7), "was nicht geschickt wurde, bleibt");
Assert.That(nachher.CapGb, Is.EqualTo(20).Within(0.01), "und der Kratzplatz auch");
});
}
}