using System.Net; using System.Net.Http.Json; using System.Text.Json; using Flaro.Api.Endpoints; namespace Flaro.Api.Tests; /// Ein abgelehnter Versuch sagt, warum — und zwar dort, wo gelesen wird. /// /// /// /// Was hier schiefging, war unsichtbar. Der Server schrieb seine /// deutschen Sätze in errors (bei ValidationProblem) oder in ein /// selbstgebautes Feld fehler. Beide Clients lesen detail — und /// nur das —, weil in title und errors sonst die englischen /// Prüftexte des Rahmenwerks stehen. Also las man im Browser bei jedem /// abgelehnten Versuch denselben Ersatzsatz: „Die Anfrage war so nicht zu /// gebrauchen." /// /// /// Auffallen konnte das nicht: die vorhandenen Tests prüften den Rumpf als /// Zeichenkette (Does.Contain("eingeh")), und darin stand der Satz ja. /// Nur eben an der falschen Stelle. Deshalb prüft dieser Test nicht, dass /// ein Satz im Rumpf steht, sondern in welchem Feld. /// /// public class FehlersatzTests { private static async Task<(HttpStatusCode Code, string? Detail)> Lesen(HttpResponseMessage antwort) { var roh = await antwort.Content.ReadAsStringAsync(); using var papier = JsonDocument.Parse(roh); return (antwort.StatusCode, papier.RootElement.TryGetProperty("detail", out var d) ? d.GetString() : null); } [Test] public async Task Ein_zu_kurzes_Passwort_sagt_warum() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var versuch = await alsAdmin.PostAsJsonAsync("/api/v1/users", new CreateUserRequest("kurz", "1234", false)); var (code, detail) = await Lesen(versuch); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(detail, Does.Contain("8 Zeichen"), "Der Satz muss in `detail` stehen — nur das lesen die Clients."); }); } [Test] public async Task Ein_vergebener_Name_sagt_welcher() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); await alsAdmin.PostAsJsonAsync("/api/v1/users", new CreateUserRequest("doppelt", "einlangespasswort", false)); var zweites = await alsAdmin.PostAsJsonAsync("/api/v1/users", new CreateUserRequest("doppelt", "einlangespasswort", false)); var (code, detail) = await Lesen(zweites); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(HttpStatusCode.Conflict)); Assert.That(detail, Does.Contain("doppelt")); }); } /// Die zweite der beiden alten Formen — der selbstgebaute Rumpf. [Test] public async Task Eine_Meldung_ohne_Text_sagt_warum() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); // Die Kennung gibt es nicht — das ist hier gerade recht: auch dieser // Satz ging früher den unsichtbaren Weg. var versuch = await alsAdmin.PostAsJsonAsync( $"/api/v1/items/{Guid.CreateVersion7()}/reports", new MeldungRequest("bild", "Etwas stimmt nicht.")); var (code, detail) = await Lesen(versuch); Assert.Multiple(() => { Assert.That(code, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(detail, Is.EqualTo("Diesen Titel gibt es nicht.")); }); } /// Damit die beiden Formen nicht zurückkommen. /// /// /// Ein Wächter am Quelltext, kein Test am Verhalten. Die nächste /// Ablehnung wird irgendwann neu geschrieben, und dann liegt /// Results.BadRequest(new { fehler = … }) näher als /// — es sieht ja richtig aus. Ein Test, der /// jede Ablehnung einzeln abklappert, wäre dagegen genau so vollständig wie /// die Liste, die jemand pflegt. Dieselbe Art Wächter steht in /// FreigabeTests über den Inhaltsrouten. /// [Test] public void Keine_Ablehnung_geht_mehr_an_detail_vorbei() { var wurzel = Quellordner(); var endpunkte = Directory.GetFiles( Path.Combine(wurzel, "src", "Flaro.Api"), "*.cs", SearchOption.AllDirectories); var ertappt = new List(); foreach (var datei in endpunkte) { if (Path.GetFileName(datei) is "Einwand.cs") continue; var inhalt = File.ReadAllText(datei); if (inhalt.Contains("ValidationProblem")) ertappt.Add($"{Path.GetFileName(datei)}: ValidationProblem"); // Auf die Form, nicht auf das Wort: `fehler` heisst auch manche // örtliche Variable, etwa im Ausnahmebehandler in `Program.cs`. if (System.Text.RegularExpressions.Regex.IsMatch(inhalt, @"new\s*\{\s*fehler")) ertappt.Add($"{Path.GetFileName(datei)}: new {{ fehler = … }}"); } Assert.That(ertappt, Is.Empty, "Diese Form legt den Satz in ein Feld, das kein Client liest. " + "Einwand.Bitte/Fehlt/Belegt schreibt ihn nach `detail`."); } /// Findet `server/` von den Testbinärdateien aus. private static string Quellordner() { var ordner = new DirectoryInfo(AppContext.BaseDirectory); while (ordner is not null && !Directory.Exists(Path.Combine(ordner.FullName, "src", "Flaro.Api"))) ordner = ordner.Parent; Assert.That(ordner, Is.Not.Null, "Der Quellordner war von den Tests aus nicht zu finden."); return ordner!.FullName; } }