Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie freigibt — oder ihnen einen Termin gibt. Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst umgeschaltet; damit stimmt sie nach einem Neustart, nach einer Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan. Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien. Schriebe jede ihren eigenen Filter, reichte eine vergessene — und auffallen würde es nicht beim Programmieren, sondern wenn jemand im Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt, und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die Middleware. Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter umgeht, fällt dort durch. Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und der gesamte Bestand wäre für alle ausser Administratoren verschwunden, sofort beim Start im Container und ohne Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
6.2 KiB
C#
135 lines
6.2 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using Flaro.Api.Endpoints;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
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; }
|
|
|
|
/// <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 */ }
|
|
}
|
|
}
|