using Flaro.Core.Delivery; namespace Flaro.Core.Tests; /// Welche umgepackten Dateien weggeräumt werden. /// /// /// Der Anlass: 39 Gigabyte Kratzplatz. Für jede Mitnahme baut der /// Server eine MP4 in Quellgrösse und löschte sie nie. Hier steht die Regel, /// die das beendet — ohne Platte, ohne Server, damit die Entscheidung selbst /// nachlesbar ist und nicht nur ihr Ergebnis. /// public class KratzplatzregelTests { private static readonly DateTimeOffset Jetzt = new(2026, 10, 8, 12, 0, 0, TimeSpan.Zero); private const int Bauart = 2; private static readonly Guid EinsId = Guid.Parse("00000000-0000-0000-0000-000000000001"); private static readonly Guid ZweiId = Guid.Parse("00000000-0000-0000-0000-000000000002"); private const long Gigabyte = 1024L * 1024 * 1024; /// Eine Datei, wie sie im Kratzplatz läge. private static Paketakte Akte( Guid id, long gigabyte, TimeSpan vorWieLange, long? geaendertMs = null, int bauart = Bauart) { var ms = geaendertMs ?? 1_000; var name = Paketname.Bauen(id, DateTimeOffset.FromUnixTimeMilliseconds(ms), bauart); return new Paketakte($"/kratz/packages/{name}", gigabyte * Gigabyte, Jetzt - vorWieLange, Paketname.Lesen(name)); } /// Die Mediathek kennt beide Dateien in genau diesem Stand. private static readonly Dictionary Bekannt = new() { [EinsId] = 1_000, [ZweiId] = 1_000 }; private static IReadOnlyList Weg( IReadOnlyList akten, Kratzplatzregeln? regeln = null, IReadOnlyDictionary? bekannt = null, Func? inBenutzung = null) => Kratzplatzauswahl.Wegzuwerfen( akten, bekannt ?? Bekannt, Bauart, regeln ?? Kratzplatzregeln.Vorgabe, Jetzt, inBenutzung ?? (_ => false)) .Select(a => a.Pfad).ToList(); /// Was nie wieder ausgeliefert wird, geht — auch wenn Platz wäre. [Test] public void Eine_Datei_ohne_Eintrag_in_der_Mediathek_geht_weg() { var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2)) }; Assert.That(Weg(akten, bekannt: new Dictionary()), Has.Count.EqualTo(1), "der Titel ist aus der Mediathek verschwunden, das Paket ist wertlos"); } [Test] public void Ein_veralteter_Zeitstempel_im_Namen_gilt_als_verwaist() { var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2), geaendertMs: 999) }; Assert.That(Weg(akten), Has.Count.EqualTo(1), "die Quelle wurde ersetzt; dieses Paket gehört zum alten Stand"); } [Test] public void Eine_andere_Bauart_gilt_als_verwaist() { var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1) }; Assert.That(Weg(akten), Has.Count.EqualTo(1), "so gebaut würde der Server es heute nicht mehr ausliefern"); } [Test] public void Ein_unleserlicher_Name_gilt_als_verwaist() { var akten = new[] { new Paketakte("/kratz/packages/irgendwas.mp4", Gigabyte, Jetzt, Paketname.Lesen("irgendwas.mp4")), }; Assert.That(Weg(akten), Has.Count.EqualTo(1)); } /// Die Schonfrist schützt auch Verwaiste nicht. /// /// /// Sie schützt das, was gleich gebraucht wird. Eine Datei, die niemand /// mehr ausliefern kann, wird nicht gleich gebraucht — sie frisch /// liegenzulassen hiesse, auf etwas zu warten, das nicht kommt. /// [Test] public void Eine_frische_Verwaiste_geht_trotzdem() { var akten = new[] { Akte(EinsId, 1, TimeSpan.Zero, bauart: 1) }; Assert.That(Weg(akten), Has.Count.EqualTo(1)); } [Test] public void Unter_der_Obergrenze_bleibt_alles_liegen() { var akten = new[] { Akte(EinsId, 5, TimeSpan.FromHours(2)), Akte(ZweiId, 5, TimeSpan.FromHours(3)), }; Assert.That(Weg(akten), Is.Empty); } /// Über der Obergrenze geht das am längsten Ungenutzte zuerst. [Test] public void Ueber_der_Obergrenze_geht_das_aelteste_zuerst() { var jung = Akte(EinsId, 15, TimeSpan.FromHours(2)); var alt = Akte(ZweiId, 15, TimeSpan.FromHours(20)); var weg = Weg([jung, alt]); Assert.Multiple(() => { Assert.That(weg, Has.Count.EqualTo(1), "eines von beiden reicht, um unter 20 GB zu kommen"); Assert.That(weg[0], Is.EqualTo(alt.Pfad)); }); } [Test] public void Was_in_der_Schonfrist_liegt_bleibt_auch_ueber_der_Obergrenze() { var eben = Akte(EinsId, 15, TimeSpan.FromMinutes(1)); var auchEben = Akte(ZweiId, 15, TimeSpan.FromMinutes(2)); Assert.That(Weg([eben, auchEben]), Is.Empty, "beide wurden gerade gebraucht — wegräumen hiesse, jemandem den Download abzuschneiden"); } [Test] public void Was_in_Benutzung_ist_bleibt_auch_ueber_der_Obergrenze() { var laeuft = Akte(EinsId, 15, TimeSpan.FromDays(2)); var daneben = Akte(ZweiId, 15, TimeSpan.FromHours(2)); var weg = Weg([laeuft, daneben], inBenutzung: p => p == laeuft.Pfad); Assert.That(weg, Does.Not.Contain(laeuft.Pfad), "ein Download über Mobilfunk kann Stunden dauern, ohne dass eine Anfrage kommt"); } /// Unbenutztes geht auch, wenn noch Platz wäre. [Test] public void Lange_Unbenutzte_gehen_auch_unter_der_Obergrenze() { var akten = new[] { Akte(EinsId, 1, TimeSpan.FromDays(30)) }; Assert.That(Weg(akten), Has.Count.EqualTo(1)); } [Test] public void Eine_Obergrenze_von_null_heisst_unbegrenzt() { var regeln = Kratzplatzregeln.Vorgabe with { ObergrenzeBytes = 0 }; var akten = new[] { Akte(EinsId, 500, TimeSpan.FromHours(2)), Akte(ZweiId, 500, TimeSpan.FromHours(3)), }; Assert.That(Weg(akten, regeln), Is.Empty); } [Test] public void Ohne_Altersabbau_bleibt_auch_sehr_Altes_liegen() { var regeln = Kratzplatzregeln.Vorgabe with { Unbenutzt = TimeSpan.Zero }; Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromDays(400))], regeln), Is.Empty); } /// Auch mit abgeschalteten Regeln fliegt Verwaistes. /// /// /// „Unbegrenzt" heisst „so viel du willst", nicht „sammle auch Müll". /// [Test] public void Verwaistes_geht_auch_bei_abgeschalteten_Grenzen() { var regeln = new Kratzplatzregeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30)); Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1)], regeln), Has.Count.EqualTo(1)); } [Test] public void Eine_zu_kurze_Schonfrist_wird_hochgesetzt() { var regeln = Kratzplatzregeln.Geprueft(20, 7, 1); Assert.That(regeln.Schonfrist, Is.EqualTo(TimeSpan.FromMinutes(5)), "eine Minute löschte die Datei mitten im Herunterladen weg"); } [Test] public void Null_und_negative_Werte_heissen_abgeschaltet() { var regeln = Kratzplatzregeln.Geprueft(0, -3, 30); Assert.Multiple(() => { Assert.That(regeln.ObergrenzeBytes, Is.Zero); Assert.That(regeln.Unbenutzt, Is.EqualTo(TimeSpan.Zero)); }); } } /// Der Name einer umgepackten Datei, in beide Richtungen. public class PaketnameTests { [Test] public void Ein_gebauter_Name_laesst_sich_wieder_lesen() { var id = Guid.CreateVersion7(); var geaendert = DateTimeOffset.FromUnixTimeMilliseconds(1_700_000_000_123); var gelesen = Paketname.Lesen(Paketname.Bauen(id, geaendert, 2)); Assert.That(gelesen, Is.EqualTo(new Paketname.Teile(id, 1_700_000_000_123, 2))); } [Test] public void Der_Pfad_davor_stoert_nicht() { var id = Guid.CreateVersion7(); var name = Paketname.Bauen(id, DateTimeOffset.FromUnixTimeMilliseconds(5), 2); Assert.That(Paketname.Lesen($"/kratz/packages/{name}")?.DateiId, Is.EqualTo(id)); } [TestCase("irgendwas.mp4")] [TestCase("abc-123-2.mp4")] [TestCase("00000000000000000000000000000001-123.mp4")] [TestCase("00000000000000000000000000000001-123-2.teil")] [TestCase("00000000000000000000000000000001-nichtzahl-2.mp4")] public void Ein_fremder_Name_ergibt_nichts(string name) => Assert.That(Paketname.Lesen(name), Is.Null); }