Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
941 lines
23 KiB
Go
941 lines
23 KiB
Go
package ucca
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// Helper to get the project root for testing
|
|
func getProjectRoot(t *testing.T) string {
|
|
// Start from the current directory and walk up to find go.mod
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get working directory: %v", err)
|
|
}
|
|
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
t.Fatalf("Could not find project root (no go.mod found)")
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
func TestNewPolicyEngineFromPath(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
if engine.GetPolicyVersion() != "1.0.0" {
|
|
t.Errorf("Expected policy version 1.0.0, got %s", engine.GetPolicyVersion())
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateSimpleCase(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Simple RAG chatbot for utilities (low risk)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Chatbot für Stadtwerke mit FAQ-Suche",
|
|
Domain: DomainUtilities,
|
|
DataTypes: DataTypes{
|
|
PersonalData: false,
|
|
PublicData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
CustomerSupport: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
RAG: true,
|
|
Training: false,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityYES {
|
|
t.Errorf("Expected feasibility YES, got %s", result.Feasibility)
|
|
}
|
|
|
|
if result.RiskLevel != RiskLevelMINIMAL {
|
|
t.Errorf("Expected risk level MINIMAL, got %s", result.RiskLevel)
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateHighRiskCase(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: HR scoring with full automation (should be blocked)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Automatische Mitarbeiterbewertung",
|
|
Domain: DomainHR,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
EmployeeData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
EvaluationScoring: true,
|
|
},
|
|
Automation: AutomationFullyAutomated,
|
|
Outputs: Outputs{
|
|
RankingsOrScores: true,
|
|
},
|
|
ModelUsage: ModelUsage{
|
|
Training: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityNO {
|
|
t.Errorf("Expected feasibility NO for HR scoring, got %s", result.Feasibility)
|
|
}
|
|
|
|
// Should have at least one BLOCK severity rule triggered
|
|
hasBlock := false
|
|
for _, rule := range result.TriggeredRules {
|
|
if rule.Severity == SeverityBLOCK {
|
|
hasBlock = true
|
|
break
|
|
}
|
|
}
|
|
if !hasBlock {
|
|
t.Error("Expected at least one BLOCK severity rule for HR scoring")
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateConditionalCase(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Personal data with marketing (should be conditional)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Marketing-Personalisierung",
|
|
Domain: DomainMarketing,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
Marketing: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
RAG: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityCONDITIONAL {
|
|
t.Errorf("Expected feasibility CONDITIONAL for marketing with PII, got %s", result.Feasibility)
|
|
}
|
|
|
|
// Should require consent control
|
|
hasConsentControl := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "C_EXPLICIT_CONSENT" {
|
|
hasConsentControl = true
|
|
break
|
|
}
|
|
}
|
|
if !hasConsentControl {
|
|
t.Error("Expected C_EXPLICIT_CONSENT control for marketing with PII")
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateArticle9Data(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Healthcare with Art. 9 data
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Patientendaten-Analyse",
|
|
Domain: DomainHealthcare,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
Article9Data: true,
|
|
},
|
|
Purpose: Purpose{
|
|
Analytics: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
RAG: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Art. 9 data should trigger DSFA recommendation
|
|
if !result.DSFARecommended {
|
|
t.Error("Expected DSFA recommended for Art. 9 data")
|
|
}
|
|
|
|
// Should have triggered Art. 9 rule
|
|
hasArt9Rule := false
|
|
for _, rule := range result.TriggeredRules {
|
|
if rule.Code == "R-A002" {
|
|
hasArt9Rule = true
|
|
break
|
|
}
|
|
}
|
|
if !hasArt9Rule {
|
|
t.Error("Expected R-A002 (Art. 9 data) rule to be triggered")
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateLicensePlates(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Parking with license plates
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Parkhaus-Kennzeichenerkennung",
|
|
Domain: DomainRealEstate,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
LicensePlates: true,
|
|
},
|
|
Purpose: Purpose{
|
|
Automation: true,
|
|
},
|
|
Automation: AutomationSemiAutomated,
|
|
ModelUsage: ModelUsage{
|
|
Inference: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should suggest pixelization pattern
|
|
hasPixelization := false
|
|
for _, pattern := range result.RecommendedArchitecture {
|
|
if pattern.PatternID == "P_PIXELIZATION" {
|
|
hasPixelization = true
|
|
break
|
|
}
|
|
}
|
|
if !hasPixelization {
|
|
t.Error("Expected P_PIXELIZATION pattern to be recommended for license plates")
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_EvaluateThirdCountryTransfer(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Third country hosting with PII
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "US-hosted AI service",
|
|
Domain: DomainITServices,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
InternalTools: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
RAG: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "third_country",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
// Should require SCC control
|
|
hasSCC := false
|
|
for _, ctrl := range result.RequiredControls {
|
|
if ctrl.ID == "C_SCC" {
|
|
hasSCC = true
|
|
break
|
|
}
|
|
}
|
|
if !hasSCC {
|
|
t.Error("Expected C_SCC control for third country transfer with PII")
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_GetAllRules(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
rules := engine.GetAllRules()
|
|
if len(rules) == 0 {
|
|
t.Error("Expected at least some rules")
|
|
}
|
|
|
|
// Check that rules have required fields
|
|
for _, rule := range rules {
|
|
if rule.ID == "" {
|
|
t.Error("Found rule without ID")
|
|
}
|
|
if rule.Category == "" {
|
|
t.Error("Found rule without category")
|
|
}
|
|
if rule.Title == "" {
|
|
t.Error("Found rule without title")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_GetAllPatterns(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
patterns := engine.GetAllPatterns()
|
|
if len(patterns) == 0 {
|
|
t.Error("Expected at least some patterns")
|
|
}
|
|
|
|
// Verify expected patterns exist
|
|
expectedPatterns := []string{"P_RAG_ONLY", "P_PRE_ANON", "P_PIXELIZATION", "P_HITL_ENFORCED"}
|
|
for _, expected := range expectedPatterns {
|
|
if _, exists := patterns[expected]; !exists {
|
|
t.Errorf("Expected pattern %s to exist", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_GetAllControls(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
controls := engine.GetAllControls()
|
|
if len(controls) == 0 {
|
|
t.Error("Expected at least some controls")
|
|
}
|
|
|
|
// Verify expected controls exist
|
|
expectedControls := []string{"C_EXPLICIT_CONSENT", "C_DSFA", "C_ENCRYPTION", "C_SCC"}
|
|
for _, expected := range expectedControls {
|
|
if _, exists := controls[expected]; !exists {
|
|
t.Errorf("Expected control %s to exist", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_TrainingWithMinorData(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Training with minor data (should be blocked)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "KI-Training mit Schülerdaten",
|
|
Domain: DomainEducation,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
MinorData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
Research: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
Training: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.TrainingAllowed != TrainingNO {
|
|
t.Errorf("Expected training NOT allowed for minor data, got %s", result.TrainingAllowed)
|
|
}
|
|
}
|
|
|
|
func TestPolicyEngine_CompositeConditions(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Test case: Fully automated with legal effects (R-C004 uses all_of)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Automatische Vertragsgenehmigung",
|
|
Domain: DomainLegal,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
DecisionMaking: true,
|
|
},
|
|
Automation: AutomationFullyAutomated,
|
|
Outputs: Outputs{
|
|
LegalEffects: true,
|
|
},
|
|
ModelUsage: ModelUsage{
|
|
Inference: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityNO {
|
|
t.Errorf("Expected NO feasibility for fully automated legal decisions, got %s", result.Feasibility)
|
|
}
|
|
|
|
// Check that R-C004 was triggered
|
|
hasC004 := false
|
|
for _, rule := range result.TriggeredRules {
|
|
if rule.Code == "R-C004" {
|
|
hasC004 = true
|
|
break
|
|
}
|
|
}
|
|
if !hasC004 {
|
|
t.Error("Expected R-C004 (automated legal effects) to be triggered")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Determinism Tests - Ensure consistent results
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_Determinism(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Test Case für Determinismus",
|
|
Domain: DomainEducation,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
MinorData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
EvaluationScoring: true,
|
|
},
|
|
Automation: AutomationFullyAutomated,
|
|
Outputs: Outputs{
|
|
RankingsOrScores: true,
|
|
},
|
|
ModelUsage: ModelUsage{
|
|
Training: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
// Run evaluation 10 times and ensure identical results
|
|
firstResult := engine.Evaluate(intake)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != firstResult.Feasibility {
|
|
t.Errorf("Run %d: Feasibility mismatch: %s vs %s", i, result.Feasibility, firstResult.Feasibility)
|
|
}
|
|
if result.RiskScore != firstResult.RiskScore {
|
|
t.Errorf("Run %d: RiskScore mismatch: %d vs %d", i, result.RiskScore, firstResult.RiskScore)
|
|
}
|
|
if result.RiskLevel != firstResult.RiskLevel {
|
|
t.Errorf("Run %d: RiskLevel mismatch: %s vs %s", i, result.RiskLevel, firstResult.RiskLevel)
|
|
}
|
|
if len(result.TriggeredRules) != len(firstResult.TriggeredRules) {
|
|
t.Errorf("Run %d: TriggeredRules count mismatch: %d vs %d", i, len(result.TriggeredRules), len(firstResult.TriggeredRules))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Education Domain Specific Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_EducationScoring_AlwaysBlocked(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Education + Scoring + Fully Automated = BLOCK (R-F001)
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "Automatische Schülerbewertung",
|
|
Domain: DomainEducation,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
MinorData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
EvaluationScoring: true,
|
|
},
|
|
Automation: AutomationFullyAutomated,
|
|
Outputs: Outputs{
|
|
RankingsOrScores: true,
|
|
},
|
|
ModelUsage: ModelUsage{
|
|
Inference: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityNO {
|
|
t.Errorf("Expected NO for education automated scoring, got %s", result.Feasibility)
|
|
}
|
|
|
|
// Should have Art. 22 risk flagged
|
|
if !result.Art22Risk {
|
|
t.Error("Expected Art. 22 risk for automated individual decisions in education")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// RAG-Only Use Cases (Low Risk)
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_RAGOnly_LowRisk(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
intake := &UseCaseIntake{
|
|
UseCaseText: "FAQ-Suche mit öffentlichen Dokumenten",
|
|
Domain: DomainUtilities,
|
|
DataTypes: DataTypes{
|
|
PublicData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
CustomerSupport: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{
|
|
RAG: true,
|
|
},
|
|
Hosting: Hosting{
|
|
Region: "eu",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
if result.Feasibility != FeasibilityYES {
|
|
t.Errorf("Expected YES for RAG-only public data, got %s", result.Feasibility)
|
|
}
|
|
|
|
if result.RiskLevel != RiskLevelMINIMAL {
|
|
t.Errorf("Expected MINIMAL risk for RAG-only, got %s", result.RiskLevel)
|
|
}
|
|
|
|
// Should recommend P_RAG_ONLY pattern
|
|
hasRAGPattern := false
|
|
for _, pattern := range result.RecommendedArchitecture {
|
|
if pattern.PatternID == "P_RAG_ONLY" {
|
|
hasRAGPattern = true
|
|
break
|
|
}
|
|
}
|
|
if !hasRAGPattern {
|
|
t.Error("Expected P_RAG_ONLY pattern recommendation")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Risk Score Calculation Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_RiskScoreCalculation(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
intake *UseCaseIntake
|
|
minScore int
|
|
maxScore int
|
|
expectedRiskLevel RiskLevel
|
|
}{
|
|
{
|
|
name: "Public data only → minimal risk",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainUtilities,
|
|
DataTypes: DataTypes{
|
|
PublicData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
minScore: 0,
|
|
maxScore: 20,
|
|
expectedRiskLevel: RiskLevelMINIMAL,
|
|
},
|
|
{
|
|
name: "Personal data → low risk",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainITServices,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
minScore: 5,
|
|
maxScore: 40,
|
|
expectedRiskLevel: RiskLevelLOW,
|
|
},
|
|
{
|
|
name: "Art. 9 data → medium risk",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainHealthcare,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
Article9Data: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
minScore: 20,
|
|
maxScore: 60,
|
|
expectedRiskLevel: RiskLevelMEDIUM,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := engine.Evaluate(tt.intake)
|
|
|
|
if result.RiskScore < tt.minScore || result.RiskScore > tt.maxScore {
|
|
t.Errorf("RiskScore %d outside expected range [%d, %d]", result.RiskScore, tt.minScore, tt.maxScore)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Training Allowed Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_TrainingAllowed_Scenarios(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
intake *UseCaseIntake
|
|
expectedAllowed TrainingAllowed
|
|
}{
|
|
{
|
|
name: "Public data training → allowed",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainUtilities,
|
|
DataTypes: DataTypes{
|
|
PublicData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{Training: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectedAllowed: TrainingYES,
|
|
},
|
|
{
|
|
name: "Minor data training → not allowed",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainEducation,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
MinorData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{Training: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectedAllowed: TrainingNO,
|
|
},
|
|
{
|
|
name: "Art. 9 data training → not allowed",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainHealthcare,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
Article9Data: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{Training: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectedAllowed: TrainingNO,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := engine.Evaluate(tt.intake)
|
|
|
|
if result.TrainingAllowed != tt.expectedAllowed {
|
|
t.Errorf("Expected TrainingAllowed=%s, got %s", tt.expectedAllowed, result.TrainingAllowed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DSFA Recommendation Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_DSFARecommendation(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
intake *UseCaseIntake
|
|
expectDSFA bool
|
|
expectArt22 bool
|
|
}{
|
|
{
|
|
name: "Art. 9 data → DSFA required",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainHealthcare,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
Article9Data: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectDSFA: true,
|
|
expectArt22: false,
|
|
},
|
|
{
|
|
name: "Systematic evaluation → DSFA + Art. 22",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainHR,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
EmployeeData: true,
|
|
},
|
|
Purpose: Purpose{
|
|
EvaluationScoring: true,
|
|
},
|
|
Automation: AutomationFullyAutomated,
|
|
Outputs: Outputs{
|
|
RankingsOrScores: true,
|
|
},
|
|
ModelUsage: ModelUsage{Inference: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectDSFA: true,
|
|
expectArt22: true,
|
|
},
|
|
{
|
|
name: "Public data RAG → no DSFA",
|
|
intake: &UseCaseIntake{
|
|
Domain: DomainUtilities,
|
|
DataTypes: DataTypes{
|
|
PublicData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{Region: "eu"},
|
|
},
|
|
expectDSFA: false,
|
|
expectArt22: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := engine.Evaluate(tt.intake)
|
|
|
|
if result.DSFARecommended != tt.expectDSFA {
|
|
t.Errorf("Expected DSFARecommended=%v, got %v", tt.expectDSFA, result.DSFARecommended)
|
|
}
|
|
if result.Art22Risk != tt.expectArt22 {
|
|
t.Errorf("Expected Art22Risk=%v, got %v", tt.expectArt22, result.Art22Risk)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Required Controls Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_RequiredControls(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
// Third country with PII should require SCC
|
|
intake := &UseCaseIntake{
|
|
Domain: DomainITServices,
|
|
DataTypes: DataTypes{
|
|
PersonalData: true,
|
|
},
|
|
Automation: AutomationAssistive,
|
|
ModelUsage: ModelUsage{RAG: true},
|
|
Hosting: Hosting{
|
|
Region: "third_country",
|
|
},
|
|
}
|
|
|
|
result := engine.Evaluate(intake)
|
|
|
|
controlIDs := make(map[string]bool)
|
|
for _, ctrl := range result.RequiredControls {
|
|
controlIDs[ctrl.ID] = true
|
|
}
|
|
|
|
if !controlIDs["C_SCC"] {
|
|
t.Error("Expected C_SCC control for third country transfer")
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Policy Version Tests
|
|
// ============================================================================
|
|
|
|
func TestPolicyEngine_PolicyVersion(t *testing.T) {
|
|
root := getProjectRoot(t)
|
|
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
|
|
|
engine, err := NewPolicyEngineFromPath(policyPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create policy engine: %v", err)
|
|
}
|
|
|
|
version := engine.GetPolicyVersion()
|
|
|
|
// Version should be non-empty and follow semver pattern
|
|
if version == "" {
|
|
t.Error("Policy version should not be empty")
|
|
}
|
|
|
|
// Check for basic semver pattern (x.y.z)
|
|
parts := 0
|
|
for _, c := range version {
|
|
if c == '.' {
|
|
parts++
|
|
}
|
|
}
|
|
if parts < 2 {
|
|
t.Errorf("Policy version should follow semver (x.y.z), got %s", version)
|
|
}
|
|
}
|