# #region Test.CleanRelease.PreparationService [C:3] [TYPE Module] [SEMANTICS test,clean-release,preparation,policy,manifest] # @BRIEF Tests for prepare_candidate — policy evaluation, manifest creation, status transitions, violation handling. # @RELATION BINDS_TO -> [PreparationService] # @TEST_EDGE: missing_candidate -> raises ValueError # @TEST_EDGE: missing_policy -> raises ValueError # @TEST_EDGE: missing_registry -> raises ValueError # @TEST_EDGE: invalid_policy -> raises ValueError # @TEST_EDGE: clean_preparation -> PREPARED status, manifest created # @TEST_EDGE: preparation_with_violations -> BLOCKED status # @TEST_EDGE: preparation_with_back_transition -> still PREPARED # @TEST_EDGE: legacy_wrapper from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest from src.models.clean_release import ( CleanPolicySnapshot, ReleaseCandidate, SourceRegistrySnapshot, ) from src.services.clean_release.enums import CandidateStatus from src.services.clean_release.repository import CleanReleaseRepository from src.services.clean_release.preparation_service import prepare_candidate, prepare_candidate_legacy def _make_repo_with_policy_registry( policy_registry_ref: str = "registry-prep-1", policy_content: dict | None = None, ) -> CleanReleaseRepository: """Create a repository with candidate, policy, and registry.""" repo = CleanReleaseRepository() repo.save_candidate(ReleaseCandidate( id="cand-prep-1", version="1.0.0", source_snapshot_ref="git:sha1", created_by="tester", created_at=datetime.now(UTC), status=CandidateStatus.DRAFT.value, )) repo.save_candidate(ReleaseCandidate( id="cand-prep-transitioned", version="1.0.0", source_snapshot_ref="git:sha2", created_by="tester", created_at=datetime.now(UTC), status=CandidateStatus.DRAFT.value, )) repo.save_policy(CleanPolicySnapshot( id="policy-prep-1", policy_id="policy-prep-1", policy_version="1", content_json=policy_content or { "profile": "enterprise-clean", "prohibited_artifact_categories": ["malware"], "required_system_categories": [], "external_source_forbidden": True, }, registry_snapshot_id=policy_registry_ref, immutable=True, )) repo.save_registry(SourceRegistrySnapshot( id=policy_registry_ref, registry_id=policy_registry_ref, registry_version="1", allowed_hosts=["internal.local"], immutable=True, )) return repo class TestPrepareCandidate: """prepare_candidate — full preparation flow.""" # #region test_missing_candidate_raises [C:2] [TYPE Function] def test_missing_candidate_raises(self): """Missing candidate → ValueError.""" repo = CleanReleaseRepository() with pytest.raises(ValueError, match="not found"): prepare_candidate( repository=repo, candidate_id="nonexistent", artifacts=[], sources=[], operator_id="tester", ) # #endregion test_missing_candidate_raises # #region test_missing_policy_raises [C:2] [TYPE Function] def test_missing_policy_raises(self): """Missing active policy → ValueError.""" repo = CleanReleaseRepository() repo.save_candidate(ReleaseCandidate( id="cand-nopolicy", version="1.0.0", source_snapshot_ref="ref", created_by="tester", created_at=datetime.now(UTC), status=CandidateStatus.DRAFT.value, )) with pytest.raises(ValueError, match="policy"): prepare_candidate( repository=repo, candidate_id="cand-nopolicy", artifacts=[], sources=[], operator_id="tester", ) # #endregion test_missing_policy_raises # #region test_clean_preparation [C:2] [TYPE Function] def test_clean_preparation(self): """Clean artifacts → PREPARED status, manifest created.""" repo = _make_repo_with_policy_registry() artifacts = [ {"path": "bin/app", "category": "generic", "reason": "test", "checksum": "abc"}, ] result = prepare_candidate( repository=repo, candidate_id="cand-prep-1", artifacts=artifacts, sources=["internal.local"], operator_id="tester", ) assert result["status"] == "PREPARED" assert result["candidate_id"] == "cand-prep-1" assert result["manifest_id"] is not None assert result["violations"] == [] assert "prepared_at" in result candidate = repo.get_candidate("cand-prep-1") assert candidate.status == CandidateStatus.PREPARED.value assert len(repo.manifests) == 1 # #endregion test_clean_preparation # #region test_preparation_with_violations [C:2] [TYPE Function] def test_preparation_with_violations(self): """Artifacts with prohibited category → BLOCKED status.""" repo = _make_repo_with_policy_registry() artifacts = [ {"path": "lib/bad.exe", "category": "malware", "reason": "test", "checksum": "bad"}, ] result = prepare_candidate( repository=repo, candidate_id="cand-prep-1", artifacts=artifacts, sources=["internal.local"], operator_id="tester", ) assert result["status"] == "CHECK_BLOCKED" assert len(result["violations"]) == 1 assert result["violations"][0]["category"] == "data-purity" # #endregion test_preparation_with_violations # #region test_preparation_with_external_source_violation [C:2] [TYPE Function] def test_preparation_with_external_source_violation(self): """External source → BLOCKED status.""" repo = _make_repo_with_policy_registry() artifacts = [ {"path": "bin/app", "category": "generic", "reason": "test", "checksum": "abc"}, ] result = prepare_candidate( repository=repo, candidate_id="cand-prep-1", artifacts=artifacts, sources=["external.bad.com"], operator_id="tester", ) assert result["status"] == "CHECK_BLOCKED" assert len(result["violations"]) == 1 assert result["violations"][0]["category"] == "external-source" # #endregion test_preparation_with_external_source_violation # #region test_preparation_legacy_wrapper [C:2] [TYPE Function] def test_preparation_legacy_wrapper(self): """prepare_candidate_legacy delegates to prepare_candidate.""" repo = _make_repo_with_policy_registry() artifacts = [ {"path": "bin/app", "category": "generic", "reason": "test", "checksum": "abc"}, ] result_legacy = prepare_candidate_legacy( repository=repo, candidate_id="cand-prep-transitioned", artifacts=artifacts, sources=["internal.local"], operator_id="tester", ) result_canonical = prepare_candidate( repository=repo, candidate_id="cand-prep-transitioned", artifacts=artifacts, sources=["internal.local"], operator_id="tester", ) assert result_legacy["status"] == result_canonical["status"] # #endregion test_preparation_legacy_wrapper # #region test_registry_not_found_raises [C:2] [TYPE Function] def test_registry_not_found_raises(self): """Registry lookup returns None → ValueError (line 44).""" from src.services.clean_release.repository import CleanReleaseRepository from src.models.clean_release import ReleaseCandidate, CleanPolicySnapshot from src.services.clean_release.enums import CandidateStatus repo = CleanReleaseRepository() repo.save_candidate(ReleaseCandidate( id="cand-noregistry", version="1.0.0", source_snapshot_ref="git:sha1", created_by="tester", created_at=datetime.now(UTC), status=CandidateStatus.DRAFT.value, )) # Policy references a registry_id that does not exist in repository repo.save_policy(CleanPolicySnapshot( id="policy-noreg", policy_id="policy-noreg", policy_version="1", content_json={ "profile": "enterprise-clean", "prohibited_artifact_categories": ["malware"], "required_system_categories": [], "external_source_forbidden": True, }, registry_snapshot_id="registry-nonexistent", immutable=True, )) with pytest.raises(ValueError, match="Registry not found"): prepare_candidate( repository=repo, candidate_id="cand-noregistry", artifacts=[{"path": "bin/app", "category": "generic", "reason": "test", "checksum": "abc"}], sources=[], operator_id="tester", ) # #endregion test_registry_not_found_raises # #region test_invalid_policy_raises [C:2] [TYPE Function] def test_invalid_policy_raises(self): """Policy validation fails → ValueError (line 49).""" from src.services.clean_release.repository import CleanReleaseRepository from src.models.clean_release import ReleaseCandidate, CleanPolicySnapshot, SourceRegistrySnapshot from src.services.clean_release.enums import CandidateStatus repo = CleanReleaseRepository() repo.save_candidate(ReleaseCandidate( id="cand-badpolicy", version="1.0.0", source_snapshot_ref="git:sha1", created_by="tester", created_at=datetime.now(UTC), status=CandidateStatus.DRAFT.value, )) # Create policy with missing profile settings to make validate_policy fail repo.save_policy(CleanPolicySnapshot( id="policy-invalid", policy_id="policy-invalid", policy_version="1", content_json={ "profile": "enterprise-clean", # Missing prohibited_artifact_categories — should fail validation "external_source_forbidden": True, }, registry_snapshot_id="registry-invalid", immutable=True, )) repo.save_registry(SourceRegistrySnapshot( id="registry-invalid", registry_id="registry-invalid", registry_version="1", allowed_hosts=["internal.local"], immutable=True, )) with pytest.raises(ValueError, match="Invalid policy"): prepare_candidate( repository=repo, candidate_id="cand-badpolicy", artifacts=[{"path": "bin/app", "category": "generic", "reason": "test", "checksum": "abc"}], sources=[], operator_id="tester", ) # #endregion test_invalid_policy_raises