Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/document_export_test.go
Benjamin Admin 6d2de9b897
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
feat(iace): complete CE risk assessment — LLM tech-file generation, multi-format export, TipTap editor
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check)
Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment
Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON)
Phase 4: Company profile → IACE data flow with auto component/classification creation
Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections
Phase 6: User journey tests, developer portal API reference, updated documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:50:53 +01:00

306 lines
10 KiB
Go

package iace
import (
"bytes"
"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
// ============================================================================
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)
}
})
}
}