Files
ScarriffleandClaude Opus 5 ed13d5fa43 Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe
„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>
2026-10-16 16:26:57 +02:00

150 lines
6.9 KiB
C#

using System.Net.Http.Headers;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Microsoft.AspNetCore.Hosting;
using Flaro.Data;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Flaro.Core.Metadata;
namespace Flaro.Api.Tests;
/// <summary>Ein frisch gestarteter Server mit leerer Datenbank.</summary>
///
/// <remarks>
/// <b>Eine echte SQLite-Datei, kein Ersatz im Arbeitsspeicher.</b> Damit läuft
/// auch das durch, worauf es beim ersten Start ankommt: Verzeichnisse anlegen,
/// Migration wandern lassen, Einrichtung anbieten. Genau diese Kette sieht ein
/// Fremder als Erstes — und genau sie würde ein Ersatzspeicher überspringen.
/// </remarks>
public sealed class FlaroServer : IDisposable
{
private readonly WebApplicationFactory<Program> _grundlage;
private readonly WebApplicationFactory<Program> _fabrik;
private readonly string _verzeichnis;
public HttpClient Client { get; }
/// <summary>Ein Blick in die Datenbank — für das, was kein Endpunkt anlegt.</summary>
///
/// <remarks>
/// <b>Sparsam benutzen.</b> Ein Test, der am Server vorbei schreibt,
/// prüft nicht mehr, ob der Server es könnte. Für Segmente ist es nötig:
/// sie entstehen aus einem Fingerprintabgleich, und der braucht Mediendateien
/// mit echtem Ton, die es hier nicht gibt.
/// </remarks>
public async Task InDerDatenbank(Func<FlaroContext, Task> tun)
{
using var raum = _fabrik.Services.CreateScope();
await tun(raum.ServiceProvider.GetRequiredService<FlaroContext>());
}
/// <param name="inhaltswurzel">
/// Ein anderes Stammverzeichnis für den Server — nur nötig, wenn ein Test
/// eine ausgelieferte Oberfläche braucht, denn die sucht der Server unter
/// <c>wwwroot</c> neben sich.
/// </param>
public FlaroServer(Action<IServiceCollection>? dienste = null, string? inhaltswurzel = null)
{
_verzeichnis = Path.Combine(Path.GetTempPath(), "flaro-test-" + Guid.CreateVersion7().ToString("N"));
Environment.SetEnvironmentVariable("FLARO_DATA", Path.Combine(_verzeichnis, "config"));
Environment.SetEnvironmentVariable("FLARO_CACHE", Path.Combine(_verzeichnis, "cache"));
Environment.SetEnvironmentVariable("FLARO_TRANSCODE", Path.Combine(_verzeichnis, "transcode"));
// Die Zugriffsgrenze zählt je Adresse, und im Test kommt alles von
// derselben. Ohne das Hochsetzen prüfte man hier die Grenze statt der
// Anmeldung.
Environment.SetEnvironmentVariable("FLARO_RATELIMIT_SIGNIN", "10000");
_grundlage = new WebApplicationFactory<Program>();
_fabrik = _grundlage.WithWebHostBuilder(b =>
{
if (inhaltswurzel is not null) b.UseContentRoot(inhaltswurzel);
b.ConfigureServices(sammlung =>
{
// **Ohne das dauert der ganze Lauf eine Viertelstunde.** Ein Host
// wartet beim Herunterfahren fünf Sekunden auf seine Dienste, und
// bei einem Testserver je Test summiert sich das auf mehr Zeit,
// als sämtliche Prüfungen zusammen brauchen. Hier gibt es nichts,
// worauf zu warten wäre.
sammlung.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromMilliseconds(50));
// **Keine Anfragen nach draussen, solange ein Test es nicht
// ausdrücklich will.** Seit ein Schlüssel mitgeliefert wird, würde
// jeder Scan sonst wirklich bei TMDB nachfragen: die Tests hingen
// am WLAN, und ihre Erwartungen an fremden Daten. „Der Untergang"
// hiess prompt „Downfall", weil die Voreinstellung Englisch ist.
//
// Vor `dienste`, damit ein Test seinen eigenen Anbieter
// nachschieben kann — die spätere Eintragung gewinnt.
sammlung.AddScoped<IMetadataProvider, AbgeschalteterAnbieter>();
dienste?.Invoke(sammlung);
});
});
Client = _fabrik.CreateClient();
}
/// <summary>Richtet ein und meldet den Administrator an.</summary>
public async Task<LoginResponse> SetUpAndSignIn(string name = "guido", string passwort = "ein-langes-passwort")
{
var einrichten = await Client.PostAsJsonAsync("/api/v1/system/setup", new SetupRequest(name, passwort));
einrichten.EnsureSuccessStatusCode();
return await SignIn(name, passwort);
}
public async Task<LoginResponse> SignIn(string name, string passwort, string gerät = "iPhone", string kennung = "geraet-1")
{
var antwort = await Client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest(name, passwort, gerät, kennung));
antwort.EnsureSuccessStatusCode();
return (await antwort.Content.ReadFromJsonAsync<LoginResponse>())!;
}
/// <summary>Legt einen gewöhnlichen Benutzer an und meldet ihn an.</summary>
///
/// <remarks>
/// Für alles, was aus der Sicht eines Nicht-Administrators geprüft werden
/// muss — und das ist mehr, als es klingt: der Unterschied zwischen
/// „sieht es" und „sieht es nicht" ist bei der Freigabe der ganze Zweck.
/// </remarks>
public async Task<HttpClient> NeuerBenutzer(HttpClient alsAdmin, string name, string passwort)
{
var angelegt = await alsAdmin.PostAsJsonAsync("/api/v1/users",
new CreateUserRequest(name, passwort, false));
angelegt.EnsureSuccessStatusCode();
var anmeldung = await SignIn(name, passwort, "Prüfgerät", "pruefgeraet-" + name);
return AlsAngemeldeter(anmeldung.Token);
}
/// <summary>Ein eigener Client, der ein Token vorzeigt.</summary>
public HttpClient AlsAngemeldeter(string token)
{
var client = _fabrik.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
/// <summary>Ein Client, der Cookies weder behält noch mitschickt.</summary>
///
/// <remarks>
/// Der gewöhnliche Testclient führt im Hintergrund einen Cookie-Behälter
/// mit. Wer prüfen will, <i>ob</i> ein Cookie wirkt, darf sich nicht
/// darauf verlassen — sonst prüft er den Behälter, nicht den Server.
/// </remarks>
public HttpClient OhneGedaechtnis() =>
_fabrik.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = false });
public void Dispose()
{
_fabrik.Dispose();
// Auch die Grundlage: `WithWebHostBuilder` gibt eine neue Fabrik
// zurück und lässt die alte stehen. Wer nur die neue entsorgt, häuft
// je Test einen Host an, der erst beim Prozessende abgeräumt wird.
_grundlage.Dispose();
try { Directory.Delete(_verzeichnis, recursive: true); } catch (IOException) { /* Aufräumen ist Kür */ }
}
}