namespace Flaro.Api.Tests;
/// Was beim allerersten Start passiert, bevor irgendetwas anderes greift.
public class FlaroPathsTests
{
private static readonly string[] Variablen = ["FLARO_DATA", "FLARO_CACHE", "FLARO_TRANSCODE"];
private string _wurzel = "";
private Dictionary _vorher = [];
/// Merkt sich die Umgebung, bevor diese Klasse daran rührt.
///
///
/// Ohne das kostete der ganze Lauf eine Viertelstunde. Zwei Tests
/// hier löschen die Pfadvariablen, um den Rückfall auf die Voreinstellung
/// zu prüfen — und liessen sie gelöscht zurück. Jeder danach gestartete
/// Testserver fiel dann auf <Projektverzeichnis>/data zurück,
/// und Dutzende Server teilten sich **eine** SQLite-Datei. Die warteten
/// dann aufeinander, statt zu prüfen.
///
/// Prozessweiter Zustand gehört zurückgesetzt, wo er verändert wurde. Das
/// ist keine Formsache: der Schaden zeigt sich erst in einer anderen Klasse.
///
[SetUp]
public void Anlegen()
{
_vorher = Variablen.ToDictionary(v => v, Environment.GetEnvironmentVariable);
_wurzel = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-pfade-" + Guid.CreateVersion7().ToString("N"))).FullName;
}
[TearDown]
public void Aufraeumen()
{
// Zurücksetzen, sonst lässt sich das gesperrte Verzeichnis nicht löschen.
if (!OperatingSystem.IsWindows())
{
foreach (var d in Directory.EnumerateDirectories(_wurzel, "*", SearchOption.AllDirectories))
File.SetUnixFileMode(d, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}
try { Directory.Delete(_wurzel, recursive: true); } catch (IOException) { }
foreach (var (name, wert) in _vorher) Environment.SetEnvironmentVariable(name, wert);
}
private FlaroPaths Pfade(string? daten = null) => new()
{
Data = daten ?? Path.Combine(_wurzel, "config"),
Cache = Path.Combine(_wurzel, "cache"),
Transcode = Path.Combine(_wurzel, "transcode"),
};
[Test]
public void Fehlende_Verzeichnisse_werden_angelegt()
{
var pfade = Pfade();
pfade.EnsureExists();
Assert.That(Directory.Exists(pfade.Data), Is.True);
Assert.That(Directory.Exists(pfade.Cache), Is.True);
Assert.That(Directory.Exists(pfade.Transcode), Is.True);
}
[Test]
public void Ein_zweiter_Start_stoert_sich_nicht_am_ersten()
{
var pfade = Pfade();
pfade.EnsureExists();
Assert.DoesNotThrow(() => pfade.EnsureExists());
}
/// Der häufigste Stolperstein beim ersten Containerstart.
///
///
/// Hier wird die Diagnose geprüft, nicht die Funktion. Ein
/// vorhandenes, aber gesperrtes Verzeichnis lässt
/// Directory.CreateDirectory durchgehen; ohne eigene Schreibprobe
/// stirbt der Server erst später an „unable to open database file" — einer
/// Meldung, die weder das Verzeichnis nennt noch verrät, dass es um
/// Dateirechte geht.
///
[Test]
public void Ein_gesperrtes_Verzeichnis_faellt_beim_Start_auf_und_nennt_den_Grund()
{
if (OperatingSystem.IsWindows()) Assert.Ignore("Dateirechte verhalten sich dort anders.");
var gesperrt = Directory.CreateDirectory(Path.Combine(_wurzel, "gesperrt")).FullName;
if (OperatingSystem.IsWindows()) return;
File.SetUnixFileMode(gesperrt, UnixFileMode.UserRead | UnixFileMode.UserExecute);
var fehler = Assert.Throws(() => Pfade(daten: gesperrt).EnsureExists());
Assert.That(fehler!.Message, Does.Contain("FLARO_DATA"), "Welche Einstellung gemeint ist");
Assert.That(fehler.Message, Does.Contain(gesperrt), "Welches Verzeichnis");
Assert.That(fehler.Message, Does.Contain("uid="), "Als wer der Server läuft");
Assert.That(fehler.Message, Does.Contain("chown"), "Was zu tun ist");
}
[Test]
public void Ohne_Umgebungsvariablen_liegt_alles_neben_dem_Programm()
{
foreach (var name in new[] { "FLARO_DATA", "FLARO_CACHE", "FLARO_TRANSCODE" })
Environment.SetEnvironmentVariable(name, null);
var pfade = FlaroPaths.FromEnvironment(_wurzel);
Assert.That(pfade.Data, Is.EqualTo(Path.Combine(_wurzel, "data")));
Assert.That(pfade.DatabaseFile, Does.EndWith("flaro.db"));
}
[Test]
public void Die_Umgebung_sticht_die_Voreinstellung()
{
Environment.SetEnvironmentVariable("FLARO_DATA", "/woanders");
Assert.That(FlaroPaths.FromEnvironment(_wurzel).Data, Is.EqualTo("/woanders"));
// Zurückgesetzt wird im TearDown, für alle drei Variablen gemeinsam.
}
}
/// Das Ende eines Protokolls lesen, ohne die ganze Datei zu holen.
public class TagebuchEndeTests
{
private string _wurzel = "";
private Tagebuch _tagebuch = null!;
[SetUp]
public void Anlegen()
{
_wurzel = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-log-" + Guid.CreateVersion7().ToString("N"))).FullName;
_tagebuch = new Tagebuch(
new FlaroPaths { Data = _wurzel, Cache = _wurzel, Transcode = _wurzel },
TimeProvider.System);
}
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_wurzel, recursive: true); } catch (IOException) { }
}
[Test]
public void Liefert_die_letzten_Zeilen()
{
var heute = DateOnly.FromDateTime(DateTime.Now);
for (var i = 1; i <= 5000; i++) _tagebuch.Schreiben($"Zeile {i}");
var ende = _tagebuch.Ende(heute, 10).TrimEnd('\n').Split('\n');
Assert.That(ende, Has.Length.EqualTo(10));
Assert.That(ende[^1], Does.Contain("Zeile 5000"));
Assert.That(ende[0], Does.Contain("Zeile 4991"));
}
/// Die Ausgabe beginnt nie mitten in einer Zeile.
[Test]
public void Faengt_an_einem_Zeilenanfang_an()
{
var heute = DateOnly.FromDateTime(DateTime.Now);
for (var i = 1; i <= 3000; i++) _tagebuch.Schreiben(new string('x', 300) + $" {i}");
var ende = _tagebuch.Ende(heute, 5).Split('\n')[0];
Assert.That(ende, Does.Match(@"^\d{2}:\d{2}:\d{2} "),
"Sonst beginnt die Anzeige mitten im Satz.");
}
/// Weniger Zeilen als verlangt ist kein Fehler.
[Test]
public void Eine_kurze_Datei_kommt_ganz()
{
var heute = DateOnly.FromDateTime(DateTime.Now);
_tagebuch.Schreiben("Eine einzige Zeile");
Assert.That(_tagebuch.Ende(heute, 800), Does.Contain("Eine einzige Zeile"));
}
[Test]
public void Ein_Tag_ohne_Protokoll_ist_leer() =>
Assert.That(_tagebuch.Ende(new DateOnly(2000, 1, 1), 10), Is.Empty);
}