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).
137 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|