package web_test import ( "context" "encoding/json" "net/http" "net/url" "strings" "testing" "github.com/netcell-it/deklarix/internal/store" ) 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) } } // fullAntragForm liefert b1=ja (Datenklasse personenbezogen) und // c1..c3=nein (keine der verboten/hochrisiko/transparenzpflicht- // Varianten trifft zu) — die Einstufung fällt damit auf die // Auffangregel "minimal" zurück (siehe rules/kivo_einstufung.yaml). func TestAntragCreateComputesBewertung(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) cookie := seedAccount(t, fs, "Test-Mandant", "test@example.com") resp := postForm(t, s, cookie, "/antraege", fullAntragForm()) if resp.Code != http.StatusSeeOther { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } var antragID string for id := range fs.antraege { antragID = id } b, err := fs.GetLatestBewertungForAntrag(context.Background(), antragID) if err != nil { t.Fatalf("GetLatestBewertungForAntrag: %v", err) } if b.Datenklasse != "personenbezogen" { t.Errorf("Datenklasse = %q, want personenbezogen", b.Datenklasse) } if b.Einstufung != "minimal" { t.Errorf("Einstufung = %q, want minimal", b.Einstufung) } if b.Verboten { t.Error("Verboten = true, want false") } detail := getWithCookie(t, s, cookie, resp.Header().Get("Location")) if !strings.Contains(detail.Body.String(), "Das System entscheidet nicht") { t.Errorf("expected the mandatory disclaimer on the detail page, got: %s", detail.Body.String()) } } // TestAntragCreateVerbotenSkipsWerkzeugSearch prüft die K.-o.-Prüfung: // c3=ja mit c3_art=social_scoring löst "verboten" aus (Art. 5 KI-VO) — // dann darf keine Werkzeugsuche stattfinden (siehe CLAUDE.md, // Bewertungslogik). func TestAntragCreateVerbotenSkipsWerkzeugSearch(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) cookie := seedAccount(t, fs, "Test-Mandant", "test@example.com") form := fullAntragForm() form.Set("c3", "ja") form.Set("c3_art", "social_scoring") resp := postForm(t, s, cookie, "/antraege", form) if resp.Code != http.StatusSeeOther { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } var antragID string for id := range fs.antraege { antragID = id } b, err := fs.GetLatestBewertungForAntrag(context.Background(), antragID) if err != nil { t.Fatalf("GetLatestBewertungForAntrag: %v", err) } if !b.Verboten || b.Einstufung != "verboten" { t.Fatalf("Bewertung = %+v, want Verboten=true, Einstufung=verboten", b) } if len(b.ZulaessigeWerkzeuge) != 0 || len(b.AusgeschlosseneWerkzeuge) != 0 { t.Errorf("expected no werkzeug search for a verboten Bewertung, got %+v", b) } } // TestAntragCreateFiltersWerkzeugeByAnforderungen prüft den harten // Filter: personenbezogen verlangt avv_erforderlich — ein Werkzeug ohne // AVV wird ausgeschlossen und im Ergebnis mit Begründung genannt statt // stillschweigend wegzufallen, ein Werkzeug mit AVV bleibt zulässig. func TestAntragCreateFiltersWerkzeugeByAnforderungen(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) cookie := seedAccount(t, fs, "Test-Mandant", "test@example.com") fs.werkzeuge["werkzeug-ok"] = store.Werkzeug{ ID: "werkzeug-ok", Name: "ChatGPT Enterprise", AVVVerfuegbar: true, TrainingStandard: true, } fs.werkzeuge["werkzeug-ohne-avv"] = store.Werkzeug{ ID: "werkzeug-ohne-avv", Name: "Freies KI-Tool", AVVVerfuegbar: false, TrainingStandard: true, } resp := postForm(t, s, cookie, "/antraege", fullAntragForm()) if resp.Code != http.StatusSeeOther { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } var antragID string for id := range fs.antraege { antragID = id } b, err := fs.GetLatestBewertungForAntrag(context.Background(), antragID) if err != nil { t.Fatalf("GetLatestBewertungForAntrag: %v", err) } if len(b.ZulaessigeWerkzeuge) != 1 || b.ZulaessigeWerkzeuge[0] != "werkzeug-ok" { t.Errorf("ZulaessigeWerkzeuge = %v, want [werkzeug-ok]", b.ZulaessigeWerkzeuge) } if len(b.AusgeschlosseneWerkzeuge) != 1 || b.AusgeschlosseneWerkzeuge[0].WerkzeugID != "werkzeug-ohne-avv" { t.Fatalf("AusgeschlosseneWerkzeuge = %+v, want [werkzeug-ohne-avv]", b.AusgeschlosseneWerkzeuge) } if got := b.AusgeschlosseneWerkzeuge[0].NichtErfuellt; len(got) != 1 || got[0] != "avv_erforderlich" { t.Errorf("NichtErfuellt = %v, want [avv_erforderlich]", got) } detail := getWithCookie(t, s, cookie, resp.Header().Get("Location")) body := detail.Body.String() if !strings.Contains(body, "ChatGPT Enterprise") || !strings.Contains(body, "Freies KI-Tool") { t.Errorf("expected both werkzeuge (by name) on the detail page, got: %s", body) } } 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) } } }