package ucca import ( "testing" ) func TestNIS2Module_Classification(t *testing.T) { module, err := NewNIS2Module() if err != nil { t.Fatalf("Failed to create NIS2 module: %v", err) } tests := []struct { name string facts *UnifiedFacts expectedClass NIS2Classification expectedApply bool }{ { name: "Large energy company - Essential Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 500, AnnualRevenue: 100_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "energy", }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "Medium energy company - Important Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 100, AnnualRevenue: 20_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "energy", }, }, expectedClass: NIS2ImportantEntity, expectedApply: true, }, { name: "Small energy company - Not Affected", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 20, AnnualRevenue: 5_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "energy", }, }, expectedClass: NIS2NotAffected, expectedApply: false, }, { name: "Large healthcare company - Essential Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 300, AnnualRevenue: 80_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "health", }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "Medium manufacturing company (Annex II) - Important Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 150, AnnualRevenue: 30_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "manufacturing", }, }, expectedClass: NIS2ImportantEntity, expectedApply: true, }, { name: "Large manufacturing company (Annex II) - Important Entity (not Essential)", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 500, AnnualRevenue: 100_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "manufacturing", }, }, expectedClass: NIS2ImportantEntity, expectedApply: true, }, { name: "Retail company - Not Affected (not in NIS2 sectors)", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 500, AnnualRevenue: 100_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "retail", }, }, expectedClass: NIS2NotAffected, expectedApply: false, }, { name: "Small KRITIS operator - Essential Entity (exception)", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 30, AnnualRevenue: 8_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "energy", IsKRITIS: true, KRITISThresholdMet: true, }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "Cloud provider (special service) - Essential Entity regardless of size", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 20, AnnualRevenue: 5_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "it_services", SpecialServices: []string{"cloud"}, }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "DNS provider (special service) - Essential Entity regardless of size", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 10, AnnualRevenue: 2_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "digital_infrastructure", SpecialServices: []string{"dns"}, }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "MSP provider - Essential Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 15, AnnualRevenue: 3_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "ict_service_mgmt", SpecialServices: []string{"msp"}, }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, { name: "Group company (counts group size) - Essential Entity", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 50, AnnualRevenue: 15_000_000, IsPartOfGroup: true, GroupEmployees: 500, GroupRevenue: 200_000_000, Country: "DE", EUMember: true, }, Sector: SectorFacts{ PrimarySector: "energy", }, }, expectedClass: NIS2EssentialEntity, expectedApply: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { classification := module.Classify(tt.facts) if classification != tt.expectedClass { t.Errorf("Classify() = %v, want %v", classification, tt.expectedClass) } isApplicable := module.IsApplicable(tt.facts) if isApplicable != tt.expectedApply { t.Errorf("IsApplicable() = %v, want %v", isApplicable, tt.expectedApply) } }) } } func TestNIS2Module_DeriveObligations(t *testing.T) { module, err := NewNIS2Module() if err != nil { t.Fatalf("Failed to create NIS2 module: %v", err) } tests := []struct { name string facts *UnifiedFacts minObligations int expectedContains []string // Obligation IDs that should be present expectedMissing []string // Obligation IDs that should NOT be present }{ { name: "Essential Entity should have all obligations including audits", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 500, AnnualRevenue: 100_000_000, }, Sector: SectorFacts{ PrimarySector: "energy", }, }, minObligations: 10, expectedContains: []string{"NIS2-OBL-001", "NIS2-OBL-002", "NIS2-OBL-012"}, // BSI registration, risk mgmt, audits }, { name: "Important Entity should have obligations but NOT audit requirement", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 100, AnnualRevenue: 20_000_000, }, Sector: SectorFacts{ PrimarySector: "manufacturing", }, }, minObligations: 8, expectedContains: []string{"NIS2-OBL-001", "NIS2-OBL-002"}, expectedMissing: []string{"NIS2-OBL-012"}, // Audits only for essential entities }, { name: "Not affected entity should have no obligations", facts: &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 20, AnnualRevenue: 5_000_000, }, Sector: SectorFacts{ PrimarySector: "retail", }, }, minObligations: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { obligations := module.DeriveObligations(tt.facts) if len(obligations) < tt.minObligations { t.Errorf("DeriveObligations() returned %d obligations, want at least %d", len(obligations), tt.minObligations) } // Check expected contains for _, expectedID := range tt.expectedContains { found := false for _, obl := range obligations { if obl.ID == expectedID { found = true break } } if !found { t.Errorf("Expected obligation %s not found in results", expectedID) } } // Check expected missing for _, missingID := range tt.expectedMissing { for _, obl := range obligations { if obl.ID == missingID { t.Errorf("Obligation %s should NOT be present for this classification", missingID) } } } }) } } func TestNIS2Module_IncidentDeadlines(t *testing.T) { module, err := NewNIS2Module() if err != nil { t.Fatalf("Failed to create NIS2 module: %v", err) } // Test for affected entity affectedFacts := &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 100, AnnualRevenue: 20_000_000, }, Sector: SectorFacts{ PrimarySector: "energy", }, } deadlines := module.GetIncidentDeadlines(affectedFacts) if len(deadlines) != 3 { t.Errorf("Expected 3 incident deadlines, got %d", len(deadlines)) } // Check phases expectedPhases := map[string]string{ "Frühwarnung": "24 Stunden", "Vorfallmeldung": "72 Stunden", "Abschlussbericht": "1 Monat", } for _, deadline := range deadlines { if expected, ok := expectedPhases[deadline.Phase]; ok { if deadline.Deadline != expected { t.Errorf("Phase %s: expected deadline %s, got %s", deadline.Phase, expected, deadline.Deadline) } } } // Test for not affected entity notAffectedFacts := &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 20, AnnualRevenue: 5_000_000, }, Sector: SectorFacts{ PrimarySector: "retail", }, } deadlines = module.GetIncidentDeadlines(notAffectedFacts) if len(deadlines) != 0 { t.Errorf("Expected 0 incident deadlines for not affected entity, got %d", len(deadlines)) } } func TestNIS2Module_DecisionTree(t *testing.T) { module, err := NewNIS2Module() if err != nil { t.Fatalf("Failed to create NIS2 module: %v", err) } tree := module.GetDecisionTree() if tree == nil { t.Fatal("Expected decision tree, got nil") } if tree.ID != "nis2_applicability" { t.Errorf("Expected tree ID 'nis2_applicability', got %s", tree.ID) } if tree.RootNode == nil { t.Fatal("Expected root node, got nil") } // Verify structure if tree.RootNode.YesNode == nil || tree.RootNode.NoNode == nil { t.Error("Root node should have both yes and no branches") } } func TestNIS2Module_DeriveControls(t *testing.T) { module, err := NewNIS2Module() if err != nil { t.Fatalf("Failed to create NIS2 module: %v", err) } // Test for affected entity affectedFacts := &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 100, AnnualRevenue: 20_000_000, }, Sector: SectorFacts{ PrimarySector: "energy", }, } controls := module.DeriveControls(affectedFacts) if len(controls) == 0 { t.Error("Expected controls for affected entity, got none") } // Check that controls have regulation ID set for _, ctrl := range controls { if ctrl.RegulationID != "nis2" { t.Errorf("Control %s has wrong regulation ID: %s", ctrl.ID, ctrl.RegulationID) } } // Test for not affected entity notAffectedFacts := &UnifiedFacts{ Organization: OrganizationFacts{ EmployeeCount: 20, AnnualRevenue: 5_000_000, }, Sector: SectorFacts{ PrimarySector: "retail", }, } controls = module.DeriveControls(notAffectedFacts) if len(controls) != 0 { t.Errorf("Expected 0 controls for not affected entity, got %d", len(controls)) } } func TestOrganizationFacts_SizeCalculations(t *testing.T) { tests := []struct { name string org OrganizationFacts expectedSize string expectedSME bool expectedNIS2 bool expectedLarge bool }{ { name: "Micro enterprise", org: OrganizationFacts{ EmployeeCount: 5, AnnualRevenue: 1_000_000, }, expectedSize: "micro", expectedSME: true, expectedNIS2: false, expectedLarge: false, }, { name: "Small enterprise", org: OrganizationFacts{ EmployeeCount: 30, AnnualRevenue: 8_000_000, }, expectedSize: "small", expectedSME: true, expectedNIS2: false, expectedLarge: false, }, { name: "Medium enterprise (by employees)", org: OrganizationFacts{ EmployeeCount: 100, AnnualRevenue: 20_000_000, }, expectedSize: "medium", expectedSME: true, expectedNIS2: true, expectedLarge: false, }, { name: "Medium enterprise (by revenue/balance)", org: OrganizationFacts{ EmployeeCount: 40, AnnualRevenue: 15_000_000, BalanceSheetTotal: 15_000_000, }, expectedSize: "medium", // < 50 employees BUT revenue AND balance > €10m → not small expectedSME: true, expectedNIS2: true, // >= €10m revenue AND balance expectedLarge: false, }, { name: "Large enterprise", org: OrganizationFacts{ EmployeeCount: 500, AnnualRevenue: 100_000_000, }, expectedSize: "large", expectedSME: false, expectedNIS2: true, expectedLarge: true, }, { name: "Group company (uses group figures)", org: OrganizationFacts{ EmployeeCount: 50, AnnualRevenue: 15_000_000, IsPartOfGroup: true, GroupEmployees: 500, GroupRevenue: 200_000_000, }, expectedSize: "large", expectedSME: false, expectedNIS2: true, expectedLarge: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { size := tt.org.CalculateSizeCategory() if size != tt.expectedSize { t.Errorf("CalculateSizeCategory() = %v, want %v", size, tt.expectedSize) } isSME := tt.org.IsSME() if isSME != tt.expectedSME { t.Errorf("IsSME() = %v, want %v", isSME, tt.expectedSME) } meetsNIS2 := tt.org.MeetsNIS2SizeThreshold() if meetsNIS2 != tt.expectedNIS2 { t.Errorf("MeetsNIS2SizeThreshold() = %v, want %v", meetsNIS2, tt.expectedNIS2) } isLarge := tt.org.MeetsNIS2LargeThreshold() if isLarge != tt.expectedLarge { t.Errorf("MeetsNIS2LargeThreshold() = %v, want %v", isLarge, tt.expectedLarge) } }) } }