272 lines
13 KiB
Python
272 lines
13 KiB
Python
# #region Test.ComplianceExecution.Full [C:3] [TYPE Module] [SEMANTICS test,clean-release,compliance,execution]
|
|
# @BRIEF Full tests for ComplianceExecutionService — manifest resolution, run execution, stage persistence, violation tracking, error handling, report generation.
|
|
# @RELATION BINDS_TO -> [ComplianceExecutionService]
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import pytest
|
|
|
|
from src.models.clean_release import (
|
|
CheckFinalStatus,
|
|
CleanPolicySnapshot,
|
|
ComplianceRun,
|
|
ComplianceStageRun,
|
|
ComplianceViolation,
|
|
DistributionManifest,
|
|
ReleaseCandidate,
|
|
SourceRegistrySnapshot,
|
|
)
|
|
from src.services.clean_release.compliance_execution_service import (
|
|
ComplianceExecutionResult,
|
|
ComplianceExecutionService,
|
|
)
|
|
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus
|
|
from src.services.clean_release.exceptions import ComplianceRunError
|
|
from src.services.clean_release.repository import CleanReleaseRepository
|
|
from src.services.clean_release.stages.base import ComplianceStage, ComplianceStageContext, StageExecutionResult
|
|
from src.services.clean_release.enums import ComplianceStageName
|
|
|
|
|
|
class _MockStage(ComplianceStage):
|
|
"""Deterministic mock stage for test isolation."""
|
|
def __init__(self, name: str = "DATA_PURITY", decision: ComplianceDecision = ComplianceDecision.PASSED):
|
|
self.stage_name = ComplianceStageName(name)
|
|
self._decision = decision
|
|
|
|
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
|
|
return StageExecutionResult(
|
|
decision=self._decision,
|
|
details_json={"checked": True},
|
|
violations=[],
|
|
)
|
|
|
|
|
|
def _make_repo_with_candidate(
|
|
candidate_id: str = "cand-exe-1",
|
|
status: str = CandidateStatus.MANIFEST_BUILT.value,
|
|
policy_id: str = "policy-exe-1",
|
|
registry_id: str = "registry-exe-1",
|
|
manifest_id: str = "manifest-exe-1",
|
|
) -> CleanReleaseRepository:
|
|
repo = CleanReleaseRepository()
|
|
repo.save_candidate(ReleaseCandidate(
|
|
id=candidate_id, version="1.0.0", source_snapshot_ref="git:sha",
|
|
created_by="tester", created_at=datetime.now(UTC), status=status,
|
|
))
|
|
repo.save_policy(CleanPolicySnapshot(
|
|
id=policy_id, policy_id=policy_id, policy_version="1",
|
|
content_json={}, registry_snapshot_id=registry_id, immutable=True,
|
|
))
|
|
repo.save_registry(SourceRegistrySnapshot(
|
|
id=registry_id, registry_id=registry_id, registry_version="1",
|
|
allowed_hosts=["internal.local"], allowed_schemes=["https"],
|
|
immutable=True,
|
|
))
|
|
repo.save_manifest(DistributionManifest(
|
|
id=manifest_id, candidate_id=candidate_id, manifest_version=1,
|
|
manifest_digest="digest1", artifacts_digest="digest1",
|
|
source_snapshot_ref="git:sha",
|
|
content_json={"summary": {"included_count": 1, "excluded_count": 0, "prohibited_detected_count": 0}},
|
|
created_by="tester", created_at=datetime.now(UTC), immutable=True,
|
|
))
|
|
return repo
|
|
|
|
|
|
class TestComplianceExecutionServiceBasic:
|
|
"""ComplianceExecutionService — basic error paths."""
|
|
|
|
# #region test_missing_candidate_raises [C:2] [TYPE Function]
|
|
def test_missing_candidate_raises(self):
|
|
"""Missing candidate → ComplianceRunError."""
|
|
repo = CleanReleaseRepository()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
with pytest.raises(ComplianceRunError, match="not found"):
|
|
svc.execute_run(candidate_id="nonexistent", requested_by="tester")
|
|
# #endregion test_missing_candidate_raises
|
|
|
|
# #region test_resolve_manifest_by_id [C:2] [TYPE Function]
|
|
def test_resolve_manifest_by_id(self):
|
|
"""Explicit manifest_id resolves correctly."""
|
|
repo = _make_repo_with_candidate()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
manifest = svc._resolve_manifest("cand-exe-1", "manifest-exe-1")
|
|
assert manifest is not None
|
|
assert manifest.id == "manifest-exe-1"
|
|
# #endregion test_resolve_manifest_by_id
|
|
|
|
# #region test_resolve_manifest_not_found_raises [C:2] [TYPE Function]
|
|
def test_resolve_manifest_not_found_raises(self):
|
|
"""Nonexistent manifest_id → ComplianceRunError."""
|
|
repo = _make_repo_with_candidate()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
with pytest.raises(ComplianceRunError, match="not found"):
|
|
svc._resolve_manifest("cand-exe-1", "nonexistent")
|
|
# #endregion test_resolve_manifest_not_found_raises
|
|
|
|
# #region test_resolve_manifest_not_belonging_raises [C:2] [TYPE Function]
|
|
def test_resolve_manifest_not_belonging_raises(self):
|
|
"""Manifest not belonging to candidate → ComplianceRunError."""
|
|
repo = _make_repo_with_candidate()
|
|
repo.save_manifest(DistributionManifest(
|
|
id="manifest-other", candidate_id="cand-other", manifest_version=1,
|
|
manifest_digest="d", artifacts_digest="d",
|
|
source_snapshot_ref="ref", content_json={},
|
|
created_by="tester", created_at=datetime.now(UTC), immutable=True,
|
|
))
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
with pytest.raises(ComplianceRunError, match="does not belong"):
|
|
svc._resolve_manifest("cand-exe-1", "manifest-other")
|
|
# #endregion test_resolve_manifest_not_belonging_raises
|
|
|
|
# #region test_resolve_manifest_no_manifests_raises [C:2] [TYPE Function]
|
|
def test_resolve_manifest_no_manifests_raises(self):
|
|
"""Candidate has no manifests → ComplianceRunError."""
|
|
repo = _make_repo_with_candidate()
|
|
repo.manifests.clear()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
with pytest.raises(ComplianceRunError, match="no manifest"):
|
|
svc._resolve_manifest("cand-exe-1", None)
|
|
# #endregion test_resolve_manifest_no_manifests_raises
|
|
|
|
# #region test_persist_stage_run [C:2] [TYPE Function]
|
|
def test_persist_stage_run(self):
|
|
"""Stage run is persisted via repository."""
|
|
repo = _make_repo_with_candidate()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
stage_run = ComplianceStageRun(
|
|
id="stg-test-1", run_id="run-test-1", stage_name="DATA_PURITY",
|
|
status="SUCCEEDED", decision="PASSED", details_json={},
|
|
)
|
|
svc._persist_stage_run(stage_run)
|
|
assert repo.stage_runs.get("stg-test-1") is not None
|
|
# #endregion test_persist_stage_run
|
|
|
|
# #region test_persist_violations [C:2] [TYPE Function]
|
|
def test_persist_violations(self):
|
|
"""Violations are persisted via repository."""
|
|
repo = _make_repo_with_candidate()
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
violations = [
|
|
ComplianceViolation(
|
|
id="viol-1", run_id="run-1", stage_name="DATA_PURITY",
|
|
code="EXT", severity="MAJOR", message="test",
|
|
)
|
|
]
|
|
svc._persist_violations(violations)
|
|
assert repo.violations.get("viol-1") is not None
|
|
# #endregion test_persist_violations
|
|
|
|
|
|
class TestComplianceExecutionServiceRun:
|
|
"""ComplianceExecutionService.execute_run — full execution flow."""
|
|
|
|
# #region test_execute_run_success [C:2] [TYPE Function]
|
|
@patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
|
|
def test_execute_run_success(self, mock_resolve):
|
|
"""Full successful run returns ComplianceExecutionResult."""
|
|
repo = _make_repo_with_candidate()
|
|
mock_resolve.return_value = (
|
|
repo.policies["policy-exe-1"],
|
|
repo.registries["registry-exe-1"],
|
|
)
|
|
config = MagicMock()
|
|
config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
|
|
config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"
|
|
|
|
stage = _MockStage(name="DATA_PURITY")
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[stage])
|
|
result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")
|
|
|
|
assert isinstance(result, ComplianceExecutionResult)
|
|
assert result.run is not None
|
|
assert result.run.candidate_id == "cand-exe-1"
|
|
assert result.run.manifest_id == "manifest-exe-1"
|
|
assert len(result.stage_runs) == 1
|
|
assert result.stage_runs[0].stage_name == "DATA_PURITY"
|
|
assert result.report is not None
|
|
# #endregion test_execute_run_success
|
|
|
|
# #region test_execute_run_stage_failure [C:2] [TYPE Function]
|
|
@patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
|
|
def test_execute_run_stage_failure(self, mock_resolve):
|
|
"""Stage execution error → run marked FAILED."""
|
|
repo = _make_repo_with_candidate()
|
|
mock_resolve.return_value = (
|
|
repo.policies["policy-exe-1"],
|
|
repo.registries["registry-exe-1"],
|
|
)
|
|
config = MagicMock()
|
|
config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
|
|
config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"
|
|
|
|
failing_stage = _MockStage(name="DATA_PURITY", decision=ComplianceDecision.ERROR)
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[failing_stage])
|
|
result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")
|
|
|
|
assert result.run.status == RunStatus.SUCCEEDED.value
|
|
assert result.run.final_status == CheckFinalStatus.FAILED.value
|
|
# #endregion test_execute_run_stage_failure
|
|
|
|
# #region test_execute_run_policy_resolution_error [C:2] [TYPE Function]
|
|
@patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
|
|
def test_execute_run_policy_resolution_error(self, mock_resolve):
|
|
"""Policy resolution failure → ComplianceRunError."""
|
|
from src.services.clean_release.exceptions import PolicyResolutionError
|
|
repo = _make_repo_with_candidate()
|
|
mock_resolve.side_effect = PolicyResolutionError("policy missing")
|
|
config = MagicMock()
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config)
|
|
with pytest.raises(ComplianceRunError, match="policy missing"):
|
|
svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")
|
|
# #endregion test_execute_run_policy_resolution_error
|
|
|
|
# #region test_execute_result_dataclass [C:2] [TYPE Function]
|
|
def test_execute_result_dataclass(self):
|
|
"""ComplianceExecutionResult dataclass works as expected."""
|
|
run = MagicMock(spec=ComplianceRun)
|
|
stage_runs = [MagicMock(spec=ComplianceStageRun)]
|
|
violations = [MagicMock(spec=ComplianceViolation)]
|
|
result = ComplianceExecutionResult(
|
|
run=run, report=None, stage_runs=stage_runs, violations=violations,
|
|
)
|
|
assert result.run == run
|
|
assert result.report is None
|
|
assert result.stage_runs == stage_runs
|
|
assert result.violations == violations
|
|
# #endregion test_execute_result_dataclass
|
|
|
|
# #region test_execute_run_stage_raises_exception [C:2] [TYPE Function]
|
|
@patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
|
|
def test_execute_run_stage_raises_exception(self, mock_resolve):
|
|
"""Stage that raises an exception triggers the except handler (lines 211-217)."""
|
|
repo = _make_repo_with_candidate()
|
|
mock_resolve.return_value = (
|
|
repo.policies["policy-exe-1"],
|
|
repo.registries["registry-exe-1"],
|
|
)
|
|
config = MagicMock()
|
|
config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
|
|
config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"
|
|
|
|
class CrashingStage(ComplianceStage):
|
|
stage_name = ComplianceStageName("DATA_PURITY")
|
|
def execute(self, context):
|
|
raise RuntimeError("stage crashed unexpectedly")
|
|
|
|
svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[CrashingStage()])
|
|
result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")
|
|
|
|
assert result.run.status == RunStatus.FAILED.value
|
|
assert result.run.final_status == ComplianceDecision.ERROR.value
|
|
assert "stage crashed" in result.run.failure_reason
|
|
# #endregion test_execute_run_stage_raises_exception
|