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");
});
}
}