Files
deklarix/internal/web/antrag_handlers_test.go
noroot db9c5b7482 feat: Fragebogen mit adaptiver Logik (Schritt 2 der Baureihenfolge)
Ebene 2 ("Antrag stellen") als einseitiges Formular statt mehrseitigem
Assistenten — jede zusätzliche Seite kostet Zeit, und laut Spezifikation
wird ein Antrag umgangen, wenn er länger als fünf Minuten dauert.

- GET /antraege/neu: Fragebogen-Formular (Abschnitte A-D exakt nach
  Spezifikation). Adaptive Folgefragen (C2: welche Art von Entscheidung,
  C3: welche Art von Erkennung) werden rein per CSS :has() ein-/
  ausgeblendet, kein JavaScript nötig — visuell mit Chromium-Screenshots
  verifiziert (unchecked vs. checked).
- POST /antraege: legt an und reicht direkt ein (entwurf->eingereicht in
  einem Schritt, kein Zwischenspeichern als Entwurf für v1). Die
  antworten-JSON nutzt exakt dieselben Fakten-Schlüssel wie
  rules/*.yaml (b1-b7, c1-c5, c2_folge, c3_art) — Schritt 3 kann sie
  direkt auswerten, ohne Felder umzubenennen. "Unsicher" wird bewusst
  NICHT zu "ja" normalisiert (das ist eine Auswertungsregel für Schritt
  3, keine Speicherregel) — der Antrag hält fest, was der Mitarbeiter
  tatsächlich geantwortet hat.
- GET /antraege: eigene Anträge (Ebene 2 sieht nur eigene, nicht die
  des ganzen Mandanten — dafür neue Store-Methode
  ListAntraegeForUser, getrennt von ListAntraegeForAccount für den
  späteren Ebene-3-Posteingang).
- GET /antraege/{id}: Detail, fremder Antrag liefert 404 (nicht 403,
  gleiches Muster wie überall sonst im Projekt).
- betreiber-Rolle wird von allen Antrags-Routen weggeleitet (Ebene 5
  ist technisch getrennt vom Mandantenbereich).

rules/OPEN.md Punkt 3 (fehlende Fragebogen-Unterscheidung für die
"verboten"-Varianten) damit geklärt: c3_art existiert jetzt mit den
bereits in rules/kivo_einstufung.yaml erwarteten Werten.

Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Antrag anlegen, antworten-JSON in
der DB korrekt, Detail-/Listenansicht, Mandantentrennung).
2026-08-28 23:09:24 +02:00

137 lines
4.5 KiB
Go

package web_test
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"testing"
)
func fullAntragForm() url.Values {
return url.Values{
"titel": {"Angebotstexte generieren"}, "beschreibung": {"KI schreibt Angebotstexte"},
"ergebnis": {"Fertiger Text zur Freigabe"}, "haeufigkeit": {"gelegentlich"},
"b1": {"ja"}, "b2": {"nein"}, "b3": {"nein"}, "b4": {"nein"}, "b5": {"nein"}, "b6": {"nein"}, "b7": {"nein"},
"c1": {"nein"}, "c2": {"nein"}, "c2_folge": {"keine"}, "c3": {"nein"}, "c3_art": {"keine"},
"c4": {"nein"}, "c5": {"ja"},
"d_werkzeug_freitext": {"ChatGPT"}, "d_zugang": {"firma"}, "d_geraet": {"firma"},
}
}
func TestAntragNewFormRendersFragebogen(t *testing.T) {
s, _, cookie := newAuthedTestServer(t)
resp := getWithCookie(t, s, cookie, "/antraege/neu")
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
}
body := resp.Body.String()
for _, want := range []string{`name="titel"`, `name="b1"`, `name="c2_folge"`, `name="d_zugang"`} {
if !strings.Contains(body, want) {
t.Errorf("expected %q in the Fragebogen, got: %s", want, body)
}
}
}
func TestAntragCreateThenDetailShowsAnswers(t *testing.T) {
s, fs, cookie := newAuthedTestServer(t)
resp := postForm(t, s, cookie, "/antraege", fullAntragForm())
if resp.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303, body: %s", resp.Code, resp.Body.String())
}
loc := resp.Header().Get("Location")
if !strings.HasPrefix(loc, "/antraege/") {
t.Fatalf("Location = %q, want a redirect to the new antrag", loc)
}
var antragID string
for id := range fs.antraege {
antragID = id
}
antrag, err := fs.GetAntrag(context.Background(), antragID)
if err != nil {
t.Fatalf("GetAntrag: %v", err)
}
if antrag.Status != "eingereicht" {
t.Fatalf("Status = %q, want eingereicht", antrag.Status)
}
var antworten map[string]any
if err := json.Unmarshal(antrag.Antworten, &antworten); err != nil {
t.Fatalf("Antworten unmarshal: %v", err)
}
if antworten["b1"] != "ja" {
t.Errorf("b1 = %v, want ja", antworten["b1"])
}
if antworten["c5"] != true {
t.Errorf("c5 = %v, want true", antworten["c5"])
}
detailResp := getWithCookie(t, s, cookie, loc)
if detailResp.Code != http.StatusOK {
t.Fatalf("detail status = %d, body: %s", detailResp.Code, detailResp.Body.String())
}
if !strings.Contains(detailResp.Body.String(), "Angebotstexte generieren") {
t.Errorf("expected the antrag title on the detail page, got: %s", detailResp.Body.String())
}
}
func TestAntragCreateRejectsMissingFields(t *testing.T) {
s, _, cookie := newAuthedTestServer(t)
resp := postForm(t, s, cookie, "/antraege", url.Values{"titel": {"Nur Titel"}})
if resp.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400 for missing required fields", resp.Code)
}
}
func TestAntragListShowsOnlyOwnAntraege(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
cookieA := seedAccount(t, fs, "Mandant A", "a@example.com")
cookieB := seedAccount(t, fs, "Mandant B", "b@example.com")
if resp := postForm(t, s, cookieA, "/antraege", fullAntragForm()); resp.Code != http.StatusSeeOther {
t.Fatalf("create (A) status = %d, body: %s", resp.Code, resp.Body.String())
}
listA := getWithCookie(t, s, cookieA, "/antraege")
if !strings.Contains(listA.Body.String(), "Angebotstexte generieren") {
t.Errorf("expected Mandant A to see their own antrag, got: %s", listA.Body.String())
}
listB := getWithCookie(t, s, cookieB, "/antraege")
if strings.Contains(listB.Body.String(), "Angebotstexte generieren") {
t.Errorf("expected Mandant B to see no antrag, got: %s", listB.Body.String())
}
}
func TestAntragDetailRejectsForeignUser(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
cookieA := seedAccount(t, fs, "Mandant A", "a@example.com")
cookieB := seedAccount(t, fs, "Mandant B", "b@example.com")
createResp := postForm(t, s, cookieA, "/antraege", fullAntragForm())
antragURL := createResp.Header().Get("Location")
resp := getWithCookie(t, s, cookieB, antragURL)
if resp.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404 for a foreign user's antrag", resp.Code)
}
}
func TestBetreiberRedirectedAwayFromAntragPages(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
for _, path := range []string{"/antraege", "/antraege/neu"} {
resp := getWithCookie(t, s, betreiberCookie, path)
if resp.Code != http.StatusSeeOther {
t.Errorf("GET %s status = %d, want 303 redirect for a betreiber", path, resp.Code)
}
}
}