package policy import ( "regexp" "testing" ) // ============================================================================= // MODEL TESTS // ============================================================================= func TestBundeslandValidation(t *testing.T) { tests := []struct { name string bl Bundesland expected bool }{ {"valid NI", BundeslandNI, true}, {"valid BY", BundeslandBY, true}, {"valid BW", BundeslandBW, true}, {"valid NW", BundeslandNW, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { found := false for _, valid := range ValidBundeslaender { if valid == tt.bl { found = true break } } if found != tt.expected { t.Errorf("Expected %v to be valid=%v, got valid=%v", tt.bl, tt.expected, found) } }) } } func TestLicenseValues(t *testing.T) { licenses := []License{ LicenseDLDEBY20, LicenseCCBY, LicenseCCBYSA, LicenseCC0, LicenseParagraph5, } for _, l := range licenses { if l == "" { t.Errorf("License should not be empty") } } } func TestOperationValues(t *testing.T) { if len(ValidOperations) != 4 { t.Errorf("Expected 4 operations, got %d", len(ValidOperations)) } expectedOps := []Operation{OperationLookup, OperationRAG, OperationTraining, OperationExport} for _, expected := range expectedOps { found := false for _, op := range ValidOperations { if op == expected { found = true break } } if !found { t.Errorf("Expected operation %s not found in ValidOperations", expected) } } } // ============================================================================= // PII DETECTOR TESTS // ============================================================================= func TestPIIDetector_EmailDetection(t *testing.T) { tests := []struct { name string text string hasEmail bool }{ {"simple email", "Contact: test@example.com", true}, {"email with plus", "Email: user+tag@domain.org", true}, {"no email", "This is plain text", false}, {"partial email", "user@ is not an email", false}, {"multiple emails", "Send to a@b.com and x@y.de", true}, } // Test using regex pattern directly since we don't have a store emailPattern := `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Simple test without database rule := &PIIRule{ Name: "Email", RuleType: PIIRuleTypeRegex, Pattern: emailPattern, Severity: PIISeverityBlock, } detector := &PIIDetector{ compiledRules: make(map[string]*regexp.Regexp), } matches := detector.findMatches(tt.text, rule) hasMatch := len(matches) > 0 if hasMatch != tt.hasEmail { t.Errorf("Expected hasEmail=%v, got %v for text: %s", tt.hasEmail, hasMatch, tt.text) } }) } } func TestPIIDetector_PhoneDetection(t *testing.T) { tests := []struct { name string text string hasPhone bool }{ {"german mobile", "Call +49 170 1234567", true}, {"german landline", "Tel: 030-12345678", true}, {"with spaces", "Phone: 0170 123 4567", true}, {"no phone", "This is just text", false}, {"US format", "Call 555-123-4567", false}, // Should not match German pattern } phonePattern := `(?:\+49|0)[\s.-]?\d{2,4}[\s.-]?\d{3,}[\s.-]?\d{2,}` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rule := &PIIRule{ Name: "Phone", RuleType: PIIRuleTypeRegex, Pattern: phonePattern, Severity: PIISeverityBlock, } detector := &PIIDetector{ compiledRules: make(map[string]*regexp.Regexp), } matches := detector.findMatches(tt.text, rule) hasMatch := len(matches) > 0 if hasMatch != tt.hasPhone { t.Errorf("Expected hasPhone=%v, got %v for text: %s", tt.hasPhone, hasMatch, tt.text) } }) } } func TestPIIDetector_IBANDetection(t *testing.T) { tests := []struct { name string text string hasIBAN bool }{ {"valid IBAN", "IBAN: DE89 3704 0044 0532 0130 00", true}, {"compact IBAN", "DE89370400440532013000", true}, {"no IBAN", "Just a number: 12345678", false}, {"partial", "DE12 is not complete", false}, } ibanPattern := `DE\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rule := &PIIRule{ Name: "IBAN", RuleType: PIIRuleTypeRegex, Pattern: ibanPattern, Severity: PIISeverityBlock, } detector := &PIIDetector{ compiledRules: make(map[string]*regexp.Regexp), } matches := detector.findMatches(tt.text, rule) hasMatch := len(matches) > 0 if hasMatch != tt.hasIBAN { t.Errorf("Expected hasIBAN=%v, got %v for text: %s", tt.hasIBAN, hasMatch, tt.text) } }) } } func TestPIIDetector_KeywordMatching(t *testing.T) { tests := []struct { name string text string keywords string expected int }{ {"single keyword", "The password is secret", "password", 1}, {"multiple keywords", "Password and secret", "password,secret", 2}, {"case insensitive", "PASSWORD and Secret", "password,secret", 2}, {"no match", "This is safe text", "password,secret", 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rule := &PIIRule{ Name: "Keywords", RuleType: PIIRuleTypeKeyword, Pattern: tt.keywords, Severity: PIISeverityWarn, } detector := &PIIDetector{ compiledRules: make(map[string]*regexp.Regexp), } matches := detector.findKeywordMatches(tt.text, rule) if len(matches) != tt.expected { t.Errorf("Expected %d matches, got %d for text: %s", tt.expected, len(matches), tt.text) } }) } } func TestPIIDetector_Redaction(t *testing.T) { detector := &PIIDetector{ compiledRules: make(map[string]*regexp.Regexp), } tests := []struct { name string text string matches []PIIMatch expected string }{ { "single redaction", "Email: test@example.com", []PIIMatch{{StartIndex: 7, EndIndex: 23, Severity: PIISeverityBlock}}, "Email: ****************", }, { "no matches", "Plain text", []PIIMatch{}, "Plain text", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := detector.RedactText(tt.text, tt.matches) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } func TestCompareSeverity(t *testing.T) { tests := []struct { a, b PIISeverity expected int }{ {PIISeverityBlock, PIISeverityWarn, 1}, {PIISeverityWarn, PIISeverityBlock, -1}, {PIISeverityBlock, PIISeverityBlock, 0}, {PIISeverityRedact, PIISeverityWarn, 1}, {PIISeverityRedact, PIISeverityBlock, -1}, } for _, tt := range tests { t.Run(string(tt.a)+"_vs_"+string(tt.b), func(t *testing.T) { result := compareSeverity(tt.a, tt.b) if result != tt.expected { t.Errorf("Expected %d, got %d for %s vs %s", tt.expected, result, tt.a, tt.b) } }) } } // ============================================================================= // ENFORCER TESTS // ============================================================================= func TestExtractDomain(t *testing.T) { tests := []struct { name string url string expected string hasError bool }{ {"full URL", "https://www.example.com/path", "example.com", false}, {"with port", "http://example.com:8080/path", "example.com", false}, {"subdomain", "https://sub.domain.example.com", "sub.domain.example.com", false}, {"no scheme", "example.com/path", "example.com", false}, {"www prefix", "https://www.test.de", "test.de", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := extractDomain(tt.url) if tt.hasError && err == nil { t.Error("Expected error, got nil") } if !tt.hasError && err != nil { t.Errorf("Expected no error, got %v", err) } if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } // ============================================================================= // YAML LOADER TESTS // ============================================================================= func TestParseYAML(t *testing.T) { yamlData := ` federal: name: "Test Federal" sources: - domain: "test.gov" name: "Test Source" license: "ยง5 UrhG" trust_boost: 0.9 NI: name: "Niedersachsen" sources: - domain: "ni.gov" name: "NI Source" license: "DL-DE-BY-2.0" default_operations: lookup: allowed: true requires_citation: true training: allowed: false requires_citation: false pii_rules: - name: "Test Rule" type: "regex" pattern: "test.*pattern" severity: "block" ` config, err := ParseYAML([]byte(yamlData)) if err != nil { t.Fatalf("Failed to parse YAML: %v", err) } // Test federal if config.Federal.Name != "Test Federal" { t.Errorf("Expected federal name 'Test Federal', got '%s'", config.Federal.Name) } if len(config.Federal.Sources) != 1 { t.Errorf("Expected 1 federal source, got %d", len(config.Federal.Sources)) } if config.Federal.Sources[0].Domain != "test.gov" { t.Errorf("Expected domain 'test.gov', got '%s'", config.Federal.Sources[0].Domain) } if config.Federal.Sources[0].TrustBoost != 0.9 { t.Errorf("Expected trust_boost 0.9, got %f", config.Federal.Sources[0].TrustBoost) } // Test Bundesland if len(config.Bundeslaender) != 1 { t.Errorf("Expected 1 Bundesland, got %d", len(config.Bundeslaender)) } ni, ok := config.Bundeslaender["NI"] if !ok { t.Error("Expected NI in Bundeslaender") } if ni.Name != "Niedersachsen" { t.Errorf("Expected name 'Niedersachsen', got '%s'", ni.Name) } // Test operations if !config.DefaultOperations.Lookup.Allowed { t.Error("Expected lookup to be allowed") } if config.DefaultOperations.Training.Allowed { t.Error("Expected training to be NOT allowed") } // Test PII rules if len(config.PIIRules) != 1 { t.Errorf("Expected 1 PII rule, got %d", len(config.PIIRules)) } if config.PIIRules[0].Name != "Test Rule" { t.Errorf("Expected rule name 'Test Rule', got '%s'", config.PIIRules[0].Name) } } // ============================================================================= // AUDIT TESTS // ============================================================================= func TestMaskPII(t *testing.T) { tests := []struct { name string input string expected string }{ {"short", "ab", "****"}, {"medium", "test@email.com", "te****om"}, {"long", "very-long-email@example.com", "ve****om"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := maskPII(tt.input) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } // ============================================================================= // DEFAULT PII RULES TEST // ============================================================================= func TestDefaultPIIRules(t *testing.T) { rules := DefaultPIIRules() if len(rules) == 0 { t.Error("Expected default PII rules, got none") } // Check that each rule has required fields for _, rule := range rules { if rule.Name == "" { t.Error("Rule name should not be empty") } if rule.Type == "" { t.Error("Rule type should not be empty") } if rule.Pattern == "" { t.Error("Rule pattern should not be empty") } } // Check for email rule hasEmailRule := false for _, rule := range rules { if rule.Name == "Email Addresses" { hasEmailRule = true break } } if !hasEmailRule { t.Error("Expected email addresses rule in defaults") } } // ============================================================================= // INTEGRATION TEST HELPERS // ============================================================================= // TestFilteredURL tests the FilteredURL struct. func TestFilteredURL(t *testing.T) { fu := FilteredURL{ URL: "https://example.com", IsAllowed: true, RequiresCitation: true, } if fu.URL != "https://example.com" { t.Error("URL not set correctly") } if !fu.IsAllowed { t.Error("IsAllowed should be true") } if !fu.RequiresCitation { t.Error("RequiresCitation should be true") } }