feat: konfigurierbaren Freigabe-Workflow mit Genehmiger-Rollen einführen
Mandanten können jetzt selbst festlegen, wer bei welcher abgeleiteten Bedingung (Anforderung/Einstufung/Datenklasse) zusätzlich zur Fachebene-Entscheidung zustimmen muss (z. B. Datenschutzbeauftragter, Geschäftsführung) - Genehmiger-Rollen sind orthogonal zu den fünf bestehenden Zugriffsrollen. Alle ausgelösten Rollen müssen zustimmen, eine Ablehnung kippt kaskadierend den gesamten Antrag. Mandanten ohne Freigabe-Regeln (aktuell alle) sind unverändert vom alten Direktpfad betroffen, siehe TestOhneFreigabeRegelnVerhaeltSichWieVorher. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
283
internal/web/freigabe_handlers_test.go
Normal file
283
internal/web/freigabe_handlers_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user