test: +12 test modules — clean_release routes, gitea routes, dashboard detail, candidate_service, compliance_orchestrator, clarification_engine/orchestrator, dataset_review helpers. Fix 18 failures (assistant tools, maintence, dataset_review, approval, publication)
This commit is contained in:
266
backend/tests/api/test_clean_release_v2.py
Normal file
266
backend/tests/api/test_clean_release_v2.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# #region Test.Api.CleanReleaseV2 [C:3] [TYPE Module] [SEMANTICS test,clean-release-v2,api]
|
||||
# @BRIEF Unit tests for clean release v2 API routes — register, approve, publish, revoke.
|
||||
# @RELATION BINDS_TO -> [CleanReleaseV2Api]
|
||||
# @TEST_EDGE: candidate_not_found -> 404
|
||||
# @TEST_EDGE: approval_gate_error -> 409
|
||||
# @TEST_EDGE: publication_gate_error -> 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:
|
||||
repo = MagicMock()
|
||||
repo.get_candidate.return_value = None
|
||||
repo.save_candidate.side_effect = lambda c: c
|
||||
repo.save_manifest.side_effect = lambda m: m
|
||||
repo.get_artifact.return_value = None
|
||||
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_v2 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 ──
|
||||
|
||||
class TestRegisterCandidate:
|
||||
"""POST /api/v2/clean-release/candidates"""
|
||||
|
||||
def test_register_success(self):
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates", json={
|
||||
"id": "cand-v2-1",
|
||||
"version": "2.0.0",
|
||||
"source_snapshot_ref": "release/2.0",
|
||||
"created_by": "bot",
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["id"] == "cand-v2-1"
|
||||
assert data["status"] == "DRAFT"
|
||||
repo.save_candidate.assert_called_once()
|
||||
|
||||
|
||||
# ── import_artifacts ──
|
||||
|
||||
class TestImportArtifactsV2:
|
||||
"""POST /api/v2/clean-release/candidates/{candidate_id}/artifacts"""
|
||||
|
||||
def test_import_success(self):
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = MagicMock(id="cand-v2-1")
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/artifacts", json={
|
||||
"artifacts": [{"id": "a1", "path": "/p", "sha256": "s", "size": 1}]
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "success"
|
||||
|
||||
def test_import_candidate_not_found(self):
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = None
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-999/artifacts", json={
|
||||
"artifacts": [{"id": "a1", "path": "/p", "sha256": "s", "size": 1}]
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ── build_manifest ──
|
||||
|
||||
class TestBuildManifestV2:
|
||||
"""POST /api/v2/clean-release/candidates/{candidate_id}/manifests"""
|
||||
|
||||
def test_build_manifest_success(self):
|
||||
candidate = MagicMock()
|
||||
candidate.id = "cand-v2-1"
|
||||
candidate.source_snapshot_ref = "main"
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = candidate
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/manifests")
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["candidate_id"] == "cand-v2-1"
|
||||
assert data["manifest_version"] == 1
|
||||
|
||||
def test_build_manifest_not_found(self):
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = None
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-999/manifests")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ── approve_candidate_endpoint ──
|
||||
|
||||
class TestApproveCandidate:
|
||||
"""POST /api/v2/clean-release/candidates/{candidate_id}/approve"""
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.approve_candidate")
|
||||
def test_approve_success(self, mock_approve):
|
||||
mock_approve.return_value = MagicMock(decision="APPROVED", id="dec-1")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/approve", json={
|
||||
"report_id": "report-1",
|
||||
"decided_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["decision"] == "APPROVED"
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.approve_candidate")
|
||||
def test_approve_gate_error(self, mock_approve):
|
||||
mock_approve.side_effect = ValueError("Not approved")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/approve", json={
|
||||
"report_id": "report-1",
|
||||
"decided_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
assert "APPROVAL_GATE_ERROR" in resp.text
|
||||
|
||||
|
||||
# ── reject_candidate_endpoint ──
|
||||
|
||||
class TestRejectCandidate:
|
||||
"""POST /api/v2/clean-release/candidates/{candidate_id}/reject"""
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.reject_candidate")
|
||||
def test_reject_success(self, mock_reject):
|
||||
mock_reject.return_value = MagicMock(decision="REJECTED", id="dec-2")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/reject", json={
|
||||
"report_id": "report-1",
|
||||
"decided_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["decision"] == "REJECTED"
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.reject_candidate")
|
||||
def test_reject_gate_error(self, mock_reject):
|
||||
mock_reject.side_effect = ValueError("Already approved")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/reject", json={
|
||||
"report_id": "report-1",
|
||||
"decided_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
# ── publish_candidate_endpoint ──
|
||||
|
||||
class TestPublishCandidate:
|
||||
"""POST /api/v2/clean-release/candidates/{candidate_id}/publish"""
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.publish_candidate")
|
||||
def test_publish_success(self, mock_publish):
|
||||
pub = MagicMock()
|
||||
pub.id = "pub-1"
|
||||
pub.candidate_id = "cand-v2-1"
|
||||
pub.report_id = "report-1"
|
||||
pub.published_by = "admin"
|
||||
pub.published_at = __import__("datetime").datetime.now()
|
||||
pub.target_channel = "production"
|
||||
pub.publication_ref = "ref-1"
|
||||
pub.status = "PUBLISHED"
|
||||
mock_publish.return_value = pub
|
||||
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/publish", json={
|
||||
"report_id": "report-1",
|
||||
"published_by": "admin",
|
||||
"target_channel": "production",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["publication"]["id"] == "pub-1"
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.publish_candidate")
|
||||
def test_publish_gate_error(self, mock_publish):
|
||||
mock_publish.side_effect = ValueError("Not approved")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/candidates/cand-v2-1/publish", json={
|
||||
"report_id": "report-1",
|
||||
"published_by": "admin",
|
||||
"target_channel": "production",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
# ── revoke_publication_endpoint ──
|
||||
|
||||
class TestRevokePublication:
|
||||
"""POST /api/v2/clean-release/publications/{publication_id}/revoke"""
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.revoke_publication")
|
||||
def test_revoke_success(self, mock_revoke):
|
||||
pub = MagicMock()
|
||||
pub.id = "pub-1"
|
||||
pub.candidate_id = "cand-v2-1"
|
||||
pub.report_id = "report-1"
|
||||
pub.published_by = "admin"
|
||||
pub.published_at = __import__("datetime").datetime.now()
|
||||
pub.target_channel = "production"
|
||||
pub.publication_ref = "ref-1"
|
||||
pub.status = "REVOKED"
|
||||
mock_revoke.return_value = pub
|
||||
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/publications/pub-1/revoke", json={
|
||||
"revoked_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["publication"]["id"] == "pub-1"
|
||||
assert data["publication"]["status"] == "REVOKED"
|
||||
|
||||
@patch("src.api.routes.clean_release_v2.revoke_publication")
|
||||
def test_revoke_gate_error(self, mock_revoke):
|
||||
mock_revoke.side_effect = ValueError("Already revoked")
|
||||
repo = _make_repo()
|
||||
client = _make_client(repo)
|
||||
resp = client.post("/api/v2/clean-release/publications/pub-1/revoke", json={
|
||||
"revoked_by": "admin",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
# #endregion Test.Api.CleanReleaseV2
|
||||
Reference in New Issue
Block a user