package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) func init() { gin.SetMode(gin.TestMode) } // getProjectRoot returns the project root directory func getProjectRoot(t *testing.T) string { 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 } } // mockTenantContext sets up a gin context with tenant ID func mockTenantContext(c *gin.Context, tenantID, userID uuid.UUID) { c.Set("tenant_id", tenantID) c.Set("user_id", userID) } // ============================================================================ // Policy Engine Integration Tests (No DB) // ============================================================================ func TestUCCAHandlers_ListPatterns(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := ucca.NewPolicyEngineFromPath(policyPath) if err != nil { t.Skipf("Skipping test - could not load policy engine: %v", err) } handler := &UCCAHandlers{ policyEngine: engine, } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListPatterns(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } patterns, ok := response["patterns"].([]interface{}) if !ok { t.Fatal("Expected patterns array in response") } if len(patterns) == 0 { t.Error("Expected at least some patterns") } } func TestUCCAHandlers_ListControls(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := ucca.NewPolicyEngineFromPath(policyPath) if err != nil { t.Skipf("Skipping test - could not load policy engine: %v", err) } handler := &UCCAHandlers{ policyEngine: engine, } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListControls(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } controls, ok := response["controls"].([]interface{}) if !ok { t.Fatal("Expected controls array in response") } if len(controls) == 0 { t.Error("Expected at least some controls") } } func TestUCCAHandlers_ListRules(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := ucca.NewPolicyEngineFromPath(policyPath) if err != nil { t.Skipf("Skipping test - could not load policy engine: %v", err) } handler := &UCCAHandlers{ policyEngine: engine, } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListRules(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } rules, ok := response["rules"].([]interface{}) if !ok { t.Fatal("Expected rules array in response") } if len(rules) == 0 { t.Error("Expected at least some rules") } // Check that policy version is returned if _, ok := response["policy_version"]; !ok { t.Error("Expected policy_version in response") } } func TestUCCAHandlers_ListExamples(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListExamples(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } examples, ok := response["examples"].([]interface{}) if !ok { t.Fatal("Expected examples array in response") } if len(examples) == 0 { t.Error("Expected at least some examples") } } func TestUCCAHandlers_ListProblemSolutions_WithEngine(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := ucca.NewPolicyEngineFromPath(policyPath) if err != nil { t.Skipf("Skipping test - could not load policy engine: %v", err) } handler := &UCCAHandlers{ policyEngine: engine, } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListProblemSolutions(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } if _, ok := response["problem_solutions"]; !ok { t.Error("Expected problem_solutions in response") } } func TestUCCAHandlers_ListProblemSolutions_WithoutEngine(t *testing.T) { handler := &UCCAHandlers{ policyEngine: nil, } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListProblemSolutions(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } if _, ok := response["message"]; !ok { t.Error("Expected message when policy engine not available") } } // ============================================================================ // Request Validation Tests // ============================================================================ func TestUCCAHandlers_Assess_MissingTenantID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/assess", nil) // Don't set tenant ID handler.Assess(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } } func TestUCCAHandlers_Assess_InvalidJSON(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, _ := ucca.NewPolicyEngineFromPath(policyPath) handler := &UCCAHandlers{ policyEngine: engine, legacyRuleEngine: ucca.NewRuleEngine(), } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/assess", bytes.NewBufferString("invalid json")) c.Request.Header.Set("Content-Type", "application/json") c.Set("tenant_id", uuid.New()) c.Set("user_id", uuid.New()) handler.Assess(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid JSON, got %d", w.Code) } } func TestUCCAHandlers_GetAssessment_InvalidID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "not-a-uuid"}} c.Request = httptest.NewRequest("GET", "/assessments/not-a-uuid", nil) handler.GetAssessment(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid ID, got %d", w.Code) } } func TestUCCAHandlers_DeleteAssessment_InvalidID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "invalid"}} c.Request = httptest.NewRequest("DELETE", "/assessments/invalid", nil) handler.DeleteAssessment(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid ID, got %d", w.Code) } } func TestUCCAHandlers_Export_InvalidID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "not-valid"}} c.Request = httptest.NewRequest("GET", "/export/not-valid", nil) handler.Export(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid ID, got %d", w.Code) } } func TestUCCAHandlers_Explain_InvalidID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "bad-id"}} c.Request = httptest.NewRequest("POST", "/assessments/bad-id/explain", nil) handler.Explain(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid ID, got %d", w.Code) } } func TestUCCAHandlers_ListAssessments_MissingTenantID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/assessments", nil) handler.ListAssessments(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } } func TestUCCAHandlers_GetStats_MissingTenantID(t *testing.T) { handler := &UCCAHandlers{} w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/stats", nil) handler.GetStats(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } } // ============================================================================ // Markdown Export Generation Tests // ============================================================================ func TestGenerateMarkdownExport(t *testing.T) { assessment := &ucca.Assessment{ ID: uuid.New(), Title: "Test Assessment", Domain: ucca.DomainEducation, Feasibility: ucca.FeasibilityCONDITIONAL, RiskLevel: ucca.RiskLevelMEDIUM, RiskScore: 45, Complexity: ucca.ComplexityMEDIUM, TriggeredRules: []ucca.TriggeredRule{ {Code: "R-A001", Title: "Test Rule", Severity: "WARN", ScoreDelta: 10}, }, RequiredControls: []ucca.RequiredControl{ {ID: "C-001", Title: "Test Control", Description: "Test Description"}, }, DSFARecommended: true, Art22Risk: false, TrainingAllowed: ucca.TrainingCONDITIONAL, PolicyVersion: "1.0.0", } markdown := generateMarkdownExport(assessment) // Check for expected content if markdown == "" { t.Error("Expected non-empty markdown") } expectedContents := []string{ "# UCCA Use-Case Assessment", "CONDITIONAL", "MEDIUM", "45/100", "Test Rule", "Test Control", "DSFA", "1.0.0", } for _, expected := range expectedContents { if !bytes.Contains([]byte(markdown), []byte(expected)) { t.Errorf("Expected markdown to contain '%s'", expected) } } } func TestGenerateMarkdownExport_WithExplanation(t *testing.T) { explanation := "Dies ist eine KI-generierte Erklärung." assessment := &ucca.Assessment{ ID: uuid.New(), Feasibility: ucca.FeasibilityYES, RiskLevel: ucca.RiskLevelMINIMAL, RiskScore: 10, ExplanationText: &explanation, PolicyVersion: "1.0.0", } markdown := generateMarkdownExport(assessment) if !bytes.Contains([]byte(markdown), []byte("KI-Erklärung")) { t.Error("Expected markdown to contain explanation section") } if !bytes.Contains([]byte(markdown), []byte(explanation)) { t.Error("Expected markdown to contain the explanation text") } } func TestGenerateMarkdownExport_WithForbiddenPatterns(t *testing.T) { assessment := &ucca.Assessment{ ID: uuid.New(), Feasibility: ucca.FeasibilityNO, RiskLevel: ucca.RiskLevelHIGH, RiskScore: 85, ForbiddenPatterns: []ucca.ForbiddenPattern{ {PatternID: "FP-001", Title: "Forbidden Pattern", Reason: "Not allowed"}, }, PolicyVersion: "1.0.0", } markdown := generateMarkdownExport(assessment) if !bytes.Contains([]byte(markdown), []byte("Verbotene Patterns")) { t.Error("Expected markdown to contain forbidden patterns section") } if !bytes.Contains([]byte(markdown), []byte("Not allowed")) { t.Error("Expected markdown to contain forbidden pattern reason") } } // ============================================================================ // Explanation Prompt Building Tests // ============================================================================ func TestBuildExplanationPrompt(t *testing.T) { assessment := &ucca.Assessment{ Feasibility: ucca.FeasibilityCONDITIONAL, RiskLevel: ucca.RiskLevelMEDIUM, RiskScore: 50, Complexity: ucca.ComplexityMEDIUM, TriggeredRules: []ucca.TriggeredRule{ {Code: "R-001", Title: "Test", Severity: "WARN"}, }, RequiredControls: []ucca.RequiredControl{ {Title: "Control", Description: "Desc"}, }, DSFARecommended: true, Art22Risk: true, } prompt := buildExplanationPrompt(assessment, "de", "") // Check prompt contains expected elements expectedElements := []string{ "CONDITIONAL", "MEDIUM", "50/100", "Ausgelöste Regeln", "Erforderliche Maßnahmen", "DSFA", "Art. 22", } for _, expected := range expectedElements { if !bytes.Contains([]byte(prompt), []byte(expected)) { t.Errorf("Expected prompt to contain '%s'", expected) } } } func TestBuildExplanationPrompt_WithLegalContext(t *testing.T) { assessment := &ucca.Assessment{ Feasibility: ucca.FeasibilityYES, RiskLevel: ucca.RiskLevelLOW, RiskScore: 15, Complexity: ucca.ComplexityLOW, } legalContext := "**Relevante Rechtsgrundlagen:**\nArt. 6 DSGVO - Rechtmäßigkeit" prompt := buildExplanationPrompt(assessment, "de", legalContext) if !bytes.Contains([]byte(prompt), []byte("Relevante Rechtsgrundlagen")) { t.Error("Expected prompt to contain legal context") } } // ============================================================================ // Legacy Rule Engine Fallback Tests // ============================================================================ func TestUCCAHandlers_ListRules_LegacyFallback(t *testing.T) { handler := &UCCAHandlers{ policyEngine: nil, // No YAML engine legacyRuleEngine: ucca.NewRuleEngine(), } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListRules(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } // Should have legacy policy version policyVersion, ok := response["policy_version"].(string) if !ok { t.Fatal("Expected policy_version string") } if policyVersion != "1.0.0-legacy" { t.Errorf("Expected legacy policy version, got %s", policyVersion) } } func TestUCCAHandlers_ListPatterns_LegacyFallback(t *testing.T) { handler := &UCCAHandlers{ policyEngine: nil, // No YAML engine } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListPatterns(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } patterns, ok := response["patterns"].([]interface{}) if !ok { t.Fatal("Expected patterns array in response") } // Legacy patterns should still be returned if len(patterns) == 0 { t.Error("Expected at least some legacy patterns") } } func TestUCCAHandlers_ListControls_LegacyFallback(t *testing.T) { handler := &UCCAHandlers{ policyEngine: nil, // No YAML engine } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handler.ListControls(c) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } controls, ok := response["controls"].([]interface{}) if !ok { t.Fatal("Expected controls array in response") } // Legacy controls should still be returned if len(controls) == 0 { t.Error("Expected at least some legacy controls") } }