""" Unit tests for ISMS (Information Security Management System) API Routes. Tests all ISO 27001 certification-related endpoints: - ISMS Scope (Chapter 4.3) - ISMS Policies (Chapter 5.2) - Security Objectives (Chapter 6.2) - Statement of Applicability (SoA) - Audit Findings & CAPA - Management Reviews (Chapter 9.3) - Internal Audits (Chapter 9.2) - Readiness Check Run with: pytest backend/compliance/tests/test_isms_routes.py -v """ import pytest from datetime import datetime, date, timezone from unittest.mock import MagicMock from uuid import uuid4 from sqlalchemy.orm import Session import sys sys.path.insert(0, '/Users/benjaminadmin/Projekte/breakpilot-pwa/backend') from compliance.db.models import ( ISMSScopeDB, ISMSPolicyDB, SecurityObjectiveDB, StatementOfApplicabilityDB, AuditFindingDB, CorrectiveActionDB, ManagementReviewDB, InternalAuditDB, AuditTrailDB, ISMSReadinessCheckDB, ApprovalStatusEnum, FindingTypeEnum, FindingStatusEnum, CAPATypeEnum ) # ============================================================================ # Test Fixtures # ============================================================================ @pytest.fixture def mock_db(): """Create a mock database session.""" return MagicMock(spec=Session) @pytest.fixture def sample_scope(): """Create a sample ISMS scope for testing.""" return ISMSScopeDB( id=str(uuid4()), scope_statement="BreakPilot ISMS covers all digital learning platform operations", included_locations=["Frankfurt Office", "AWS eu-central-1"], included_processes=["Software Development", "Data Processing", "Customer Support"], included_services=["BreakPilot PWA", "Consent Service", "AI Assistant"], excluded_items=["Marketing Website"], exclusion_justification="Marketing website is static and contains no user data", status=ApprovalStatusEnum.DRAFT, version="1.0", created_by="admin@breakpilot.de", created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_approved_scope(sample_scope): """Create an approved ISMS scope for testing.""" sample_scope.status = ApprovalStatusEnum.APPROVED sample_scope.approved_by = "ceo@breakpilot.de" sample_scope.approved_at = datetime.now(timezone.utc) sample_scope.effective_date = date.today() sample_scope.review_date = date(date.today().year + 1, date.today().month, date.today().day) sample_scope.approval_signature = "sha256_signature_hash" return sample_scope @pytest.fixture def sample_policy(): """Create a sample ISMS policy for testing.""" return ISMSPolicyDB( id=str(uuid4()), policy_id="POL-ISMS-001", title="Information Security Policy", policy_type="master", description="Master ISMS policy for BreakPilot", policy_text="This policy establishes the framework for information security...", applies_to=["All Employees", "Contractors", "Partners"], review_frequency_months=12, related_controls=["GOV-001", "GOV-002"], authored_by="iso@breakpilot.de", status=ApprovalStatusEnum.DRAFT, version="1.0", created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_objective(): """Create a sample security objective for testing.""" return SecurityObjectiveDB( id=str(uuid4()), objective_id="OBJ-2026-001", title="Reduce Security Incidents", description="Reduce the number of security incidents by 30% compared to previous year", category="operational", specific="Reduce security incidents from 10 to 7 per year", measurable="Number of security incidents recorded in ticketing system", achievable="Based on trend analysis and planned control improvements", relevant="Directly supports information security goals", time_bound="By end of Q4 2026", kpi_name="Security Incident Count", kpi_target=7.0, kpi_unit="incidents/year", kpi_current=10.0, measurement_frequency="monthly", owner="security@breakpilot.de", target_date=date(2026, 12, 31), related_controls=["OPS-003"], status="active", progress_percentage=0.0, created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_soa_entry(): """Create a sample SoA entry for testing.""" return StatementOfApplicabilityDB( id=str(uuid4()), annex_a_control="A.5.1", annex_a_title="Policies for information security", annex_a_category="organizational", is_applicable=True, applicability_justification="Required for ISMS governance", implementation_status="implemented", implementation_notes="Implemented via GOV-001, GOV-002 controls", breakpilot_control_ids=["GOV-001", "GOV-002"], coverage_level="full", evidence_description="ISMS Policy v2.0, signed by CEO", version="1.0", created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_finding(): """Create a sample audit finding for testing.""" return AuditFindingDB( id=str(uuid4()), finding_id="FIND-2026-001", finding_type=FindingTypeEnum.MINOR, iso_chapter="9.2", annex_a_control="A.5.35", title="Internal audit schedule not documented", description="The internal audit schedule for 2026 was not formally documented", objective_evidence="No document found in DMS", impact_description="Cannot demonstrate planned approach to internal audits", owner="iso@breakpilot.de", auditor="external.auditor@cert.de", identified_date=date.today(), due_date=date(2026, 3, 31), status=FindingStatusEnum.OPEN, created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_major_finding(): """Create a major finding (certification blocking).""" return AuditFindingDB( id=str(uuid4()), finding_id="FIND-2026-002", finding_type=FindingTypeEnum.MAJOR, iso_chapter="5.2", title="Information Security Policy not approved", description="The ISMS master policy has not been approved by top management", objective_evidence="Policy document shows 'Draft' status", owner="ceo@breakpilot.de", auditor="external.auditor@cert.de", identified_date=date.today(), due_date=date(2026, 2, 28), status=FindingStatusEnum.OPEN, created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_capa(sample_finding): """Create a sample CAPA for testing.""" return CorrectiveActionDB( id=str(uuid4()), capa_id="CAPA-2026-001", finding_id=sample_finding.id, capa_type=CAPATypeEnum.CORRECTIVE, title="Create and approve internal audit schedule", description="Create a formal internal audit schedule document and get management approval", expected_outcome="Approved internal audit schedule for 2026", assigned_to="iso@breakpilot.de", planned_start=date.today(), planned_completion=date(2026, 2, 15), effectiveness_criteria="Document approved and distributed to audit team", status="planned", created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_management_review(): """Create a sample management review for testing.""" return ManagementReviewDB( id=str(uuid4()), review_id="MR-2026-Q1", title="Q1 2026 Management Review", review_date=date(2026, 1, 15), review_period_start=date(2025, 10, 1), review_period_end=date(2025, 12, 31), chairperson="ceo@breakpilot.de", attendees=[ {"name": "CEO", "role": "Chairperson"}, {"name": "CTO", "role": "Technical Lead"}, {"name": "ISO", "role": "ISMS Manager"}, ], status="draft", created_at=datetime.now(timezone.utc), ) @pytest.fixture def sample_internal_audit(): """Create a sample internal audit for testing.""" return InternalAuditDB( id=str(uuid4()), audit_id="IA-2026-001", title="Annual ISMS Internal Audit 2026", audit_type="full", scope_description="Complete ISMS audit covering all ISO 27001 chapters and Annex A controls", iso_chapters_covered=["4", "5", "6", "7", "8", "9", "10"], annex_a_controls_covered=["A.5", "A.6", "A.7", "A.8"], criteria="ISO 27001:2022, Internal ISMS procedures", planned_date=date(2026, 3, 1), lead_auditor="internal.auditor@breakpilot.de", audit_team=["internal.auditor@breakpilot.de", "qa@breakpilot.de"], status="planned", created_at=datetime.now(timezone.utc), ) # ============================================================================ # Test: ISMS Scope # ============================================================================ class TestISMSScope: """Tests for ISMS Scope endpoints.""" def test_scope_has_required_fields(self, sample_scope): """ISMS scope should have all required fields.""" assert sample_scope.scope_statement is not None assert sample_scope.status == ApprovalStatusEnum.DRAFT assert sample_scope.created_by is not None def test_scope_approval_sets_correct_fields(self, sample_approved_scope): """Approving scope should set approval fields.""" assert sample_approved_scope.status == ApprovalStatusEnum.APPROVED assert sample_approved_scope.approved_by is not None assert sample_approved_scope.approved_at is not None assert sample_approved_scope.effective_date is not None assert sample_approved_scope.review_date is not None assert sample_approved_scope.approval_signature is not None def test_scope_can_include_multiple_locations(self, sample_scope): """Scope should support multiple locations.""" assert isinstance(sample_scope.included_locations, list) assert len(sample_scope.included_locations) >= 1 def test_scope_exclusions_require_justification(self, sample_scope): """Scope exclusions should have justification (ISO 27001 requirement).""" if sample_scope.excluded_items: assert sample_scope.exclusion_justification is not None # ============================================================================ # Test: ISMS Policies # ============================================================================ class TestISMSPolicy: """Tests for ISMS Policy endpoints.""" def test_policy_has_unique_id(self, sample_policy): """Each policy should have a unique policy_id.""" assert sample_policy.policy_id is not None assert sample_policy.policy_id.startswith("POL-") def test_master_policy_type_exists(self, sample_policy): """Master policy type should be 'master'.""" assert sample_policy.policy_type == "master" def test_policy_has_review_frequency(self, sample_policy): """Policy should specify review frequency.""" assert sample_policy.review_frequency_months > 0 assert sample_policy.review_frequency_months <= 36 # Max 3 years def test_policy_can_link_to_controls(self, sample_policy): """Policy should link to related controls.""" assert isinstance(sample_policy.related_controls, list) # ============================================================================ # Test: Security Objectives # ============================================================================ class TestSecurityObjective: """Tests for Security Objectives endpoints.""" def test_objective_follows_smart_criteria(self, sample_objective): """Objectives should follow SMART criteria.""" # S - Specific assert sample_objective.specific is not None # M - Measurable assert sample_objective.measurable is not None # A - Achievable assert sample_objective.achievable is not None # R - Relevant assert sample_objective.relevant is not None # T - Time-bound assert sample_objective.time_bound is not None def test_objective_has_kpi(self, sample_objective): """Objectives should have measurable KPIs.""" assert sample_objective.kpi_name is not None assert sample_objective.kpi_target is not None assert sample_objective.kpi_unit is not None def test_objective_progress_calculation(self, sample_objective): """Objective progress should be calculable.""" if sample_objective.kpi_target and sample_objective.kpi_current: # Progress towards reducing incidents (lower is better for this KPI) expected_progress = max(0, min(100, (sample_objective.kpi_target / sample_objective.kpi_current) * 100 )) assert expected_progress >= 0 assert expected_progress <= 100 # ============================================================================ # Test: Statement of Applicability (SoA) # ============================================================================ class TestStatementOfApplicability: """Tests for SoA endpoints.""" def test_soa_entry_has_annex_a_reference(self, sample_soa_entry): """SoA entry should reference Annex A control.""" assert sample_soa_entry.annex_a_control is not None assert sample_soa_entry.annex_a_control.startswith("A.") def test_soa_entry_requires_justification_for_not_applicable(self): """Non-applicable controls must have justification.""" soa_entry = StatementOfApplicabilityDB( id=str(uuid4()), annex_a_control="A.7.2", annex_a_title="Physical entry", annex_a_category="physical", is_applicable=False, applicability_justification="Cloud-only infrastructure, no physical data center", ) assert not soa_entry.is_applicable assert soa_entry.applicability_justification is not None def test_soa_entry_tracks_implementation_status(self, sample_soa_entry): """SoA should track implementation status.""" valid_statuses = ["planned", "in_progress", "implemented", "not_implemented"] assert sample_soa_entry.implementation_status in valid_statuses def test_soa_entry_maps_to_breakpilot_controls(self, sample_soa_entry): """SoA should map Annex A controls to Breakpilot controls.""" assert isinstance(sample_soa_entry.breakpilot_control_ids, list) # ============================================================================ # Test: Audit Findings # ============================================================================ class TestAuditFinding: """Tests for Audit Finding endpoints.""" def test_finding_has_classification(self, sample_finding): """Finding should have Major/Minor/OFI classification.""" valid_types = [FindingTypeEnum.MAJOR, FindingTypeEnum.MINOR, FindingTypeEnum.OFI, FindingTypeEnum.POSITIVE] assert sample_finding.finding_type in valid_types def test_major_finding_blocks_certification(self, sample_major_finding): """Major findings should be identified as certification blocking.""" assert sample_major_finding.finding_type == FindingTypeEnum.MAJOR # is_blocking is a property, so we check the type is_blocking = (sample_major_finding.finding_type == FindingTypeEnum.MAJOR and sample_major_finding.status != FindingStatusEnum.CLOSED) assert is_blocking def test_finding_has_objective_evidence(self, sample_finding): """Findings should have objective evidence.""" assert sample_finding.objective_evidence is not None def test_finding_has_due_date(self, sample_finding): """Findings should have a due date for closure.""" assert sample_finding.due_date is not None def test_finding_lifecycle_statuses(self): """Finding should follow proper lifecycle.""" valid_statuses = [ FindingStatusEnum.OPEN, FindingStatusEnum.CORRECTIVE_ACTION_PENDING, FindingStatusEnum.VERIFICATION_PENDING, FindingStatusEnum.CLOSED, ] for status in valid_statuses: assert status in FindingStatusEnum # ============================================================================ # Test: Corrective Actions (CAPA) # ============================================================================ class TestCorrectiveAction: """Tests for CAPA endpoints.""" def test_capa_links_to_finding(self, sample_capa, sample_finding): """CAPA should link to a finding.""" assert sample_capa.finding_id == sample_finding.id def test_capa_has_type(self, sample_capa): """CAPA should have corrective or preventive type.""" valid_types = [CAPATypeEnum.CORRECTIVE, CAPATypeEnum.PREVENTIVE] assert sample_capa.capa_type in valid_types def test_capa_has_effectiveness_criteria(self, sample_capa): """CAPA should define how effectiveness will be verified.""" assert sample_capa.effectiveness_criteria is not None def test_capa_has_completion_date(self, sample_capa): """CAPA should have planned completion date.""" assert sample_capa.planned_completion is not None # ============================================================================ # Test: Management Review # ============================================================================ class TestManagementReview: """Tests for Management Review endpoints.""" def test_review_has_chairperson(self, sample_management_review): """Management review must have a chairperson (top management).""" assert sample_management_review.chairperson is not None def test_review_has_review_period(self, sample_management_review): """Review should cover a specific period.""" assert sample_management_review.review_period_start is not None assert sample_management_review.review_period_end is not None def test_review_id_includes_quarter(self, sample_management_review): """Review ID should indicate the quarter.""" assert "Q" in sample_management_review.review_id def test_review_tracks_attendees(self, sample_management_review): """Review should track attendees.""" assert sample_management_review.attendees is not None assert len(sample_management_review.attendees) >= 1 # ============================================================================ # Test: Internal Audit # ============================================================================ class TestInternalAudit: """Tests for Internal Audit endpoints.""" def test_audit_has_scope(self, sample_internal_audit): """Internal audit should define scope.""" assert sample_internal_audit.scope_description is not None def test_audit_covers_iso_chapters(self, sample_internal_audit): """Audit should specify which ISO chapters are covered.""" assert sample_internal_audit.iso_chapters_covered is not None assert len(sample_internal_audit.iso_chapters_covered) >= 1 def test_audit_has_lead_auditor(self, sample_internal_audit): """Audit should have a lead auditor.""" assert sample_internal_audit.lead_auditor is not None def test_audit_has_criteria(self, sample_internal_audit): """Audit should define audit criteria.""" assert sample_internal_audit.criteria is not None # ============================================================================ # Test: ISMS Readiness Check # ============================================================================ class TestISMSReadinessCheck: """Tests for ISMS Readiness Check.""" def test_readiness_check_identifies_potential_majors(self): """Readiness check should identify potential major findings.""" check = ISMSReadinessCheckDB( id=str(uuid4()), check_date=datetime.now(timezone.utc), triggered_by="admin@breakpilot.de", overall_status="not_ready", certification_possible=False, chapter_4_status="fail", chapter_5_status="fail", chapter_6_status="warning", chapter_7_status="pass", chapter_8_status="pass", chapter_9_status="fail", chapter_10_status="pass", potential_majors=[ {"check": "ISMS Scope not approved", "iso_reference": "4.3"}, {"check": "Master policy not approved", "iso_reference": "5.2"}, {"check": "No internal audit conducted", "iso_reference": "9.2"}, ], potential_minors=[ {"check": "Risk treatment incomplete", "iso_reference": "6.1.2"}, ], readiness_score=30.0, ) assert not check.certification_possible assert len(check.potential_majors) >= 1 assert check.readiness_score < 100 def test_readiness_check_shows_chapter_status(self): """Readiness check should show status for each ISO chapter.""" check = ISMSReadinessCheckDB( id=str(uuid4()), check_date=datetime.now(timezone.utc), triggered_by="admin@breakpilot.de", overall_status="ready", certification_possible=True, chapter_4_status="pass", chapter_5_status="pass", chapter_6_status="pass", chapter_7_status="pass", chapter_8_status="pass", chapter_9_status="pass", chapter_10_status="pass", potential_majors=[], potential_minors=[], readiness_score=100.0, ) assert check.chapter_4_status == "pass" assert check.chapter_5_status == "pass" assert check.chapter_9_status == "pass" assert check.certification_possible # ============================================================================ # Test: ISO 27001 Annex A Coverage # ============================================================================ class TestAnnexACoverage: """Tests for ISO 27001 Annex A control coverage.""" def test_annex_a_has_93_controls(self): """ISO 27001:2022 has exactly 93 controls.""" from compliance.data.iso27001_annex_a import ISO27001_ANNEX_A_CONTROLS, ANNEX_A_SUMMARY assert len(ISO27001_ANNEX_A_CONTROLS) == 93 assert ANNEX_A_SUMMARY["total_controls"] == 93 def test_annex_a_categories(self): """Annex A should have 4 control categories.""" from compliance.data.iso27001_annex_a import ANNEX_A_SUMMARY # A.5 Organizational (37), A.6 People (8), A.7 Physical (14), A.8 Technological (34) assert ANNEX_A_SUMMARY["organizational_controls"] == 37 assert ANNEX_A_SUMMARY["people_controls"] == 8 assert ANNEX_A_SUMMARY["physical_controls"] == 14 assert ANNEX_A_SUMMARY["technological_controls"] == 34 def test_annex_a_control_structure(self): """Each Annex A control should have required fields.""" from compliance.data.iso27001_annex_a import ISO27001_ANNEX_A_CONTROLS for control in ISO27001_ANNEX_A_CONTROLS: assert "control_id" in control assert "title" in control assert "category" in control assert "description" in control assert control["control_id"].startswith("A.") # ============================================================================ # Test: Audit Trail # ============================================================================ class TestAuditTrail: """Tests for Audit Trail functionality.""" def test_audit_trail_entry_has_required_fields(self): """Audit trail entry should have all required fields.""" entry = AuditTrailDB( id=str(uuid4()), entity_type="isms_scope", entity_id=str(uuid4()), entity_name="ISMS Scope v1.0", action="approve", performed_by="ceo@breakpilot.de", performed_at=datetime.now(timezone.utc), checksum="sha256_hash", ) assert entry.entity_type is not None assert entry.entity_id is not None assert entry.action is not None assert entry.performed_by is not None assert entry.performed_at is not None assert entry.checksum is not None def test_audit_trail_tracks_changes(self): """Audit trail should track field changes.""" entry = AuditTrailDB( id=str(uuid4()), entity_type="isms_policy", entity_id=str(uuid4()), entity_name="POL-ISMS-001", action="update", field_changed="status", old_value="draft", new_value="approved", change_summary="Policy approved by CEO", performed_by="ceo@breakpilot.de", performed_at=datetime.now(timezone.utc), checksum="sha256_hash", ) assert entry.field_changed == "status" assert entry.old_value == "draft" assert entry.new_value == "approved" # ============================================================================ # Test: Certification Blockers # ============================================================================ class TestCertificationBlockers: """Tests for certification blocking scenarios.""" def test_open_major_blocks_certification(self): """Open major findings should block certification.""" finding = AuditFindingDB( id=str(uuid4()), finding_id="FIND-2026-001", finding_type=FindingTypeEnum.MAJOR, title="Critical finding", description="Test", auditor="auditor@test.de", status=FindingStatusEnum.OPEN, ) is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and finding.status != FindingStatusEnum.CLOSED) assert is_blocking def test_closed_major_allows_certification(self): """Closed major findings should not block certification.""" finding = AuditFindingDB( id=str(uuid4()), finding_id="FIND-2026-001", finding_type=FindingTypeEnum.MAJOR, title="Critical finding", description="Test", auditor="auditor@test.de", status=FindingStatusEnum.CLOSED, closed_date=date.today(), ) is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and finding.status != FindingStatusEnum.CLOSED) assert not is_blocking def test_minor_findings_dont_block_certification(self): """Minor findings should not block certification.""" finding = AuditFindingDB( id=str(uuid4()), finding_id="FIND-2026-002", finding_type=FindingTypeEnum.MINOR, title="Minor finding", description="Test", auditor="auditor@test.de", status=FindingStatusEnum.OPEN, ) is_blocking = (finding.finding_type == FindingTypeEnum.MAJOR and finding.status != FindingStatusEnum.CLOSED) assert not is_blocking