Refactor mock campaign send reporting
This commit is contained in:
@@ -299,6 +299,237 @@ def _mock_sent_folder(config: Any) -> str:
|
|||||||
return "Sent"
|
return "Sent"
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_campaign_version(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
campaign_id: str,
|
||||||
|
version_id: str | None,
|
||||||
|
) -> tuple[Campaign, CampaignVersion]:
|
||||||
|
campaign = (
|
||||||
|
session.query(Campaign)
|
||||||
|
.filter(Campaign.id == campaign_id, Campaign.tenant_id == tenant_id)
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
if not campaign:
|
||||||
|
raise MockCampaignSendError("Campaign not found or not accessible")
|
||||||
|
wanted_version_id = version_id or campaign.current_version_id
|
||||||
|
if not wanted_version_id:
|
||||||
|
raise MockCampaignSendError("Campaign has no current version")
|
||||||
|
version = session.get(CampaignVersion, wanted_version_id)
|
||||||
|
if not version or version.campaign_id != campaign.id:
|
||||||
|
raise MockCampaignSendError(
|
||||||
|
"Campaign version not found or not part of campaign"
|
||||||
|
)
|
||||||
|
return campaign, version
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_mailbox_for_run(*, send: bool, clear_mailbox: bool) -> Any | None:
|
||||||
|
mailbox = _require_mock_mailbox() if send or clear_mailbox else _mock_mailbox()
|
||||||
|
if clear_mailbox and mailbox is not None:
|
||||||
|
mailbox.clear_records()
|
||||||
|
return mailbox
|
||||||
|
|
||||||
|
|
||||||
|
def _build_mock_campaign_run(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
campaign: Campaign,
|
||||||
|
version: CampaignVersion,
|
||||||
|
mailbox: Any | None,
|
||||||
|
send: bool,
|
||||||
|
include_warnings: bool,
|
||||||
|
include_needs_review: bool,
|
||||||
|
append_sent: bool,
|
||||||
|
check_files: bool,
|
||||||
|
) -> tuple[Any, Any, _MockSendBatch]:
|
||||||
|
files = files_integration()
|
||||||
|
raw_json = version.raw_json if isinstance(version.raw_json, dict) else {}
|
||||||
|
assert_server_safe_campaign_paths(
|
||||||
|
raw_json,
|
||||||
|
managed_files_available=files.available,
|
||||||
|
)
|
||||||
|
with files.prepared_campaign_snapshot(
|
||||||
|
session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
campaign_id=campaign.id,
|
||||||
|
raw_json=raw_json,
|
||||||
|
include_bytes=True,
|
||||||
|
prefix="govoplan-mock-send-",
|
||||||
|
) as prepared:
|
||||||
|
prepared_raw = load_campaign_json(prepared.path)
|
||||||
|
config = load_campaign_config_from_json(
|
||||||
|
session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
raw_json=prepared_raw,
|
||||||
|
campaign_id=campaign.id,
|
||||||
|
)
|
||||||
|
validation_report = validate_campaign_config(
|
||||||
|
config,
|
||||||
|
campaign_file=prepared.path,
|
||||||
|
check_files=check_files,
|
||||||
|
)
|
||||||
|
build_result = build_campaign_messages(
|
||||||
|
config,
|
||||||
|
campaign_file=prepared.path,
|
||||||
|
write_eml=False,
|
||||||
|
)
|
||||||
|
files.annotate_built_messages_with_managed_files(
|
||||||
|
build_result.built_messages,
|
||||||
|
prepared.managed_files_by_local_path,
|
||||||
|
)
|
||||||
|
send_batch = _mock_send_batch(
|
||||||
|
config=config,
|
||||||
|
built_messages=build_result.built_messages,
|
||||||
|
mailbox=mailbox,
|
||||||
|
send=send,
|
||||||
|
include_warnings=include_warnings,
|
||||||
|
include_needs_review=include_needs_review,
|
||||||
|
append_sent=append_sent,
|
||||||
|
)
|
||||||
|
return validation_report, build_result, send_batch
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_validation_payload(validation_report: Any) -> dict[str, Any]:
|
||||||
|
payload = validation_report.model_dump(mode="json")
|
||||||
|
payload.update(
|
||||||
|
{
|
||||||
|
"ok": validation_report.ok,
|
||||||
|
"error_count": validation_report.error_count,
|
||||||
|
"warning_count": validation_report.warning_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_build_payload(build_result: Any) -> dict[str, Any]:
|
||||||
|
report = build_result.report
|
||||||
|
payload = report.model_dump(mode="json")
|
||||||
|
payload.update(
|
||||||
|
{
|
||||||
|
"built_count": report.built_count,
|
||||||
|
"queueable_count": report.queueable_count,
|
||||||
|
"needs_review_count": report.needs_review_count,
|
||||||
|
"blocked_count": report.blocked_count,
|
||||||
|
"warning_count": report.warning_count,
|
||||||
|
"ready_count": report.ready_count,
|
||||||
|
"messages": [_message_payload(message) for message in report.messages],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_send_steps(
|
||||||
|
*,
|
||||||
|
validation_report: Any,
|
||||||
|
validation_payload: dict[str, Any],
|
||||||
|
build_report: Any,
|
||||||
|
send_batch: _MockSendBatch,
|
||||||
|
send: bool,
|
||||||
|
append_sent: bool,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "validate",
|
||||||
|
"label": "Validate campaign JSON",
|
||||||
|
"status": "ok" if validation_report.ok else "needs_review",
|
||||||
|
"summary": validation_payload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "build",
|
||||||
|
"label": "Build messages",
|
||||||
|
"status": "ok" if build_report.queueable_count else "needs_review",
|
||||||
|
"summary": {
|
||||||
|
"built": build_report.built_count,
|
||||||
|
"queueable": build_report.queueable_count,
|
||||||
|
"needs_review": build_report.needs_review_count,
|
||||||
|
"blocked": build_report.blocked_count,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "send",
|
||||||
|
"label": "Mock SMTP delivery",
|
||||||
|
"status": (
|
||||||
|
"skipped"
|
||||||
|
if not send
|
||||||
|
else "ok"
|
||||||
|
if send_batch.failed_count == 0
|
||||||
|
else "needs_review"
|
||||||
|
),
|
||||||
|
"summary": {
|
||||||
|
"attempted": send_batch.attempted_count,
|
||||||
|
"sent": send_batch.sent_count,
|
||||||
|
"failed": send_batch.failed_count,
|
||||||
|
"skipped": send_batch.skipped_count,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "imap",
|
||||||
|
"label": "Mock IMAP Sent append",
|
||||||
|
"status": (
|
||||||
|
"skipped"
|
||||||
|
if not send or not append_sent
|
||||||
|
else "ok"
|
||||||
|
if send_batch.imap_failed_count == 0
|
||||||
|
else "needs_review"
|
||||||
|
),
|
||||||
|
"summary": {
|
||||||
|
"appended": send_batch.imap_appended_count,
|
||||||
|
"failed": send_batch.imap_failed_count,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_campaign_send_response(
|
||||||
|
*,
|
||||||
|
campaign: Campaign,
|
||||||
|
version: CampaignVersion,
|
||||||
|
mailbox: Any | None,
|
||||||
|
validation_report: Any,
|
||||||
|
validation_payload: dict[str, Any],
|
||||||
|
build_result: Any,
|
||||||
|
build_payload: dict[str, Any],
|
||||||
|
send_batch: _MockSendBatch,
|
||||||
|
send: bool,
|
||||||
|
include_warnings: bool,
|
||||||
|
include_needs_review: bool,
|
||||||
|
append_sent: bool,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"campaign_id": campaign.id,
|
||||||
|
"version_id": version.id,
|
||||||
|
"version_number": version.version_number,
|
||||||
|
"send_requested": send,
|
||||||
|
"include_warnings": include_warnings,
|
||||||
|
"include_needs_review": include_needs_review,
|
||||||
|
"append_sent": append_sent,
|
||||||
|
"steps": _mock_send_steps(
|
||||||
|
validation_report=validation_report,
|
||||||
|
validation_payload=validation_payload,
|
||||||
|
build_report=build_result.report,
|
||||||
|
send_batch=send_batch,
|
||||||
|
send=send,
|
||||||
|
append_sent=append_sent,
|
||||||
|
),
|
||||||
|
"validation": validation_payload,
|
||||||
|
"build": build_payload,
|
||||||
|
"send": {
|
||||||
|
"attempted_count": send_batch.attempted_count,
|
||||||
|
"sent_count": send_batch.sent_count,
|
||||||
|
"failed_count": send_batch.failed_count,
|
||||||
|
"skipped_count": send_batch.skipped_count,
|
||||||
|
"imap_appended_count": send_batch.imap_appended_count,
|
||||||
|
"imap_failed_count": send_batch.imap_failed_count,
|
||||||
|
"results": send_batch.results,
|
||||||
|
},
|
||||||
|
"mailbox": {
|
||||||
|
"messages": mailbox.list_records(limit=200) if mailbox is not None else []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def run_mock_campaign_send(
|
def run_mock_campaign_send(
|
||||||
session: Session,
|
session: Session,
|
||||||
*,
|
*,
|
||||||
@@ -320,87 +551,38 @@ def run_mock_campaign_send(
|
|||||||
mailbox only when send=True.
|
mailbox only when send=True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
campaign = session.query(Campaign).filter(Campaign.id == campaign_id, Campaign.tenant_id == tenant_id).one_or_none()
|
campaign, version = _mock_campaign_version(
|
||||||
if not campaign:
|
|
||||||
raise MockCampaignSendError("Campaign not found or not accessible")
|
|
||||||
wanted_version_id = version_id or campaign.current_version_id
|
|
||||||
if not wanted_version_id:
|
|
||||||
raise MockCampaignSendError("Campaign has no current version")
|
|
||||||
version = session.get(CampaignVersion, wanted_version_id)
|
|
||||||
if not version or version.campaign_id != campaign.id:
|
|
||||||
raise MockCampaignSendError("Campaign version not found or not part of campaign")
|
|
||||||
|
|
||||||
mailbox = _require_mock_mailbox() if send or clear_mailbox else _mock_mailbox()
|
|
||||||
if clear_mailbox and mailbox is not None:
|
|
||||||
mailbox.clear_records()
|
|
||||||
|
|
||||||
files = files_integration()
|
|
||||||
assert_server_safe_campaign_paths(
|
|
||||||
version.raw_json if isinstance(version.raw_json, dict) else {},
|
|
||||||
managed_files_available=files.available,
|
|
||||||
)
|
|
||||||
with files.prepared_campaign_snapshot(
|
|
||||||
session,
|
session,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
campaign_id=campaign.id,
|
campaign_id=campaign_id,
|
||||||
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {},
|
version_id=version_id,
|
||||||
include_bytes=True,
|
)
|
||||||
prefix="govoplan-mock-send-",
|
mailbox = _mock_mailbox_for_run(send=send, clear_mailbox=clear_mailbox)
|
||||||
) as prepared:
|
validation_report, build_result, send_batch = _build_mock_campaign_run(
|
||||||
prepared_raw = load_campaign_json(prepared.path)
|
session,
|
||||||
config = load_campaign_config_from_json(session, tenant_id=tenant_id, raw_json=prepared_raw, campaign_id=campaign.id)
|
tenant_id=tenant_id,
|
||||||
validation_report = validate_campaign_config(config, campaign_file=prepared.path, check_files=check_files)
|
campaign=campaign,
|
||||||
build_result = build_campaign_messages(config, campaign_file=prepared.path, write_eml=False)
|
version=version,
|
||||||
files.annotate_built_messages_with_managed_files(build_result.built_messages, prepared.managed_files_by_local_path)
|
|
||||||
|
|
||||||
send_batch = _mock_send_batch(
|
|
||||||
config=config,
|
|
||||||
built_messages=build_result.built_messages,
|
|
||||||
mailbox=mailbox,
|
mailbox=mailbox,
|
||||||
send=send,
|
send=send,
|
||||||
include_warnings=include_warnings,
|
include_warnings=include_warnings,
|
||||||
include_needs_review=include_needs_review,
|
include_needs_review=include_needs_review,
|
||||||
append_sent=append_sent,
|
append_sent=append_sent,
|
||||||
|
check_files=check_files,
|
||||||
|
)
|
||||||
|
validation_payload = _mock_validation_payload(validation_report)
|
||||||
|
build_payload = _mock_build_payload(build_result)
|
||||||
|
return _mock_campaign_send_response(
|
||||||
|
campaign=campaign,
|
||||||
|
version=version,
|
||||||
|
mailbox=mailbox,
|
||||||
|
validation_report=validation_report,
|
||||||
|
validation_payload=validation_payload,
|
||||||
|
build_result=build_result,
|
||||||
|
build_payload=build_payload,
|
||||||
|
send_batch=send_batch,
|
||||||
|
send=send,
|
||||||
|
include_warnings=include_warnings,
|
||||||
|
include_needs_review=include_needs_review,
|
||||||
|
append_sent=append_sent,
|
||||||
)
|
)
|
||||||
|
|
||||||
validation_json = validation_report.model_dump(mode="json")
|
|
||||||
validation_json.update({"ok": validation_report.ok, "error_count": validation_report.error_count, "warning_count": validation_report.warning_count})
|
|
||||||
build_report = build_result.report
|
|
||||||
build_json = build_report.model_dump(mode="json")
|
|
||||||
build_json.update({
|
|
||||||
"built_count": build_report.built_count,
|
|
||||||
"queueable_count": build_report.queueable_count,
|
|
||||||
"needs_review_count": build_report.needs_review_count,
|
|
||||||
"blocked_count": build_report.blocked_count,
|
|
||||||
"warning_count": build_report.warning_count,
|
|
||||||
"ready_count": build_report.ready_count,
|
|
||||||
"messages": [_message_payload(message) for message in build_report.messages],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"campaign_id": campaign.id,
|
|
||||||
"version_id": version.id,
|
|
||||||
"version_number": version.version_number,
|
|
||||||
"send_requested": send,
|
|
||||||
"include_warnings": include_warnings,
|
|
||||||
"include_needs_review": include_needs_review,
|
|
||||||
"append_sent": append_sent,
|
|
||||||
"steps": [
|
|
||||||
{"key": "validate", "label": "Validate campaign JSON", "status": "ok" if validation_report.ok else "needs_review", "summary": validation_json},
|
|
||||||
{"key": "build", "label": "Build messages", "status": "ok" if build_report.queueable_count else "needs_review", "summary": {"built": build_report.built_count, "queueable": build_report.queueable_count, "needs_review": build_report.needs_review_count, "blocked": build_report.blocked_count}},
|
|
||||||
{"key": "send", "label": "Mock SMTP delivery", "status": "skipped" if not send else ("ok" if send_batch.failed_count == 0 else "needs_review"), "summary": {"attempted": send_batch.attempted_count, "sent": send_batch.sent_count, "failed": send_batch.failed_count, "skipped": send_batch.skipped_count}},
|
|
||||||
{"key": "imap", "label": "Mock IMAP Sent append", "status": "skipped" if not send or not append_sent else ("ok" if send_batch.imap_failed_count == 0 else "needs_review"), "summary": {"appended": send_batch.imap_appended_count, "failed": send_batch.imap_failed_count}},
|
|
||||||
],
|
|
||||||
"validation": validation_json,
|
|
||||||
"build": build_json,
|
|
||||||
"send": {
|
|
||||||
"attempted_count": send_batch.attempted_count,
|
|
||||||
"sent_count": send_batch.sent_count,
|
|
||||||
"failed_count": send_batch.failed_count,
|
|
||||||
"skipped_count": send_batch.skipped_count,
|
|
||||||
"imap_appended_count": send_batch.imap_appended_count,
|
|
||||||
"imap_failed_count": send_batch.imap_failed_count,
|
|
||||||
"results": send_batch.results,
|
|
||||||
},
|
|
||||||
"mailbox": {"messages": mailbox.list_records(limit=200) if mailbox is not None else []},
|
|
||||||
}
|
|
||||||
|
|||||||
66
tests/test_mock_campaign_helpers.py
Normal file
66
tests/test_mock_campaign_helpers.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from govoplan_campaign.backend.dev.mock_campaign import (
|
||||||
|
_MockSendBatch,
|
||||||
|
_mock_send_steps,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MockCampaignStepTests(unittest.TestCase):
|
||||||
|
def test_preview_marks_delivery_steps_as_skipped(self):
|
||||||
|
validation = SimpleNamespace(ok=True)
|
||||||
|
build = SimpleNamespace(
|
||||||
|
built_count=2,
|
||||||
|
queueable_count=2,
|
||||||
|
needs_review_count=0,
|
||||||
|
blocked_count=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = _mock_send_steps(
|
||||||
|
validation_report=validation,
|
||||||
|
validation_payload={"ok": True},
|
||||||
|
build_report=build,
|
||||||
|
send_batch=_MockSendBatch(results=[]),
|
||||||
|
send=False,
|
||||||
|
append_sent=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[step["status"] for step in steps],
|
||||||
|
["ok", "ok", "skipped", "skipped"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_delivery_failures_require_review(self):
|
||||||
|
validation = SimpleNamespace(ok=False)
|
||||||
|
build = SimpleNamespace(
|
||||||
|
built_count=1,
|
||||||
|
queueable_count=0,
|
||||||
|
needs_review_count=1,
|
||||||
|
blocked_count=0,
|
||||||
|
)
|
||||||
|
batch = _MockSendBatch(
|
||||||
|
results=[],
|
||||||
|
failed_count=1,
|
||||||
|
imap_failed_count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = _mock_send_steps(
|
||||||
|
validation_report=validation,
|
||||||
|
validation_payload={"ok": False},
|
||||||
|
build_report=build,
|
||||||
|
send_batch=batch,
|
||||||
|
send=True,
|
||||||
|
append_sent=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[step["status"] for step in steps],
|
||||||
|
["needs_review", "needs_review", "needs_review", "needs_review"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user