"""Integration tests for ISMS routes (isms_routes.py). Tests the ISO 27001 ISMS API endpoints using TestClient + SQLite + ORM: - Scope CRUD + Approval - Policy CRUD + Approval + Duplicate check - Overview / Dashboard endpoint - Readiness check - Edge cases (not found, invalid data, etc.) Run with: cd backend-compliance && python3 -m pytest tests/test_isms_routes.py -v """ import os import sys import pytest from datetime import date, datetime, timezone from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from classroom_engine.database import Base, get_db from compliance.api.isms_routes import router as isms_router # ============================================================================= # Test App + SQLite Setup # ============================================================================= SQLALCHEMY_DATABASE_URL = "sqlite:///./test_isms.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @event.listens_for(engine, "connect") def _set_sqlite_pragma(dbapi_conn, connection_record): """Enable foreign keys and register NOW() for SQLite.""" cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() dbapi_conn.create_function("NOW", 0, lambda: datetime.now(timezone.utc).isoformat()) app = FastAPI() app.include_router(isms_router) def override_get_db(): db = TestSessionLocal() try: yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db client = TestClient(app) # ============================================================================= # Fixtures # ============================================================================= @pytest.fixture(autouse=True) def setup_db(): """Create all tables before each test module, drop after.""" # Import all models so Base.metadata knows about them import compliance.db.models # noqa: F401 Base.metadata.create_all(bind=engine) yield Base.metadata.drop_all(bind=engine) # ============================================================================= # Helper data builders # ============================================================================= def _scope_payload(**overrides): data = { "scope_statement": "ISMS covers all BreakPilot digital learning operations", "included_locations": ["Frankfurt Office", "AWS eu-central-1"], "included_processes": ["Software Development", "Data Processing"], "included_services": ["BreakPilot PWA", "AI Assistant"], "excluded_items": ["Marketing Website"], "exclusion_justification": "Static site, no user data", } data.update(overrides) return data def _policy_payload(policy_id="POL-ISMS-001", **overrides): data = { "policy_id": policy_id, "title": "Information Security Policy", "policy_type": "master", "description": "Master ISMS policy", "policy_text": "This policy establishes the framework for information security...", "applies_to": ["All Employees"], "review_frequency_months": 12, "related_controls": ["GOV-001"], "authored_by": "iso@breakpilot.de", } data.update(overrides) return data def _objective_payload(objective_id="OBJ-2026-001", **overrides): data = { "objective_id": objective_id, "title": "Reduce Security Incidents", "description": "Reduce incidents by 30%", "category": "operational", "specific": "Reduce from 10 to 7 per year", "measurable": "Incident count in ticketing system", "achievable": "Based on trend analysis", "relevant": "Supports info sec goals", "time_bound": "By Q4 2026", "kpi_name": "Security Incident Count", "kpi_target": "7", "kpi_unit": "incidents/year", "measurement_frequency": "monthly", "owner": "security@breakpilot.de", "target_date": "2026-12-31", "related_controls": ["OPS-003"], } data.update(overrides) return data def _soa_payload(annex_a_control="A.5.1", **overrides): data = { "annex_a_control": annex_a_control, "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": "Covered by GOV-001", "breakpilot_control_ids": ["GOV-001"], "coverage_level": "full", "evidence_description": "ISMS Policy v2.0", } data.update(overrides) return data def _finding_payload(**overrides): data = { "finding_type": "minor", "iso_chapter": "9.2", "annex_a_control": "A.5.35", "title": "Audit schedule not documented", "description": "No formal internal audit schedule found", "objective_evidence": "No document in DMS", "impact_description": "Cannot demonstrate planned approach", "owner": "iso@breakpilot.de", "auditor": "external.auditor@cert.de", "due_date": "2026-03-31", } data.update(overrides) return data def _mgmt_review_payload(**overrides): data = { "title": "Q1 2026 Management Review", "review_date": "2026-01-15", "review_period_start": "2025-10-01", "review_period_end": "2025-12-31", "chairperson": "ceo@breakpilot.de", "attendees": [ {"name": "CEO", "role": "Chairperson"}, {"name": "CTO", "role": "Technical Lead"}, ], } data.update(overrides) return data def _internal_audit_payload(**overrides): data = { "title": "ISMS Internal Audit 2026", "audit_type": "scheduled", "scope_description": "Complete ISMS audit covering all chapters", "iso_chapters_covered": ["4", "5", "6", "7", "8", "9", "10"], "annex_a_controls_covered": ["A.5", "A.6"], "criteria": "ISO 27001:2022", "planned_date": "2026-03-01", "lead_auditor": "internal.auditor@breakpilot.de", "audit_team": ["internal.auditor@breakpilot.de", "qa@breakpilot.de"], } data.update(overrides) return data # ============================================================================= # Test: ISMS Scope CRUD # ============================================================================= class TestISMSScopeCRUD: """Tests for ISMS Scope CRUD endpoints.""" def test_create_scope(self): """POST /isms/scope should create a new scope.""" r = client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) assert r.status_code == 200 body = r.json() assert body["scope_statement"] == "ISMS covers all BreakPilot digital learning operations" assert body["status"] == "draft" assert body["version"] == "1.0" assert "id" in body def test_get_scope(self): """GET /isms/scope should return the current scope.""" client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) r = client.get("/isms/scope") assert r.status_code == 200 assert r.json()["scope_statement"] is not None def test_get_scope_not_found(self): """GET /isms/scope should return 404 when no scope exists.""" r = client.get("/isms/scope") assert r.status_code == 404 def test_update_scope(self): """PUT /isms/scope/{id} should update draft scope.""" create = client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) scope_id = create.json()["id"] r = client.put( f"/isms/scope/{scope_id}", json={"scope_statement": "Updated scope statement"}, params={"updated_by": "admin@bp.de"}, ) assert r.status_code == 200 assert r.json()["scope_statement"] == "Updated scope statement" assert r.json()["version"] == "1.1" def test_update_scope_not_found(self): """PUT /isms/scope/{id} should return 404 for unknown id.""" r = client.put( "/isms/scope/nonexistent-id", json={"scope_statement": "x"}, params={"updated_by": "admin@bp.de"}, ) assert r.status_code == 404 def test_create_scope_supersedes_existing(self): """Creating a new scope should supersede the old one.""" client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) client.post( "/isms/scope", json=_scope_payload(scope_statement="New scope v2"), params={"created_by": "admin@bp.de"}, ) r = client.get("/isms/scope") assert r.status_code == 200 assert r.json()["scope_statement"] == "New scope v2" def test_approve_scope(self): """POST /isms/scope/{id}/approve should approve scope.""" create = client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) scope_id = create.json()["id"] r = client.post( f"/isms/scope/{scope_id}/approve", json={ "approved_by": "ceo@breakpilot.de", "effective_date": "2026-03-01", "review_date": "2027-03-01", }, ) assert r.status_code == 200 assert r.json()["status"] == "approved" assert r.json()["approved_by"] == "ceo@breakpilot.de" def test_approve_scope_not_found(self): """POST /isms/scope/{id}/approve should return 404 for unknown scope.""" r = client.post( "/isms/scope/fake-id/approve", json={ "approved_by": "ceo@breakpilot.de", "effective_date": "2026-03-01", "review_date": "2027-03-01", }, ) assert r.status_code == 404 def test_update_approved_scope_rejected(self): """PUT on approved scope should return 400.""" create = client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) scope_id = create.json()["id"] client.post( f"/isms/scope/{scope_id}/approve", json={ "approved_by": "ceo@breakpilot.de", "effective_date": "2026-03-01", "review_date": "2027-03-01", }, ) r = client.put( f"/isms/scope/{scope_id}", json={"scope_statement": "changed"}, params={"updated_by": "admin@bp.de"}, ) assert r.status_code == 400 assert "approved" in r.json()["detail"].lower() # ============================================================================= # Test: ISMS Policy CRUD # ============================================================================= class TestISMSPolicyCRUD: """Tests for ISMS Policy CRUD endpoints.""" def test_create_policy(self): """POST /isms/policies should create a new policy.""" r = client.post("/isms/policies", json=_policy_payload()) assert r.status_code == 200 body = r.json() assert body["policy_id"] == "POL-ISMS-001" assert body["status"] == "draft" assert body["version"] == "1.0" def test_list_policies(self): """GET /isms/policies should list all policies.""" client.post("/isms/policies", json=_policy_payload("POL-ISMS-001")) client.post("/isms/policies", json=_policy_payload("POL-ISMS-002", title="Access Control Policy")) r = client.get("/isms/policies") assert r.status_code == 200 assert r.json()["total"] == 2 assert len(r.json()["policies"]) == 2 def test_list_policies_filter_by_type(self): """GET /isms/policies?policy_type=master should filter.""" client.post("/isms/policies", json=_policy_payload("POL-001")) client.post("/isms/policies", json=_policy_payload("POL-002", policy_type="operational")) r = client.get("/isms/policies", params={"policy_type": "master"}) assert r.status_code == 200 assert r.json()["total"] == 1 def test_get_policy_by_id(self): """GET /isms/policies/{id} should return a policy by its UUID.""" create = client.post("/isms/policies", json=_policy_payload()) policy_uuid = create.json()["id"] r = client.get(f"/isms/policies/{policy_uuid}") assert r.status_code == 200 assert r.json()["policy_id"] == "POL-ISMS-001" def test_get_policy_by_policy_id(self): """GET /isms/policies/{policy_id} should also match the human-readable id.""" client.post("/isms/policies", json=_policy_payload()) r = client.get("/isms/policies/POL-ISMS-001") assert r.status_code == 200 assert r.json()["title"] == "Information Security Policy" def test_get_policy_not_found(self): """GET /isms/policies/{id} should return 404 for unknown policy.""" r = client.get("/isms/policies/nonexistent") assert r.status_code == 404 def test_update_policy(self): """PUT /isms/policies/{id} should update a draft policy.""" create = client.post("/isms/policies", json=_policy_payload()) pid = create.json()["id"] r = client.put( f"/isms/policies/{pid}", json={"title": "Updated Title"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["title"] == "Updated Title" def test_update_policy_not_found(self): """PUT /isms/policies/{id} should return 404 for unknown policy.""" r = client.put( "/isms/policies/fake-id", json={"title": "x"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 404 def test_duplicate_policy_id_rejected(self): """POST /isms/policies with duplicate policy_id should return 400.""" client.post("/isms/policies", json=_policy_payload("POL-DUP")) r = client.post("/isms/policies", json=_policy_payload("POL-DUP")) assert r.status_code == 400 assert "already exists" in r.json()["detail"] def test_approve_policy(self): """POST /isms/policies/{id}/approve should approve a policy.""" create = client.post("/isms/policies", json=_policy_payload()) pid = create.json()["id"] r = client.post( f"/isms/policies/{pid}/approve", json={ "reviewed_by": "cto@breakpilot.de", "approved_by": "ceo@breakpilot.de", "effective_date": "2026-03-01", }, ) assert r.status_code == 200 assert r.json()["status"] == "approved" assert r.json()["approved_by"] == "ceo@breakpilot.de" assert r.json()["next_review_date"] is not None def test_approve_policy_not_found(self): """POST /isms/policies/{id}/approve should 404 for unknown policy.""" r = client.post( "/isms/policies/fake/approve", json={ "reviewed_by": "x", "approved_by": "y", "effective_date": "2026-03-01", }, ) assert r.status_code == 404 def test_update_approved_policy_bumps_version(self): """Updating an approved policy should increment major version and reset to draft.""" create = client.post("/isms/policies", json=_policy_payload()) pid = create.json()["id"] client.post( f"/isms/policies/{pid}/approve", json={ "reviewed_by": "cto@bp.de", "approved_by": "ceo@bp.de", "effective_date": "2026-03-01", }, ) r = client.put( f"/isms/policies/{pid}", json={"title": "Updated after approval"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["version"] == "2.0" assert r.json()["status"] == "draft" # ============================================================================= # Test: Security Objectives # ============================================================================= class TestSecurityObjectivesCRUD: """Tests for Security Objectives endpoints.""" def test_create_objective(self): """POST /isms/objectives should create a new objective.""" r = client.post("/isms/objectives", json=_objective_payload(), params={"created_by": "iso@bp.de"}) assert r.status_code == 200 body = r.json() assert body["objective_id"] == "OBJ-2026-001" assert body["status"] == "active" def test_list_objectives(self): """GET /isms/objectives should list all objectives.""" client.post("/isms/objectives", json=_objective_payload("OBJ-001"), params={"created_by": "a"}) client.post("/isms/objectives", json=_objective_payload("OBJ-002", title="Uptime"), params={"created_by": "a"}) r = client.get("/isms/objectives") assert r.status_code == 200 assert r.json()["total"] == 2 def test_update_objective_progress(self): """PUT /isms/objectives/{id} should update progress.""" create = client.post("/isms/objectives", json=_objective_payload(), params={"created_by": "a"}) oid = create.json()["id"] r = client.put( f"/isms/objectives/{oid}", json={"progress_percentage": 50}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["progress_percentage"] == 50 def test_update_objective_auto_achieved(self): """Setting progress to 100% should auto-set status to 'achieved'.""" create = client.post("/isms/objectives", json=_objective_payload(), params={"created_by": "a"}) oid = create.json()["id"] r = client.put( f"/isms/objectives/{oid}", json={"progress_percentage": 100}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["status"] == "achieved" def test_update_objective_not_found(self): """PUT /isms/objectives/{id} should 404 for unknown objective.""" r = client.put( "/isms/objectives/fake", json={"progress_percentage": 10}, params={"updated_by": "a"}, ) assert r.status_code == 404 # ============================================================================= # Test: Statement of Applicability (SoA) # ============================================================================= class TestSoACRUD: """Tests for SoA endpoints.""" def test_create_soa_entry(self): """POST /isms/soa should create an SoA entry.""" r = client.post("/isms/soa", json=_soa_payload(), params={"created_by": "iso@bp.de"}) assert r.status_code == 200 body = r.json() assert body["annex_a_control"] == "A.5.1" assert body["is_applicable"] is True def test_list_soa_entries(self): """GET /isms/soa should list all SoA entries.""" client.post("/isms/soa", json=_soa_payload("A.5.1"), params={"created_by": "a"}) client.post("/isms/soa", json=_soa_payload("A.6.1", is_applicable=False, applicability_justification="N/A"), params={"created_by": "a"}) r = client.get("/isms/soa") assert r.status_code == 200 assert r.json()["total"] == 2 assert r.json()["applicable_count"] == 1 assert r.json()["not_applicable_count"] == 1 def test_duplicate_soa_control_rejected(self): """POST /isms/soa with duplicate annex_a_control should return 400.""" client.post("/isms/soa", json=_soa_payload("A.5.1"), params={"created_by": "a"}) r = client.post("/isms/soa", json=_soa_payload("A.5.1"), params={"created_by": "a"}) assert r.status_code == 400 assert "already exists" in r.json()["detail"] def test_update_soa_entry(self): """PUT /isms/soa/{id} should update an SoA entry.""" create = client.post("/isms/soa", json=_soa_payload(), params={"created_by": "a"}) eid = create.json()["id"] r = client.put( f"/isms/soa/{eid}", json={"implementation_status": "in_progress"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["implementation_status"] == "in_progress" assert r.json()["version"] == "1.1" def test_update_soa_not_found(self): """PUT /isms/soa/{id} should 404 for unknown entry.""" r = client.put( "/isms/soa/fake", json={"implementation_status": "implemented"}, params={"updated_by": "a"}, ) assert r.status_code == 404 def test_approve_soa_entry(self): """POST /isms/soa/{id}/approve should approve an SoA entry.""" create = client.post("/isms/soa", json=_soa_payload(), params={"created_by": "a"}) eid = create.json()["id"] r = client.post( f"/isms/soa/{eid}/approve", json={"reviewed_by": "cto@bp.de", "approved_by": "ceo@bp.de"}, ) assert r.status_code == 200 assert r.json()["approved_by"] == "ceo@bp.de" # ============================================================================= # Test: Audit Findings # ============================================================================= class TestAuditFindingsCRUD: """Tests for Audit Finding endpoints.""" def test_create_finding(self): """POST /isms/findings should create a finding with auto-generated ID.""" r = client.post("/isms/findings", json=_finding_payload()) assert r.status_code == 200 body = r.json() assert body["finding_id"].startswith("FIND-") assert body["status"] == "open" def test_list_findings(self): """GET /isms/findings should list all findings.""" client.post("/isms/findings", json=_finding_payload()) client.post("/isms/findings", json=_finding_payload(finding_type="major", title="Major finding")) r = client.get("/isms/findings") assert r.status_code == 200 assert r.json()["total"] == 2 assert r.json()["major_count"] == 1 assert r.json()["minor_count"] == 1 def test_update_finding(self): """PUT /isms/findings/{id} should update a finding.""" create = client.post("/isms/findings", json=_finding_payload()) fid = create.json()["id"] r = client.put( f"/isms/findings/{fid}", json={"root_cause": "Missing documentation process"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["root_cause"] == "Missing documentation process" def test_update_finding_not_found(self): """PUT /isms/findings/{id} should 404 for unknown finding.""" r = client.put( "/isms/findings/fake", json={"root_cause": "x"}, params={"updated_by": "a"}, ) assert r.status_code == 404 def test_close_finding_no_capas(self): """POST /isms/findings/{id}/close should succeed if no CAPAs exist.""" create = client.post("/isms/findings", json=_finding_payload()) fid = create.json()["id"] r = client.post( f"/isms/findings/{fid}/close", json={ "closure_notes": "Verified corrected", "closed_by": "auditor@cert.de", "verification_method": "Document review", "verification_evidence": "Updated schedule approved", }, ) assert r.status_code == 200 assert r.json()["status"] == "closed" def test_close_finding_not_found(self): """POST /isms/findings/{id}/close should 404 for unknown finding.""" r = client.post( "/isms/findings/fake/close", json={ "closure_notes": "x", "closed_by": "a", "verification_method": "x", "verification_evidence": "x", }, ) assert r.status_code == 404 # ============================================================================= # Test: Management Reviews # ============================================================================= class TestManagementReviewCRUD: """Tests for Management Review endpoints.""" def test_create_management_review(self): """POST /isms/management-reviews should create a review.""" r = client.post( "/isms/management-reviews", json=_mgmt_review_payload(), params={"created_by": "iso@bp.de"}, ) assert r.status_code == 200 body = r.json() assert body["review_id"].startswith("MR-") assert body["status"] == "draft" def test_list_management_reviews(self): """GET /isms/management-reviews should list reviews.""" client.post("/isms/management-reviews", json=_mgmt_review_payload(), params={"created_by": "a"}) r = client.get("/isms/management-reviews") assert r.status_code == 200 assert r.json()["total"] == 1 def test_get_management_review(self): """GET /isms/management-reviews/{id} should return a review.""" create = client.post("/isms/management-reviews", json=_mgmt_review_payload(), params={"created_by": "a"}) rid = create.json()["id"] r = client.get(f"/isms/management-reviews/{rid}") assert r.status_code == 200 assert r.json()["chairperson"] == "ceo@breakpilot.de" def test_get_management_review_not_found(self): """GET /isms/management-reviews/{id} should 404 for unknown review.""" r = client.get("/isms/management-reviews/fake") assert r.status_code == 404 def test_update_management_review(self): """PUT /isms/management-reviews/{id} should update a review.""" create = client.post("/isms/management-reviews", json=_mgmt_review_payload(), params={"created_by": "a"}) rid = create.json()["id"] r = client.put( f"/isms/management-reviews/{rid}", json={"input_previous_actions": "All actions completed", "status": "conducted"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["input_previous_actions"] == "All actions completed" def test_approve_management_review(self): """POST /isms/management-reviews/{id}/approve should approve.""" create = client.post("/isms/management-reviews", json=_mgmt_review_payload(), params={"created_by": "a"}) rid = create.json()["id"] r = client.post( f"/isms/management-reviews/{rid}/approve", json={ "approved_by": "ceo@bp.de", "next_review_date": "2026-07-01", }, ) assert r.status_code == 200 assert r.json()["status"] == "approved" assert r.json()["approved_by"] == "ceo@bp.de" # ============================================================================= # Test: Internal Audits # ============================================================================= class TestInternalAuditCRUD: """Tests for Internal Audit endpoints.""" def test_create_internal_audit(self): """POST /isms/internal-audits should create an audit.""" r = client.post( "/isms/internal-audits", json=_internal_audit_payload(), params={"created_by": "iso@bp.de"}, ) assert r.status_code == 200 body = r.json() assert body["audit_id"].startswith("IA-") assert body["status"] == "planned" def test_list_internal_audits(self): """GET /isms/internal-audits should list audits.""" client.post("/isms/internal-audits", json=_internal_audit_payload(), params={"created_by": "a"}) r = client.get("/isms/internal-audits") assert r.status_code == 200 assert r.json()["total"] == 1 def test_update_internal_audit(self): """PUT /isms/internal-audits/{id} should update an audit.""" create = client.post("/isms/internal-audits", json=_internal_audit_payload(), params={"created_by": "a"}) aid = create.json()["id"] r = client.put( f"/isms/internal-audits/{aid}", json={"status": "in_progress", "actual_start_date": "2026-03-01"}, params={"updated_by": "iso@bp.de"}, ) assert r.status_code == 200 assert r.json()["status"] == "in_progress" def test_update_internal_audit_not_found(self): """PUT /isms/internal-audits/{id} should 404 for unknown audit.""" r = client.put( "/isms/internal-audits/fake", json={"status": "in_progress"}, params={"updated_by": "a"}, ) assert r.status_code == 404 def test_complete_internal_audit(self): """POST /isms/internal-audits/{id}/complete should complete an audit.""" create = client.post("/isms/internal-audits", json=_internal_audit_payload(), params={"created_by": "a"}) aid = create.json()["id"] r = client.post( f"/isms/internal-audits/{aid}/complete", json={ "audit_conclusion": "Overall conforming with minor observations", "overall_assessment": "conforming", "follow_up_audit_required": False, }, params={"completed_by": "auditor@bp.de"}, ) assert r.status_code == 200 assert r.json()["status"] == "completed" assert r.json()["follow_up_audit_required"] is False def test_complete_internal_audit_not_found(self): """POST /isms/internal-audits/{id}/complete should 404 for unknown audit.""" r = client.post( "/isms/internal-audits/fake/complete", json={ "audit_conclusion": "x", "overall_assessment": "conforming", "follow_up_audit_required": False, }, params={"completed_by": "a"}, ) assert r.status_code == 404 # ============================================================================= # Test: Readiness Check # ============================================================================= class TestReadinessCheck: """Tests for the ISMS Readiness Check endpoint.""" def test_readiness_check_empty_isms(self): """POST /isms/readiness-check on empty DB should show not_ready.""" r = client.post("/isms/readiness-check", json={"triggered_by": "test"}) assert r.status_code == 200 body = r.json() assert body["certification_possible"] is False assert body["overall_status"] == "not_ready" assert len(body["potential_majors"]) > 0 def test_readiness_check_latest_not_found(self): """GET /isms/readiness-check/latest should 404 when no check has run.""" r = client.get("/isms/readiness-check/latest") assert r.status_code == 404 def test_readiness_check_latest_returns_most_recent(self): """GET /isms/readiness-check/latest should return last check.""" client.post("/isms/readiness-check", json={"triggered_by": "first"}) client.post("/isms/readiness-check", json={"triggered_by": "second"}) r = client.get("/isms/readiness-check/latest") assert r.status_code == 200 assert r.json()["triggered_by"] == "second" # ============================================================================= # Test: Overview / Dashboard # ============================================================================= class TestOverviewDashboard: """Tests for the ISO 27001 overview endpoint.""" def test_overview_empty_isms(self): """GET /isms/overview on empty DB should return not_started with 0% readiness.""" r = client.get("/isms/overview") assert r.status_code == 200 body = r.json() assert body["overall_status"] == "not_started" assert body["certification_readiness"] == 0.0 assert body["scope_approved"] is False assert body["open_major_findings"] == 0 assert body["policies_count"] == 0 # All chapters should show 0% on empty DB for ch in body["chapters"]: assert ch["completion_percentage"] == 0.0, f"Chapter {ch['chapter']} should be 0% on empty DB" def test_overview_with_data(self): """GET /isms/overview should reflect created data.""" # Create and approve a scope scope = client.post("/isms/scope", json=_scope_payload(), params={"created_by": "a"}) client.post( f"/isms/scope/{scope.json()['id']}/approve", json={"approved_by": "ceo@bp.de", "effective_date": "2026-01-01", "review_date": "2027-01-01"}, ) # Create a policy client.post("/isms/policies", json=_policy_payload()) # Create an objective client.post("/isms/objectives", json=_objective_payload(), params={"created_by": "a"}) r = client.get("/isms/overview") assert r.status_code == 200 body = r.json() assert body["scope_approved"] is True assert body["policies_count"] == 1 assert body["objectives_count"] == 1 # ============================================================================= # Test: Audit Trail # ============================================================================= class TestAuditTrail: """Tests for the Audit Trail endpoint.""" def test_audit_trail_records_actions(self): """Creating entities should generate audit trail entries.""" client.post("/isms/scope", json=_scope_payload(), params={"created_by": "admin@bp.de"}) client.post("/isms/policies", json=_policy_payload()) r = client.get("/isms/audit-trail") assert r.status_code == 200 assert r.json()["total"] >= 2 def test_audit_trail_filter_by_entity_type(self): """GET /isms/audit-trail?entity_type=isms_policy should filter.""" client.post("/isms/scope", json=_scope_payload(), params={"created_by": "a"}) client.post("/isms/policies", json=_policy_payload()) r = client.get("/isms/audit-trail", params={"entity_type": "isms_policy"}) assert r.status_code == 200 for entry in r.json()["entries"]: assert entry["entity_type"] == "isms_policy" def test_audit_trail_pagination(self): """GET /isms/audit-trail should support pagination.""" # Create several entries for i in range(5): client.post("/isms/policies", json=_policy_payload(f"POL-PAGI-{i:03d}")) r = client.get("/isms/audit-trail", params={"page": 1, "page_size": 2}) assert r.status_code == 200 assert len(r.json()["entries"]) == 2 assert r.json()["pagination"]["has_next"] is True