174 lines
6.5 KiB
Python
174 lines
6.5 KiB
Python
# [DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]
|
|
# @TIER: CRITICAL
|
|
# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization
|
|
# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.
|
|
# @LAYER: Tests
|
|
# @RELATION: TESTS -> backend.src.services.clean_release.compliance_orchestrator
|
|
# @RELATION: TESTS -> backend.src.services.clean_release.report_builder
|
|
# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from src.models.clean_release import (
|
|
CleanPolicySnapshot,
|
|
ComplianceDecision,
|
|
DistributionManifest,
|
|
ReleaseCandidate,
|
|
SourceRegistrySnapshot,
|
|
)
|
|
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
|
from src.services.clean_release.enums import CandidateStatus, RunStatus
|
|
from src.services.clean_release.report_builder import ComplianceReportBuilder
|
|
from src.services.clean_release.repository import CleanReleaseRepository
|
|
|
|
|
|
# [DEF:_seed_with_candidate_policy_registry:Function]
|
|
# @PURPOSE: Build deterministic repository state for run startup tests.
|
|
# @PRE: candidate_id and snapshot ids are non-empty.
|
|
# @POST: Returns repository with candidate, policy and registry; manifest is optional.
|
|
def _seed_with_candidate_policy_registry(
|
|
*,
|
|
with_manifest: bool,
|
|
prohibited_detected_count: int = 0,
|
|
) -> tuple[CleanReleaseRepository, str, str, str]:
|
|
repository = CleanReleaseRepository()
|
|
candidate_id = "cand-us2-1"
|
|
policy_id = "policy-us2-1"
|
|
registry_id = "registry-us2-1"
|
|
manifest_id = "manifest-us2-1"
|
|
|
|
repository.save_candidate(
|
|
ReleaseCandidate(
|
|
id=candidate_id,
|
|
version="1.0.0",
|
|
source_snapshot_ref="git:sha-us2",
|
|
created_by="tester",
|
|
created_at=datetime.now(timezone.utc),
|
|
status=CandidateStatus.MANIFEST_BUILT.value,
|
|
)
|
|
)
|
|
repository.save_registry(
|
|
SourceRegistrySnapshot(
|
|
id=registry_id,
|
|
registry_id="trusted-registry",
|
|
registry_version="1.0.0",
|
|
allowed_hosts=["repo.internal.local"],
|
|
allowed_schemes=["https"],
|
|
allowed_source_types=["repo"],
|
|
immutable=True,
|
|
)
|
|
)
|
|
repository.save_policy(
|
|
CleanPolicySnapshot(
|
|
id=policy_id,
|
|
policy_id="trusted-policy",
|
|
policy_version="1.0.0",
|
|
content_json={"rules": []},
|
|
registry_snapshot_id=registry_id,
|
|
immutable=True,
|
|
)
|
|
)
|
|
|
|
if with_manifest:
|
|
repository.save_manifest(
|
|
DistributionManifest(
|
|
id=manifest_id,
|
|
candidate_id=candidate_id,
|
|
manifest_version=1,
|
|
manifest_digest="digest-us2-1",
|
|
artifacts_digest="digest-us2-1",
|
|
source_snapshot_ref="git:sha-us2",
|
|
content_json={
|
|
"summary": {
|
|
"included_count": 1,
|
|
"excluded_count": 0 if prohibited_detected_count == 0 else prohibited_detected_count,
|
|
"prohibited_detected_count": prohibited_detected_count,
|
|
}
|
|
},
|
|
created_by="tester",
|
|
created_at=datetime.now(timezone.utc),
|
|
immutable=True,
|
|
)
|
|
)
|
|
|
|
return repository, candidate_id, policy_id, manifest_id
|
|
# [/DEF:_seed_with_candidate_policy_registry:Function]
|
|
|
|
|
|
# [DEF:test_run_without_manifest_rejected:Function]
|
|
# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.
|
|
# @PRE: Candidate/policy exist but manifest is missing.
|
|
# @POST: start_check_run raises ValueError and no run is persisted.
|
|
def test_run_without_manifest_rejected():
|
|
repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=False)
|
|
orchestrator = CleanComplianceOrchestrator(repository)
|
|
|
|
with pytest.raises(ValueError, match="Manifest or Policy not found"):
|
|
orchestrator.start_check_run(
|
|
candidate_id=candidate_id,
|
|
policy_id=policy_id,
|
|
requested_by="tester",
|
|
manifest_id=manifest_id,
|
|
)
|
|
|
|
assert len(repository.check_runs) == 0
|
|
# [/DEF:test_run_without_manifest_rejected:Function]
|
|
|
|
|
|
# [DEF:test_task_crash_mid_run_marks_failed:Function]
|
|
# @PURPOSE: Ensure execution crash conditions force FAILED run status.
|
|
# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.
|
|
# @POST: execute_stages persists run with FAILED status.
|
|
def test_task_crash_mid_run_marks_failed():
|
|
repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=True)
|
|
orchestrator = CleanComplianceOrchestrator(repository)
|
|
|
|
run = orchestrator.start_check_run(
|
|
candidate_id=candidate_id,
|
|
policy_id=policy_id,
|
|
requested_by="tester",
|
|
manifest_id=manifest_id,
|
|
)
|
|
|
|
# Simulate mid-run crash dependency loss: registry snapshot disappears.
|
|
repository.registries.clear()
|
|
|
|
failed = orchestrator.execute_stages(run)
|
|
assert failed.status == RunStatus.FAILED
|
|
# [/DEF:test_task_crash_mid_run_marks_failed:Function]
|
|
|
|
|
|
# [DEF:test_blocked_run_finalization_blocks_report_builder:Function]
|
|
# @PURPOSE: Ensure blocked runs require blocking violations before report creation.
|
|
# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.
|
|
# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.
|
|
def test_blocked_run_finalization_blocks_report_builder():
|
|
repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(
|
|
with_manifest=True,
|
|
prohibited_detected_count=1,
|
|
)
|
|
orchestrator = CleanComplianceOrchestrator(repository)
|
|
builder = ComplianceReportBuilder(repository)
|
|
|
|
run = orchestrator.start_check_run(
|
|
candidate_id=candidate_id,
|
|
policy_id=policy_id,
|
|
requested_by="tester",
|
|
manifest_id=manifest_id,
|
|
)
|
|
run = orchestrator.execute_stages(run)
|
|
run = orchestrator.finalize_run(run)
|
|
|
|
assert run.final_status == ComplianceDecision.BLOCKED
|
|
assert run.status == RunStatus.SUCCEEDED
|
|
|
|
with pytest.raises(ValueError, match="Blocked run requires at least one blocking violation"):
|
|
builder.build_report_payload(run, [])
|
|
# [/DEF:test_blocked_run_finalization_blocks_report_builder:Function]
|
|
|
|
# [/DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]
|