Files
breakpilot-compliance/backend-compliance/tests/test_isms_routes.py
Benjamin Admin 56758e8b55
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 29s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
fix(mock-data): Fake-Daten bei leerer DB entfernt — ISMS 0%, Dashboard keine simulierten Trends, Compliance-Hub keine Fallback-Zahlen
- ISMS Overview: 14% → 0% bei leerer DB, "not_started" Status, alle Kapitel 0%
- Dashboard: 12-Monate simulierte Trend-Historie entfernt
- Compliance-Hub: Hardcoded Fallback-Statistiken (474/180/95/120/79/44/558/19) → 0
- SQLAlchemy Bug: `is not None` → `.isnot(None)` in SoA-Query
- Hardcoded chapter_7/8_status="pass" → berechnet aus Findings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:25:25 +01:00

891 lines
35 KiB
Python

"""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
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.utcnow().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