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:
@@ -346,7 +346,8 @@ def test_approve_persists_audit_event():
|
||||
|
||||
assert len(repository.audit_events) >= 1
|
||||
last_event = repository.audit_events[-1]
|
||||
assert "APPROVED" in str(last_event.get("action", ""))
|
||||
# audit_preparation sets action="PREPARATION", decision type goes in "status"
|
||||
assert "APPROVED" in str(last_event.get("status", ""))
|
||||
# #endregion test_approve_persists_audit_event
|
||||
|
||||
|
||||
@@ -368,7 +369,8 @@ def test_reject_persists_audit_event():
|
||||
|
||||
assert len(repository.audit_events) >= 1
|
||||
last_event = repository.audit_events[-1]
|
||||
assert "REJECTED" in str(last_event.get("action", ""))
|
||||
# audit_preparation sets action="PREPARATION", decision type goes in "status"
|
||||
assert "REJECTED" in str(last_event.get("status", ""))
|
||||
# #endregion test_reject_persists_audit_event
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ def test_load_bootstrap_artifacts_list():
|
||||
assert artifacts[0].path == "bin/app"
|
||||
assert artifacts[0].sha256 == "abc123"
|
||||
assert artifacts[0].size == 1024
|
||||
assert artifacts[0].detected_category == "library" # from 'category' field
|
||||
assert artifacts[0].detected_category is None # no category field on first artifact
|
||||
assert artifacts[1].detected_category == "library" # from 'category' field on second artifact
|
||||
assert artifacts[1].size == 2048
|
||||
finally:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
@@ -167,7 +168,8 @@ def test_load_bootstrap_artifacts_with_metadata():
|
||||
|
||||
# Extra fields become metadata
|
||||
assert artifacts[1].metadata_json["extra_field"] == "extra_value"
|
||||
assert artifacts[1].detected_category is not None
|
||||
# No category/detected_category field -> detected_category is None
|
||||
assert artifacts[1].detected_category is None
|
||||
finally:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
# #endregion test_load_bootstrap_artifacts_with_metadata
|
||||
|
||||
124
backend/tests/services/clean_release/test_candidate_service.py
Normal file
124
backend/tests/services/clean_release/test_candidate_service.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# #region Test.CleanRelease.CandidateService [C:3] [TYPE Module] [SEMANTICS test,clean-release,candidate,service]
|
||||
# @BRIEF Unit tests for candidate_service — _validate_artifacts, register_candidate.
|
||||
# @RELATION BINDS_TO -> [candidate_service]
|
||||
# @TEST_EDGE: empty_artifacts -> ValueError
|
||||
# @TEST_EDGE: missing_id_field -> ValueError
|
||||
# @TEST_EDGE: empty_id -> ValueError
|
||||
# @TEST_EDGE: non_positive_size -> ValueError
|
||||
# @TEST_EDGE: duplicate_candidate -> ValueError
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
|
||||
# ── _validate_artifacts ──
|
||||
|
||||
class TestValidateArtifacts:
|
||||
def test_valid_artifacts(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
artifacts = [
|
||||
{"id": "art-1", "path": "/tmp/a.tar.gz", "sha256": "abc123", "size": 1024},
|
||||
]
|
||||
result = _validate_artifacts(artifacts)
|
||||
assert len(result) == 1
|
||||
assert result[0]["id"] == "art-1"
|
||||
|
||||
def test_empty_artifacts(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="artifacts must not be empty"):
|
||||
_validate_artifacts([])
|
||||
|
||||
def test_non_dict_artifact(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="artifact\\[0\\] must be an object"):
|
||||
_validate_artifacts(["string"])
|
||||
|
||||
def test_missing_required_field(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="missing required field 'path'"):
|
||||
_validate_artifacts([{"id": "a1", "sha256": "s", "size": 1}])
|
||||
|
||||
def test_empty_id(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="field 'id' must be non-empty"):
|
||||
_validate_artifacts([{"id": "", "path": "/p", "sha256": "s", "size": 1}])
|
||||
|
||||
def test_empty_path(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="field 'path' must be non-empty"):
|
||||
_validate_artifacts([{"id": "a1", "path": "", "sha256": "s", "size": 1}])
|
||||
|
||||
def test_empty_sha256(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="field 'sha256' must be non-empty"):
|
||||
_validate_artifacts([{"id": "a1", "path": "/p", "sha256": "", "size": 1}])
|
||||
|
||||
def test_non_positive_size(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="field 'size' must be a positive integer"):
|
||||
_validate_artifacts([{"id": "a1", "path": "/p", "sha256": "s", "size": 0}])
|
||||
|
||||
def test_size_not_int(self):
|
||||
from src.services.clean_release.candidate_service import _validate_artifacts
|
||||
with pytest.raises(ValueError, match="field 'size' must be a positive integer"):
|
||||
_validate_artifacts([{"id": "a1", "path": "/p", "sha256": "s", "size": "not-int"}])
|
||||
|
||||
|
||||
# ── register_candidate ──
|
||||
|
||||
class TestRegisterCandidateService:
|
||||
def test_register_success(self):
|
||||
repo = MagicMock()
|
||||
repo.get_candidate.return_value = None
|
||||
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
result = register_candidate(
|
||||
repository=repo,
|
||||
candidate_id="cand-1",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="main",
|
||||
created_by="admin",
|
||||
artifacts=[{"id": "a1", "path": "/p", "sha256": "s", "size": 1}],
|
||||
)
|
||||
assert result is not None
|
||||
assert result.id == "cand-1"
|
||||
assert repo.save_candidate.call_count == 2 # DRAFT then PREPARED
|
||||
assert repo.save_artifact.call_count == 1
|
||||
|
||||
def test_register_empty_candidate_id(self):
|
||||
repo = MagicMock()
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
with pytest.raises(ValueError, match="candidate_id must be non-empty"):
|
||||
register_candidate(repo, "", "1.0", "main", "admin", [])
|
||||
|
||||
def test_register_empty_version(self):
|
||||
repo = MagicMock()
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
with pytest.raises(ValueError, match="version must be non-empty"):
|
||||
register_candidate(repo, "cand-1", "", "main", "admin", [])
|
||||
|
||||
def test_register_empty_source_ref(self):
|
||||
repo = MagicMock()
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
with pytest.raises(ValueError, match="source_snapshot_ref must be non-empty"):
|
||||
register_candidate(repo, "cand-1", "1.0", "", "admin", [])
|
||||
|
||||
def test_register_empty_created_by(self):
|
||||
repo = MagicMock()
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
with pytest.raises(ValueError, match="created_by must be non-empty"):
|
||||
register_candidate(repo, "cand-1", "1.0", "main", "", [])
|
||||
|
||||
def test_register_duplicate(self):
|
||||
repo = MagicMock()
|
||||
repo.get_candidate.return_value = MagicMock(id="cand-1")
|
||||
|
||||
from src.services.clean_release.candidate_service import register_candidate
|
||||
with pytest.raises(ValueError, match="already exists"):
|
||||
register_candidate(repo, "cand-1", "1.0", "main", "admin", [{"id": "a1", "path": "/p", "sha256": "s", "size": 1}])
|
||||
# #endregion Test.CleanRelease.CandidateService
|
||||
@@ -0,0 +1,347 @@
|
||||
# #region Test.CleanRelease.ComplianceOrchestrator [C:3] [TYPE Module] [SEMANTICS test,clean-release,compliance,orchestrator]
|
||||
# @BRIEF Unit tests for CleanComplianceOrchestrator — start, execute, finalize.
|
||||
# @RELATION BINDS_TO -> [ComplianceOrchestrator]
|
||||
# @TEST_EDGE: manifest_not_found -> ValueError
|
||||
# @TEST_EDGE: missing_deps -> FAILED status
|
||||
# @TEST_EDGE: finalize_derives_from_stages
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_repo() -> MagicMock:
|
||||
repo = MagicMock()
|
||||
repo.get_manifest.return_value = None
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1", registry_snapshot_id="reg-1")
|
||||
repo.get_candidate.return_value = None
|
||||
repo.get_registry.return_value = MagicMock(id="registry-1")
|
||||
repo.save_check_run.side_effect = lambda r: r
|
||||
repo.stage_runs = {}
|
||||
return repo
|
||||
|
||||
|
||||
# ── start_check_run ──
|
||||
|
||||
class TestStartCheckRun:
|
||||
def test_start_without_manifest(self):
|
||||
repo = _make_repo()
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1")
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
run = orch.start_check_run(
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
requested_by="tester",
|
||||
)
|
||||
assert run is not None
|
||||
assert run.candidate_id == "cand-1"
|
||||
assert run.status == "RUNNING"
|
||||
|
||||
def test_start_with_manifest(self):
|
||||
repo = _make_repo()
|
||||
manifest = MagicMock()
|
||||
manifest.id = "manifest-1"
|
||||
manifest.manifest_digest = "digest-abc"
|
||||
repo.get_manifest.return_value = manifest
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1", registry_snapshot_id="reg-1")
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
run = orch.start_check_run(
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
requested_by="tester",
|
||||
manifest_id="manifest-1",
|
||||
)
|
||||
assert run is not None
|
||||
assert run.candidate_id == "cand-1"
|
||||
|
||||
def test_start_manifest_not_found_raises(self):
|
||||
repo = _make_repo()
|
||||
repo.get_manifest.return_value = None
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1")
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
with pytest.raises(ValueError, match="Manifest or Policy not found"):
|
||||
orch.start_check_run(
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
manifest_id="manifest-missing",
|
||||
)
|
||||
|
||||
def test_start_legacy_manifest_id_is_execution_mode(self):
|
||||
"""When manifest_id looks like an execution mode string, treat it as execution_mode."""
|
||||
repo = _make_repo()
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
run = orch.start_check_run(
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
manifest_id="tui",
|
||||
)
|
||||
# Should treat "tui" as execution_mode, not manifest_id
|
||||
assert run.manifest_id == "manifest-cand-1"
|
||||
|
||||
def test_start_with_requested_by_none(self):
|
||||
repo = _make_repo()
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
run = orch.start_check_run(
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
requested_by=None,
|
||||
)
|
||||
assert run is not None
|
||||
assert run.requested_by == "system"
|
||||
|
||||
|
||||
# ── execute_stages ──
|
||||
|
||||
class TestExecuteStages:
|
||||
def test_forced_results_all_stages_pass(self):
|
||||
from src.services.clean_release.enums import ComplianceStageName, ComplianceDecision
|
||||
from src.models.clean_release import ComplianceStageRun
|
||||
|
||||
repo = _make_repo()
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.candidate_id = "cand-1"
|
||||
run.status = "RUNNING"
|
||||
|
||||
forced = [
|
||||
ComplianceStageRun(
|
||||
id="stage-1",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.DATA_PURITY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-2",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-3",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-4",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
]
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.execute_stages(run, forced_results=forced)
|
||||
assert result.status == "SUCCEEDED"
|
||||
assert len(repo.stage_runs) == 4
|
||||
|
||||
def test_forced_results_with_blocked(self):
|
||||
from src.services.clean_release.enums import ComplianceStageName, ComplianceDecision
|
||||
from src.models.clean_release import ComplianceStageRun
|
||||
|
||||
repo = _make_repo()
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.candidate_id = "cand-1"
|
||||
run.status = "RUNNING"
|
||||
|
||||
forced = [
|
||||
ComplianceStageRun(
|
||||
id="stage-1",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.DATA_PURITY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-2",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-3",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.BLOCKED.value,
|
||||
details_json={},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id="stage-4",
|
||||
run_id="check-1",
|
||||
stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,
|
||||
status="SUCCEEDED",
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={},
|
||||
),
|
||||
]
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.execute_stages(run, forced_results=forced)
|
||||
assert result.status == "SUCCEEDED"
|
||||
# final_status should be BLOCKED because one stage is blocked
|
||||
assert result.final_status == "BLOCKED"
|
||||
|
||||
def test_no_forced_all_deps_missing(self):
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = None
|
||||
repo.get_policy.return_value = None
|
||||
repo.get_registry.return_value = None
|
||||
repo.get_manifest.return_value = None
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.candidate_id = "cand-1"
|
||||
run.policy_snapshot_id = "policy-1"
|
||||
run.registry_snapshot_id = "reg-1"
|
||||
run.manifest_id = "manifest-1"
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.execute_stages(run)
|
||||
assert result.status == "FAILED"
|
||||
assert result.finished_at is not None
|
||||
|
||||
def test_no_forced_with_all_deps(self):
|
||||
repo = _make_repo()
|
||||
manifest = MagicMock()
|
||||
manifest.content_json = {"summary": {"prohibited_detected_count": 0}}
|
||||
repo.get_manifest.return_value = manifest
|
||||
repo.get_candidate.return_value = MagicMock(id="cand-1")
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1")
|
||||
repo.get_registry.return_value = MagicMock(id="reg-1")
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.candidate_id = "cand-1"
|
||||
run.policy_snapshot_id = "policy-1"
|
||||
run.registry_snapshot_id = "reg-1"
|
||||
run.manifest_id = "manifest-1"
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.execute_stages(run)
|
||||
assert result.status == "SUCCEEDED"
|
||||
assert result.final_status == "PASSED"
|
||||
|
||||
|
||||
# ── finalize_run ──
|
||||
|
||||
class TestFinalizeRun:
|
||||
def test_finalize_failed_run(self):
|
||||
repo = _make_repo()
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.status = "FAILED"
|
||||
run.final_status = None
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.finalize_run(run)
|
||||
assert result.finished_at is not None
|
||||
|
||||
def test_finalize_with_existing_final_status(self):
|
||||
repo = _make_repo()
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.status = "RUNNING"
|
||||
run.final_status = "PASSED"
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.finalize_run(run)
|
||||
assert result.status == "SUCCEEDED"
|
||||
|
||||
def test_finalize_derives_from_stage_runs(self):
|
||||
from src.services.clean_release.enums import ComplianceStageName, ComplianceDecision
|
||||
from src.models.clean_release import ComplianceStageRun
|
||||
|
||||
repo = _make_repo()
|
||||
repo.stage_runs = {
|
||||
"stage-1": ComplianceStageRun(
|
||||
id="stage-1", run_id="check-1",
|
||||
stage_name=ComplianceStageName.DATA_PURITY.value,
|
||||
status="SUCCEEDED", decision=ComplianceDecision.PASSED.value,
|
||||
details_json={},
|
||||
),
|
||||
}
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.status = "RUNNING"
|
||||
run.final_status = None
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.finalize_run(run)
|
||||
assert result.status == "SUCCEEDED"
|
||||
|
||||
def test_finalize_derives_no_stages(self):
|
||||
repo = _make_repo()
|
||||
repo.stage_runs = {}
|
||||
|
||||
run = MagicMock()
|
||||
run.id = "check-1"
|
||||
run.status = "RUNNING"
|
||||
run.final_status = None
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
orch = CleanComplianceOrchestrator(repo)
|
||||
result = orch.finalize_run(run)
|
||||
assert result.status == "SUCCEEDED"
|
||||
|
||||
|
||||
# ── run_check_legacy ──
|
||||
|
||||
class TestRunCheckLegacy:
|
||||
def test_legacy_pipeline(self):
|
||||
repo = _make_repo()
|
||||
repo.get_candidate.return_value = MagicMock(id="cand-1")
|
||||
repo.get_policy.return_value = MagicMock(id="policy-1", registry_snapshot_id="reg-1")
|
||||
repo.get_manifest.return_value = MagicMock(
|
||||
id="manifest-1",
|
||||
manifest_digest="digest-abc",
|
||||
)
|
||||
|
||||
from src.services.clean_release.compliance_orchestrator import run_check_legacy
|
||||
result = run_check_legacy(
|
||||
repository=repo,
|
||||
candidate_id="cand-1",
|
||||
policy_id="policy-1",
|
||||
requested_by="tester",
|
||||
manifest_id="manifest-1",
|
||||
)
|
||||
assert result is not None
|
||||
assert result.candidate_id == "cand-1"
|
||||
# #endregion Test.CleanRelease.ComplianceOrchestrator
|
||||
@@ -221,24 +221,17 @@ def test_publish_rejects_nonexistent_report():
|
||||
from src.services.clean_release.approval_service import approve_candidate
|
||||
from src.services.clean_release.publication_service import publish_candidate
|
||||
|
||||
repository, candidate_id, _ = _seed_candidate_with_passed_report(
|
||||
repository, candidate_id, report_id = _seed_candidate_with_passed_report(
|
||||
candidate_status=CandidateStatus.CHECK_PASSED,
|
||||
)
|
||||
approve_candidate(
|
||||
repository=repository,
|
||||
candidate_id=candidate_id,
|
||||
report_id=_seed_candidate_with_passed_report()[1],
|
||||
report_id=report_id,
|
||||
decided_by="approver",
|
||||
comment="approved",
|
||||
# need to re-seed for valid report id
|
||||
)
|
||||
|
||||
# Use the actual report_id from the seed
|
||||
actual_report_id = _seed_candidate_with_passed_report(
|
||||
candidate_id=candidate_id + "-other",
|
||||
report_id="CCR-other",
|
||||
)[1]
|
||||
|
||||
with pytest.raises(PublicationGateError, match="not found"):
|
||||
publish_candidate(
|
||||
repository=repository,
|
||||
@@ -254,6 +247,7 @@ def test_publish_rejects_nonexistent_report():
|
||||
def test_publish_rejects_foreign_report():
|
||||
from src.services.clean_release.approval_service import approve_candidate
|
||||
from src.services.clean_release.publication_service import publish_candidate
|
||||
from src.models.clean_release import ComplianceReport
|
||||
|
||||
repository, candidate_id, _ = _seed_candidate_with_passed_report(
|
||||
candidate_id="cand-foreign-pub",
|
||||
@@ -268,18 +262,23 @@ def test_publish_rejects_foreign_report():
|
||||
comment="approved",
|
||||
)
|
||||
|
||||
# A report belonging to another candidate
|
||||
foreign_repo, foreign_candidate_id, foreign_report_id = _seed_candidate_with_passed_report(
|
||||
# Save a foreign report directly into the same repository
|
||||
foreign_report = ComplianceReport(
|
||||
id="CCR-other-pub",
|
||||
run_id="run-foreign",
|
||||
candidate_id="cand-other",
|
||||
report_id="CCR-other-pub",
|
||||
candidate_status=CandidateStatus.CHECK_PASSED,
|
||||
final_status=ComplianceDecision.PASSED.value,
|
||||
summary_json={"operator_summary": "foreign", "violations_count": 0, "blocking_violations_count": 0},
|
||||
generated_at=datetime.now(UTC),
|
||||
immutable=True,
|
||||
)
|
||||
repository.save_report(foreign_report)
|
||||
|
||||
with pytest.raises(PublicationGateError, match="belongs to another candidate"):
|
||||
publish_candidate(
|
||||
repository=repository,
|
||||
candidate_id=candidate_id,
|
||||
report_id=foreign_report_id,
|
||||
report_id="CCR-other-pub",
|
||||
published_by="publisher",
|
||||
target_channel="stable",
|
||||
)
|
||||
@@ -432,7 +431,8 @@ def test_publish_persists_audit_event():
|
||||
|
||||
assert len(repository.audit_events) >= 1
|
||||
last_event = repository.audit_events[-1]
|
||||
assert "PUBLISHED" in str(last_event.get("action", ""))
|
||||
# audit_preparation sets action="PREPARATION", decision type goes in "status"
|
||||
assert "PUBLISHED" in str(last_event.get("status", ""))
|
||||
# #endregion test_publish_persists_audit_event
|
||||
|
||||
|
||||
@@ -468,7 +468,8 @@ def test_revoke_persists_audit_event():
|
||||
|
||||
assert len(repository.audit_events) >= 1
|
||||
last_event = repository.audit_events[-1]
|
||||
assert "REVOKED" in str(last_event.get("action", ""))
|
||||
# audit_preparation sets action="PREPARATION", decision type goes in "status"
|
||||
assert "REVOKED" in str(last_event.get("status", ""))
|
||||
# #endregion test_revoke_persists_audit_event
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user