package web_test import ( "context" "net/http" "net/url" "strings" "testing" "github.com/netcell-it/deklarix/internal/web" ) // besondereKategorieAntragForm loest ueber b2=ja die Datenklasse // besondere_kategorie aus, die laut rules/anforderungen.yaml die // Anforderung dsfa_erforderlich nach sich zieht — Grundlage fuer die // Freigabe-Workflow-Tests. func besondereKategorieAntragForm() url.Values { return url.Values{ "titel": {"Gesundheitsdaten auswerten"}, "beschreibung": {"KI wertet Gesundheitsdaten aus"}, "ergebnis": {"Auswertung"}, "haeufigkeit": {"gelegentlich"}, "b1": {"nein"}, "b2": {"ja"}, "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"}, } } // seedFreigabeSzenario legt einen Mandanten mit Mitarbeiter (reicht // einen Antrag ein, der dsfa_erforderlich ausloest), Verantwortlichem // und einem zentralen Werkzeug an — die Grundlage, auf der die // einzelnen Freigabe-Workflow-Tests aufbauen. func seedFreigabeSzenario(t *testing.T, fs *fakeStore, s *web.Server) (accountID, antragID string, verantwortlicherCookie *http.Cookie) { t.Helper() ctx := context.Background() acc, err := fs.CreateAccount(ctx, "Freigabe-Mandant") if err != nil { t.Fatalf("CreateAccount: %v", err) } mitarbeiterCookie := seedUserInAccount(t, fs, acc.ID, "mitarbeiter-fg@example.com", "mitarbeiter") werkzeugForm := url.Values{ "name": {"ChatGPT Enterprise"}, "anbieter": {"OpenAI"}, "verarbeitungslaender": {"Irland"}, "avv_verfuegbar": {"on"}, "avv_url": {"https://example.com/avv"}, "training_standard": {"on"}, "letzte_pruefung": {"2026-08-01"}, "quelle": {"https://example.com/beleg"}, } betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber FG", "betreiber-fg@example.com", "betreiber") if resp := postForm(t, s, betreiberCookie, "/betreiber/werkzeuge", werkzeugForm); resp.Code != http.StatusSeeOther { t.Fatalf("Werkzeug anlegen: status = %d, body: %s", resp.Code, resp.Body.String()) } resp := postForm(t, s, mitarbeiterCookie, "/antraege", besondereKategorieAntragForm()) if resp.Code != http.StatusSeeOther { t.Fatalf("Antrag anlegen: status = %d, body: %s", resp.Code, resp.Body.String()) } for id, a := range fs.antraege { if a.AccountID == acc.ID { antragID = id } } if antragID == "" { t.Fatal("kein Antrag im Mandanten gefunden") } verantwortlicherCookie = seedUserInAccount(t, fs, acc.ID, "verantwortlicher-fg@example.com", "verantwortlicher") return acc.ID, antragID, verantwortlicherCookie } // werkzeugIDFor findet die ID des einzigen im fakeStore angelegten // Werkzeugs — fuer die Genehmigungs-Formulare in diesen Tests reicht ein // einziges zentrales Werkzeug. func werkzeugIDFor(fs *fakeStore, name string) string { for id, w := range fs.werkzeuge { if w.Name == name { return id } } return "" } func TestOhneFreigabeRegelnVerhaeltSichWieVorher(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) _, antragID, verantwortlicherCookie := seedFreigabeSzenario(t, fs, s) werkzeugID := werkzeugIDFor(fs, "ChatGPT Enterprise") resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{ "entscheidung": {"genehmigt"}, "werkzeug_id": {werkzeugID}, }) if resp.Code != http.StatusSeeOther { t.Fatalf("entscheiden: status = %d, body: %s", resp.Code, resp.Body.String()) } if fs.antraege[antragID].Status != "entschieden" { t.Fatalf("Status = %q, want entschieden (keine Freigabe-Regel konfiguriert)", fs.antraege[antragID].Status) } regEintraege := fs.registereintraege[fs.antraege[antragID].AccountID] if len(regEintraege) != 1 { t.Fatalf("erwartet 1 Registereintrag ohne Freigabe-Regeln, got %d", len(regEintraege)) } } func TestFreigabeWorkflowGatesBisAlleZugestimmtHaben(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) accountID, antragID, verantwortlicherCookie := seedFreigabeSzenario(t, fs, s) werkzeugID := werkzeugIDFor(fs, "ChatGPT Enterprise") dsb, err := fs.CreateGenehmigerRolle(context.Background(), accountID, "Datenschutzbeauftragter", "") if err != nil { t.Fatalf("CreateGenehmigerRolle: %v", err) } if _, err := fs.CreateFreigabeRegel(context.Background(), accountID, "anforderung", "dsfa_erforderlich", dsb.ID); err != nil { t.Fatalf("CreateFreigabeRegel: %v", err) } dsbCookie := seedUserInAccount(t, fs, accountID, "dsb@example.com", "mitarbeiter") dsbUser, err := fs.GetUserByEmail(context.Background(), "dsb@example.com") if err != nil { t.Fatalf("GetUserByEmail: %v", err) } if err := fs.AddNutzerGenehmigerRolle(context.Background(), dsbUser.ID, dsb.ID); err != nil { t.Fatalf("AddNutzerGenehmigerRolle: %v", err) } resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{ "entscheidung": {"genehmigt"}, "werkzeug_id": {werkzeugID}, }) if resp.Code != http.StatusSeeOther { t.Fatalf("entscheiden: status = %d, body: %s", resp.Code, resp.Body.String()) } if got := fs.antraege[antragID].Status; got != "wartet_auf_freigabe" { t.Fatalf("Status = %q, want wartet_auf_freigabe", got) } if len(fs.registereintraege[accountID]) != 0 { t.Fatalf("erwartet noch keinen Registereintrag vor vollstaendiger Freigabe, got %d", len(fs.registereintraege[accountID])) } listResp := getWithCookie(t, s, dsbCookie, "/freigaben") if listResp.Code != http.StatusOK || !strings.Contains(listResp.Body.String(), "Gesundheitsdaten auswerten") { t.Fatalf("Meine Freigaben zeigt den Antrag nicht, status=%d body=%s", listResp.Code, listResp.Body.String()) } var schrittID string for id, fsr := range fs.freigabeschritte { if fsr.AntragID == antragID { schrittID = id } } if schrittID == "" { t.Fatal("kein Freigabeschritt fuer den Antrag gefunden") } decideResp := postForm(t, s, dsbCookie, "/freigaben/"+schrittID+"/entscheiden", url.Values{"entscheidung": {"genehmigt"}}) if decideResp.Code != http.StatusSeeOther { t.Fatalf("freigabe entscheiden: status = %d, body: %s", decideResp.Code, decideResp.Body.String()) } if got := fs.antraege[antragID].Status; got != "entschieden" { t.Fatalf("Status nach vollstaendiger Freigabe = %q, want entschieden", got) } if len(fs.registereintraege[accountID]) != 1 { t.Fatalf("erwartet 1 Registereintrag nach vollstaendiger Freigabe, got %d", len(fs.registereintraege[accountID])) } } func TestFreigabeAblehnungKipptGesamtenAntrag(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) accountID, antragID, verantwortlicherCookie := seedFreigabeSzenario(t, fs, s) werkzeugID := werkzeugIDFor(fs, "ChatGPT Enterprise") dsb, _ := fs.CreateGenehmigerRolle(context.Background(), accountID, "Datenschutzbeauftragter", "") gf, _ := fs.CreateGenehmigerRolle(context.Background(), accountID, "Geschaeftsfuehrung", "") fs.CreateFreigabeRegel(context.Background(), accountID, "anforderung", "dsfa_erforderlich", dsb.ID) fs.CreateFreigabeRegel(context.Background(), accountID, "datenklasse", "besondere_kategorie", gf.ID) dsbCookie := seedUserInAccount(t, fs, accountID, "dsb2@example.com", "mitarbeiter") dsbUser, _ := fs.GetUserByEmail(context.Background(), "dsb2@example.com") fs.AddNutzerGenehmigerRolle(context.Background(), dsbUser.ID, dsb.ID) seedUserInAccount(t, fs, accountID, "gf@example.com", "mitarbeiter") resp := postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{ "entscheidung": {"genehmigt"}, "werkzeug_id": {werkzeugID}, }) if resp.Code != http.StatusSeeOther { t.Fatalf("entscheiden: status = %d, body: %s", resp.Code, resp.Body.String()) } var dsbSchrittID string for id, fsr := range fs.freigabeschritte { if fsr.AntragID == antragID && fsr.GenehmigerRolleID == dsb.ID { dsbSchrittID = id } } if dsbSchrittID == "" { t.Fatal("kein DSB-Freigabeschritt gefunden") } decideResp := postForm(t, s, dsbCookie, "/freigaben/"+dsbSchrittID+"/entscheiden", url.Values{ "entscheidung": {"abgelehnt"}, "kommentar": {"Nicht ausreichend pseudonymisiert"}, }) if decideResp.Code != http.StatusSeeOther { t.Fatalf("freigabe ablehnen: status = %d, body: %s", decideResp.Code, decideResp.Body.String()) } if got := fs.antraege[antragID].Status; got != "entschieden" { t.Fatalf("Status nach Ablehnung = %q, want entschieden", got) } letzte := fs.entscheidungen[antragID] if len(letzte) == 0 || letzte[len(letzte)-1].Entscheidung != "abgelehnt" { t.Fatalf("letzte Entscheidung = %+v, want abgelehnt", letzte) } for _, fsr := range fs.freigabeschritte { if fsr.AntragID == antragID && fsr.Status == "ausstehend" { t.Fatalf("erwartet keine ausstehenden Freigabeschritte mehr nach Ablehnung, gefunden: %+v", fsr) } } if len(fs.registereintraege[accountID]) != 0 { t.Fatalf("erwartet keinen Registereintrag nach Ablehnung, got %d", len(fs.registereintraege[accountID])) } } func TestFreigabeEntscheidenOhneRolleWirdAbgelehnt(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) accountID, antragID, verantwortlicherCookie := seedFreigabeSzenario(t, fs, s) werkzeugID := werkzeugIDFor(fs, "ChatGPT Enterprise") dsb, _ := fs.CreateGenehmigerRolle(context.Background(), accountID, "Datenschutzbeauftragter", "") fs.CreateFreigabeRegel(context.Background(), accountID, "anforderung", "dsfa_erforderlich", dsb.ID) fremdCookie := seedUserInAccount(t, fs, accountID, "fremd@example.com", "mitarbeiter") postForm(t, s, verantwortlicherCookie, "/faelle/"+antragID+"/entscheiden", url.Values{ "entscheidung": {"genehmigt"}, "werkzeug_id": {werkzeugID}, }) var schrittID string for id, fsr := range fs.freigabeschritte { if fsr.AntragID == antragID { schrittID = id } } resp := postForm(t, s, fremdCookie, "/freigaben/"+schrittID+"/entscheiden", url.Values{"entscheidung": {"genehmigt"}}) if resp.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404 fuer eine Person ohne die noetige Genehmiger-Rolle", resp.Code) } } func TestGenehmigerRolleCRUD(t *testing.T) { fs := newFakeStore() s := newServer(t, fs) adminCookie := seedAccountWithRole(t, fs, "GR-Mandant", "admin-gr@example.com", "admin") admin, err := fs.GetUserByEmail(context.Background(), "admin-gr@example.com") if err != nil { t.Fatalf("GetUserByEmail: %v", err) } resp := postForm(t, s, adminCookie, "/verwaltung/genehmiger-rollen", url.Values{"name": {"Geschaeftsfuehrung"}}) if resp.Code != http.StatusSeeOther { t.Fatalf("anlegen: status = %d, body: %s", resp.Code, resp.Body.String()) } var rolleID string for id, g := range fs.genehmigerRollen { if g.Name == "Geschaeftsfuehrung" { rolleID = id } } if rolleID == "" { t.Fatal("Genehmiger-Rolle wurde nicht angelegt") } assignResp := postForm(t, s, adminCookie, "/verwaltung/genehmiger-rollen/"+rolleID+"/mitglieder", url.Values{"user_id": {admin.ID}}) if assignResp.Code != http.StatusSeeOther { t.Fatalf("zuweisen: status = %d, body: %s", assignResp.Code, assignResp.Body.String()) } list := getWithCookie(t, s, adminCookie, "/verwaltung/genehmiger-rollen") if !strings.Contains(list.Body.String(), "admin-gr@example.com") { t.Errorf("erwartet das zugewiesene Mitglied auf der Liste, got: %s", list.Body.String()) } removeResp := postForm(t, s, adminCookie, "/verwaltung/genehmiger-rollen/"+rolleID+"/mitglieder/"+admin.ID+"/entfernen", url.Values{}) if removeResp.Code != http.StatusSeeOther { t.Fatalf("entfernen: status = %d, body: %s", removeResp.Code, removeResp.Body.String()) } }