„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>
335 lines
14 KiB
C#
335 lines
14 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Flaro.Api.Endpoints;
|
|
|
|
namespace Flaro.Api.Tests;
|
|
|
|
/// <summary>Einladen statt ein Konto für jemanden erfinden.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der Test, der hier wirklich zählt, ist der mit den zwei gleichzeitigen
|
|
/// Einlösungen.</b> Ein einmaliger Link, zweimal im selben Augenblick
|
|
/// geöffnet, ist der einzige Fall, den man von Hand nie findet — und der
|
|
/// einzige, bei dem ein falscher Entwurf zwei Konten statt einem anlegt.
|
|
/// </remarks>
|
|
public class EinladungTests
|
|
{
|
|
private static async Task<EinladungErstellt> Einladen(
|
|
HttpClient alsAdmin, int tage = 7, int? wieOft = 1)
|
|
{
|
|
var antwort = await alsAdmin.PostAsJsonAsync("/api/v1/invites",
|
|
new EinladungRequest(tage, wieOft));
|
|
antwort.EnsureSuccessStatusCode();
|
|
return (await antwort.Content.ReadFromJsonAsync<EinladungErstellt>())!;
|
|
}
|
|
|
|
private static Task<HttpResponseMessage> Einloesen(
|
|
HttpClient ohne, string token, string name, string passwort = "einlangespasswort") =>
|
|
ohne.PostAsJsonAsync($"/api/v1/invites/{token}/redeem", new EinloesenRequest(name, passwort));
|
|
|
|
[Test]
|
|
public async Task Wer_den_Link_hat_legt_sich_ein_Konto_an()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var fremder = server.OhneGedaechtnis();
|
|
var schau = await fremder.GetFromJsonAsync<Einladungsschau>(
|
|
$"/api/v1/invites/{einladung.Token}");
|
|
var angelegt = await Einloesen(fremder, einladung.Token, "nachbarin");
|
|
|
|
var konten = await alsAdmin.GetFromJsonAsync<List<AuthenticatedUser>>("/api/v1/users");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(schau!.InvitedBy, Is.EqualTo("guido"));
|
|
Assert.That(angelegt.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
Assert.That(konten!.Select(k => k.UserName), Does.Contain("nachbarin"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Wer_eingeladen_wird_ist_kein_Administrator()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
await Einloesen(server.OhneGedaechtnis(), einladung.Token, "nachbarin");
|
|
|
|
var anmeldung = await server.SignIn("nachbarin", "einlangespasswort", "Prüfgerät", "p1");
|
|
var alsGast = server.AlsAngemeldeter(anmeldung.Token);
|
|
var verwaltung = await alsGast.GetAsync("/api/v1/invites");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(anmeldung.User.IsAdministrator, Is.False);
|
|
Assert.That(verwaltung.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ein_eingeloester_Link_gilt_nicht_zweimal()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
await Einloesen(server.OhneGedaechtnis(), einladung.Token, "erste");
|
|
var zweite = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zweite");
|
|
|
|
Assert.That(zweite.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(await zweite.Content.ReadAsStringAsync(), Does.Contain("schon benutzt"));
|
|
}
|
|
|
|
/// <summary>Zwei im selben Augenblick — und trotzdem nur ein Konto.</summary>
|
|
[Test]
|
|
public async Task Zwei_gleichzeitige_Einloesungen_ergeben_ein_Konto()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var beide = await Task.WhenAll(
|
|
Einloesen(server.OhneGedaechtnis(), einladung.Token, "eine"),
|
|
Einloesen(server.OhneGedaechtnis(), einladung.Token, "andere"));
|
|
|
|
var konten = await alsAdmin.GetFromJsonAsync<List<AuthenticatedUser>>("/api/v1/users");
|
|
var dazu = konten!.Count(k => k.UserName is "eine" or "andere");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(beide.Count(a => a.IsSuccessStatusCode), Is.EqualTo(1),
|
|
"Genau eine Einlösung darf durchkommen.");
|
|
Assert.That(dazu, Is.EqualTo(1), "Und genau ein Konto darf entstanden sein.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Mehrfachlinks_gelten_so_oft_wie_angegeben()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin, wieOft: 2);
|
|
|
|
var eine = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "eine");
|
|
var zwei = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zwei");
|
|
var drei = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "drei");
|
|
|
|
var liste = await alsAdmin.GetFromJsonAsync<List<Einladungseintrag>>("/api/v1/invites");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(eine.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
Assert.That(zwei.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
Assert.That(drei.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(liste![0].UsedCount, Is.EqualTo(2));
|
|
Assert.That(liste[0].Valid, Is.False);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ein_zurueckgezogener_Link_wird_abgewiesen()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
(await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}")).EnsureSuccessStatusCode();
|
|
|
|
var versuch = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zuspaet");
|
|
var schau = await server.OhneGedaechtnis().GetAsync($"/api/v1/invites/{einladung.Token}");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(schau.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
});
|
|
Assert.That(await versuch.Content.ReadAsStringAsync(), Does.Contain("zurückgezogen"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ein_abgelaufener_Link_wird_abgewiesen()
|
|
{
|
|
var uhr = new Stelluhr(DateTimeOffset.UtcNow);
|
|
using var server = new FlaroServer(dienste => dienste.AddSingleton<TimeProvider>(uhr));
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin, tage: 1);
|
|
|
|
uhr.Weiter(TimeSpan.FromDays(2));
|
|
|
|
var versuch = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zuspaet");
|
|
|
|
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(await versuch.Content.ReadAsStringAsync(), Does.Contain("abgelaufen"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Einen_Link_den_es_nicht_gibt_kennt_niemand()
|
|
{
|
|
using var server = new FlaroServer();
|
|
await server.SetUpAndSignIn();
|
|
|
|
var versuch = await Einloesen(server.OhneGedaechtnis(), "ausgedacht", "wer");
|
|
var schau = await server.OhneGedaechtnis().GetAsync("/api/v1/invites/ausgedacht");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
|
Assert.That(schau.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Einladen_darf_nur_die_Verwaltung()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswortlang");
|
|
|
|
var erstellen = await gast.PostAsJsonAsync("/api/v1/invites", new EinladungRequest());
|
|
var lesen = await gast.GetAsync("/api/v1/invites");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(erstellen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
|
Assert.That(lesen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
|
});
|
|
}
|
|
|
|
/// <summary>Der Link steht genau einmal da — danach nicht einmal mehr dem Admin.</summary>
|
|
[Test]
|
|
public async Task Der_Token_steht_nur_im_Erstellen_nicht_in_der_Liste()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var roh = await (await alsAdmin.GetAsync("/api/v1/invites")).Content.ReadAsStringAsync();
|
|
|
|
Assert.That(einladung.Token, Is.Not.Empty);
|
|
Assert.That(roh, Does.Not.Contain(einladung.Token),
|
|
"Gespeichert ist nur der Fingerfingerprint — aus der Liste darf kein Link zu bauen sein.");
|
|
}
|
|
|
|
/// <summary>Ein Tippfehler im Namen darf den Link nicht verbrennen.</summary>
|
|
[Test]
|
|
public async Task Ein_vergebener_Name_verbraucht_die_Einladung_nicht()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var belegt = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "guido");
|
|
var danach = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "nachbarin");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(belegt.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
|
|
Assert.That(danach.StatusCode, Is.EqualTo(HttpStatusCode.NoContent),
|
|
"Der Link muss nach dem Fehlgriff noch gelten.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ohne_langes_Passwort_geht_es_nicht()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var kurz = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "wer", "1234");
|
|
var leer = await Einloesen(server.OhneGedaechtnis(), einladung.Token, " ");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(kurz.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(leer.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
});
|
|
}
|
|
|
|
/// <summary>Eine zurückgezogene Einladung lässt sich endgültig entfernen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Warum das überhaupt nötig ist.</b> Zurückziehen behält die Spur
|
|
/// absichtlich — man will wissen, dass es den Link gab, wenn jemand
|
|
/// Unerwartetes im Haushalt auftaucht. Nur verlässt die Zeile die Liste
|
|
/// dann nie, und eine Liste, aus der nichts verschwindet, wird
|
|
/// irgendwann unlesbar. Weggeworfen wird deshalb nur, was ohnehin keine
|
|
/// Tür mehr öffnet.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Eine_zurueckgezogene_Einladung_laesst_sich_entfernen()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
(await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}")).EnsureSuccessStatusCode();
|
|
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
|
|
|
|
var liste = await alsAdmin.GetFromJsonAsync<List<Einladungseintrag>>("/api/v1/invites");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
Assert.That(liste, Is.Empty, "die Zeile muss die Liste verlassen");
|
|
});
|
|
}
|
|
|
|
/// <summary>Ein gültiger Link lässt sich nicht wegwischen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Sonst wäre „Entfernen" ein stilles Zurückziehen mit Gedächtnisverlust:
|
|
/// der Link gälte weiter, stünde aber nirgends mehr. Erst zurückziehen,
|
|
/// dann entfernen — ein Handgriff mehr, mit Absicht.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ein_gueltiger_Link_laesst_sich_nicht_entfernen()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin);
|
|
|
|
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
|
|
var liste = await alsAdmin.GetFromJsonAsync<List<Einladungseintrag>>("/api/v1/invites");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
|
Assert.That(liste, Has.Count.EqualTo(1));
|
|
Assert.That(liste![0].Valid, Is.True, "und er gilt danach immer noch");
|
|
});
|
|
}
|
|
|
|
/// <summary>Ein aufgebrauchter Link darf weg, auch ohne Zurückziehen.</summary>
|
|
[Test]
|
|
public async Task Ein_aufgebrauchter_Link_laesst_sich_entfernen()
|
|
{
|
|
using var server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var alsAdmin = server.AlsAngemeldeter(admin.Token);
|
|
var einladung = await Einladen(alsAdmin, wieOft: 1);
|
|
|
|
(await Einloesen(server.OhneGedaechtnis(), einladung.Token, "gast")).EnsureSuccessStatusCode();
|
|
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
|
|
|
|
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
}
|
|
}
|