Release v0.1.4
This commit is contained in:
@@ -71,6 +71,123 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
payload = response.json()
|
||||
return {"Authorization": f"Bearer {payload['access_token']}"}, payload
|
||||
|
||||
def test_recipient_import_mapping_profiles_are_db_backed(self) -> None:
|
||||
headers, _ = self._login()
|
||||
payload = {
|
||||
"name": "ERP export",
|
||||
"columnCount": 3,
|
||||
"headers": ["email", "name", "field.customer_id"],
|
||||
"normalizedHeaders": ["email", "name", "field_customer_id"],
|
||||
"orderedHeaderFingerprint": "ordered-123",
|
||||
"unorderedHeaderFingerprint": "unordered-123",
|
||||
"delimiter": ";",
|
||||
"headerRows": 1,
|
||||
"quoted": True,
|
||||
"valueSeparators": ",;|",
|
||||
"mappings": [
|
||||
{"columnIndex": 0, "kind": "to"},
|
||||
{"columnIndex": 1, "kind": "name"},
|
||||
{"columnIndex": 2, "kind": "new_field", "newFieldName": "customer_id"},
|
||||
],
|
||||
}
|
||||
|
||||
created = self.client.post("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers, json=payload)
|
||||
self.assertEqual(created.status_code, 201, created.text)
|
||||
created_body = created.json()
|
||||
self.assertEqual(created_body["name"], "ERP export")
|
||||
self.assertEqual(created_body["columnCount"], 3)
|
||||
self.assertEqual(created_body["mappings"][2]["newFieldName"], "customer_id")
|
||||
profile_id = created_body["id"]
|
||||
|
||||
listed = self.client.get("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers)
|
||||
self.assertEqual(listed.status_code, 200, listed.text)
|
||||
self.assertEqual([profile["id"] for profile in listed.json()["profiles"]], [profile_id])
|
||||
|
||||
updated_payload = {**payload, "name": "ERP export updated", "valueSeparators": "|"}
|
||||
updated = self.client.put(
|
||||
f"/api/v1/campaigns/recipient-import/mapping-profiles/{profile_id}",
|
||||
headers=headers,
|
||||
json=updated_payload,
|
||||
)
|
||||
self.assertEqual(updated.status_code, 200, updated.text)
|
||||
self.assertEqual(updated.json()["name"], "ERP export updated")
|
||||
self.assertEqual(updated.json()["valueSeparators"], "|")
|
||||
|
||||
deleted = self.client.delete(f"/api/v1/campaigns/recipient-import/mapping-profiles/{profile_id}", headers=headers)
|
||||
self.assertEqual(deleted.status_code, 204, deleted.text)
|
||||
|
||||
listed_after_delete = self.client.get("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers)
|
||||
self.assertEqual(listed_after_delete.status_code, 200, listed_after_delete.text)
|
||||
self.assertEqual(listed_after_delete.json()["profiles"], [])
|
||||
|
||||
def test_campaign_entries_store_recipient_import_provenance(self) -> None:
|
||||
headers, _ = self._login()
|
||||
campaign_json = {
|
||||
"version": "1.0",
|
||||
"campaign": {"id": "import-provenance", "name": "Import provenance", "mode": "test"},
|
||||
"fields": [{"name": "department", "type": "string", "required": False}],
|
||||
"global_values": {},
|
||||
"server": {},
|
||||
"recipients": {
|
||||
"from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
|
||||
"allow_individual_to": True,
|
||||
},
|
||||
"template": {"subject": "Hello", "text": "Hello"},
|
||||
"attachments": {"base_path": ".", "global": [], "allow_individual": False},
|
||||
"entries": {
|
||||
"inline": [
|
||||
{
|
||||
"id": "recipient-1",
|
||||
"to": [{"email": "recipient@example.org", "type": "to"}],
|
||||
"fields": {"department": "Finance"},
|
||||
}
|
||||
],
|
||||
"imports": [
|
||||
{
|
||||
"id": "recipient-import-test",
|
||||
"imported_at": "2026-06-26T12:00:00Z",
|
||||
"mode": "replace",
|
||||
"source_type": "xlsx",
|
||||
"filename": "recipients.xlsx",
|
||||
"sheet_name": "Recipients",
|
||||
"encoding": None,
|
||||
"delimiter": None,
|
||||
"header_rows": 1,
|
||||
"quoted": None,
|
||||
"value_separators": ",;|",
|
||||
"rows_total": 1,
|
||||
"valid_rows": 1,
|
||||
"invalid_rows": 0,
|
||||
"imported_rows": 1,
|
||||
"field_names_created": [],
|
||||
"attachment_patterns": 0,
|
||||
"mapping": [
|
||||
{"column_index": 0, "header": "email", "kind": "to"},
|
||||
{"column_index": 1, "header": "field.department", "kind": "field", "field_name": "department"},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
"validation_policy": {"missing_email": "block", "template_error": "block"},
|
||||
"delivery": {"rate_limit": {"messages_per_minute": 60}},
|
||||
"status_tracking": {"enabled": True},
|
||||
}
|
||||
created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
|
||||
self.assertEqual(created.status_code, 200, created.text)
|
||||
campaign_id = created.json()["campaign"]["id"]
|
||||
version_id = created.json()["version"]["id"]
|
||||
|
||||
detail = self.client.get(f"/api/v1/campaigns/{campaign_id}/versions/{version_id}", headers=headers)
|
||||
self.assertEqual(detail.status_code, 200, detail.text)
|
||||
stored_import = detail.json()["raw_json"]["entries"]["imports"][0]
|
||||
self.assertEqual(stored_import["source_type"], "xlsx")
|
||||
self.assertEqual(stored_import["sheet_name"], "Recipients")
|
||||
self.assertEqual(stored_import["mapping"][1]["field_name"], "department")
|
||||
|
||||
validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
|
||||
self.assertEqual(validated.status_code, 200, validated.text)
|
||||
self.assertTrue(validated.json()["ok"], validated.text)
|
||||
|
||||
def _create_built_delivery_campaign(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
@@ -587,6 +704,7 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
campaign_files = self.client.get("/api/v1/files", headers=headers, params={"campaign_id": campaign_id})
|
||||
self.assertEqual(campaign_files.status_code, 200, campaign_files.text)
|
||||
self.assertEqual({item["id"] for item in campaign_files.json()["files"]}, {first_file["id"], second_file["id"]})
|
||||
self.assertTrue(all(item["shares"] for item in campaign_files.json()["files"]))
|
||||
|
||||
|
||||
download = self.client.get(f"/api/v1/files/{first_file['id']}/download", headers=headers)
|
||||
@@ -1744,14 +1862,40 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
set_failures(smtp_reject_recipients_containing=None)
|
||||
|
||||
from govoplan_campaign.backend.db.models import CampaignJob, SendAttempt
|
||||
from govoplan_campaign.backend.sending.jobs import send_campaign_job
|
||||
with SessionLocal() as session:
|
||||
job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
|
||||
self.assertEqual(job.send_status, "smtp_accepted")
|
||||
self.assertIn("reject-me@example.org", job.last_error or "")
|
||||
job_id = job.id
|
||||
attempt_count = job.attempt_count
|
||||
attempt = session.query(SendAttempt).filter(SendAttempt.job_id == job.id).one()
|
||||
self.assertEqual(attempt.status, "smtp_accepted_with_refusals")
|
||||
self.assertIn("reject-me@example.org", attempt.smtp_response or "")
|
||||
|
||||
retry_accepted = self.client.post(
|
||||
f"/api/v1/campaigns/{campaign_id}/jobs/retry",
|
||||
headers=headers,
|
||||
json={"version_id": version_id, "job_ids": [job_id], "include_permanent": True, "enqueue_celery": False},
|
||||
)
|
||||
self.assertEqual(retry_accepted.status_code, 200, retry_accepted.text)
|
||||
self.assertEqual(retry_accepted.json()["result"]["selected_count"], 0)
|
||||
self.assertIn("not an explicit retry candidate", retry_accepted.json()["result"]["skipped"][0]["reason"])
|
||||
|
||||
unattempted_accepted = self.client.post(
|
||||
f"/api/v1/campaigns/{campaign_id}/jobs/send-unattempted",
|
||||
headers=headers,
|
||||
json={"version_id": version_id, "job_ids": [job_id], "enqueue_celery": False},
|
||||
)
|
||||
self.assertEqual(unattempted_accepted.status_code, 200, unattempted_accepted.text)
|
||||
self.assertEqual(unattempted_accepted.json()["result"]["selected_count"], 0)
|
||||
|
||||
with SessionLocal() as session:
|
||||
direct_result = send_campaign_job(session, job_id=job_id, use_rate_limit=False)
|
||||
self.assertEqual(direct_result.status, "already_accepted")
|
||||
job = session.get(CampaignJob, job_id)
|
||||
self.assertEqual(job.attempt_count, attempt_count)
|
||||
|
||||
def test_worker_loss_becomes_unknown_and_requires_reconciliation_before_retry(self) -> None:
|
||||
headers, _ = self._login()
|
||||
campaign_id, version_id = self._create_built_delivery_campaign(
|
||||
@@ -1767,6 +1911,11 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(queued.status_code, 200, queued.text)
|
||||
self.assertEqual(queued.json()["queued_count"], 2)
|
||||
|
||||
queued_summary = self.client.get(f"/api/v1/campaigns/{campaign_id}/summary", headers=headers, params={"version_id": version_id})
|
||||
self.assertEqual(queued_summary.status_code, 200, queued_summary.text)
|
||||
self.assertEqual(queued_summary.json()["cards"]["queued_or_active"], 2)
|
||||
self.assertEqual(queued_summary.json()["status_counts"]["send"]["queued"], 2)
|
||||
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignVersion, SendAttempt
|
||||
from govoplan_campaign.backend.sending.jobs import send_campaign_job
|
||||
|
||||
@@ -1839,6 +1988,11 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(filtered_page.json()["total_unfiltered"], 2)
|
||||
self.assertEqual(filtered_page.json()["filtered_counts"]["send"]["outcome_unknown"], 1)
|
||||
|
||||
summary = self.client.get(f"/api/v1/campaigns/{campaign_id}/summary", headers=headers, params={"version_id": version_id})
|
||||
self.assertEqual(summary.status_code, 200, summary.text)
|
||||
self.assertEqual(summary.json()["cards"]["outcome_unknown"], 1)
|
||||
self.assertEqual(summary.json()["status_counts"]["send"]["outcome_unknown"], 1)
|
||||
|
||||
detail = self.client.get(
|
||||
f"/api/v1/campaigns/{campaign_id}/jobs/{uncertain_job_id}",
|
||||
headers=headers,
|
||||
@@ -1855,6 +2009,12 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(resolved.status_code, 200, resolved.text)
|
||||
self.assertEqual(resolved.json()["result"]["send_status"], "failed_temporary")
|
||||
|
||||
reconciled_report = self.client.get(f"/api/v1/campaigns/{campaign_id}/report", headers=headers, params={"version_id": version_id})
|
||||
self.assertEqual(reconciled_report.status_code, 200, reconciled_report.text)
|
||||
self.assertEqual(reconciled_report.json()["cards"]["outcome_unknown"], 0)
|
||||
self.assertEqual(reconciled_report.json()["cards"]["failed"], 1)
|
||||
self.assertEqual(reconciled_report.json()["status_counts"]["send"]["failed_temporary"], 1)
|
||||
|
||||
retry = self.client.post(
|
||||
f"/api/v1/campaigns/{campaign_id}/jobs/retry",
|
||||
headers=headers,
|
||||
|
||||
@@ -80,6 +80,8 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
}
|
||||
system_settings_columns = {column["name"] for column in inspector.get_columns("system_settings")}
|
||||
governance_assignment_columns = {column["name"] for column in inspector.get_columns("governance_template_assignments")}
|
||||
import_profile_columns = {column["name"] for column in inspector.get_columns("campaign_recipient_import_mapping_profiles")}
|
||||
import_profile_indexes = {index["name"] for index in inspector.get_indexes("campaign_recipient_import_mapping_profiles")}
|
||||
account_count = connection.execute(text("SELECT COUNT(*) FROM accounts")).scalar_one()
|
||||
user_count = connection.execute(text("SELECT COUNT(*) FROM users")).scalar_one()
|
||||
system_owner_count = connection.execute(text(
|
||||
@@ -114,8 +116,13 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
self.assertIn("governance_templates", tables)
|
||||
self.assertIn("governance_template_assignments", tables)
|
||||
self.assertIn("campaign_shares", tables)
|
||||
self.assertIn("campaign_recipient_import_mapping_profiles", tables)
|
||||
self.assertIn("owner_user_id", campaign_columns)
|
||||
self.assertIn("owner_group_id", campaign_columns)
|
||||
self.assertIn("ordered_header_fingerprint", import_profile_columns)
|
||||
self.assertIn("unordered_header_fingerprint", import_profile_columns)
|
||||
self.assertIn("mappings", import_profile_columns)
|
||||
self.assertIn("ix_recipient_import_profiles_ordered_fp", import_profile_indexes)
|
||||
self.assertIn("scope", audit_columns)
|
||||
self.assertIn("ix_audit_log_scope", audit_indexes)
|
||||
self.assertIn("ix_audit_log_scope_created_at", audit_indexes)
|
||||
@@ -409,4 +416,3 @@ class DatabaseMigrationTests(unittest.TestCase):
|
||||
self.assertIn("uq_file_folders_active_group_path", folder_indexes)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user