Reine Auswertungslogik in internal/rules, operiert auf rules.Antworten (geparst aus antrag.antworten) und den drei Regelwerken aus Schritt 1 — ohne DB-/Web-Zugriff, vollständig isoliert testbar. - EvaluateDatenklasse: "höchste zutreffende Stufe gewinnt" (höherer Rang). Trifft keine Stufe zu (z. B. B7 fälschlich "nein" trotz keiner anderen Kategorie), wird konservativ "intern" angenommen statt "oeffentlich" — im Zweifel mehr Schutz. Nicht fachlich bestätigt, siehe rules/OPEN.md Punkt 5. - EvaluateEinstufung: Prüfreihenfolge wie im Regelwerk (verboten zuerst = K.-o.-Prüfung), erste zutreffende Stufe/Variante gewinnt. IstVerboten prüft die K.-o.-Bedingung direkt. - DeriveAnforderungen: Anforderungsprofil aus Datenklasse+Einstufung. - FilterWerkzeuge/ErfuelltAnforderung: harter Filter gegen einen Werkzeugkatalog. WerkzeugEigenschaften ist ein eigener, schlanker Typ statt store.Werkzeug — internal/rules bleibt unabhängig von internal/store. Nur technische Werkzeug-Eigenschaften (avv_erforderlich, eu_verarbeitung, kein_training_auf_eingabe) werden hart gefiltert; Prozess-Anforderungen (menschliche_aufsicht, kennzeichnungspflicht, dsfa_erforderlich) sortieren kein Werkzeug aus, sondern werden später als Auflage vermerkt — Annahme, siehe rules/OPEN.md Punkt 6. rules/OPEN.md um Punkt 4 (Auswirkung der fehlenden Löschfristen auf den Filter), Punkt 5 (Datenklasse-Fallback) und Punkt 6 (welche Anforderungen hart filtern) ergänzt — Annahmen dokumentiert statt geraten, damit Schritt 3 nicht auf die fachliche Klärung warten musste. 16 neue Tests gegen die ECHTEN rules/*.yaml-Dateien (nicht nur synthetische Fixtures) — deckt Rang-Konflikte, "Unsicher zählt wie Ja", alle vier Einstufungsstufen inkl. Auffangregel, Anforderungs-Ableitung und Werkzeug-Filterung ab. Noch nicht ans Web angebunden (Schritt 4).
209 lines
7.2 KiB
Go
209 lines
7.2 KiB
Go
package rules_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/rules"
|
|
)
|
|
|
|
func loadRealRegelwerke(t *testing.T) (rules.DatenklasseRegelwerk, rules.EinstufungRegelwerk, rules.AnforderungsRegelwerk) {
|
|
t.Helper()
|
|
fsys := realRulesFS(t)
|
|
dk, err := rules.LoadDatenklasse(fsys, "datenklasse.yaml")
|
|
if err != nil {
|
|
t.Fatalf("LoadDatenklasse: %v", err)
|
|
}
|
|
ei, err := rules.LoadEinstufung(fsys, "kivo_einstufung.yaml")
|
|
if err != nil {
|
|
t.Fatalf("LoadEinstufung: %v", err)
|
|
}
|
|
an, err := rules.LoadAnforderungen(fsys, "anforderungen.yaml")
|
|
if err != nil {
|
|
t.Fatalf("LoadAnforderungen: %v", err)
|
|
}
|
|
return dk, ei, an
|
|
}
|
|
|
|
func TestEvaluateDatenklasseOnlyOeffentlich(t *testing.T) {
|
|
dk, _, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"b1": "nein", "b2": "nein", "b3": "nein", "b4": "nein", "b5": "nein", "b6": "nein", "b7": "ja"}
|
|
got := rules.EvaluateDatenklasse(dk, antworten)
|
|
if got != "oeffentlich" {
|
|
t.Fatalf("EvaluateDatenklasse = %q, want oeffentlich", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDatenklasseHighestWins(t *testing.T) {
|
|
dk, _, _ := loadRealRegelwerke(t)
|
|
// b1 (personenbezogen) UND b2 (besondere_kategorie) beide ja ->
|
|
// besondere_kategorie hat den hoeheren Rang und muss gewinnen.
|
|
antworten := rules.Antworten{"b1": "ja", "b2": "ja", "b7": "nein"}
|
|
got := rules.EvaluateDatenklasse(dk, antworten)
|
|
if got != "besondere_kategorie" {
|
|
t.Fatalf("EvaluateDatenklasse = %q, want besondere_kategorie", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDatenklasseUnsicherZaehltWieJa(t *testing.T) {
|
|
dk, _, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"b3": "unsicher"}
|
|
got := rules.EvaluateDatenklasse(dk, antworten)
|
|
if got != "berufsgeheimnis" {
|
|
t.Fatalf("EvaluateDatenklasse = %q, want berufsgeheimnis (unsicher zaehlt wie ja)", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDatenklasseFallsBackToInternWhenNothingMatches(t *testing.T) {
|
|
dk, _, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"b1": "nein", "b2": "nein", "b3": "nein", "b4": "nein", "b5": "nein", "b6": "nein", "b7": "nein"}
|
|
got := rules.EvaluateDatenklasse(dk, antworten)
|
|
if got != "intern" {
|
|
t.Fatalf("EvaluateDatenklasse = %q, want intern (konservativer Fallback)", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEinstufungVerboten(t *testing.T) {
|
|
_, ei, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"c3": true, "c3_art": "social_scoring"}
|
|
got, err := rules.EvaluateEinstufung(ei, antworten)
|
|
if err != nil {
|
|
t.Fatalf("EvaluateEinstufung: %v", err)
|
|
}
|
|
if got != "verboten" {
|
|
t.Fatalf("EvaluateEinstufung = %q, want verboten", got)
|
|
}
|
|
if !rules.IstVerboten(got) {
|
|
t.Error("IstVerboten sollte true liefern")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEinstufungHochrisiko(t *testing.T) {
|
|
_, ei, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"c2": true, "c2_folge": "kreditwuerdigkeit", "c3": false}
|
|
got, err := rules.EvaluateEinstufung(ei, antworten)
|
|
if err != nil {
|
|
t.Fatalf("EvaluateEinstufung: %v", err)
|
|
}
|
|
if got != "hochrisiko" {
|
|
t.Fatalf("EvaluateEinstufung = %q, want hochrisiko", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEinstufungTransparenzpflicht(t *testing.T) {
|
|
_, ei, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"c1": true, "c2": false, "c3": false, "c5": false}
|
|
got, err := rules.EvaluateEinstufung(ei, antworten)
|
|
if err != nil {
|
|
t.Fatalf("EvaluateEinstufung: %v", err)
|
|
}
|
|
if got != "transparenzpflicht" {
|
|
t.Fatalf("EvaluateEinstufung = %q, want transparenzpflicht", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEinstufungMinimalFallback(t *testing.T) {
|
|
_, ei, _ := loadRealRegelwerke(t)
|
|
antworten := rules.Antworten{"c1": false, "c2": false, "c3": false, "c4": false, "c5": true}
|
|
got, err := rules.EvaluateEinstufung(ei, antworten)
|
|
if err != nil {
|
|
t.Fatalf("EvaluateEinstufung: %v", err)
|
|
}
|
|
if got != "minimal" {
|
|
t.Fatalf("EvaluateEinstufung = %q, want minimal", got)
|
|
}
|
|
if rules.IstVerboten(got) {
|
|
t.Error("IstVerboten sollte fuer minimal false liefern")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAnforderungenPersonenbezogen(t *testing.T) {
|
|
_, _, an := loadRealRegelwerke(t)
|
|
got := rules.DeriveAnforderungen(an, "personenbezogen", "minimal")
|
|
want := map[string]bool{"avv_erforderlich": true, "kein_training_auf_eingabe": true, "loeschfrist_max_tage": true}
|
|
gotSet := map[string]bool{}
|
|
for _, id := range got {
|
|
gotSet[id] = true
|
|
}
|
|
for id := range want {
|
|
if !gotSet[id] {
|
|
t.Errorf("expected Anforderung %q for personenbezogen/minimal, got %v", id, got)
|
|
}
|
|
}
|
|
if gotSet["eu_verarbeitung"] {
|
|
t.Error("eu_verarbeitung sollte fuer personenbezogen (ohne berufsgeheimnis/besondere_kategorie) nicht ausgeloest werden")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAnforderungenHochrisikoAddsAufsichtUndDsfa(t *testing.T) {
|
|
_, _, an := loadRealRegelwerke(t)
|
|
got := rules.DeriveAnforderungen(an, "oeffentlich", "hochrisiko")
|
|
gotSet := map[string]bool{}
|
|
for _, id := range got {
|
|
gotSet[id] = true
|
|
}
|
|
if !gotSet["menschliche_aufsicht"] || !gotSet["dsfa_erforderlich"] {
|
|
t.Errorf("expected menschliche_aufsicht und dsfa_erforderlich fuer hochrisiko, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestErfuelltAnforderungAVV(t *testing.T) {
|
|
if rules.ErfuelltAnforderung("avv_erforderlich", rules.WerkzeugEigenschaften{AVVVerfuegbar: false}) {
|
|
t.Error("erwartet: nicht erfuellt ohne AVV")
|
|
}
|
|
if !rules.ErfuelltAnforderung("avv_erforderlich", rules.WerkzeugEigenschaften{AVVVerfuegbar: true}) {
|
|
t.Error("erwartet: erfuellt mit AVV")
|
|
}
|
|
}
|
|
|
|
func TestErfuelltAnforderungUnbekannteAnforderungIstUnkritisch(t *testing.T) {
|
|
if !rules.ErfuelltAnforderung("menschliche_aufsicht", rules.WerkzeugEigenschaften{}) {
|
|
t.Error("Prozess-Anforderungen duerfen ein Werkzeug nicht hart aussortieren")
|
|
}
|
|
}
|
|
|
|
func TestFilterWerkzeugeSortsOutMissingAVV(t *testing.T) {
|
|
kandidaten := []rules.WerkzeugKandidat{
|
|
{ID: "mit-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: true, Verarbeitungsort: "EU", TrainingStandard: true}},
|
|
{ID: "ohne-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: false, Verarbeitungsort: "EU", TrainingStandard: true}},
|
|
}
|
|
zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, []string{"avv_erforderlich"})
|
|
if len(zulaessig) != 1 || zulaessig[0] != "mit-avv" {
|
|
t.Fatalf("zulaessig = %v, want [mit-avv]", zulaessig)
|
|
}
|
|
if len(ausgeschlossen) != 1 || ausgeschlossen[0].WerkzeugID != "ohne-avv" {
|
|
t.Fatalf("ausgeschlossen = %v, want genau ohne-avv", ausgeschlossen)
|
|
}
|
|
if len(ausgeschlossen[0].NichtErfuellt) != 1 || ausgeschlossen[0].NichtErfuellt[0] != "avv_erforderlich" {
|
|
t.Fatalf("NichtErfuellt = %v, want [avv_erforderlich]", ausgeschlossen[0].NichtErfuellt)
|
|
}
|
|
}
|
|
|
|
func TestFilterWerkzeugeNoRequirementsAllowsEverything(t *testing.T) {
|
|
kandidaten := []rules.WerkzeugKandidat{{ID: "x"}, {ID: "y"}}
|
|
zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, nil)
|
|
if len(zulaessig) != 2 || len(ausgeschlossen) != 0 {
|
|
t.Fatalf("zulaessig=%v ausgeschlossen=%v, want beide zulaessig", zulaessig, ausgeschlossen)
|
|
}
|
|
}
|
|
|
|
func TestParseAntwortenRoundTrip(t *testing.T) {
|
|
raw := []byte(`{"b1":"ja","c2":true,"c2_folge":"bildung"}`)
|
|
a, err := rules.ParseAntworten(raw)
|
|
if err != nil {
|
|
t.Fatalf("ParseAntworten: %v", err)
|
|
}
|
|
if a["b1"] != "ja" || a["c2_folge"] != "bildung" {
|
|
t.Fatalf("ParseAntworten = %+v, unerwartete Werte", a)
|
|
}
|
|
}
|
|
|
|
func TestParseAntwortenEmpty(t *testing.T) {
|
|
a, err := rules.ParseAntworten(nil)
|
|
if err != nil {
|
|
t.Fatalf("ParseAntworten: %v", err)
|
|
}
|
|
if len(a) != 0 {
|
|
t.Fatalf("ParseAntworten(nil) = %+v, want empty", a)
|
|
}
|
|
}
|