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; /// Ein frisch gestarteter Server mit leerer Datenbank. /// /// /// Eine echte SQLite-Datei, kein Ersatz im Arbeitsspeicher. 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. /// public sealed class FlaroServer : IDisposable { private readonly WebApplicationFactory _grundlage; private readonly WebApplicationFactory _fabrik; private readonly string _verzeichnis; public HttpClient Client { get; } /// /// Ein anderes Stammverzeichnis für den Server — nur nötig, wenn ein Test /// eine ausgelieferte Oberfläche braucht, denn die sucht der Server unter /// wwwroot neben sich. /// public FlaroServer(Action? 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(); _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(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(); dienste?.Invoke(sammlung); }); }); Client = _fabrik.CreateClient(); } /// Richtet ein und meldet den Administrator an. public async Task 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 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())!; } /// Ein eigener Client, der ein Token vorzeigt. public HttpClient AlsAngemeldeter(string token) { var client = _fabrik.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } /// Ein Client, der Cookies weder behält noch mitschickt. /// /// /// Der gewöhnliche Testclient führt im Hintergrund einen Cookie-Behälter /// mit. Wer prüfen will, ob ein Cookie wirkt, darf sich nicht /// darauf verlassen — sonst prüft er den Behälter, nicht den Server. /// 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 */ } } }