package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) func init() { gin.SetMode(gin.TestMode) } // ============================================================================ // Helper: create a gin test context with optional JSON body, headers, params // ============================================================================ func newTestContext(method, path string, body interface{}, headers map[string]string, params gin.Params) (*httptest.ResponseRecorder, *gin.Context) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) var reqBody *bytes.Reader if body != nil { b, _ := json.Marshal(body) reqBody = bytes.NewReader(b) } else { reqBody = bytes.NewReader(nil) } c.Request, _ = http.NewRequest(method, path, reqBody) c.Request.Header.Set("Content-Type", "application/json") for k, v := range headers { c.Request.Header.Set(k, v) } if params != nil { c.Params = params } return w, c } func parseResponse(w *httptest.ResponseRecorder) map[string]interface{} { var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) return resp } // ============================================================================ // InitFromProfile Tests // ============================================================================ func TestInitFromProfile_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects/not-a-uuid/init-from-profile", nil, nil, gin.Params{ {Key: "id", Value: "not-a-uuid"}, }) handler.InitFromProfile(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } resp := parseResponse(w) if resp["error"] == nil { t.Error("Expected error message in response") } } func TestInitFromProfile_EmptyProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects//init-from-profile", nil, nil, gin.Params{ {Key: "id", Value: ""}, }) handler.InitFromProfile(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestInitFromProfile_RequestBinding(t *testing.T) { // Verify the InitFromProfileRequest properly binds company_profile and compliance_scope body := `{ "company_profile": {"company_name": "TestCorp GmbH", "contact_email": "info@testcorp.de"}, "compliance_scope": {"machine_name": "Robot XY", "has_ai": true, "applicable_regulations": ["machinery_regulation"]} }` var parsed struct { CompanyProfile json.RawMessage `json:"company_profile"` ComplianceScope json.RawMessage `json:"compliance_scope"` } if err := json.Unmarshal([]byte(body), &parsed); err != nil { t.Fatalf("Failed to parse request body: %v", err) } if parsed.CompanyProfile == nil { t.Error("company_profile should not be nil") } if parsed.ComplianceScope == nil { t.Error("compliance_scope should not be nil") } // Verify scope parsing var scope struct { MachineName string `json:"machine_name"` HasAI bool `json:"has_ai"` ApplicableRegulations []string `json:"applicable_regulations"` } if err := json.Unmarshal(parsed.ComplianceScope, &scope); err != nil { t.Fatalf("Failed to parse compliance_scope: %v", err) } if scope.MachineName != "Robot XY" { t.Errorf("Expected machine_name 'Robot XY', got %q", scope.MachineName) } if !scope.HasAI { t.Error("Expected has_ai to be true") } if len(scope.ApplicableRegulations) != 1 || scope.ApplicableRegulations[0] != "machinery_regulation" { t.Errorf("Unexpected applicable_regulations: %v", scope.ApplicableRegulations) } // Verify profile parsing var profile struct { CompanyName string `json:"company_name"` ContactEmail string `json:"contact_email"` } if err := json.Unmarshal(parsed.CompanyProfile, &profile); err != nil { t.Fatalf("Failed to parse company_profile: %v", err) } if profile.CompanyName != "TestCorp GmbH" { t.Errorf("Expected company_name 'TestCorp GmbH', got %q", profile.CompanyName) } if profile.ContactEmail != "info@testcorp.de" { t.Errorf("Expected contact_email 'info@testcorp.de', got %q", profile.ContactEmail) } } // ============================================================================ // GenerateSingleSection Tests // ============================================================================ func TestGenerateSingleSection_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects/invalid/tech-file/risk_assessment_report/generate", nil, nil, gin.Params{ {Key: "id", Value: "invalid"}, {Key: "section", Value: "risk_assessment_report"}, }) handler.GenerateSingleSection(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } resp := parseResponse(w) errMsg, _ := resp["error"].(string) if errMsg != "invalid project ID" { t.Errorf("Expected 'invalid project ID' error, got %q", errMsg) } } func TestGenerateSingleSection_EmptySectionType_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects/00000000-0000-0000-0000-000000000001/tech-file//generate", nil, nil, gin.Params{ {Key: "id", Value: "00000000-0000-0000-0000-000000000001"}, {Key: "section", Value: ""}, }) handler.GenerateSingleSection(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } resp := parseResponse(w) errMsg, _ := resp["error"].(string) if errMsg != "section type required" { t.Errorf("Expected 'section type required' error, got %q", errMsg) } } func TestGenerateSingleSection_SectionTitleMapping(t *testing.T) { // Verify the section title map covers all 19 section types sectionTitles := map[string]string{ "general_description": "General Description of the Machinery", "risk_assessment_report": "Risk Assessment Report", "hazard_log_combined": "Combined Hazard Log", "essential_requirements": "Essential Health and Safety Requirements", "design_specifications": "Design Specifications and Drawings", "test_reports": "Test Reports and Verification Results", "standards_applied": "Applied Harmonised Standards", "declaration_of_conformity": "EU Declaration of Conformity", "component_list": "Component List", "classification_report": "Regulatory Classification Report", "mitigation_report": "Mitigation Measures Report", "verification_report": "Verification Report", "evidence_index": "Evidence Index", "instructions_for_use": "Instructions for Use", "monitoring_plan": "Post-Market Monitoring Plan", "ai_intended_purpose": "AI System Intended Purpose", "ai_model_description": "AI Model Description and Training Data", "ai_risk_management": "AI Risk Management System", "ai_human_oversight": "AI Human Oversight Measures", } if len(sectionTitles) != 19 { t.Errorf("Expected 19 section types, got %d", len(sectionTitles)) } for key, title := range sectionTitles { if key == "" { t.Error("Section key must not be empty") } if title == "" { t.Errorf("Section title for %q must not be empty", key) } } } // ============================================================================ // ExportTechFile Tests // ============================================================================ func TestExportTechFile_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("GET", "/projects/invalid/tech-file/export", nil, nil, gin.Params{ {Key: "id", Value: "invalid"}, }) handler.ExportTechFile(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestExportTechFile_FormatQueryParam(t *testing.T) { // Verify that DefaultQuery correctly parses format parameter tests := []struct { name string queryString string expectedQuery string }{ {"default json", "", "json"}, {"explicit pdf", "format=pdf", "pdf"}, {"explicit xlsx", "format=xlsx", "xlsx"}, {"explicit docx", "format=docx", "docx"}, {"explicit md", "format=md", "md"}, {"explicit json", "format=json", "json"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) url := "/projects/test/tech-file/export" if tt.queryString != "" { url += "?" + tt.queryString } c.Request, _ = http.NewRequest("GET", url, nil) result := c.DefaultQuery("format", "json") if result != tt.expectedQuery { t.Errorf("Expected format %q, got %q", tt.expectedQuery, result) } }) } } func TestExportTechFile_ContentDispositionHeaders(t *testing.T) { // Verify the header format for different export types tests := []struct { format string contentType string }{ {"pdf", "application/pdf"}, {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {"md", "text/markdown"}, } for _, tt := range tests { t.Run(tt.format, func(t *testing.T) { if tt.contentType == "" { t.Errorf("Content-Type for format %q must not be empty", tt.format) } }) } } // ============================================================================ // CheckCompleteness Tests // ============================================================================ func TestCheckCompleteness_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects/invalid/completeness-check", nil, nil, gin.Params{ {Key: "id", Value: "not-valid-uuid"}, }) handler.CheckCompleteness(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } // ============================================================================ // getTenantID Tests // ============================================================================ func TestGetTenantID_MissingHeader_ReturnsError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "/test", nil) _, err := getTenantID(c) if err == nil { t.Error("Expected error for missing X-Tenant-Id header") } } func TestGetTenantID_InvalidUUID_ReturnsError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "/test", nil) c.Request.Header.Set("X-Tenant-Id", "not-a-uuid") _, err := getTenantID(c) if err == nil { t.Error("Expected error for invalid UUID in X-Tenant-Id header") } } func TestGetTenantID_ValidUUID_ReturnsUUID(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "/test", nil) c.Request.Header.Set("X-Tenant-Id", "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e") tid, err := getTenantID(c) if err != nil { t.Fatalf("Unexpected error: %v", err) } if tid.String() != "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" { t.Errorf("Expected tenant ID '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', got %q", tid.String()) } } // ============================================================================ // CreateProject / ListProjects — Input Validation // ============================================================================ func TestCreateProject_MissingTenantID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects", map[string]string{"machine_name": "Test"}, nil, nil) handler.CreateProject(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestListProjects_MissingTenantID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("GET", "/projects", nil, nil, nil) handler.ListProjects(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } // ============================================================================ // Component Handlers — Input Validation // ============================================================================ func TestCreateComponent_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("POST", "/projects/invalid/components", map[string]string{"name": "Test"}, nil, gin.Params{ {Key: "id", Value: "invalid"}, }) handler.CreateComponent(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestListComponents_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("GET", "/projects/invalid/components", nil, nil, gin.Params{ {Key: "id", Value: "invalid"}, }) handler.ListComponents(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestUpdateComponent_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("PUT", "/projects/invalid/components/abc", map[string]string{"name": "New"}, nil, gin.Params{ {Key: "id", Value: "invalid"}, {Key: "cid", Value: "00000000-0000-0000-0000-000000000001"}, }) handler.UpdateComponent(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestUpdateComponent_InvalidComponentID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("PUT", "/projects/00000000-0000-0000-0000-000000000001/components/invalid", nil, nil, gin.Params{ {Key: "id", Value: "00000000-0000-0000-0000-000000000001"}, {Key: "cid", Value: "invalid"}, }) handler.UpdateComponent(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestDeleteComponent_InvalidProjectID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("DELETE", "/projects/invalid/components/abc", nil, nil, gin.Params{ {Key: "id", Value: "invalid"}, {Key: "cid", Value: "00000000-0000-0000-0000-000000000001"}, }) handler.DeleteComponent(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } } func TestDeleteComponent_InvalidComponentID_Returns400(t *testing.T) { handler := &IACEHandler{} w, c := newTestContext("DELETE", "/projects/00000000-0000-0000-0000-000000000001/components/invalid", nil, nil, gin.Params{ {Key: "id", Value: "00000000-0000-0000-0000-000000000001"}, {Key: "cid", Value: "invalid"}, }) handler.DeleteComponent(c) if w.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", w.Code) } }