Files
breakpilot-compliance/backend-compliance/compliance/tests/test_isms_routes.py
Sharang Parnerkar 3320ef94fc refactor: phase 0 guardrails + phase 1 step 2 (models.py split)
Squash of branch refactor/phase0-guardrails-and-models-split — 4 commits,
81 files, 173/173 pytest green, OpenAPI contract preserved (360 paths /
484 operations).

## Phase 0 — Architecture guardrails

Three defense-in-depth layers to keep the architecture rules enforced
regardless of who opens Claude Code in this repo:

  1. .claude/settings.json PreToolUse hook on Write/Edit blocks any file
     that would exceed the 500-line hard cap. Auto-loads in every Claude
     session in this repo.
  2. scripts/githooks/pre-commit (install via scripts/install-hooks.sh)
     enforces the LOC cap locally, freezes migrations/ without
     [migration-approved], and protects guardrail files without
     [guardrail-change].
  3. .gitea/workflows/ci.yaml gains loc-budget + guardrail-integrity +
     sbom-scan (syft+grype) jobs, adds mypy --strict for the new Python
     packages (compliance/{services,repositories,domain,schemas}), and
     tsc --noEmit for admin-compliance + developer-portal.

Per-language conventions documented in AGENTS.python.md, AGENTS.go.md,
AGENTS.typescript.md at the repo root — layering, tooling, and explicit
"what you may NOT do" lists. Root CLAUDE.md is prepended with the six
non-negotiable rules. Each of the 10 services gets a README.md.

scripts/check-loc.sh enforces soft 300 / hard 500 and surfaces the
current baseline of 205 hard + 161 soft violations so Phases 1-4 can
drain it incrementally. CI gates only CHANGED files in PRs so the
legacy baseline does not block unrelated work.

## Deprecation sweep

47 files. Pydantic V1 regex= -> pattern= (2 sites), class Config ->
ConfigDict in source_policy_router.py (schemas.py intentionally skipped;
it is the Phase 1 Step 3 split target). datetime.utcnow() ->
datetime.now(timezone.utc) everywhere including SQLAlchemy default=
callables. All DB columns already declare timezone=True, so this is a
latent-bug fix at the Python side, not a schema change.

DeprecationWarning count dropped from 158 to 35.

## Phase 1 Step 1 — Contract test harness

tests/contracts/test_openapi_baseline.py diffs the live FastAPI /openapi.json
against tests/contracts/openapi.baseline.json on every test run. Fails on
removed paths, removed status codes, or new required request body fields.
Regenerate only via tests/contracts/regenerate_baseline.py after a
consumer-updated contract change. This is the safety harness for all
subsequent refactor commits.

## Phase 1 Step 2 — models.py split (1466 -> 85 LOC shim)

compliance/db/models.py is decomposed into seven sibling aggregate modules
following the existing repo pattern (dsr_models.py, vvt_models.py, ...):

  regulation_models.py       (134) — Regulation, Requirement
  control_models.py          (279) — Control, Mapping, Evidence, Risk
  ai_system_models.py        (141) — AISystem, AuditExport
  service_module_models.py   (176) — ServiceModule, ModuleRegulation, ModuleRisk
  audit_session_models.py    (177) — AuditSession, AuditSignOff
  isms_governance_models.py  (323) — ISMSScope, Context, Policy, Objective, SoA
  isms_audit_models.py       (468) — Finding, CAPA, MgmtReview, InternalAudit,
                                     AuditTrail, Readiness

models.py becomes an 85-line re-export shim in dependency order so
existing imports continue to work unchanged. Schema is byte-identical:
__tablename__, column definitions, relationship strings, back_populates,
cascade directives all preserved.

All new sibling files are under the 500-line hard cap; largest is
isms_audit_models.py at 468. No file in compliance/db/ now exceeds
the hard cap.

## Phase 1 Step 3 — infrastructure only

backend-compliance/compliance/{schemas,domain,repositories}/ packages
are created as landing zones with docstrings. compliance/domain/
exports DomainError / NotFoundError / ConflictError / ValidationError /
PermissionError — the base classes services will use to raise
domain-level errors instead of HTTPException.

PHASE1_RUNBOOK.md at backend-compliance/PHASE1_RUNBOOK.md documents
the nine-step execution plan for Phase 1: snapshot baseline,
characterization tests, split models.py (this commit), split schemas.py
(next), extract services, extract repositories, mypy --strict, coverage.

## Verification

  backend-compliance/.venv-phase1: uv python install 3.12 + pip -r requirements.txt
  PYTHONPATH=. pytest compliance/tests/ tests/contracts/
  -> 173 passed, 0 failed, 35 warnings, OpenAPI 360/484 unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:18:29 +02:00

697 lines
27 KiB
Python

"""
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