feat(clean-release): complete compliance redesign phases and polish tasks T047-T052
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# [DEF:test_candidate_manifest_services:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
|
||||
# @LAYER: Tests
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.core.database import Base
|
||||
from src.models.clean_release import ReleaseCandidate, DistributionManifest, CandidateArtifact
|
||||
from backend.src.services.clean_release.enums import CandidateStatus
|
||||
from backend.src.services.clean_release.candidate_service import register_candidate
|
||||
from backend.src.services.clean_release.manifest_service import build_manifest_snapshot
|
||||
from backend.src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
def test_candidate_lifecycle_transitions(db_session):
|
||||
"""
|
||||
@PURPOSE: Verify legal state transitions for ReleaseCandidate.
|
||||
"""
|
||||
candidate = ReleaseCandidate(
|
||||
id="test-candidate-1",
|
||||
name="Test Candidate",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="ref-1",
|
||||
created_by="operator",
|
||||
status=CandidateStatus.DRAFT
|
||||
)
|
||||
db_session.add(candidate)
|
||||
db_session.commit()
|
||||
|
||||
# Valid transition: DRAFT -> PREPARED
|
||||
candidate.transition_to(CandidateStatus.PREPARED)
|
||||
assert candidate.status == CandidateStatus.PREPARED
|
||||
|
||||
# Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)
|
||||
from backend.src.services.clean_release.exceptions import IllegalTransitionError
|
||||
with pytest.raises(IllegalTransitionError, match="Forbidden transition"):
|
||||
candidate.transition_to(CandidateStatus.DRAFT)
|
||||
|
||||
def test_manifest_versioning_and_immutability(db_session):
|
||||
"""
|
||||
@PURPOSE: Verify manifest versioning and immutability invariants.
|
||||
"""
|
||||
candidate_id = "test-candidate-2"
|
||||
|
||||
# Create version 1
|
||||
m1 = DistributionManifest(
|
||||
id="manifest-v1",
|
||||
candidate_id=candidate_id,
|
||||
manifest_version=1,
|
||||
manifest_digest="hash1",
|
||||
artifacts_digest="hash1",
|
||||
source_snapshot_ref="ref1",
|
||||
content_json={},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="operator"
|
||||
)
|
||||
db_session.add(m1)
|
||||
|
||||
# Create version 2
|
||||
m2 = DistributionManifest(
|
||||
id="manifest-v2",
|
||||
candidate_id=candidate_id,
|
||||
manifest_version=2,
|
||||
manifest_digest="hash2",
|
||||
artifacts_digest="hash2",
|
||||
source_snapshot_ref="ref1",
|
||||
content_json={},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="operator"
|
||||
)
|
||||
db_session.add(m2)
|
||||
db_session.commit()
|
||||
|
||||
latest = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).order_by(DistributionManifest.manifest_version.desc()).first()
|
||||
assert latest.manifest_version == 2
|
||||
assert latest.id == "manifest-v2"
|
||||
|
||||
all_manifests = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).all()
|
||||
assert len(all_manifests) == 2
|
||||
|
||||
|
||||
def _valid_artifacts():
|
||||
return [
|
||||
{
|
||||
"id": "art-1",
|
||||
"path": "bin/app",
|
||||
"sha256": "abc123",
|
||||
"size": 42,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_register_candidate_rejects_duplicate_candidate_id():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="dup-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha1",
|
||||
created_by="operator",
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="already exists"):
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="dup-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha1",
|
||||
created_by="operator",
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
|
||||
def test_register_candidate_rejects_malformed_artifact_input():
|
||||
repository = CleanReleaseRepository()
|
||||
bad_artifacts = [{"id": "art-1", "path": "bin/app", "size": 42}] # missing sha256
|
||||
|
||||
with pytest.raises(ValueError, match="missing required field 'sha256'"):
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="bad-art-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha2",
|
||||
created_by="operator",
|
||||
artifacts=bad_artifacts,
|
||||
)
|
||||
|
||||
|
||||
def test_register_candidate_rejects_empty_artifact_set():
|
||||
repository = CleanReleaseRepository()
|
||||
|
||||
with pytest.raises(ValueError, match="artifacts must not be empty"):
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="empty-art-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha3",
|
||||
created_by="operator",
|
||||
artifacts=[],
|
||||
)
|
||||
|
||||
|
||||
def test_manifest_service_rebuild_creates_new_version():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="manifest-version-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha10",
|
||||
created_by="operator",
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
first = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")
|
||||
second = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")
|
||||
|
||||
assert first.manifest_version == 1
|
||||
assert second.manifest_version == 2
|
||||
assert first.id != second.id
|
||||
|
||||
|
||||
def test_manifest_service_existing_manifest_cannot_be_mutated():
|
||||
repository = CleanReleaseRepository()
|
||||
register_candidate(
|
||||
repository=repository,
|
||||
candidate_id="manifest-immutable-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha11",
|
||||
created_by="operator",
|
||||
artifacts=_valid_artifacts(),
|
||||
)
|
||||
|
||||
created = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
|
||||
original_digest = created.manifest_digest
|
||||
|
||||
rebuilt = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
|
||||
old_manifest = repository.get_manifest(created.id)
|
||||
|
||||
assert old_manifest is not None
|
||||
assert old_manifest.manifest_digest == original_digest
|
||||
assert old_manifest.id == created.id
|
||||
assert rebuilt.id != created.id
|
||||
|
||||
|
||||
def test_manifest_service_rejects_missing_candidate():
|
||||
repository = CleanReleaseRepository()
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
build_manifest_snapshot(repository=repository, candidate_id="missing-candidate", created_by="operator")
|
||||
|
||||
# [/DEF:test_candidate_manifest_services:Module]
|
||||
Reference in New Issue
Block a user