Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/ucca_handlers_test.go
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
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>
2026-02-11 23:47:28 +01:00

627 lines
16 KiB
Go

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")
}
}