„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>
268 lines
11 KiB
C#
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");
|
|
});
|
|
}
|
|
}
|