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>
627 lines
16 KiB
Go
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")
|
|
}
|
|
}
|