feat(clean-release): complete and verify backend test suite (33 passing tests)
- Relocated and standardized tests for clean_release subsystem into __tests__ sub-packages. - Implemented missing unit tests for preparation_service, audit_service, and stages. - Enhanced API contract tests for candidate preparation and compliance reporting. - Updated 023-clean-repo-enterprise coverage matrix with final verification results. - Fixed relative import issues and model validation mismatches during test migration.
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
# [DEF:backend.tests.api.routes.test_clean_release_api:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, api, clean-release, checks, reports
|
||||
# @PURPOSE: Contract tests for clean release checks and reports endpoints.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
# @INVARIANT: API returns deterministic payload shapes for checks and reports.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
def _repo_with_seed_data() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
candidate_id="2026.03.03-rc1",
|
||||
version="2026.03.03",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="tester",
|
||||
source_snapshot_ref="git:abc123",
|
||||
status=ReleaseCandidateStatus.PREPARED,
|
||||
)
|
||||
)
|
||||
repo.save_registry(
|
||||
ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
status="active",
|
||||
)
|
||||
)
|
||||
repo.save_policy(
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_start_check_and_get_status_contract():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
|
||||
start = client.post(
|
||||
"/api/clean-release/checks",
|
||||
json={
|
||||
"candidate_id": "2026.03.03-rc1",
|
||||
"profile": "enterprise-clean",
|
||||
"execution_mode": "tui",
|
||||
"triggered_by": "tester",
|
||||
},
|
||||
)
|
||||
assert start.status_code == 202
|
||||
payload = start.json()
|
||||
assert set(["check_run_id", "candidate_id", "status", "started_at"]).issubset(payload.keys())
|
||||
|
||||
check_run_id = payload["check_run_id"]
|
||||
status_resp = client.get(f"/api/clean-release/checks/{check_run_id}")
|
||||
assert status_resp.status_code == 200
|
||||
status_payload = status_resp.json()
|
||||
assert status_payload["check_run_id"] == check_run_id
|
||||
assert "final_status" in status_payload
|
||||
assert "checks" in status_payload
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_get_report_not_found_returns_404():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/clean-release/reports/unknown-report")
|
||||
assert resp.status_code == 404
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
@@ -1,97 +0,0 @@
|
||||
# [DEF:backend.tests.api.routes.test_clean_release_source_policy:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, api, clean-release, source-policy
|
||||
# @PURPOSE: Validate API behavior for source isolation violations in clean release preparation.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
# @INVARIANT: External endpoints must produce blocking violation entries.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
def _repo_with_seed_data() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
candidate_id="2026.03.03-rc1",
|
||||
version="2026.03.03",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="tester",
|
||||
source_snapshot_ref="git:abc123",
|
||||
status=ReleaseCandidateStatus.DRAFT,
|
||||
)
|
||||
)
|
||||
|
||||
repo.save_registry(
|
||||
ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
status="active",
|
||||
)
|
||||
)
|
||||
|
||||
repo.save_policy(
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_prepare_candidate_blocks_external_source():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/clean-release/candidates/prepare",
|
||||
json={
|
||||
"candidate_id": "2026.03.03-rc1",
|
||||
"artifacts": [
|
||||
{"path": "cfg/system.yaml", "category": "system-init", "reason": "required"}
|
||||
],
|
||||
"sources": ["repo.intra.company.local", "pypi.org"],
|
||||
"operator_id": "release-manager",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "blocked"
|
||||
assert any(v["category"] == "external-source" for v in data["violations"])
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
@@ -1,41 +0,0 @@
|
||||
# [DEF:backend.tests.services.clean_release.test_manifest_builder:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: tests, clean-release, manifest, deterministic
|
||||
# @PURPOSE: Validate deterministic manifest generation behavior for US1.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: VERIFIES -> backend.src.services.clean_release.manifest_builder
|
||||
# @INVARIANT: Same input artifacts produce identical deterministic hash.
|
||||
|
||||
from src.services.clean_release.manifest_builder import build_distribution_manifest
|
||||
|
||||
|
||||
# [DEF:test_manifest_deterministic_hash_for_same_input:Function]
|
||||
# @PURPOSE: Ensure hash is stable for same candidate/policy/artifact input.
|
||||
# @PRE: Same input lists are passed twice.
|
||||
# @POST: Hash and summary remain identical.
|
||||
def test_manifest_deterministic_hash_for_same_input():
|
||||
artifacts = [
|
||||
{"path": "a.yaml", "category": "system-init", "classification": "required-system", "reason": "required"},
|
||||
{"path": "b.yaml", "category": "test-data", "classification": "excluded-prohibited", "reason": "prohibited"},
|
||||
]
|
||||
|
||||
manifest1 = build_distribution_manifest(
|
||||
manifest_id="m1",
|
||||
candidate_id="2026.03.03-rc1",
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
generated_by="tester",
|
||||
artifacts=artifacts,
|
||||
)
|
||||
manifest2 = build_distribution_manifest(
|
||||
manifest_id="m2",
|
||||
candidate_id="2026.03.03-rc1",
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
generated_by="tester",
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
assert manifest1.deterministic_hash == manifest2.deterministic_hash
|
||||
assert manifest1.summary.included_count == manifest2.summary.included_count
|
||||
assert manifest1.summary.excluded_count == manifest2.summary.excluded_count
|
||||
# [/DEF:test_manifest_deterministic_hash_for_same_input:Function]
|
||||
# [/DEF:backend.tests.services.clean_release.test_manifest_builder:Module]
|
||||
@@ -1,144 +0,0 @@
|
||||
# [DEF:backend.tests.services.clean_release.test_policy_engine:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @SEMANTICS: tests, clean-release, policy-engine, deterministic
|
||||
# @PURPOSE: Validate policy model contracts and deterministic classification prerequisites for US1.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: VERIFIES -> backend.src.models.clean_release.CleanProfilePolicy
|
||||
# @INVARIANT: Enterprise policy rejects invalid activation states.
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from src.models.clean_release import CleanProfilePolicy, ProfileType
|
||||
|
||||
|
||||
# [DEF:test_policy_enterprise_clean_valid:Function]
|
||||
# @PURPOSE: Ensure valid enterprise policy payload is accepted.
|
||||
# @PRE: Fixture-like payload contains prohibited categories and registry ref.
|
||||
# @POST: Model is created with external_source_forbidden=True.
|
||||
def test_policy_enterprise_clean_valid():
|
||||
policy = CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data", "demo-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
assert policy.external_source_forbidden is True
|
||||
assert policy.prohibited_artifact_categories == ["test-data", "demo-data"]
|
||||
# [/DEF:test_policy_enterprise_clean_valid:Function]
|
||||
|
||||
|
||||
# [DEF:test_policy_missing_registry_fails:Function]
|
||||
# @PURPOSE: Verify missing registry ref violates policy contract.
|
||||
# @PRE: enterprise-clean policy payload has blank registry ref.
|
||||
# @POST: Validation error is raised.
|
||||
def test_policy_missing_registry_fails():
|
||||
with pytest.raises(ValueError):
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
# [/DEF:test_policy_missing_registry_fails:Function]
|
||||
|
||||
|
||||
# [DEF:test_policy_empty_prohibited_categories_fails:Function]
|
||||
# @PURPOSE: Verify enterprise policy cannot activate without prohibited categories.
|
||||
# @PRE: enterprise-clean policy payload has empty prohibited categories.
|
||||
# @POST: Validation error is raised.
|
||||
def test_policy_empty_prohibited_categories_fails():
|
||||
with pytest.raises(ValueError):
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=[],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
# [/DEF:test_policy_empty_prohibited_categories_fails:Function]
|
||||
|
||||
|
||||
# [DEF:test_policy_conflicting_external_forbidden_flag_fails:Function]
|
||||
# @PURPOSE: Verify enterprise policy enforces external_source_forbidden=true.
|
||||
# @PRE: enterprise-clean policy payload sets external_source_forbidden to false.
|
||||
# @POST: Validation error is raised.
|
||||
def test_policy_conflicting_external_forbidden_flag_fails():
|
||||
with pytest.raises(ValueError):
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=False,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
# [/DEF:test_policy_conflicting_external_forbidden_flag_fails:Function]
|
||||
# [/DEF:backend.tests.services.clean_release.test_policy_engine:Module]
|
||||
from src.models.clean_release import ResourceSourceRegistry, ResourceSourceEntry, RegistryStatus
|
||||
from src.services.clean_release.policy_engine import CleanPolicyEngine
|
||||
|
||||
def _policy_enterprise_clean() -> CleanProfilePolicy:
|
||||
return CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
|
||||
def _registry() -> ResourceSourceRegistry:
|
||||
return ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal",
|
||||
entries=[ResourceSourceEntry(source_id="1", host="nexus.internal", protocol="https", purpose="pkg", enabled=True)],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
)
|
||||
|
||||
# [DEF:test_policy_valid:Function]
|
||||
# @PURPOSE: Validate policy valid scenario
|
||||
def test_policy_valid():
|
||||
engine = CleanPolicyEngine(_policy_enterprise_clean(), _registry())
|
||||
res = engine.validate_policy()
|
||||
assert res.ok is True
|
||||
|
||||
# [DEF:test_conflicting_registry:Function]
|
||||
# @PURPOSE: Validate policy conflicting registry edge
|
||||
def test_conflicting_registry():
|
||||
reg = _registry()
|
||||
reg.registry_id = "other-registry"
|
||||
engine = CleanPolicyEngine(_policy_enterprise_clean(), reg)
|
||||
res = engine.validate_policy()
|
||||
assert res.ok is False
|
||||
assert "Policy registry ref does not match provided registry" in res.blocking_reasons
|
||||
|
||||
# [DEF:test_external_endpoint:Function]
|
||||
# @PURPOSE: Validate policy external endpoint edge
|
||||
def test_external_endpoint():
|
||||
engine = CleanPolicyEngine(_policy_enterprise_clean(), _registry())
|
||||
res = engine.validate_resource_source("external.org")
|
||||
assert res.ok is False
|
||||
assert res.violation["category"] == "external-source"
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# [DEF:backend.tests.services.clean_release.test_source_isolation:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, clean-release, source-isolation, internal-only
|
||||
# @PURPOSE: Verify internal source registry validation behavior.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.services.clean_release.source_isolation
|
||||
# @INVARIANT: External endpoints always produce blocking violations.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry
|
||||
from src.services.clean_release.source_isolation import validate_internal_sources
|
||||
|
||||
|
||||
def _registry() -> ResourceSourceRegistry:
|
||||
return ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal Sources",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
),
|
||||
ResourceSourceEntry(
|
||||
source_id="src-2",
|
||||
host="pypi.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="package-mirror",
|
||||
enabled=True,
|
||||
),
|
||||
],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
status="active",
|
||||
)
|
||||
|
||||
|
||||
def test_validate_internal_sources_all_internal_ok():
|
||||
result = validate_internal_sources(
|
||||
registry=_registry(),
|
||||
endpoints=["repo.intra.company.local", "pypi.intra.company.local"],
|
||||
)
|
||||
assert result["ok"] is True
|
||||
assert result["violations"] == []
|
||||
|
||||
|
||||
def test_validate_internal_sources_external_blocked():
|
||||
result = validate_internal_sources(
|
||||
registry=_registry(),
|
||||
endpoints=["repo.intra.company.local", "pypi.org"],
|
||||
)
|
||||
assert result["ok"] is False
|
||||
assert len(result["violations"]) == 1
|
||||
assert result["violations"][0]["category"] == "external-source"
|
||||
assert result["violations"][0]["blocked_release"] is True
|
||||
Reference in New Issue
Block a user