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;
/// 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 Blick in die Datenbank — für das, was kein Endpunkt anlegt.
///
///
/// Sparsam benutzen. 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.
///
public async Task InDerDatenbank(Func tun)
{
using var raum = _fabrik.Services.CreateScope();
await tun(raum.ServiceProvider.GetRequiredService());
}
///
/// 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())!;
}
/// Legt einen gewöhnlichen Benutzer an und meldet ihn an.
///
///
/// 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.
///
public async Task 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);
}
/// 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 */ }
}
}