„Die Anfrage war so nicht zu gebrauchen" — das ist der Ersatzsatz des
Browsers für eine Ablehnung ohne Begründung. Dabei stand die Begründung
da: „Benutzername darf nicht leer sein, das Passwort braucht mindestens 8
Zeichen." Nur eben in `errors`, und beide Clients lesen `detail`.
Zwei Formen waren betroffen. `Results.ValidationProblem` legt den Satz
unter `errors` ab, `Results.BadRequest(new { fehler = … })` unter
`fehler` — in beiden Fällen kommt er nirgends an. Zusammen 36 Stellen,
darunter jede Zeile, die ich gestern für die Meldungen geschrieben habe.
Dass die Clients `title` und `errors` auslassen, ist dabei richtig: dort
stehen sonst die englischen Prüftexte des Rahmenwerks, und „Not Found" ist
keine Auskunft.
Jetzt eine Form, hinter `Einwand.Bitte` / `.Fehlt` / `.Belegt`. Dass es
drei benannte Türen sind statt eines Aufrufs mit Zahl, hat einen Grund:
man sieht am Wort, ob der Server um eine bessere Eingabe bittet, etwas
nicht findet oder auf etwas Bestehendes stösst.
**Auffallen konnte es nicht**, und das ist der eigentliche Fund. Die
vorhandenen Tests prüften den Rumpf als Zeichenkette — der Satz *stand* ja
drin, nur im falschen Feld. Der neue Test prüft deshalb, in welchem Feld,
und ein Wächter am Quelltext schlägt an, wenn eine der beiden alten Formen
zurückkommt. Er hat beim ersten Lauf gleich eine Stelle gefunden, die mein
Suchmuster übersehen hatte.
Dazu drei Kleinigkeiten, die beim Lesen auffielen:
**Passwörter wurden getrimmt.** Der Dialog schnitt jedem Feld die Ränder
ab, auch dem Passwort — `login.ts` sendet es ungeschoren. Ein Leerzeichen
am Ende, und das angelegte Konto war für immer unbetretbar.
**Beim Einrichten stapelten sich die Fehlerzeilen**, weil `.error`
weggeräumt wurde und `.fehler` entsteht. Drei Fehlversuche, drei rote
Kästen.
**Der Benutzerdialog versprach acht Zeichen und prüfte nichts.** Jetzt
sagt er es selbst, ohne den Umweg über die Leitung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
5.9 KiB
C#
145 lines
5.9 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using Flaro.Api.Endpoints;
|
|
|
|
namespace Flaro.Api.Tests;
|
|
|
|
/// <summary>Ein abgelehnter Versuch sagt, warum — und zwar dort, wo gelesen wird.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Was hier schiefging, war unsichtbar.</b> Der Server schrieb seine
|
|
/// deutschen Sätze in <c>errors</c> (bei <c>ValidationProblem</c>) oder in ein
|
|
/// selbstgebautes Feld <c>fehler</c>. Beide Clients lesen <c>detail</c> — und
|
|
/// nur das —, weil in <c>title</c> und <c>errors</c> 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."
|
|
/// </para>
|
|
/// <para>
|
|
/// Auffallen konnte das nicht: die vorhandenen Tests prüften den Rumpf als
|
|
/// Zeichenkette (<c>Does.Contain("eingeh")</c>), und darin stand der Satz ja.
|
|
/// Nur eben an der falschen Stelle. Deshalb prüft dieser Test nicht, <i>dass</i>
|
|
/// ein Satz im Rumpf steht, sondern <b>in welchem Feld</b>.
|
|
/// </para>
|
|
/// </remarks>
|
|
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"));
|
|
});
|
|
}
|
|
|
|
/// <summary>Die zweite der beiden alten Formen — der selbstgebaute Rumpf.</summary>
|
|
[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."));
|
|
});
|
|
}
|
|
|
|
/// <summary>Damit die beiden Formen nicht zurückkommen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Ein Wächter am Quelltext, kein Test am Verhalten.</b> Die nächste
|
|
/// Ablehnung wird irgendwann neu geschrieben, und dann liegt
|
|
/// <c>Results.BadRequest(new { fehler = … })</c> näher als
|
|
/// <see cref="Flaro.Api.Einwand"/> — 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
|
|
/// <c>FreigabeTests</c> über den Inhaltsrouten.
|
|
/// </remarks>
|
|
[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<string>();
|
|
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`.");
|
|
}
|
|
|
|
/// <summary>Findet `server/` von den Testbinärdateien aus.</summary>
|
|
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;
|
|
}
|
|
}
|