package iace import ( "testing" "github.com/google/uuid" ) func TestGetBuiltinHazardLibrary_EntryCount(t *testing.T) { entries := GetBuiltinHazardLibrary() if len(entries) < 150 { t.Fatalf("GetBuiltinHazardLibrary returned %d entries, want at least 150", len(entries)) } } func TestGetBuiltinHazardLibrary_AllBuiltinAndNoTenant(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if !e.IsBuiltin { t.Errorf("entries[%d] (%s): IsBuiltin = false, want true", i, e.Name) } if e.TenantID != nil { t.Errorf("entries[%d] (%s): TenantID = %v, want nil", i, e.Name, e.TenantID) } } } func TestGetBuiltinHazardLibrary_UniqueNonZeroUUIDs(t *testing.T) { entries := GetBuiltinHazardLibrary() seen := make(map[uuid.UUID]string) for i, e := range entries { if e.ID == uuid.Nil { t.Errorf("entries[%d] (%s): ID is zero UUID", i, e.Name) } if prev, exists := seen[e.ID]; exists { t.Errorf("entries[%d] (%s): duplicate UUID %s, same as %q", i, e.Name, e.ID, prev) } seen[e.ID] = e.Name } } func TestGetBuiltinHazardLibrary_AllCategoriesPresent(t *testing.T) { entries := GetBuiltinHazardLibrary() // All expected categories (original AI/SW + extended ISO 12100 physical) expectedCategories := map[string]bool{ // Original AI/cyber categories "false_classification": false, "timing_error": false, "data_poisoning": false, "model_drift": false, "sensor_spoofing": false, "communication_failure": false, "unauthorized_access": false, "firmware_corruption": false, "safety_boundary_violation": false, "mode_confusion": false, "unintended_bias": false, "update_failure": false, // Extended SW/HW categories "software_fault": false, "hmi_error": false, "mechanical_hazard": false, "electrical_hazard": false, "thermal_hazard": false, "emc_hazard": false, "configuration_error": false, "safety_function_failure": false, "logging_audit_failure": false, "integration_error": false, "environmental_hazard": false, "maintenance_hazard": false, // ISO 12100 physical categories "pneumatic_hydraulic": false, "noise_vibration": false, "ergonomic": false, "material_environmental": false, } for _, e := range entries { if _, ok := expectedCategories[e.Category]; !ok { t.Errorf("unexpected category %q in entry %q", e.Category, e.Name) } expectedCategories[e.Category] = true } for cat, found := range expectedCategories { if !found { t.Errorf("expected category %q not found in any entry", cat) } } } func TestGetBuiltinHazardLibrary_CategoryCounts(t *testing.T) { entries := GetBuiltinHazardLibrary() actualCounts := make(map[string]int) for _, e := range entries { actualCounts[e.Category]++ } // Each category must have at least 1 entry for cat, count := range actualCounts { if count < 1 { t.Errorf("category %q: count = %d, want >= 1", cat, count) } } // ISO 12100 physical categories must have substantial coverage minCounts := map[string]int{ "mechanical_hazard": 10, "electrical_hazard": 6, "thermal_hazard": 4, "pneumatic_hydraulic": 6, "noise_vibration": 4, } for cat, minCount := range minCounts { if actualCounts[cat] < minCount { t.Errorf("category %q: count = %d, want >= %d", cat, actualCounts[cat], minCount) } } } func TestGetBuiltinHazardLibrary_SeverityRange(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.DefaultSeverity < 1 || e.DefaultSeverity > 5 { t.Errorf("entries[%d] (%s): DefaultSeverity = %d, want 1-5", i, e.Name, e.DefaultSeverity) } } } func TestGetBuiltinHazardLibrary_ProbabilityRange(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.DefaultProbability < 1 || e.DefaultProbability > 5 { t.Errorf("entries[%d] (%s): DefaultProbability = %d, want 1-5", i, e.Name, e.DefaultProbability) } } } func TestGetBuiltinHazardLibrary_ExposureAndAvoidanceRange(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.DefaultExposure < 0 || e.DefaultExposure > 5 { t.Errorf("entries[%d] (%s): DefaultExposure = %d, want 0-5", i, e.Name, e.DefaultExposure) } if e.DefaultAvoidance < 0 || e.DefaultAvoidance > 5 { t.Errorf("entries[%d] (%s): DefaultAvoidance = %d, want 0-5", i, e.Name, e.DefaultAvoidance) } } } func TestGetBuiltinHazardLibrary_NonEmptyFields(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.Name == "" { t.Errorf("entries[%d]: Name is empty", i) } if e.Category == "" { t.Errorf("entries[%d] (%s): Category is empty", i, e.Name) } if len(e.ApplicableComponentTypes) == 0 { t.Errorf("entries[%d] (%s): ApplicableComponentTypes is empty", i, e.Name) } } } func TestHazardUUID_Deterministic(t *testing.T) { tests := []struct { category string index int }{ {"false_classification", 1}, {"timing_error", 2}, {"data_poisoning", 1}, {"mechanical_hazard", 7}, {"pneumatic_hydraulic", 1}, } for _, tt := range tests { t.Run(tt.category, func(t *testing.T) { id1 := hazardUUID(tt.category, tt.index) id2 := hazardUUID(tt.category, tt.index) if id1 != id2 { t.Errorf("hazardUUID(%q, %d) not deterministic: %s != %s", tt.category, tt.index, id1, id2) } if id1 == uuid.Nil { t.Errorf("hazardUUID(%q, %d) returned zero UUID", tt.category, tt.index) } }) } } func TestHazardUUID_DifferentInputsProduceDifferentUUIDs(t *testing.T) { tests := []struct { cat1 string idx1 int cat2 string idx2 int }{ {"false_classification", 1, "false_classification", 2}, {"false_classification", 1, "timing_error", 1}, {"mechanical_hazard", 7, "mechanical_hazard", 8}, {"pneumatic_hydraulic", 1, "noise_vibration", 1}, } for _, tt := range tests { name := tt.cat1 + "_" + tt.cat2 t.Run(name, func(t *testing.T) { id1 := hazardUUID(tt.cat1, tt.idx1) id2 := hazardUUID(tt.cat2, tt.idx2) if id1 == id2 { t.Errorf("hazardUUID(%q,%d) == hazardUUID(%q,%d): %s", tt.cat1, tt.idx1, tt.cat2, tt.idx2, id1) } }) } } func TestGetBuiltinHazardLibrary_CreatedAtSet(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.CreatedAt.IsZero() { t.Errorf("entries[%d] (%s): CreatedAt is zero", i, e.Name) } } } func TestGetBuiltinHazardLibrary_DescriptionPresent(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.Description == "" { t.Errorf("entries[%d] (%s): Description is empty", i, e.Name) } } } func TestGetBuiltinHazardLibrary_SuggestedMitigationsPresent(t *testing.T) { entries := GetBuiltinHazardLibrary() for i, e := range entries { if e.SuggestedMitigations == nil || len(e.SuggestedMitigations) == 0 { t.Errorf("entries[%d] (%s): SuggestedMitigations is nil/empty", i, e.Name) } } } func TestGetBuiltinHazardLibrary_ApplicableComponentTypesAreValid(t *testing.T) { entries := GetBuiltinHazardLibrary() validTypes := map[string]bool{ string(ComponentTypeSoftware): true, string(ComponentTypeFirmware): true, string(ComponentTypeAIModel): true, string(ComponentTypeHMI): true, string(ComponentTypeSensor): true, string(ComponentTypeActuator): true, string(ComponentTypeController): true, string(ComponentTypeNetwork): true, string(ComponentTypeMechanical): true, string(ComponentTypeElectrical): true, string(ComponentTypeOther): true, } for i, e := range entries { for _, ct := range e.ApplicableComponentTypes { if !validTypes[ct] { t.Errorf("entries[%d] (%s): invalid component type %q in ApplicableComponentTypes", i, e.Name, ct) } } } } func TestGetBuiltinHazardLibrary_ConsistentAcrossCalls(t *testing.T) { entries1 := GetBuiltinHazardLibrary() entries2 := GetBuiltinHazardLibrary() if len(entries1) != len(entries2) { t.Fatalf("inconsistent lengths: %d vs %d", len(entries1), len(entries2)) } for i := range entries1 { if entries1[i].ID != entries2[i].ID { t.Errorf("entries[%d]: ID mismatch across calls: %s vs %s", i, entries1[i].ID, entries2[i].ID) } if entries1[i].Name != entries2[i].Name { t.Errorf("entries[%d]: Name mismatch across calls: %q vs %q", i, entries1[i].Name, entries2[i].Name) } } } // TestGetProtectiveMeasureLibrary_EntryCount verifies the protective measures library size func TestGetProtectiveMeasureLibrary_EntryCount(t *testing.T) { entries := GetProtectiveMeasureLibrary() if len(entries) < 150 { t.Fatalf("GetProtectiveMeasureLibrary returned %d entries, want at least 150", len(entries)) } } // TestGetProtectiveMeasureLibrary_AllTypesPresent verifies all 3 reduction types exist func TestGetProtectiveMeasureLibrary_AllTypesPresent(t *testing.T) { entries := GetProtectiveMeasureLibrary() typeCounts := map[string]int{} for _, e := range entries { typeCounts[e.ReductionType]++ } for _, rt := range []string{"design", "information"} { if typeCounts[rt] < 10 { t.Errorf("reduction type %q: count = %d, want >= 10", rt, typeCounts[rt]) } } // Accept both "protection" and "protective" naming protCount := typeCounts["protection"] + typeCounts["protective"] if protCount < 10 { t.Errorf("reduction type protection/protective: count = %d, want >= 10", protCount) } } // TestGetProtectiveMeasureLibrary_UniqueIDs verifies all measure IDs are unique func TestGetProtectiveMeasureLibrary_UniqueIDs(t *testing.T) { entries := GetProtectiveMeasureLibrary() seen := make(map[string]string) for i, e := range entries { if e.ID == "" { t.Errorf("entries[%d] (%s): ID is empty", i, e.Name) continue } if prev, exists := seen[e.ID]; exists { t.Errorf("entries[%d] (%s): duplicate ID %q, same as %q", i, e.Name, e.ID, prev) } seen[e.ID] = e.Name } } // TestGetProtectiveMeasureLibrary_NonEmptyFields verifies required fields are filled func TestGetProtectiveMeasureLibrary_NonEmptyFields(t *testing.T) { entries := GetProtectiveMeasureLibrary() for i, e := range entries { if e.Name == "" { t.Errorf("entries[%d]: Name is empty", i) } if e.Description == "" { t.Errorf("entries[%d] (%s): Description is empty", i, e.Name) } if e.ReductionType == "" { t.Errorf("entries[%d] (%s): ReductionType is empty", i, e.Name) } } } // TestGetProtectiveMeasureLibrary_ValidReductionTypes verifies reduction types are valid func TestGetProtectiveMeasureLibrary_ValidReductionTypes(t *testing.T) { entries := GetProtectiveMeasureLibrary() validTypes := map[string]bool{"design": true, "protection": true, "protective": true, "information": true} for i, e := range entries { if !validTypes[e.ReductionType] { t.Errorf("entries[%d] (%s): invalid ReductionType %q", i, e.Name, e.ReductionType) } } } // TestGetControlsLibrary_EntryCount verifies the controls library size func TestGetControlsLibrary_EntryCount(t *testing.T) { entries := GetControlsLibrary() if len(entries) < 30 { t.Fatalf("GetControlsLibrary returned %d entries, want at least 30", len(entries)) } }