„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.
Kratzplatz* → Transcode* (was es ist, steht in FLARO_TRANSCODE)
Marke, Marken → Segment, Segmente
Markenerkennung → Segmenterkennung
Markenart/-suche → Segmentart/-suche
Tonabdruck → Fingerprint (Chromaprint nennt es so)
Tonabgleich → Fingerprintabgleich
Schlüsselbild → Keyframe
Bildabtaster → Frameabtaster
Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.
**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.
**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.
Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
3.6 KiB
C#
119 lines
3.6 KiB
C#
using Flaro.Core.Auth;
|
|
|
|
namespace Flaro.Core.Tests;
|
|
|
|
public class SecretsTests
|
|
{
|
|
[Test]
|
|
public void Zwei_Geheimnisse_sind_nie_gleich()
|
|
{
|
|
var gesehen = new HashSet<string>();
|
|
for (var i = 0; i < 200; i++) Assert.That(gesehen.Add(Secrets.Create()), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void Das_Geheimnis_ist_URL_sicher()
|
|
{
|
|
for (var i = 0; i < 50; i++)
|
|
{
|
|
var s = Secrets.Create();
|
|
Assert.That(s, Does.Not.Contain("+").And.Not.Contain("/").And.Not.Contain("="));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Der_Fingerfingerprint_ist_gleichbleibend_und_unterscheidet()
|
|
{
|
|
var a = Secrets.Create();
|
|
var b = Secrets.Create();
|
|
// Zweimal gerechnet, nicht zweimal derselbe Ausdruck: geprüft wird,
|
|
// dass die Funktion bei gleicher Eingabe gleich antwortet.
|
|
var ersterLauf = Secrets.Fingerprint(a);
|
|
var zweiterLauf = Secrets.Fingerprint(a);
|
|
Assert.That(zweiterLauf, Is.EqualTo(ersterLauf));
|
|
Assert.That(Secrets.Fingerprint(b), Is.Not.EqualTo(ersterLauf));
|
|
Assert.That(ersterLauf, Has.Length.EqualTo(64));
|
|
}
|
|
|
|
/// <summary>Der Fingerfingerprint darf das Geheimnis nicht enthalten.</summary>
|
|
[Test]
|
|
public void Der_Fingerfingerprint_verraet_das_Geheimnis_nicht()
|
|
{
|
|
var s = Secrets.Create();
|
|
Assert.That(Secrets.Fingerprint(s), Does.Not.Contain(s));
|
|
}
|
|
}
|
|
|
|
public class AuthCodeTests
|
|
{
|
|
private static readonly DateTimeOffset Jetzt = new(2026, 10, 3, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
private static AuthCode Angelegt(DateTimeOffset? läuftAb = null) => new()
|
|
{
|
|
Code = AuthCode.NewCode(),
|
|
HandleFingerprint = Secrets.Fingerprint(Secrets.Create()),
|
|
DeviceName = "Wohnzimmer",
|
|
DeviceId = "tv-1",
|
|
CreatedAt = Jetzt,
|
|
ExpiresAt = läuftAb ?? Jetzt + AuthCode.Lifetime,
|
|
};
|
|
|
|
[Test]
|
|
public void Der_Code_hat_immer_sechs_Ziffern()
|
|
{
|
|
for (var i = 0; i < 500; i++)
|
|
{
|
|
var code = AuthCode.NewCode();
|
|
Assert.That(code, Has.Length.EqualTo(6));
|
|
Assert.That(code, Does.Match("^[0-9]{6}$"));
|
|
}
|
|
}
|
|
|
|
/// <summary>Führende Nullen dürfen nicht wegfallen — „000123" ist ein gültiger Code.</summary>
|
|
[Test]
|
|
public void Fuehrende_Nullen_bleiben_erhalten()
|
|
{
|
|
var mitNull = Enumerable.Range(0, 20_000).Select(_ => AuthCode.NewCode()).Any(c => c[0] == '0');
|
|
Assert.That(mitNull, Is.True, "In 20 000 Ziehungen kam kein Code mit führender Null vor");
|
|
}
|
|
|
|
[Test]
|
|
public void Ohne_Bestaetigung_gibt_es_kein_Token()
|
|
{
|
|
Assert.That(Angelegt().CanBeRedeemed(Jetzt), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Nach_Bestaetigung_gibt_es_ein_Token()
|
|
{
|
|
var code = Angelegt();
|
|
code.ApprovedByUserId = Guid.CreateVersion7();
|
|
Assert.That(code.CanBeRedeemed(Jetzt), Is.True);
|
|
}
|
|
|
|
/// <summary>Einmal abgeholt ist abgeholt.</summary>
|
|
[Test]
|
|
public void Ein_verbrauchter_Code_gibt_kein_zweites_Token()
|
|
{
|
|
var code = Angelegt();
|
|
code.ApprovedByUserId = Guid.CreateVersion7();
|
|
code.ConsumedAt = Jetzt;
|
|
Assert.That(code.CanBeRedeemed(Jetzt), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Ein_abgelaufener_Code_gibt_kein_Token_auch_wenn_bestaetigt()
|
|
{
|
|
var code = Angelegt(läuftAb: Jetzt);
|
|
code.ApprovedByUserId = Guid.CreateVersion7();
|
|
Assert.That(code.IsExpired(Jetzt), Is.True);
|
|
Assert.That(code.CanBeRedeemed(Jetzt), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Die_Gueltigkeit_ist_kurz()
|
|
{
|
|
Assert.That(AuthCode.Lifetime, Is.LessThanOrEqualTo(TimeSpan.FromMinutes(10)));
|
|
}
|
|
}
|