# #region Test.Api.CleanRelease [C:3] [TYPE Module] [SEMANTICS test,clean-release,api] # @BRIEF Unit tests for clean release API routes — prepare, check, report. # @RELATION BINDS_TO -> [CleanReleaseApi] # @TEST_EDGE: active_policy_not_found -> 409 # @TEST_EDGE: candidate_not_found -> 409 # @TEST_EDGE: check_run_not_found -> 404 # @TEST_EDGE: report_not_found -> 404 # @TEST_EDGE: candidate_exists -> 409 import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_repo(overrides: dict | None = None) -> MagicMock: """Build a mock CleanReleaseRepository with bare-minimum defaults.""" repo = MagicMock() repo.get_active_policy.return_value = MagicMock(id="policy-1", policy_id="policy-1") repo.get_candidate.return_value = None repo.get_check_run.return_value = None repo.get_report.return_value = None repo.get_manifests_by_candidate.return_value = [] repo.stage_runs = {} repo.reports = {} repo.check_runs = {} repo.approval_decisions = [] repo.publication_records = [] repo.save_check_run.side_effect = lambda r: r repo.save_manifest.side_effect = lambda m: m repo.save_violation.side_effect = lambda v: v repo.save_candidate.side_effect = lambda c: c repo.save_artifact.side_effect = lambda a: a if overrides: for k, v in overrides.items(): setattr(repo, k, v) return repo def _make_client(repo: MagicMock | None = None) -> TestClient: from src.api.routes.clean_release import router from src.dependencies import get_clean_release_repository app = FastAPI() app.include_router(router) if repo is None: repo = _make_repo() app.dependency_overrides[get_clean_release_repository] = lambda: repo return TestClient(app) # ── register_candidate_v2_endpoint ── class TestRegisterCandidateV2: """POST /api/clean-release/candidates""" def test_register_success(self): repo = _make_repo() client = _make_client(repo) resp = client.post("/api/clean-release/candidates", json={ "id": "cand-1", "version": "1.0.0", "source_snapshot_ref": "main", "created_by": "test-user", }) assert resp.status_code == 201 data = resp.json() assert data["id"] == "cand-1" assert data["version"] == "1.0.0" assert data["status"] == "DRAFT" repo.save_candidate.assert_called_once() def test_register_duplicate_returns_409(self): repo = _make_repo() repo.get_candidate.return_value = MagicMock(id="cand-1") client = _make_client(repo) resp = client.post("/api/clean-release/candidates", json={ "id": "cand-1", "version": "1.0.0", "source_snapshot_ref": "main", "created_by": "test-user", }) assert resp.status_code == 409 assert "CANDIDATE_EXISTS" in resp.text # ── import_candidate_artifacts_v2_endpoint ── class TestImportArtifacts: """POST /api/clean-release/candidates/{candidate_id}/artifacts""" def test_import_success(self): candidate = MagicMock() candidate.id = "cand-1" candidate.status = "DRAFT" candidate.transition_to = MagicMock() repo = _make_repo() repo.get_candidate.return_value = candidate client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-1/artifacts", json={ "artifacts": [{ "id": "art-1", "path": "/tmp/file.tar.gz", "sha256": "abc123", "size": 1024, }] }) assert resp.status_code == 200 assert resp.json()["status"] == "success" repo.save_artifact.assert_called_once() candidate.transition_to.assert_called_once() def test_import_candidate_not_found(self): repo = _make_repo() repo.get_candidate.return_value = None client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-999/artifacts", json={ "artifacts": [{"id": "a1", "path": "/p", "sha256": "s", "size": 1}] }) assert resp.status_code == 404 def test_import_empty_artifacts(self): repo = _make_repo() repo.get_candidate.return_value = MagicMock(id="cand-1") client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-1/artifacts", json={ "artifacts": [] }) assert resp.status_code == 400 def test_import_missing_field(self): repo = _make_repo() repo.get_candidate.return_value = MagicMock(id="cand-1") client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-1/artifacts", json={ "artifacts": [{"id": "a1", "path": "/p"}] # missing sha256, size }) assert resp.status_code == 400 # ── get_candidate_overview_v2_endpoint ── class TestCandidateOverview: """GET /api/clean-release/candidates/{candidate_id}/overview""" def test_overview_success(self): candidate = MagicMock() candidate.id = "cand-1" candidate.version = "1.0.0" candidate.source_snapshot_ref = "main" candidate.status = "PREPARED" repo = _make_repo() repo.get_candidate.return_value = candidate client = _make_client(repo) resp = client.get("/api/clean-release/candidates/cand-1/overview") assert resp.status_code == 200 data = resp.json() assert data["candidate_id"] == "cand-1" assert data["status"] == "PREPARED" def test_overview_not_found(self): repo = _make_repo() repo.get_candidate.return_value = None client = _make_client(repo) resp = client.get("/api/clean-release/candidates/cand-999/overview") assert resp.status_code == 404 # ── start_check ── class TestStartCheck: """POST /api/clean-release/checks""" def test_start_check_success(self): candidate = MagicMock() candidate.id = "cand-1" candidate.status = "PREPARED" repo = _make_repo() repo.get_active_policy.return_value = MagicMock(id="policy-1", policy_id="p1") repo.get_candidate.return_value = candidate client = _make_client(repo) resp = client.post("/api/clean-release/checks", json={ "candidate_id": "cand-1", "profile": "enterprise-clean", "execution_mode": "tui", "triggered_by": "system", }) assert resp.status_code == 202 data = resp.json() assert data["status"] == "running" assert data["candidate_id"] == "cand-1" def test_start_check_no_policy(self): repo = _make_repo() repo.get_active_policy.return_value = None client = _make_client(repo) resp = client.post("/api/clean-release/checks", json={ "candidate_id": "cand-1", }) assert resp.status_code == 409 assert "POLICY_NOT_FOUND" in resp.text def test_start_check_no_candidate(self): repo = _make_repo() repo.get_active_policy.return_value = MagicMock(id="p1") repo.get_candidate.return_value = None client = _make_client(repo) resp = client.post("/api/clean-release/checks", json={ "candidate_id": "cand-999", }) assert resp.status_code == 409 assert "CANDIDATE_NOT_FOUND" in resp.text # ── get_check_status ── class TestGetCheckStatus: """GET /api/clean-release/checks/{check_run_id}""" def test_check_found(self): from datetime import datetime, timezone run = MagicMock() run.id = "check-1" run.candidate_id = "cand-1" run.final_status = "PASSED" run.started_at = datetime.now(timezone.utc) run.finished_at = datetime.now(timezone.utc) repo = _make_repo() repo.get_check_run.return_value = run client = _make_client(repo) resp = client.get("/api/clean-release/checks/check-1") assert resp.status_code == 200 data = resp.json() assert data["check_run_id"] == "check-1" def test_check_not_found(self): repo = _make_repo() repo.get_check_run.return_value = None client = _make_client(repo) resp = client.get("/api/clean-release/checks/check-999") assert resp.status_code == 404 assert "CHECK_NOT_FOUND" in resp.text # ── get_report ── class TestGetReport: """GET /api/clean-release/reports/{report_id}""" def test_report_found(self): report = MagicMock() report.id = "report-1" report.run_id = "check-1" report.candidate_id = "cand-1" report.final_status = "PASSED" repo = _make_repo() repo.get_report.return_value = report client = _make_client(repo) resp = client.get("/api/clean-release/reports/report-1") assert resp.status_code == 200 data = resp.json() assert data["report_id"] == "report-1" def test_report_not_found(self): repo = _make_repo() repo.get_report.return_value = None client = _make_client(repo) resp = client.get("/api/clean-release/reports/report-999") assert resp.status_code == 404 assert "REPORT_NOT_FOUND" in resp.text # ── prepare_candidate_endpoint ── @patch("src.api.routes.clean_release.prepare_candidate") class TestPrepareCandidateEndpoint: """POST /api/clean-release/candidates/prepare""" def test_prepare_success(self, mock_prepare): mock_prepare.return_value = {"status": "prepared", "manifest_id": "m-1"} repo = _make_repo() client = _make_client(repo) resp = client.post("/api/clean-release/candidates/prepare", json={ "candidate_id": "cand-1", "artifacts": [], "sources": ["src1"], "operator_id": "op-1", }) assert resp.status_code == 200 data = resp.json() assert data["status"] == "prepared" def test_prepare_blocked(self, mock_prepare): mock_prepare.return_value = {"status": "check_blocked"} repo = _make_repo() client = _make_client(repo) resp = client.post("/api/clean-release/candidates/prepare", json={ "candidate_id": "cand-1", "artifacts": [], "sources": [], "operator_id": "op-1", }) assert resp.status_code == 200 assert resp.json()["status"] == "blocked" def test_prepare_value_error(self, mock_prepare): mock_prepare.side_effect = ValueError("Bad request") repo = _make_repo() client = _make_client(repo) resp = client.post("/api/clean-release/candidates/prepare", json={ "candidate_id": "cand-1", "artifacts": [], "sources": [], "operator_id": "op-1", }) assert resp.status_code == 400 assert "CLEAN_PREPARATION_ERROR" in resp.text # ── manifest build endpoint ── class TestBuildManifest: """POST /api/clean-release/candidates/{candidate_id}/manifests""" def test_build_manifest_success(self): candidate = MagicMock() candidate.id = "cand-1" candidate.status = "PREPARED" repo = _make_repo() repo.get_candidate.return_value = candidate mock_manifest = MagicMock() mock_manifest.id = "manifest-cand-1" mock_manifest.candidate_id = "cand-1" mock_manifest.manifest_version = 1 mock_manifest.manifest_digest = "digest-1" mock_manifest.artifacts_digest = "art-digest-1" mock_manifest.created_at = __import__("datetime").datetime.now() mock_manifest.created_by = "system" mock_manifest.source_snapshot_ref = "main" mock_manifest.content_json = {} with patch( "src.services.clean_release.manifest_service.build_manifest_snapshot", return_value=mock_manifest ): client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-1/manifests", json={ "created_by": "system", }) assert resp.status_code == 201 data = resp.json() assert data["id"] == "manifest-cand-1" assert data["manifest_version"] == 1 def test_build_manifest_value_error(self): repo = _make_repo() repo.get_candidate.return_value = MagicMock(id="cand-1") with patch( "src.services.clean_release.manifest_service.build_manifest_snapshot", side_effect=ValueError("No artifacts") ): client = _make_client(repo) resp = client.post("/api/clean-release/candidates/cand-1/manifests", json={ "created_by": "system", }) assert resp.status_code == 400 assert "MANIFEST_BUILD_ERROR" in resp.text # #endregion Test.Api.CleanRelease