Jede neue Firma (Registrierung + Betreiber-Firmenanlage) bekommt jetzt automatisch vier leere Genehmiger-Rollen mit erklärender Beschreibung (Datenschutzbeauftragter, Geschäftsführer, KI-Manager, CISO) - Admin muss nur noch Personen zuordnen statt bei null anzufangen. Welche Bedingung welche Rolle tatsächlich auslöst, bleibt weiterhin komplett konfigurierbar pro Firma (Migration 0018 fügt genehmiger_rolle.beschreibung als reines Freitext-Orientierungsfeld hinzu, keine feste fachliche Bindung). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
284 lines
11 KiB
Go
284 lines
11 KiB
Go
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())
|
|
}
|
|
}
|