package iace import ( "bytes" "encoding/json" "strings" "testing" "github.com/google/uuid" ) // createTestExportData builds a complete set of test data for document export tests. func createTestExportData() (*Project, []TechFileSection, []Hazard, []RiskAssessment, []Mitigation, []RegulatoryClassification) { projectID := uuid.New() hazardID1 := uuid.New() hazardID2 := uuid.New() project := &Project{ ID: projectID, MachineName: "Robot Arm XY-200", MachineType: "industrial_robot", Manufacturer: "TestCorp GmbH", Description: "6-Achsen Industrieroboter fuer Schweissarbeiten", CEMarkingTarget: "2023/1230", } sections := []TechFileSection{ {ID: uuid.New(), ProjectID: projectID, SectionType: "risk_assessment_report", Title: "Risikobeurteilung", Content: "Dies ist der Risikobeurteilungsbericht...", Status: TechFileSectionStatusApproved}, {ID: uuid.New(), ProjectID: projectID, SectionType: "hazard_log_combined", Title: "Gefaehrdungsprotokoll", Content: "Protokoll aller identifizierten Gefaehrdungen...", Status: TechFileSectionStatusGenerated}, {ID: uuid.New(), ProjectID: projectID, SectionType: "declaration_of_conformity", Title: "EU-Konformitaetserklaerung", Content: "Hiermit erklaeren wir...", Status: TechFileSectionStatusDraft}, } hazards := []Hazard{ {ID: hazardID1, ProjectID: projectID, Name: "Quetschgefahr", Category: "mechanical", Description: "Quetschgefahr durch Roboterarm"}, {ID: hazardID2, ProjectID: projectID, Name: "Elektrischer Schlag", Category: "electrical", Description: "Gefahr durch freiliegende Kontakte"}, } assessments := []RiskAssessment{ {ID: uuid.New(), HazardID: hazardID1, Severity: 4, Exposure: 3, Probability: 3, InherentRisk: 36, CEff: 0.7, ResidualRisk: 10.8, RiskLevel: RiskLevelHigh, IsAcceptable: false}, {ID: uuid.New(), HazardID: hazardID2, Severity: 2, Exposure: 2, Probability: 2, InherentRisk: 8, CEff: 0.8, ResidualRisk: 1.6, RiskLevel: RiskLevelLow, IsAcceptable: true}, } mitigations := []Mitigation{ {ID: uuid.New(), HazardID: hazardID1, ReductionType: ReductionTypeDesign, Name: "Schutzabdeckung", Status: MitigationStatusVerified}, {ID: uuid.New(), HazardID: hazardID1, ReductionType: ReductionTypeProtective, Name: "Lichtschranke", Status: MitigationStatusVerified}, {ID: uuid.New(), HazardID: hazardID2, ReductionType: ReductionTypeInformation, Name: "Warnhinweis", Status: MitigationStatusPlanned}, } classifications := []RegulatoryClassification{ {Regulation: RegulationMachineryRegulation, ClassificationResult: "Annex I", RiskLevel: RiskLevelHigh}, {Regulation: RegulationCRA, ClassificationResult: "Important", RiskLevel: RiskLevelMedium}, } return project, sections, hazards, assessments, mitigations, classifications } // createEmptyExportData builds minimal test data with no hazards, sections, or mitigations. func createEmptyExportData() (*Project, []TechFileSection, []Hazard, []RiskAssessment, []Mitigation, []RegulatoryClassification) { project := &Project{ ID: uuid.New(), MachineName: "Empty Machine", MachineType: "test", Manufacturer: "TestCorp", Description: "", CEMarkingTarget: "", } return project, nil, nil, nil, nil, nil } // ============================================================================ // PDF Export Tests // ============================================================================ func TestExportPDF_ValidOutput(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, classifications := createTestExportData() data, err := exporter.ExportPDF(project, sections, hazards, assessments, mitigations, classifications) if err != nil { t.Fatalf("ExportPDF returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportPDF returned empty bytes") } // Valid PDF files start with %PDF- if !bytes.HasPrefix(data, []byte("%PDF-")) { t.Errorf("ExportPDF output does not start with %%PDF-, got first 10 bytes: %q", data[:min(10, len(data))]) } } func TestExportPDF_EmptyProject(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, classifications := createEmptyExportData() data, err := exporter.ExportPDF(project, sections, hazards, assessments, mitigations, classifications) if err != nil { t.Fatalf("ExportPDF with empty project returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportPDF with empty project returned empty bytes") } // Should still produce a valid PDF even with no content if !bytes.HasPrefix(data, []byte("%PDF-")) { t.Errorf("ExportPDF output does not start with %%PDF-, got first 10 bytes: %q", data[:min(10, len(data))]) } } // ============================================================================ // Excel Export Tests // ============================================================================ func TestExportExcel_ValidOutput(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, _ := createTestExportData() data, err := exporter.ExportExcel(project, sections, hazards, assessments, mitigations) if err != nil { t.Fatalf("ExportExcel returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportExcel returned empty bytes") } // xlsx is a zip archive, which starts with PK (0x50, 0x4b) if !bytes.HasPrefix(data, []byte("PK")) { t.Errorf("ExportExcel output does not start with PK (zip signature), got first 4 bytes: %x", data[:min(4, len(data))]) } } func TestExportExcel_EmptyProject(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, _ := createEmptyExportData() data, err := exporter.ExportExcel(project, sections, hazards, assessments, mitigations) if err != nil { t.Fatalf("ExportExcel with empty project returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportExcel with empty project returned empty bytes") } // Should still produce a valid xlsx (zip) even with no data if !bytes.HasPrefix(data, []byte("PK")) { t.Errorf("ExportExcel output does not start with PK (zip signature), got first 4 bytes: %x", data[:min(4, len(data))]) } } // ============================================================================ // Markdown Export Tests // ============================================================================ func TestExportMarkdown_ContainsSections(t *testing.T) { exporter := NewDocumentExporter() project, sections, _, _, _, _ := createTestExportData() data, err := exporter.ExportMarkdown(project, sections) if err != nil { t.Fatalf("ExportMarkdown returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportMarkdown returned empty bytes") } content := string(data) // Should contain the project name if !strings.Contains(content, project.MachineName) { t.Errorf("ExportMarkdown output does not contain project name %q", project.MachineName) } // Should contain each section title for _, section := range sections { if !strings.Contains(content, section.Title) { t.Errorf("ExportMarkdown output does not contain section title %q", section.Title) } } // Should contain markdown header syntax if !strings.Contains(content, "#") { t.Error("ExportMarkdown output does not contain any markdown headers") } } func TestExportMarkdown_EmptyProject(t *testing.T) { exporter := NewDocumentExporter() project, _, _, _, _, _ := createEmptyExportData() data, err := exporter.ExportMarkdown(project, nil) if err != nil { t.Fatalf("ExportMarkdown with empty project returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportMarkdown with empty project returned empty bytes") } content := string(data) // Should still contain the project name as a header even without sections if !strings.Contains(content, project.MachineName) { t.Errorf("ExportMarkdown output does not contain project name %q for empty project", project.MachineName) } } // ============================================================================ // DOCX Export Tests // ============================================================================ func TestExportDOCX_ValidOutput(t *testing.T) { exporter := NewDocumentExporter() project, sections, _, _, _, _ := createTestExportData() data, err := exporter.ExportDOCX(project, sections) if err != nil { t.Fatalf("ExportDOCX returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportDOCX returned empty bytes") } // docx is a zip archive, which starts with PK (0x50, 0x4b) if !bytes.HasPrefix(data, []byte("PK")) { t.Errorf("ExportDOCX output does not start with PK (zip signature), got first 4 bytes: %x", data[:min(4, len(data))]) } } func TestExportDOCX_EmptyProject(t *testing.T) { exporter := NewDocumentExporter() project, _, _, _, _, _ := createEmptyExportData() data, err := exporter.ExportDOCX(project, nil) if err != nil { t.Fatalf("ExportDOCX with empty project returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportDOCX with empty project returned empty bytes") } // Should still produce a valid docx (zip) even with no sections if !bytes.HasPrefix(data, []byte("PK")) { t.Errorf("ExportDOCX output does not start with PK (zip signature), got first 4 bytes: %x", data[:min(4, len(data))]) } } // ============================================================================ // Helper Function Tests // ============================================================================ // ============================================================================ // JSON Export Tests // ============================================================================ func TestExportJSON_ValidOutput(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, classifications := createTestExportData() data, err := exporter.ExportJSON(project, sections, hazards, assessments, mitigations, classifications) if err != nil { t.Fatalf("ExportJSON returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportJSON returned empty bytes") } // Must be valid JSON var parsed map[string]interface{} if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("ExportJSON output is not valid JSON: %v", err) } // Check required top-level keys requiredKeys := []string{"project", "sections", "hazards", "assessments", "mitigations", "classifications", "exported_at", "format_version"} for _, key := range requiredKeys { if _, ok := parsed[key]; !ok { t.Errorf("ExportJSON output missing key %q", key) } } // format_version should be "1.0" if fv, _ := parsed["format_version"].(string); fv != "1.0" { t.Errorf("Expected format_version '1.0', got %q", fv) } } func TestExportJSON_EmptyProject(t *testing.T) { exporter := NewDocumentExporter() project, sections, hazards, assessments, mitigations, classifications := createEmptyExportData() data, err := exporter.ExportJSON(project, sections, hazards, assessments, mitigations, classifications) if err != nil { t.Fatalf("ExportJSON with empty project returned error: %v", err) } var parsed map[string]interface{} if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("ExportJSON output is not valid JSON: %v", err) } // Project should still be present if parsed["project"] == nil { t.Error("ExportJSON should include project even for empty project") } } func TestExportJSON_NilProject_ReturnsError(t *testing.T) { exporter := NewDocumentExporter() _, err := exporter.ExportJSON(nil, nil, nil, nil, nil, nil) if err == nil { t.Error("ExportJSON should return error for nil project") } } // ============================================================================ // Special Character Tests // ============================================================================ func TestExportMarkdown_GermanUmlauts(t *testing.T) { exporter := NewDocumentExporter() project := &Project{ ID: uuid.New(), MachineName: "Pruefgeraet fuer Sicherheitsueberwachung", MachineType: "Pruefstand", Manufacturer: "Mueller & Soehne GmbH", } sections := []TechFileSection{ {SectionType: "general_description", Title: "Allgemeine Beschreibung", Content: "Aenderungen und Ergaenzungen"}, } data, err := exporter.ExportMarkdown(project, sections) if err != nil { t.Fatalf("ExportMarkdown with German text returned error: %v", err) } content := string(data) if !strings.Contains(content, "Pruefgeraet") { t.Error("Markdown should preserve German text") } if !strings.Contains(content, "Mueller") { t.Error("Markdown should preserve manufacturer name with special chars") } } func TestExportDOCX_SpecialCharacters(t *testing.T) { exporter := NewDocumentExporter() project := &Project{ ID: uuid.New(), MachineName: "Test & \"Quotes\"", MachineType: "test", Manufacturer: "Corp <&>", } sections := []TechFileSection{ {SectionType: "general_description", Title: "Title with & \"quotes\"", Content: "Content with & chars"}, } data, err := exporter.ExportDOCX(project, sections) if err != nil { t.Fatalf("ExportDOCX with special characters returned error: %v", err) } if len(data) == 0 { t.Fatal("ExportDOCX with special characters returned empty bytes") } // Should still produce a valid zip if !bytes.HasPrefix(data, []byte("PK")) { t.Error("ExportDOCX output should still be valid zip even with special characters") } } func TestExportPDF_GermanText(t *testing.T) { exporter := NewDocumentExporter() project := &Project{ ID: uuid.New(), MachineName: "Sicherheits-Pruefstand SP-400", Manufacturer: "Deutsche Prueftechnik AG", } sections := []TechFileSection{ {SectionType: "general_description", Title: "Beschreibung", Content: "Technische Dokumentation fuer den Sicherheits-Pruefstand"}, } data, err := exporter.ExportPDF(project, sections, nil, nil, nil, nil) if err != nil { t.Fatalf("ExportPDF with German text returned error: %v", err) } if !bytes.HasPrefix(data, []byte("%PDF-")) { t.Error("ExportPDF should produce valid PDF with German text") } } // ============================================================================ // Helper Function Tests // ============================================================================ func TestRiskLevelLabel_AllLevels(t *testing.T) { levels := []RiskLevel{ RiskLevelCritical, RiskLevelHigh, RiskLevelMedium, RiskLevelLow, RiskLevelNegligible, RiskLevelNotAcceptable, RiskLevelVeryHigh, } for _, level := range levels { t.Run(string(level), func(t *testing.T) { label := riskLevelLabel(level) if label == "" { t.Errorf("riskLevelLabel(%q) returned empty string", level) } }) } } func TestRiskLevelColor_AllLevels(t *testing.T) { levels := []RiskLevel{ RiskLevelCritical, RiskLevelHigh, RiskLevelMedium, RiskLevelLow, RiskLevelNegligible, RiskLevelNotAcceptable, RiskLevelVeryHigh, } for _, level := range levels { t.Run(string(level), func(t *testing.T) { r, g, b := riskLevelColor(level) // RGB values must be in valid range 0-255 if r < 0 || r > 255 { t.Errorf("riskLevelColor(%q) red value %d out of range [0,255]", level, r) } if g < 0 || g > 255 { t.Errorf("riskLevelColor(%q) green value %d out of range [0,255]", level, g) } if b < 0 || b > 255 { t.Errorf("riskLevelColor(%q) blue value %d out of range [0,255]", level, b) } }) } }