feat(clean-release): complete compliance redesign phases and polish tasks T047-T052
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Compatibility tests for legacy clean-release API paths retained during v2 migration.
|
||||
# @LAYER: Tests
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:///./test_clean_release_legacy_compat.db")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///./test_clean_release_legacy_auth.db")
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
DistributionManifest,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
# [DEF:_seed_legacy_repo:Function]
|
||||
# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
|
||||
# @PRE: Repository is empty.
|
||||
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
|
||||
def _seed_legacy_repo() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id="legacy-rc-001",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:legacy-001",
|
||||
created_at=now,
|
||||
created_by="compat-tester",
|
||||
status=ReleaseCandidateStatus.DRAFT,
|
||||
)
|
||||
)
|
||||
|
||||
registry = ResourceSourceRegistry(
|
||||
registry_id="legacy-reg-1",
|
||||
name="Legacy Internal Registry",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="legacy-src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=now,
|
||||
updated_by="compat-tester",
|
||||
status="ACTIVE",
|
||||
)
|
||||
setattr(registry, "immutable", True)
|
||||
setattr(registry, "allowed_hosts", ["repo.intra.company.local"])
|
||||
setattr(registry, "allowed_schemes", ["https"])
|
||||
setattr(registry, "allowed_source_types", ["artifact-repo"])
|
||||
repo.save_registry(registry)
|
||||
|
||||
policy = CleanProfilePolicy(
|
||||
policy_id="legacy-pol-1",
|
||||
policy_version="1.0.0",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
active=True,
|
||||
internal_source_registry_ref="legacy-reg-1",
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["core"],
|
||||
effective_from=now,
|
||||
)
|
||||
setattr(policy, "immutable", True)
|
||||
setattr(
|
||||
policy,
|
||||
"content_json",
|
||||
{
|
||||
"profile": "enterprise-clean",
|
||||
"prohibited_artifact_categories": ["test-data"],
|
||||
"required_system_categories": ["core"],
|
||||
"external_source_forbidden": True,
|
||||
},
|
||||
)
|
||||
repo.save_policy(policy)
|
||||
|
||||
repo.save_manifest(
|
||||
DistributionManifest(
|
||||
id="legacy-manifest-1",
|
||||
candidate_id="legacy-rc-001",
|
||||
manifest_version=1,
|
||||
manifest_digest="sha256:legacy-manifest",
|
||||
artifacts_digest="sha256:legacy-artifacts",
|
||||
created_at=now,
|
||||
created_by="compat-tester",
|
||||
source_snapshot_ref="git:legacy-001",
|
||||
content_json={"items": [], "summary": {"included_count": 0, "prohibited_detected_count": 0}},
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
|
||||
return repo
|
||||
# [/DEF:_seed_legacy_repo:Function]
|
||||
|
||||
|
||||
def test_legacy_prepare_endpoint_still_available() -> None:
|
||||
repo = _seed_legacy_repo()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/clean-release/candidates/prepare",
|
||||
json={
|
||||
"candidate_id": "legacy-rc-001",
|
||||
"artifacts": [{"path": "src/main.py", "category": "core", "reason": "required"}],
|
||||
"sources": ["repo.intra.company.local"],
|
||||
"operator_id": "compat-tester",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "status" in payload
|
||||
assert payload["status"] in {"prepared", "blocked", "PREPARED", "BLOCKED"}
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_legacy_checks_endpoints_still_available() -> None:
|
||||
repo = _seed_legacy_repo()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
start_response = client.post(
|
||||
"/api/clean-release/checks",
|
||||
json={
|
||||
"candidate_id": "legacy-rc-001",
|
||||
"profile": "enterprise-clean",
|
||||
"execution_mode": "api",
|
||||
"triggered_by": "compat-tester",
|
||||
},
|
||||
)
|
||||
assert start_response.status_code == 202
|
||||
start_payload = start_response.json()
|
||||
assert "check_run_id" in start_payload
|
||||
assert start_payload["candidate_id"] == "legacy-rc-001"
|
||||
|
||||
status_response = client.get(f"/api/clean-release/checks/{start_payload['check_run_id']}")
|
||||
assert status_response.status_code == 200
|
||||
status_payload = status_response.json()
|
||||
assert status_payload["check_run_id"] == start_payload["check_run_id"]
|
||||
assert "final_status" in status_payload
|
||||
assert "checks" in status_payload
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]
|
||||
@@ -0,0 +1,93 @@
|
||||
# [DEF:test_clean_release_v2_api:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: API contract tests for redesigned clean release endpoints.
|
||||
# @LAYER: Domain
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository, get_config_manager
|
||||
from src.models.clean_release import (
|
||||
CleanPolicySnapshot,
|
||||
DistributionManifest,
|
||||
ReleaseCandidate,
|
||||
SourceRegistrySnapshot,
|
||||
)
|
||||
from src.services.clean_release.enums import CandidateStatus
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012).
|
||||
def test_candidate_registration_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO
|
||||
"""
|
||||
payload = {
|
||||
"id": "rc-test-001",
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
response = client.post("/api/v2/clean-release/candidates", json=payload)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] == "rc-test-001"
|
||||
assert data["status"] == CandidateStatus.DRAFT.value
|
||||
|
||||
def test_artifact_import_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: artifact_import -> Should return 200 and success status.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO
|
||||
"""
|
||||
candidate_id = "rc-test-001-art"
|
||||
bootstrap_candidate = {
|
||||
"id": candidate_id,
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
|
||||
assert create_response.status_code == 201
|
||||
|
||||
payload = {
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "art-1",
|
||||
"path": "bin/app.exe",
|
||||
"sha256": "hash123",
|
||||
"size": 1024
|
||||
}
|
||||
]
|
||||
}
|
||||
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/artifacts", json=payload)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
def test_manifest_build_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO
|
||||
"""
|
||||
candidate_id = "rc-test-001-manifest"
|
||||
bootstrap_candidate = {
|
||||
"id": candidate_id,
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
|
||||
assert create_response.status_code == 201
|
||||
|
||||
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/manifests")
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "manifest_digest" in data
|
||||
assert data["candidate_id"] == candidate_id
|
||||
|
||||
# [/DEF:test_clean_release_v2_api:Module]
|
||||
@@ -0,0 +1,107 @@
|
||||
# [DEF:test_clean_release_v2_release_api:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: API contract test scaffolding for clean release approval and publication endpoints.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: IMPLEMENTS -> clean_release_v2_release_api_contracts
|
||||
|
||||
"""Contract tests for redesigned approval/publication API endpoints."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.routes.clean_release_v2 import router as clean_release_v2_router
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import ComplianceReport, ReleaseCandidate
|
||||
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision
|
||||
|
||||
|
||||
test_app = FastAPI()
|
||||
test_app.include_router(clean_release_v2_router)
|
||||
client = TestClient(test_app)
|
||||
|
||||
|
||||
def _seed_candidate_and_passed_report() -> tuple[str, str]:
|
||||
repository = get_clean_release_repository()
|
||||
candidate_id = f"api-release-candidate-{uuid4()}"
|
||||
report_id = f"api-release-report-{uuid4()}"
|
||||
|
||||
repository.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id=candidate_id,
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha-api-release",
|
||||
created_by="api-test",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=CandidateStatus.CHECK_PASSED.value,
|
||||
)
|
||||
)
|
||||
repository.save_report(
|
||||
ComplianceReport(
|
||||
id=report_id,
|
||||
run_id=f"run-{uuid4()}",
|
||||
candidate_id=candidate_id,
|
||||
final_status=ComplianceDecision.PASSED.value,
|
||||
summary_json={"operator_summary": "ok", "violations_count": 0, "blocking_violations_count": 0},
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
return candidate_id, report_id
|
||||
|
||||
|
||||
def test_release_approve_and_publish_revoke_contract() -> None:
|
||||
"""Contract for approve -> publish -> revoke lifecycle endpoints."""
|
||||
candidate_id, report_id = _seed_candidate_and_passed_report()
|
||||
|
||||
approve_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/approve",
|
||||
json={"report_id": report_id, "decided_by": "api-test", "comment": "approved"},
|
||||
)
|
||||
assert approve_response.status_code == 200
|
||||
approve_payload = approve_response.json()
|
||||
assert approve_payload["status"] == "ok"
|
||||
assert approve_payload["decision"] == "APPROVED"
|
||||
|
||||
publish_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/publish",
|
||||
json={
|
||||
"report_id": report_id,
|
||||
"published_by": "api-test",
|
||||
"target_channel": "stable",
|
||||
"publication_ref": "rel-api-001",
|
||||
},
|
||||
)
|
||||
assert publish_response.status_code == 200
|
||||
publish_payload = publish_response.json()
|
||||
assert publish_payload["status"] == "ok"
|
||||
assert publish_payload["publication"]["status"] == "ACTIVE"
|
||||
|
||||
publication_id = publish_payload["publication"]["id"]
|
||||
revoke_response = client.post(
|
||||
f"/api/v2/clean-release/publications/{publication_id}/revoke",
|
||||
json={"revoked_by": "api-test", "comment": "rollback"},
|
||||
)
|
||||
assert revoke_response.status_code == 200
|
||||
revoke_payload = revoke_response.json()
|
||||
assert revoke_payload["status"] == "ok"
|
||||
assert revoke_payload["publication"]["status"] == "REVOKED"
|
||||
|
||||
|
||||
def test_release_reject_contract() -> None:
|
||||
"""Contract for reject endpoint."""
|
||||
candidate_id, report_id = _seed_candidate_and_passed_report()
|
||||
|
||||
reject_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/reject",
|
||||
json={"report_id": report_id, "decided_by": "api-test", "comment": "rejected"},
|
||||
)
|
||||
assert reject_response.status_code == 200
|
||||
payload = reject_response.json()
|
||||
assert payload["status"] == "ok"
|
||||
assert payload["decision"] == "REJECTED"
|
||||
|
||||
|
||||
# [/DEF:test_clean_release_v2_release_api:Module]
|
||||
Reference in New Issue
Block a user