Files
ss-tools/backend/tests/test_clean_release_dto_enums_exceptions.py

434 lines
17 KiB
Python

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