Die tatsächlich gegebenen Fragebogen-Antworten (B-Fragen zur Datenklasse,
C-Fragen zur KI-VO-Einstufung, D-Werkzeugangaben) wurden bisher nirgends
angezeigt — weder dem antragstellenden Mitarbeiter noch der Fachebene
beim Entscheiden, obwohl der Code-Kommentar an handleAntragDetail schon
"zeigt einen Antrag mit allen Antworten" versprach. Nur die daraus
abgeleitete Bewertung war sichtbar, nicht die Grundlage, auf der sie
beruht — im Audit nicht nachvollziehbar (siehe CLAUDE.md, Grundregel)
und für die Fachebene beim Entscheiden nicht praktikabel.
antwortenAnzeige() baut aus den rohen JSON-Antworten eine lesbare Liste
mit denselben Fragen-Labels wie im Fragebogen selbst — jetzt sowohl auf
GET /antraege/{id} (Ebene 2) als auch GET /faelle/{id} (Ebene 3)
sichtbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
239 lines
9.4 KiB
Go
239 lines
9.4 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
"github.com/netcell-it/deklarix/internal/web"
|
|
)
|
|
|
|
// seedFallImAccount legt einen Mandanten mit einem Mitarbeiter an, der
|
|
// einen Antrag einreicht (mit personenbezogener Datenklasse, Einstufung
|
|
// minimal — siehe fullAntragForm), und liefert dessen ID sowie eine
|
|
// zweite Cookie für einen weiteren Nutzer der Rolle role im selben
|
|
// Mandanten (verantwortlicher/pruefer).
|
|
func seedFallImAccount(t *testing.T, fs *fakeStore, s *web.Server, role string) (antragID string, fachebeneCookie *http.Cookie) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
acc, err := fs.CreateAccount(ctx, "Fachebene-Mandant")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
mitarbeiterCookie := seedUserInAccount(t, fs, acc.ID, "mitarbeiter@example.com", "mitarbeiter")
|
|
resp := postForm(t, s, mitarbeiterCookie, "/antraege", fullAntragForm())
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("Antrag anlegen: status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
for id := range fs.antraege {
|
|
if fs.antraege[id].AccountID == acc.ID {
|
|
antragID = id
|
|
}
|
|
}
|
|
fachebeneCookie = seedUserInAccount(t, fs, acc.ID, role+"@example.com", role)
|
|
return antragID, fachebeneCookie
|
|
}
|
|
|
|
func TestMitarbeiterCannotAccessFaelle(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
cookie := seedAccount(t, fs, "Test-Mandant", "mitarbeiter@example.com")
|
|
|
|
resp := getWithCookie(t, s, cookie, "/faelle")
|
|
if resp.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404 for role mitarbeiter on /faelle", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestFaelleListeShowsOpenAntraegeForAccount(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
_, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
resp := getWithCookie(t, s, verantwortlicherCookie, "/faelle")
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "Angebotstexte generieren") {
|
|
t.Errorf("expected the open antrag in the Posteingang, got: %s", resp.Body.String())
|
|
}
|
|
|
|
// Ein Verantwortlicher eines anderen Mandanten darf ihn nicht sehen.
|
|
otherAcc, err := fs.CreateAccount(context.Background(), "Anderer Mandant")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
otherCookie := seedUserInAccount(t, fs, otherAcc.ID, "other-verantwortlicher@example.com", "verantwortlicher")
|
|
otherResp := getWithCookie(t, s, otherCookie, "/faelle")
|
|
if strings.Contains(otherResp.Body.String(), "Angebotstexte generieren") {
|
|
t.Errorf("expected no cross-tenant antrag in the Posteingang, got: %s", otherResp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFallDetailShowsDecideFormOnlyForVerantwortlicher(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
resp := getWithCookie(t, s, verantwortlicherCookie, "/faelle/"+antragID)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), `name="entscheidung"`) {
|
|
t.Errorf("expected the Entscheiden form for verantwortlicher, got: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestFallDetailZeigtFragebogenAntworten: die Fachebene muss die
|
|
// tatsächlich gegebenen Fragebogen-Antworten sehen, nicht nur die
|
|
// daraus abgeleitete Bewertung — sonst lässt sich eine Entscheidung
|
|
// nicht nachvollziehbar treffen (siehe CLAUDE.md, Grundregel).
|
|
func TestFallDetailZeigtFragebogenAntworten(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
resp := getWithCookie(t, s, verantwortlicherCookie, "/faelle/"+antragID)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
body := resp.Body.String()
|
|
if !strings.Contains(body, "Namen, E-Mail-Adressen oder andere Angaben zu Personen") {
|
|
t.Errorf("expected the b1 Fragebogen-Frage on the fall-detail page, got: %s", body)
|
|
}
|
|
if !strings.Contains(body, "Merkt der Empfänger, dass es von einer KI stammt?") {
|
|
t.Errorf("expected the c5 Fragebogen-Frage on the fall-detail page, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestFallDetailHidesDecideFormForPruefer(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, prueferCookie := seedFallImAccount(t, fs, s, "pruefer")
|
|
|
|
resp := getWithCookie(t, s, prueferCookie, "/faelle/"+antragID)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if strings.Contains(resp.Body.String(), `name="entscheidung"`) {
|
|
t.Errorf("expected no Entscheiden form for pruefer, got: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFallEntscheidenRejectsPruefer(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, prueferCookie := seedFallImAccount(t, fs, s, "pruefer")
|
|
|
|
resp := postForm(t, s, prueferCookie, "/faelle/"+antragID+"/entscheiden", url.Values{"entscheidung": {"rueckfrage"}})
|
|
if resp.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403 for pruefer", resp.Code)
|
|
}
|
|
}
|
|
|
|
// TestFallEntscheidenRequiresBegruendungOnAbweichung: fullAntragForm
|
|
// liefert keine passenden Werkzeuge im (leeren) Katalog, der Vorschlag
|
|
// ist also "rueckfrage" (siehe vorschlagFuer). "abgelehnt" weicht davon
|
|
// ab und braucht eine Begründung.
|
|
func TestFallEntscheidenRequiresBegruendungOnAbweichung(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{"entscheidung": {"abgelehnt"}})
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400 ohne Begründung bei Abweichung, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
resp = postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{
|
|
"entscheidung": {"abgelehnt"}, "begruendung": {"Kein Werkzeug im Katalog verfügbar, trotzdem vertretbar"},
|
|
})
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303 mit Begründung, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFallEntscheidenRueckfrageOhneAbweichungBrauchtKeineBegruendung(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, verantwortlicherCookie := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{"entscheidung": {"rueckfrage"}})
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303 (rueckfrage entspricht dem Vorschlag bei leerem Katalog), body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
detail := getWithCookie(t, s, verantwortlicherCookie, "/faelle/"+antragID)
|
|
if !strings.Contains(detail.Body.String(), "rueckfrage") {
|
|
t.Errorf("expected the Entscheidung on the fall-detail page, got: %s", detail.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFallEntscheidenGenehmigtBrauchtZulaessigesWerkzeug(t *testing.T) {
|
|
fs := newFakeStore()
|
|
acc, err := fs.CreateAccount(context.Background(), "Mit Katalog")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
mitarbeiterCookie := seedUserInAccount(t, fs, acc.ID, "mitarbeiter@example.com", "mitarbeiter")
|
|
fs.werkzeuge["werkzeug-ok"] = store.Werkzeug{ID: "werkzeug-ok", Name: "ChatGPT Enterprise", AVVVerfuegbar: true, TrainingStandard: true}
|
|
s := newServer(t, fs)
|
|
createResp := postForm(t, s, mitarbeiterCookie, "/antraege", fullAntragForm())
|
|
if createResp.Code != http.StatusSeeOther {
|
|
t.Fatalf("Antrag anlegen: status = %d, body: %s", createResp.Code, createResp.Body.String())
|
|
}
|
|
var antragID string
|
|
for id := range fs.antraege {
|
|
if fs.antraege[id].AccountID == acc.ID {
|
|
antragID = id
|
|
}
|
|
}
|
|
verantwortlicherCookie := seedUserInAccount(t, fs, acc.ID, "verantwortlicher@example.com", "verantwortlicher")
|
|
|
|
// Ohne werkzeug_id: abgelehnt.
|
|
resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{"entscheidung": {"genehmigt"}})
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400 ohne Werkzeug bei Genehmigung", resp.Code)
|
|
}
|
|
|
|
// Mit zulässigem Werkzeug: erfolgreich, Antrag wird "entschieden".
|
|
resp = postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{
|
|
"entscheidung": {"genehmigt"}, "werkzeug_id": {"werkzeug-ok"},
|
|
})
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
antrag, err := fs.GetAntrag(context.Background(), antragID)
|
|
if err != nil {
|
|
t.Fatalf("GetAntrag: %v", err)
|
|
}
|
|
if antrag.Status != "entschieden" {
|
|
t.Errorf("Status = %q, want entschieden", antrag.Status)
|
|
}
|
|
|
|
detail := getWithCookie(t, s, verantwortlicherCookie, "/faelle/"+antragID)
|
|
if !strings.Contains(detail.Body.String(), "ChatGPT Enterprise") {
|
|
t.Errorf("expected the chosen werkzeug name on the fall-detail page, got: %s", detail.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFallDetailRejectsForeignAccount(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
antragID, _ := seedFallImAccount(t, fs, s, "verantwortlicher")
|
|
|
|
otherAcc, err := fs.CreateAccount(context.Background(), "Fremder Mandant")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
otherCookie := seedUserInAccount(t, fs, otherAcc.ID, "fremd@example.com", "verantwortlicher")
|
|
|
|
resp := getWithCookie(t, s, otherCookie, "/faelle/"+antragID)
|
|
if resp.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404 for a foreign account's fall", resp.Code)
|
|
}
|
|
}
|