Files
Flaro/server/tests/Flaro.Api.Tests/FlaroServer.cs
T
ScarriffleandClaude Opus 5 8cf2118723 Vorspann und Abspann — erkannt, abgelegt, überspringbar
Beides mit derselben Rechnung, nur in einem anderen Fenster: vorn die
ersten fünfzehn Minuten, hinten die letzten zehn. Was `Tonabgleich`
findet, hängt allein daran, welchen Ausschnitt man ihm gibt — eine
zweite Erkennung für den Abspann wäre dieselbe Suche ein zweites Mal.

Je Staffel verglichen, nicht je Serie: Serien wechseln ihren Vorspann
zwischen Staffeln, und über alle hinweg fände man nichts oder das
Falsche. Gefundenes über fünf Minuten wird verworfen — zwei Folgen, die
dieselbe Datei tragen, teilen vierzig, und darüber stünde sonst ein
Überspringen-Knopf.

Der Lauf geht auf Zuruf, nicht bei jedem Scan: er liest von jeder Folge
ein Stück Ton und dauert Minuten. Der Knopf dafür steht auf der
Serienseite, bei den Verwalterknöpfen.

Die Marken gehen im Folgenvertrag mit — als „intro" und „credits", die
Wörter, die die Apps von Jellyfin her ohnehin kennen. In der App zeichnet
sie der vorhandene Überspringen-Knopf, der bisher nie etwas zu tun
hatte; im Web ist er neu, rechtsbündig über der Zeitleiste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 21:28:36 +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 Marken ist es nötig:
/// sie entstehen aus einem Tonabgleich, 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 */ }
}
}