# #region Test.CleanRelease.DtoEnumsExceptions [C:2] [TYPE Module] [SEMANTICS test,clean-release,dto,enums,exceptions] # @BRIEF Tests for clean_release/dto.py, enums.py, exceptions.py. # @RELATION BINDS_TO -> [CleanReleaseDto.Enums.Exceptions] # @TEST_EDGE: missing_field -> optional fields default correctly # @TEST_EDGE: serialization -> round-trip via model_dump/model_validate # @TEST_EDGE: exception_hierarchy -> custom exceptions form proper chain from datetime import datetime, timezone import pytest from pydantic import ValidationError # ── DTOs ──────────────────────────────────────────────────── class TestCandidateDTO: """CandidateDTO — DTO for ReleaseCandidate.""" def test_minimal(self): from src.services.clean_release.dto import CandidateDTO from src.services.clean_release.enums import CandidateStatus ts = datetime.now(timezone.utc) dto = CandidateDTO( id="c-1", version="1.0.0", source_snapshot_ref="snap-1", created_at=ts, created_by="admin", status=CandidateStatus.DRAFT, ) assert dto.id == "c-1" assert dto.build_id is None def test_with_build_id(self): from src.services.clean_release.dto import CandidateDTO from src.services.clean_release.enums import CandidateStatus ts = datetime.now(timezone.utc) dto = CandidateDTO( id="c-1", version="1.0.0", source_snapshot_ref="snap-1", build_id="build-1", created_at=ts, created_by="admin", status=CandidateStatus.PREPARED, ) assert dto.build_id == "build-1" assert dto.status == "PREPARED" def test_serialize_roundtrip(self): from src.services.clean_release.dto import CandidateDTO from src.services.clean_release.enums import CandidateStatus ts = datetime.now(timezone.utc) dto = CandidateDTO( id="c-1", version="1.0.0", source_snapshot_ref="snap-1", created_at=ts, created_by="admin", status=CandidateStatus.DRAFT, ) data = dto.model_dump() restored = CandidateDTO.model_validate(data) assert restored.id == "c-1" assert restored.version == "1.0.0" def test_missing_required_raises(self): from src.services.clean_release.dto import CandidateDTO with pytest.raises(ValidationError): CandidateDTO() class TestArtifactDTO: """ArtifactDTO — DTO for CandidateArtifact.""" def test_minimal(self): from src.services.clean_release.dto import ArtifactDTO dto = ArtifactDTO(id="a-1", candidate_id="c-1", path="/file.txt", sha256="abc123", size=1024) assert dto.id == "a-1" assert dto.path == "/file.txt" assert dto.size == 1024 assert dto.metadata == {} def test_with_all_fields(self): from src.services.clean_release.dto import ArtifactDTO dto = ArtifactDTO( id="a-1", candidate_id="c-1", path="/file.txt", sha256="abc123", size=1024, detected_category="binary", declared_category="application", source_uri="http://example.com/file.txt", source_host="example.com", metadata={"key": "value"}, ) assert dto.detected_category == "binary" assert dto.source_uri == "http://example.com/file.txt" assert dto.metadata == {"key": "value"} def test_serialize_roundtrip(self): from src.services.clean_release.dto import ArtifactDTO dto = ArtifactDTO(id="a-1", candidate_id="c-1", path="/f", sha256="h", size=1) data = dto.model_dump() restored = ArtifactDTO.model_validate(data) assert restored.id == "a-1" assert restored.size == 1 class TestManifestDTO: """ManifestDTO — DTO for DistributionManifest.""" def test_minimal(self): from src.services.clean_release.dto import ManifestDTO ts = datetime.now(timezone.utc) dto = ManifestDTO( id="m-1", candidate_id="c-1", manifest_version=1, manifest_digest="digest1", artifacts_digest="adigest1", created_at=ts, created_by="admin", source_snapshot_ref="snap-1", content_json={"key": "value"}, ) assert dto.manifest_version == 1 assert dto.manifest_digest == "digest1" assert dto.content_json == {"key": "value"} def test_serialize_roundtrip(self): from src.services.clean_release.dto import ManifestDTO ts = datetime.now(timezone.utc) dto = ManifestDTO( id="m-1", candidate_id="c-1", manifest_version=1, manifest_digest="d1", artifacts_digest="ad1", created_at=ts, created_by="admin", source_snapshot_ref="snap-1", content_json={}, ) data = dto.model_dump() restored = ManifestDTO.model_validate(data) assert restored.manifest_digest == "d1" class TestComplianceRunDTO: """ComplianceRunDTO — DTO for run status tracking.""" def test_minimal(self): from src.services.clean_release.dto import ComplianceRunDTO from src.services.clean_release.enums import RunStatus dto = ComplianceRunDTO(run_id="r-1", candidate_id="c-1", status=RunStatus.PENDING) assert dto.run_id == "r-1" assert dto.status == "PENDING" assert dto.final_status is None assert dto.report_id is None def test_with_all_fields(self): from src.services.clean_release.dto import ComplianceRunDTO from src.services.clean_release.enums import ComplianceDecision, RunStatus dto = ComplianceRunDTO( run_id="r-1", candidate_id="c-1", status=RunStatus.SUCCEEDED, final_status=ComplianceDecision.PASSED, report_id="rep-1", task_id="task-1", ) assert dto.final_status == "PASSED" assert dto.report_id == "rep-1" assert dto.task_id == "task-1" def test_serialize_roundtrip(self): from src.services.clean_release.dto import ComplianceRunDTO from src.services.clean_release.enums import RunStatus dto = ComplianceRunDTO(run_id="r-1", candidate_id="c-1", status=RunStatus.PENDING) data = dto.model_dump() restored = ComplianceRunDTO.model_validate(data) assert restored.run_id == "r-1" class TestReportDTO: """ReportDTO — compact report view.""" def test_minimal(self): from src.services.clean_release.dto import ReportDTO from src.services.clean_release.enums import ComplianceDecision ts = datetime.now(timezone.utc) dto = ReportDTO( report_id="r-1", candidate_id="c-1", final_status=ComplianceDecision.PASSED, policy_version="1.0", manifest_digest="digest1", violation_count=0, generated_at=ts, ) assert dto.report_id == "r-1" assert dto.final_status == "PASSED" assert dto.violation_count == 0 def test_with_violations(self): from src.services.clean_release.dto import ReportDTO from src.services.clean_release.enums import ComplianceDecision ts = datetime.now(timezone.utc) dto = ReportDTO( report_id="r-1", candidate_id="c-1", final_status=ComplianceDecision.BLOCKED, policy_version="1.0", manifest_digest="digest1", violation_count=5, generated_at=ts, ) assert dto.violation_count == 5 def test_serialize_roundtrip(self): from src.services.clean_release.dto import ReportDTO from src.services.clean_release.enums import ComplianceDecision ts = datetime.now(timezone.utc) dto = ReportDTO(report_id="r-1", candidate_id="c-1", final_status=ComplianceDecision.PASSED, policy_version="1", manifest_digest="d1", violation_count=0, generated_at=ts) data = dto.model_dump() restored = ReportDTO.model_validate(data) assert restored.violation_count == 0 class TestCandidateOverviewDTO: """CandidateOverviewDTO — full candidate overview with all status refs.""" def test_minimal(self): from src.services.clean_release.dto import CandidateOverviewDTO from src.services.clean_release.enums import CandidateStatus dto = CandidateOverviewDTO( candidate_id="c-1", version="1.0.0", source_snapshot_ref="snap-1", status=CandidateStatus.DRAFT, ) assert dto.candidate_id == "c-1" assert dto.latest_manifest_id is None assert dto.latest_approval_decision is None assert dto.latest_publication_id is None def test_with_all_fields(self): from src.services.clean_release.dto import CandidateOverviewDTO from src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus dto = CandidateOverviewDTO( candidate_id="c-1", version="1.0.0", source_snapshot_ref="snap-1", status=CandidateStatus.APPROVED, latest_manifest_id="m-1", latest_manifest_digest="digest1", latest_run_id="run-1", latest_run_status=RunStatus.SUCCEEDED, latest_report_id="rep-1", latest_report_final_status=ComplianceDecision.PASSED, latest_policy_snapshot_id="ps-1", latest_policy_version="1.0", latest_registry_snapshot_id="rs-1", latest_registry_version="1.0", latest_approval_decision="APPROVED", latest_publication_id="pub-1", latest_publication_status="ACTIVE", ) assert dto.latest_manifest_id == "m-1" assert dto.latest_run_status == "SUCCEEDED" assert dto.latest_approval_decision == "APPROVED" assert dto.latest_publication_status == "ACTIVE" def test_serialize_roundtrip(self): from src.services.clean_release.dto import CandidateOverviewDTO from src.services.clean_release.enums import CandidateStatus dto = CandidateOverviewDTO( candidate_id="c-1", version="1.0.0", source_snapshot_ref="snap-1", status=CandidateStatus.DRAFT, ) data = dto.model_dump() restored = CandidateOverviewDTO.model_validate(data) assert restored.candidate_id == "c-1" # ── Enums ─────────────────────────────────────────────────── class TestCleanReleaseEnums: """All enums in enums.py.""" def test_candidate_status(self): from src.services.clean_release.enums import CandidateStatus assert CandidateStatus.DRAFT == "DRAFT" assert CandidateStatus.PREPARED == "PREPARED" assert CandidateStatus.MANIFEST_BUILT == "MANIFEST_BUILT" assert CandidateStatus.CHECK_PENDING == "CHECK_PENDING" assert CandidateStatus.CHECK_RUNNING == "CHECK_RUNNING" assert CandidateStatus.CHECK_PASSED == "CHECK_PASSED" assert CandidateStatus.CHECK_BLOCKED == "CHECK_BLOCKED" assert CandidateStatus.CHECK_ERROR == "CHECK_ERROR" assert CandidateStatus.APPROVED == "APPROVED" assert CandidateStatus.PUBLISHED == "PUBLISHED" assert CandidateStatus.REVOKED == "REVOKED" def test_run_status(self): from src.services.clean_release.enums import RunStatus assert RunStatus.PENDING == "PENDING" assert RunStatus.RUNNING == "RUNNING" assert RunStatus.SUCCEEDED == "SUCCEEDED" assert RunStatus.FAILED == "FAILED" assert RunStatus.CANCELLED == "CANCELLED" def test_compliance_decision(self): from src.services.clean_release.enums import ComplianceDecision assert ComplianceDecision.PASSED == "PASSED" assert ComplianceDecision.BLOCKED == "BLOCKED" assert ComplianceDecision.ERROR == "ERROR" def test_approval_decision_type(self): from src.services.clean_release.enums import ApprovalDecisionType assert ApprovalDecisionType.APPROVED == "APPROVED" assert ApprovalDecisionType.REJECTED == "REJECTED" def test_publication_status(self): from src.services.clean_release.enums import PublicationStatus assert PublicationStatus.ACTIVE == "ACTIVE" assert PublicationStatus.REVOKED == "REVOKED" def test_compliance_stage_name(self): from src.services.clean_release.enums import ComplianceStageName assert ComplianceStageName.DATA_PURITY == "DATA_PURITY" assert ComplianceStageName.INTERNAL_SOURCES_ONLY == "INTERNAL_SOURCES_ONLY" assert ComplianceStageName.NO_EXTERNAL_ENDPOINTS == "NO_EXTERNAL_ENDPOINTS" assert ComplianceStageName.MANIFEST_CONSISTENCY == "MANIFEST_CONSISTENCY" def test_classification_type(self): from src.services.clean_release.enums import ClassificationType assert ClassificationType.REQUIRED_SYSTEM == "required-system" assert ClassificationType.ALLOWED == "allowed" assert ClassificationType.EXCLUDED_PROHIBITED == "excluded-prohibited" def test_violation_severity(self): from src.services.clean_release.enums import ViolationSeverity assert ViolationSeverity.CRITICAL == "CRITICAL" assert ViolationSeverity.MAJOR == "MAJOR" assert ViolationSeverity.MINOR == "MINOR" def test_violation_category(self): from src.services.clean_release.enums import ViolationCategory assert ViolationCategory.DATA_PURITY == "DATA_PURITY" assert ViolationCategory.SOURCE_ISOLATION == "SOURCE_ISOLATION" assert ViolationCategory.MANIFEST_CONSISTENCY == "MANIFEST_CONSISTENCY" assert ViolationCategory.EXTERNAL_ENDPOINT == "EXTERNAL_ENDPOINT" # ── Exceptions ────────────────────────────────────────────── class TestCleanReleaseExceptions: """Exception hierarchy for clean release.""" def test_base_exception(self): from src.services.clean_release.exceptions import CleanReleaseError exc = CleanReleaseError("base error") assert str(exc) == "base error" assert isinstance(exc, Exception) def test_candidate_not_found(self): from src.services.clean_release.exceptions import CandidateNotFoundError, CleanReleaseError exc = CandidateNotFoundError("candidate not found") assert isinstance(exc, CleanReleaseError) assert str(exc) == "candidate not found" def test_illegal_transition(self): from src.services.clean_release.exceptions import IllegalTransitionError, CleanReleaseError exc = IllegalTransitionError("forbidden transition") assert isinstance(exc, CleanReleaseError) def test_manifest_immutable(self): from src.services.clean_release.exceptions import ManifestImmutableError, CleanReleaseError exc = ManifestImmutableError("manifest is immutable") assert isinstance(exc, CleanReleaseError) def test_policy_resolution(self): from src.services.clean_release.exceptions import PolicyResolutionError, CleanReleaseError exc = PolicyResolutionError("cannot resolve policy") assert isinstance(exc, CleanReleaseError) def test_compliance_run(self): from src.services.clean_release.exceptions import ComplianceRunError, CleanReleaseError exc = ComplianceRunError("compliance run failed") assert isinstance(exc, CleanReleaseError) def test_approval_gate(self): from src.services.clean_release.exceptions import ApprovalGateError, CleanReleaseError exc = ApprovalGateError("approval not met") assert isinstance(exc, CleanReleaseError) def test_publication_gate(self): from src.services.clean_release.exceptions import PublicationGateError, CleanReleaseError exc = PublicationGateError("publication not met") assert isinstance(exc, CleanReleaseError) def test_all_exceptions_distinct(self): from src.services.clean_release.exceptions import ( ApprovalGateError, CandidateNotFoundError, CleanReleaseError, ComplianceRunError, IllegalTransitionError, ManifestImmutableError, PolicyResolutionError, PublicationGateError, ) assert CandidateNotFoundError is not CleanReleaseError assert IllegalTransitionError is not CleanReleaseError assert ManifestImmutableError is not CleanReleaseError assert PolicyResolutionError is not CleanReleaseError assert ComplianceRunError is not CleanReleaseError assert ApprovalGateError is not CleanReleaseError assert PublicationGateError is not CleanReleaseError # #endregion Test.CleanRelease.DtoEnumsExceptions