104 lines
2.7 KiB
Python
104 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from govoplan_campaign.backend.persistence.campaigns import (
|
|
CampaignPersistenceError,
|
|
)
|
|
from govoplan_campaign.backend.persistence.versions import (
|
|
_normalize_review_issue_decisions,
|
|
)
|
|
|
|
|
|
def test_attachment_ask_requires_reason_and_freezes_evidence() -> None:
|
|
job = _job(
|
|
issues=[
|
|
{
|
|
"code": "missing_optional_attachment",
|
|
"behavior": "ask",
|
|
"source": "attachments",
|
|
"details": {
|
|
"effective_policy": {
|
|
"effective_behavior": "ask",
|
|
}
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
with pytest.raises(
|
|
CampaignPersistenceError,
|
|
match="require an explicit reason",
|
|
):
|
|
_normalize_review_issue_decisions(
|
|
[job],
|
|
[],
|
|
user_id="reviewer-1",
|
|
build_token="build-1",
|
|
)
|
|
|
|
decisions = _normalize_review_issue_decisions(
|
|
[job],
|
|
[
|
|
{
|
|
"job_id": job.id,
|
|
"decision": "accept",
|
|
"reason": "Recipient confirmed that no attachment is expected.",
|
|
}
|
|
],
|
|
user_id="reviewer-1",
|
|
build_token="build-1",
|
|
decided_at=datetime(2026, 7, 30, 12, 0, tzinfo=UTC),
|
|
)
|
|
|
|
assert decisions == [
|
|
{
|
|
"job_id": "job-1",
|
|
"review_key": "entry-1",
|
|
"decision": "accept",
|
|
"reason": "Recipient confirmed that no attachment is expected.",
|
|
"actor_user_id": "reviewer-1",
|
|
"decided_at": "2026-07-30T12:00:00+00:00",
|
|
"build_token": "build-1",
|
|
"message_sha256": "a" * 64,
|
|
"issue_fingerprint": decisions[0]["issue_fingerprint"],
|
|
"issue_codes": ["missing_optional_attachment"],
|
|
}
|
|
]
|
|
assert len(decisions[0]["issue_fingerprint"]) == 64
|
|
|
|
|
|
def test_attachment_block_cannot_be_overridden_by_review_decision() -> None:
|
|
job = _job(
|
|
issues=[
|
|
{
|
|
"code": "missing_required_attachment",
|
|
"behavior": "block",
|
|
"source": "attachments",
|
|
}
|
|
]
|
|
)
|
|
|
|
decisions = _normalize_review_issue_decisions(
|
|
[job],
|
|
[],
|
|
user_id="reviewer-1",
|
|
build_token="build-1",
|
|
)
|
|
|
|
assert decisions == []
|
|
|
|
|
|
def _job(*, issues: list[dict[str, object]]) -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
id="job-1",
|
|
entry_id="entry-1",
|
|
entry_index=1,
|
|
validation_status="needs_review",
|
|
issues_snapshot=issues,
|
|
eml_sha256="a" * 64,
|
|
)
|