using System.Net; using System.Net.Http.Json; using Flaro.Api; using Flaro.Api.Endpoints; using Microsoft.Data.Sqlite; namespace Flaro.Api.Tests; /// Sichern, aufräumen, Bilder wegwerfen — und was dabei herauskommt. /// /// /// /// Die Sicherung ist das Stück, das im Ernstfall zählt. 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. /// /// /// Warum nicht einfach die Datei kopieren. Die Datenbank läuft im /// WAL-Modus: neben flaro.db stehen -wal und -shm, und /// wer nur die erste kopiert, bekommt einen Stand von vor Stunden oder /// einen halben. VACUUM INTO schreibt eine vollständige Kopie im /// laufenden Betrieb — genau das hält dieser Test fest. /// /// [TestFixture] public sealed class WartungsplanTests { private static async Task AlsAdmin(FlaroServer server) { var admin = await server.SetUpAndSignIn(); return server.AlsAngemeldeter(admin.Token); } private static Task Ausfuehren(HttpClient client, string aufgabe) => client.PostAsync($"/api/v1/system/maintenance/{aufgabe}", null); private static async Task Datenordner(HttpClient client) { var orte = await client.GetFromJsonAsync>("/api/v1/system/storage"); return orte!.First(o => o.Variable == "FLARO_DATA").Pfad; } /// Die Sicherung ergibt eine Datenbank, die sich öffnen lässt. [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"); } /// Es bleiben so viele Sicherungen liegen, wie eingestellt ist. /// /// /// 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. /// [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"); } /// Die Sicherung landet im eingestellten Ordner. [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 */ } } } /// Das Aufräumen der Datenbank läuft durch und meldet etwas. [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)); } /// Ein Bild, auf das nichts zeigt, fliegt — ein benutztes nicht. /// /// /// Die Schonfrist ist der Grund für die Zeitstempel. 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. /// [Test] public async Task Verwaiste_Bilder_fliegen_frische_bleiben() { using var server = new FlaroServer(); var client = await AlsAdmin(server); var orte = await client.GetFromJsonAsync>("/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"); }); } /// Der Stand nennt jede Aufgabe mit ihrem Abstand. [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>("/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"); }); } /// Nach einem Lauf steht da, wann er war und was herauskam. [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>("/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)); }); } /// Ein Abstand von null heisst: diese Aufgabe läuft nicht von selbst. [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>("/api/v1/system/maintenance"); Assert.That(stand!.First(s => s.Aufgabe == "Sicherung").Faellig, Is.Null); } /// Die Einstellungen des Plans kommen zurück, wie sie gesetzt wurden. [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("/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("/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 Transcode-Cache auch"); }); } }