598 lines
27 KiB
Python
598 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from govoplan_campaign.backend.campaign.models import CampaignConfig
|
|
from govoplan_campaign.backend.campaign.loader import validate_against_schema
|
|
from govoplan_campaign.backend.campaign.validation import validate_campaign_config
|
|
from govoplan_campaign.backend.messages.builder import build_campaign_messages
|
|
|
|
|
|
class CampaignAttachmentBuildTests(unittest.TestCase):
|
|
def _no_attachment_config(
|
|
self,
|
|
behavior: str | None = None,
|
|
legacy_allow: bool | None = None,
|
|
*,
|
|
configure_missing_rule: bool = False,
|
|
) -> CampaignConfig:
|
|
attachments: dict[str, object] = {
|
|
"base_path": ".",
|
|
"global": [],
|
|
"zip": {"enabled": False, "archives": []},
|
|
}
|
|
if configure_missing_rule:
|
|
attachments["global"] = [
|
|
{
|
|
"id": "missing",
|
|
"label": "Missing attachment",
|
|
"base_dir": ".",
|
|
"file_filter": "missing.pdf",
|
|
"required": False,
|
|
"missing_behavior": "continue",
|
|
}
|
|
]
|
|
if behavior is not None:
|
|
attachments["send_without_attachments_behavior"] = behavior
|
|
if legacy_allow is not None:
|
|
attachments["send_without_attachments"] = legacy_allow
|
|
return CampaignConfig.model_validate({
|
|
"version": "1.0",
|
|
"campaign": {"id": f"no-attachment-{behavior or legacy_allow}", "name": "No attachment policy", "mode": "test"},
|
|
"fields": [],
|
|
"global_values": {},
|
|
"server": {
|
|
"mail_profile_id": "profile-1",
|
|
"profile_capabilities": {"smtp_available": True},
|
|
},
|
|
"recipients": {"from": {"email": "sender@example.org", "type": "to"}, "allow_individual_to": True},
|
|
"template": {"subject": "Subject", "text": "Body"},
|
|
"attachments": attachments,
|
|
"entries": {"inline": [{"id": "recipient-1", "to": [{"email": "recipient@example.org", "type": "to"}]}]},
|
|
"validation_policy": {"missing_email": "block", "template_error": "block"},
|
|
"delivery": {"imap_append_sent": {"enabled": False}},
|
|
})
|
|
|
|
def _attachment_reuse_config(
|
|
self,
|
|
*,
|
|
action: str,
|
|
allow_within: str = "none",
|
|
recipient_emails: tuple[str, ...] = (
|
|
"first@example.org",
|
|
"second@example.org",
|
|
),
|
|
duplicate_rules: bool = False,
|
|
) -> CampaignConfig:
|
|
rules = [{
|
|
"id": "shared-file",
|
|
"base_dir": "documents",
|
|
"file_filter": "shared.pdf",
|
|
"required": True,
|
|
}]
|
|
if duplicate_rules:
|
|
rules.append({**rules[0], "id": "shared-file-again"})
|
|
return CampaignConfig.model_validate({
|
|
"version": "1.0",
|
|
"campaign": {
|
|
"id": f"reuse-{action}-{allow_within}",
|
|
"name": "Attachment reuse",
|
|
"mode": "test",
|
|
},
|
|
"server": {
|
|
"mail_profile_id": "profile-1",
|
|
"profile_capabilities": {"smtp_available": True},
|
|
},
|
|
"recipients": {
|
|
"from": {"email": "sender@example.org", "type": "to"},
|
|
"allow_individual_to": True,
|
|
},
|
|
"template": {"subject": "Subject", "text": "Body"},
|
|
"attachments": {
|
|
"global": rules,
|
|
"reuse_policy": {
|
|
"action": action,
|
|
"allow_within": allow_within,
|
|
},
|
|
},
|
|
"entries": {
|
|
"inline": [
|
|
{
|
|
"id": f"recipient-{index}",
|
|
"to": [{"email": email, "type": "to"}],
|
|
}
|
|
for index, email in enumerate(recipient_emails, start=1)
|
|
]
|
|
},
|
|
"validation_policy": {
|
|
"missing_email": "block",
|
|
"template_error": "block",
|
|
},
|
|
"delivery": {"imap_append_sent": {"enabled": False}},
|
|
})
|
|
|
|
def test_send_without_attachments_policy_does_not_block_when_no_rules_are_configured(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._no_attachment_config(legacy_allow=False)
|
|
|
|
validation = validate_campaign_config(config, campaign_file=campaign_file, check_files=True)
|
|
self.assertTrue(validation.ok)
|
|
self.assertNotIn("missing_attachment_coverage", {issue.code for issue in validation.issues})
|
|
|
|
result = build_campaign_messages(config, campaign_file=campaign_file, output_dir=root / "out", write_eml=True)
|
|
self.assertEqual(result.report.build_failed_count, 0)
|
|
self.assertEqual(result.report.queueable_count, 1)
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.attachment_count, 0)
|
|
self.assertIsNotNone(message.eml_path)
|
|
self.assertNotIn("missing_attachment_coverage", {issue.code for issue in message.issues})
|
|
self.assertIsNotNone(result.built_messages[0].mime)
|
|
|
|
def test_configured_attachment_rule_blocks_generation_when_no_files_resolve(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._no_attachment_config(behavior="block", configure_missing_rule=True)
|
|
|
|
validation = validate_campaign_config(config, campaign_file=campaign_file, check_files=True)
|
|
self.assertFalse(validation.ok)
|
|
self.assertIn("missing_attachment_coverage", {issue.code for issue in validation.issues})
|
|
|
|
result = build_campaign_messages(config, campaign_file=campaign_file, output_dir=root / "out", write_eml=True)
|
|
self.assertEqual(result.report.build_failed_count, 1)
|
|
self.assertEqual(result.report.queueable_count, 0)
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.attachment_count, 0)
|
|
self.assertIsNone(message.eml_path)
|
|
self.assertIn("missing_attachment_coverage", {issue.code for issue in message.issues})
|
|
self.assertIsNone(result.built_messages[0].mime)
|
|
|
|
def test_send_without_attachments_behavior_modes(self) -> None:
|
|
cases = {
|
|
"block": ("build_failed", "blocked", 0, "block", False),
|
|
"ask": ("built", "needs_review", 0, "ask", True),
|
|
"drop": ("built", "needs_review", 0, "ask", True),
|
|
"warn": ("built", "warning", 1, "warn", True),
|
|
"continue": ("built", "warning", 1, None, True),
|
|
}
|
|
for behavior, (build_status, validation_status, queueable_count, issue_behavior, has_mime) in cases.items():
|
|
with self.subTest(behavior=behavior):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._no_attachment_config(behavior=behavior, configure_missing_rule=True)
|
|
|
|
validation = validate_campaign_config(config, campaign_file=campaign_file, check_files=True)
|
|
if behavior == "block":
|
|
self.assertFalse(validation.ok)
|
|
else:
|
|
self.assertTrue(validation.ok)
|
|
|
|
result = build_campaign_messages(config, campaign_file=campaign_file, output_dir=root / "out", write_eml=True)
|
|
self.assertEqual(result.report.queueable_count, queueable_count)
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.build_status.value, build_status)
|
|
self.assertEqual(message.validation_status.value, validation_status)
|
|
if validation_status == "excluded":
|
|
self.assertEqual(message.send_status.value, "skipped")
|
|
self.assertEqual(message.imap_status.value, "skipped")
|
|
coverage_issues = [issue for issue in message.issues if issue.code == "missing_attachment_coverage"]
|
|
if issue_behavior is None:
|
|
self.assertEqual(coverage_issues, [])
|
|
else:
|
|
self.assertEqual(coverage_issues[0].behavior, issue_behavior)
|
|
self.assertEqual(result.built_messages[0].mime is not None, has_mime)
|
|
|
|
def test_required_missing_policy_cannot_be_loosened_by_rule(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._no_attachment_config(
|
|
behavior="continue",
|
|
configure_missing_rule=True,
|
|
)
|
|
rule = config.attachments.global_[0]
|
|
rule.required = True
|
|
rule.missing_behavior = "continue"
|
|
config.attachments.missing_behavior = "continue"
|
|
|
|
result = build_campaign_messages(
|
|
config,
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=True,
|
|
)
|
|
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.validation_status.value, "blocked")
|
|
issue = next(
|
|
item
|
|
for item in message.issues
|
|
if item.code == "missing_required_attachment"
|
|
)
|
|
self.assertEqual(issue.behavior, "block")
|
|
self.assertEqual(
|
|
issue.details["effective_policy"]["requirement_policy"],
|
|
"block",
|
|
)
|
|
self.assertEqual(
|
|
message.attachments[0].missing_policy["effective_behavior"],
|
|
"block",
|
|
)
|
|
|
|
def test_optional_missing_policy_warns_by_default(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._no_attachment_config(
|
|
behavior="continue",
|
|
configure_missing_rule=True,
|
|
)
|
|
config.attachments.global_[0].missing_behavior = None
|
|
|
|
result = build_campaign_messages(
|
|
config,
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=True,
|
|
)
|
|
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.validation_status.value, "warning")
|
|
issue = next(
|
|
item
|
|
for item in message.issues
|
|
if item.code == "missing_optional_attachment"
|
|
)
|
|
self.assertEqual(issue.behavior, "warn")
|
|
|
|
def test_missing_pattern_does_not_create_zip_member_or_count_as_attachment(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
documents = root / "documents"
|
|
documents.mkdir()
|
|
(documents / "matched.xlsx").write_bytes(b"matched workbook")
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = CampaignConfig.model_validate({
|
|
"version": "1.0",
|
|
"campaign": {"id": "zip-missing-pattern", "name": "ZIP missing pattern", "mode": "test"},
|
|
"fields": [],
|
|
"global_values": {},
|
|
"server": {
|
|
"mail_profile_id": "profile-1",
|
|
"profile_capabilities": {"smtp_available": True},
|
|
},
|
|
"recipients": {"from": {"email": "sender@example.org", "type": "to"}, "allow_individual_to": True},
|
|
"template": {"subject": "Subject", "text": "Body"},
|
|
"attachments": {
|
|
"base_path": ".",
|
|
"allow_individual": True,
|
|
"send_without_attachments": False,
|
|
"zip": {
|
|
"enabled": True,
|
|
"archives": [{"id": "bundle", "name": "recipient-bundle.zip", "standard": True}],
|
|
},
|
|
"global": [],
|
|
},
|
|
"entries": {
|
|
"inline": [{
|
|
"id": "recipient-1",
|
|
"to": [{"email": "recipient@example.org", "type": "to"}],
|
|
"attachments": [
|
|
{"id": "matched", "base_dir": "documents", "file_filter": "matched.xlsx", "required": True},
|
|
{
|
|
"id": "missing",
|
|
"base_dir": "documents",
|
|
"file_filter": "missing.xlsx",
|
|
"required": False,
|
|
"missing_behavior": "warn",
|
|
},
|
|
],
|
|
}]
|
|
},
|
|
"validation_policy": {
|
|
"missing_email": "block",
|
|
"template_error": "block",
|
|
"missing_optional_attachment": "warn",
|
|
},
|
|
"delivery": {"imap_append_sent": {"enabled": False}},
|
|
})
|
|
|
|
validation = validate_campaign_config(config, campaign_file=campaign_file, check_files=True)
|
|
self.assertTrue(validation.ok)
|
|
self.assertIn("missing_optional_attachment", {issue.code for issue in validation.issues})
|
|
|
|
result = build_campaign_messages(config, campaign_file=campaign_file, output_dir=root / "out", write_eml=False)
|
|
self.assertEqual(result.report.built_count, 1)
|
|
self.assertEqual(result.report.queueable_count, 1)
|
|
message = result.report.messages[0]
|
|
self.assertEqual(message.attachment_count, 1)
|
|
summaries = {attachment.attachment_id: attachment for attachment in message.attachments}
|
|
self.assertEqual(summaries["matched"].zip_filename, "recipient-bundle.zip")
|
|
self.assertEqual(summaries["matched"].zip_entry_names, ["matched.xlsx"])
|
|
self.assertIsNone(summaries["missing"].zip_filename)
|
|
self.assertEqual(summaries["missing"].zip_entry_names, [])
|
|
self.assertEqual(summaries["missing"].matches, [])
|
|
|
|
mime = result.built_messages[0].mime
|
|
self.assertIsNotNone(mime)
|
|
attachments = {part.get_filename(): part.get_payload(decode=True) for part in mime.iter_attachments()}
|
|
self.assertEqual(set(attachments), {"recipient-bundle.zip"})
|
|
with zipfile.ZipFile(io.BytesIO(attachments["recipient-bundle.zip"])) as archive:
|
|
self.assertEqual(archive.namelist(), ["matched.xlsx"])
|
|
self.assertEqual(archive.read("matched.xlsx"), b"matched workbook")
|
|
|
|
def test_attachment_reuse_action_controls_message_validation(self) -> None:
|
|
expected = {
|
|
"allow": ("ready", None, 0, 1),
|
|
"warn": ("warning", "warn", 1, 0),
|
|
"review": ("needs_review", "ask", 1, 0),
|
|
"block": ("blocked", "block", 1, 0),
|
|
}
|
|
for action, (status, behavior, violation_count, allowed_count) in expected.items():
|
|
with self.subTest(action=action), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
documents = root / "documents"
|
|
documents.mkdir()
|
|
(documents / "shared.pdf").write_bytes(b"shared")
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = self._attachment_reuse_config(action=action)
|
|
|
|
result = build_campaign_messages(
|
|
config,
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=False,
|
|
)
|
|
|
|
self.assertEqual(
|
|
[status, status],
|
|
[message.validation_status.value for message in result.report.messages],
|
|
)
|
|
report = result.report.attachment_reuse
|
|
self.assertEqual(1, report["duplicate_file_count"])
|
|
self.assertEqual(violation_count, report["violation_file_count"])
|
|
self.assertEqual(allowed_count, report["allowed_file_count"])
|
|
finding = report["findings"][0]
|
|
self.assertEqual("shared.pdf", finding["file_name"])
|
|
self.assertNotIn(str(root), str(report))
|
|
issues = [
|
|
issue
|
|
for message in result.report.messages
|
|
for issue in message.issues
|
|
if issue.code == "duplicate_attachment_reuse"
|
|
]
|
|
if behavior is None:
|
|
self.assertEqual([], issues)
|
|
else:
|
|
self.assertEqual([behavior, behavior], [issue.behavior for issue in issues])
|
|
self.assertEqual(
|
|
{"action": action, "allow_within": "none"},
|
|
issues[0].details["policy"],
|
|
)
|
|
|
|
def test_attachment_reuse_can_be_allowed_within_recipient_or_message(self) -> None:
|
|
cases = (
|
|
("same_recipient", ("same@example.org", "same@example.org"), False, 2),
|
|
("same_message", ("same@example.org",), True, 2),
|
|
)
|
|
for allow_within, recipients, duplicate_rules, expected_use_count in cases:
|
|
with self.subTest(allow_within=allow_within), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
documents = root / "documents"
|
|
documents.mkdir()
|
|
(documents / "shared.pdf").write_bytes(b"shared")
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
|
|
result = build_campaign_messages(
|
|
self._attachment_reuse_config(
|
|
action="block",
|
|
allow_within=allow_within,
|
|
recipient_emails=recipients,
|
|
duplicate_rules=duplicate_rules,
|
|
),
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=False,
|
|
)
|
|
|
|
self.assertEqual(
|
|
["ready"] * len(recipients),
|
|
[message.validation_status.value for message in result.report.messages],
|
|
)
|
|
report = result.report.attachment_reuse
|
|
self.assertEqual(1, report["allowed_file_count"])
|
|
self.assertEqual(0, report["violation_file_count"])
|
|
self.assertEqual(expected_use_count, report["findings"][0]["use_count"])
|
|
|
|
def test_attachment_reuse_policy_is_part_of_the_json_schema(self) -> None:
|
|
config = self._attachment_reuse_config(
|
|
action="review",
|
|
allow_within="same_recipient",
|
|
)
|
|
|
|
payload = config.model_dump(
|
|
mode="json",
|
|
by_alias=True,
|
|
exclude_none=True,
|
|
exclude_defaults=True,
|
|
)
|
|
payload["server"].pop("profile_capabilities", None)
|
|
|
|
validate_against_schema(payload)
|
|
|
|
def test_residual_files_become_a_separate_reviewed_report_or_attachment_message(self) -> None:
|
|
for mode, expected_attachment_count in (("report", 0), ("attach", 1)):
|
|
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
watched = root / "watched"
|
|
watched.mkdir()
|
|
(watched / "assigned.txt").write_text("assigned", encoding="utf-8")
|
|
(watched / "residual.txt").write_text("residual", encoding="utf-8")
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = CampaignConfig.model_validate({
|
|
"version": "1.0",
|
|
"campaign": {"id": f"residual-{mode}", "name": "Monthly import", "mode": "test"},
|
|
"fields": [],
|
|
"global_values": {},
|
|
"server": {
|
|
"mail_profile_id": "profile-1",
|
|
"profile_capabilities": {"smtp_available": True},
|
|
},
|
|
"recipients": {
|
|
"from": {"email": "sender@example.org", "type": "to"},
|
|
"allow_individual_to": True,
|
|
},
|
|
"template": {"subject": "Normal message", "text": "Normal body"},
|
|
"attachments": {
|
|
"base_paths": [{
|
|
"id": "watched",
|
|
"name": "Watched folder",
|
|
"path": "watched",
|
|
"unsent_warning": True,
|
|
}],
|
|
"global": [{
|
|
"id": "assigned",
|
|
"base_path_id": "watched",
|
|
"base_dir": "watched",
|
|
"file_filter": "assigned.txt",
|
|
"required": True,
|
|
}],
|
|
"residual_files": {
|
|
"mode": mode,
|
|
"recipient": {"email": "operator@example.org", "name": "Operator"},
|
|
"subject": "Residual files for {{local:campaign_name}}",
|
|
"text": "{{local:residual_file_count}} file(s):\n{{local:residual_file_list}}",
|
|
},
|
|
},
|
|
"entries": {"inline": [{
|
|
"id": "recipient-1",
|
|
"to": [{"email": "recipient@example.org", "type": "to"}],
|
|
}]},
|
|
"validation_policy": {
|
|
"missing_email": "block",
|
|
"template_error": "block",
|
|
"unsent_attachment_files": "block",
|
|
},
|
|
"delivery": {"imap_append_sent": {"enabled": False}},
|
|
})
|
|
|
|
result = build_campaign_messages(
|
|
config,
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=True,
|
|
)
|
|
|
|
self.assertEqual(len(result.report.messages), 2)
|
|
self.assertEqual(
|
|
{
|
|
"contract_version": "1",
|
|
"action": "route_report" if mode == "report" else "route_with_files",
|
|
"routing_mode": mode,
|
|
"validation_behavior": "block",
|
|
"watched_source_count": 1,
|
|
"residual_file_count": 1,
|
|
"recipient": {
|
|
"email": "operator@example.org",
|
|
"name": "Operator",
|
|
"type": "to",
|
|
},
|
|
},
|
|
result.report.residual_file_disposition,
|
|
)
|
|
normal, residual = result.report.messages
|
|
self.assertEqual(normal.validation_status.value, "ready")
|
|
self.assertEqual(residual.entry_id, "__residual_files__")
|
|
self.assertEqual(residual.validation_status.value, "needs_review")
|
|
self.assertEqual(residual.to[0].email, "operator@example.org")
|
|
self.assertEqual(residual.subject, "Residual files for Monthly import")
|
|
self.assertEqual(residual.attachment_count, expected_attachment_count)
|
|
self.assertIn(
|
|
"residual_attachment_disposition",
|
|
{issue.code for issue in residual.issues},
|
|
)
|
|
self.assertNotIn(
|
|
"unsent_attachment_files",
|
|
{issue.code for message in result.report.messages for issue in message.issues},
|
|
)
|
|
mime = result.built_messages[1].mime
|
|
self.assertIsNotNone(mime)
|
|
self.assertIn("residual.txt", mime.get_body(preferencelist=("plain",)).get_content())
|
|
filenames = [part.get_filename() for part in mime.iter_attachments()]
|
|
self.assertEqual(filenames, ["residual.txt"] if mode == "attach" else [])
|
|
|
|
def test_residual_file_policy_evidence_normalizes_block_and_ignore(self) -> None:
|
|
for behavior, action in (("block", "block"), ("continue", "ignore")):
|
|
with self.subTest(behavior=behavior), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
watched = root / "watched"
|
|
watched.mkdir()
|
|
(watched / "residual.txt").write_text("residual", encoding="utf-8")
|
|
campaign_file = root / "campaign.json"
|
|
campaign_file.write_text("{}", encoding="utf-8")
|
|
config = CampaignConfig.model_validate({
|
|
"version": "1.0",
|
|
"campaign": {"id": f"residual-{behavior}", "name": "Residual", "mode": "test"},
|
|
"fields": [],
|
|
"global_values": {},
|
|
"server": {
|
|
"mail_profile_id": "profile-1",
|
|
"profile_capabilities": {"smtp_available": True},
|
|
},
|
|
"recipients": {
|
|
"from": {"email": "sender@example.org", "type": "to"},
|
|
"allow_individual_to": True,
|
|
},
|
|
"template": {"subject": "Normal", "text": "Body"},
|
|
"attachments": {
|
|
"base_paths": [{
|
|
"id": "watched",
|
|
"name": "Watched folder",
|
|
"path": "watched",
|
|
"unsent_warning": True,
|
|
}],
|
|
},
|
|
"entries": {"inline": [{
|
|
"id": "recipient-1",
|
|
"to": [{"email": "recipient@example.org", "type": "to"}],
|
|
}]},
|
|
"validation_policy": {"unsent_attachment_files": behavior},
|
|
"delivery": {"imap_append_sent": {"enabled": False}},
|
|
})
|
|
|
|
result = build_campaign_messages(
|
|
config,
|
|
campaign_file=campaign_file,
|
|
output_dir=root / "out",
|
|
write_eml=False,
|
|
)
|
|
|
|
self.assertEqual(action, result.report.residual_file_disposition["action"])
|
|
self.assertEqual(1, result.report.residual_file_disposition["residual_file_count"])
|
|
issue_codes = {
|
|
issue.code
|
|
for message in result.report.messages
|
|
for issue in message.issues
|
|
}
|
|
self.assertEqual(behavior == "block", "unsent_attachment_files" in issue_codes)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|