Add Postbox message authoring and concurrency

This commit is contained in:
2026-07-31 18:40:39 +02:00
parent 0b6ebc3c74
commit dc6f81dd33
12 changed files with 1392 additions and 68 deletions
+24
View File
@@ -18,17 +18,24 @@ class PostboxMigrationTests(unittest.TestCase):
"govoplan_postbox.backend.migrations.versions."
"e4b7c9d2a6f1_v011_hierarchy_routes"
)
occ_migration = importlib.import_module(
"govoplan_postbox.backend.migrations.versions."
"f5c8d0e3b7a2_v012_authoring_and_occ"
)
engine = create_engine("sqlite:///:memory:")
try:
with engine.begin() as connection:
operations = Operations(MigrationContext.configure(connection))
original = migration.op
route_original = route_migration.op
occ_original = occ_migration.op
migration.op = operations
route_migration.op = operations
occ_migration.op = operations
try:
migration.upgrade()
route_migration.upgrade()
occ_migration.upgrade()
tables = set(inspect(connection).get_table_names())
self.assertIn("postboxes", tables)
self.assertIn("postbox_messages", tables)
@@ -50,6 +57,21 @@ class PostboxMigrationTests(unittest.TestCase):
"withdrawn_at",
}.issubset(message_columns)
)
self.assertIn("authoring_key", message_columns)
for table_name in (
"postbox_templates",
"postboxes",
"postbox_groupings",
):
self.assertIn(
"resource_revision",
{
column["name"]
for column in inspect(connection).get_columns(
table_name
)
},
)
route_columns = {
column["name"]
for column in inspect(connection).get_columns(
@@ -61,6 +83,7 @@ class PostboxMigrationTests(unittest.TestCase):
route_columns
)
)
occ_migration.downgrade()
route_migration.downgrade()
migration.downgrade()
self.assertFalse(
@@ -73,6 +96,7 @@ class PostboxMigrationTests(unittest.TestCase):
finally:
migration.op = original
route_migration.op = route_original
occ_migration.op = occ_original
finally:
engine.dispose()
+129
View File
@@ -209,6 +209,7 @@ class PostboxRouterTests(unittest.TestCase):
{
"postbox:postbox:read",
"postbox:message:write",
"postbox:message:reply",
"postbox:message:acknowledge",
"postbox:delivery:write",
"postbox:binding:admin",
@@ -314,6 +315,134 @@ class PostboxRouterTests(unittest.TestCase):
response.json()["diagnostics"],
)
def test_message_authoring_and_reply_are_idempotent_and_linked(self) -> None:
authored_payload = {
"postbox_id": self.postbox_id,
"idempotency_key": "compose-1",
"subject": "Status request",
"body_text": "Please provide a status update.",
"participants": [
{
"kind": "to",
"reference_type": "address",
"address": "team@example.invalid",
}
],
}
authored = self.client.post(
"/api/v1/postbox/messages",
json=authored_payload,
)
duplicate = self.client.post(
"/api/v1/postbox/messages",
json=authored_payload,
)
conflict = self.client.post(
"/api/v1/postbox/messages",
json={**authored_payload, "subject": "Different request"},
)
self.assertEqual(201, authored.status_code, authored.text)
self.assertEqual(201, duplicate.status_code, duplicate.text)
self.assertEqual(authored.json()["id"], duplicate.json()["id"])
self.assertEqual(409, conflict.status_code, conflict.text)
self.assertEqual("author", authored.json()["participants"][0]["kind"])
reply_payload = {
"idempotency_key": "reply-1",
"subject": "Re: Status request",
"body_text": "The work is complete.",
}
reply = self.client.post(
f"/api/v1/postbox/messages/{authored.json()['id']}/replies",
json=reply_payload,
)
duplicate_reply = self.client.post(
f"/api/v1/postbox/messages/{authored.json()['id']}/replies",
json=reply_payload,
)
self.assertEqual(201, reply.status_code, reply.text)
self.assertEqual(reply.json()["id"], duplicate_reply.json()["id"])
self.assertEqual(
authored.json()["id"],
reply.json()["in_reply_to_message_id"],
)
def test_mutable_admin_resources_require_strong_preconditions(self) -> None:
grouping = self.client.post(
"/api/v1/postbox/groupings",
json={
"name": "Work",
"is_default": True,
"postbox_ids": [self.postbox_id],
},
)
self.assertEqual(201, grouping.status_code, grouping.text)
grouping_data = grouping.json()
update_payload = {
"name": "Current work",
"is_default": True,
"postbox_ids": [self.postbox_id],
"base_revision": grouping_data["resource_revision"],
}
updated = self.client.put(
f"/api/v1/postbox/groupings/{grouping_data['id']}",
json=update_payload,
headers={"If-Match": grouping_data["etag"]},
)
stale = self.client.put(
f"/api/v1/postbox/groupings/{grouping_data['id']}",
json=update_payload,
headers={"If-Match": grouping_data["etag"]},
)
self.assertEqual(200, updated.status_code, updated.text)
self.assertEqual(2, updated.json()["resource_revision"])
self.assertEqual(412, stale.status_code, stale.text)
template = self.client.post(
"/api/v1/postbox/admin/templates",
json={
"slug": "case-intake",
"name": "Case intake",
"scope_kind": "tenant",
"name_pattern": "{unit_name} / {function_name}",
"address_pattern": "{template_slug}.{unit_slug}.{function_slug}",
"classification": "internal",
},
)
self.assertEqual(201, template.status_code, template.text)
template_data = template.json()
published = self.client.post(
f"/api/v1/postbox/admin/templates/{template_data['id']}/publish",
json={"base_revision": template_data["resource_revision"]},
headers={"If-Match": template_data["etag"]},
)
stale_retire = self.client.post(
f"/api/v1/postbox/admin/templates/{template_data['id']}/retire",
json={"base_revision": template_data["resource_revision"]},
headers={"If-Match": template_data["etag"]},
)
self.assertEqual(200, published.status_code, published.text)
self.assertEqual(412, stale_retire.status_code, stale_retire.text)
directory = self.client.get("/api/v1/postbox/admin/postboxes").json()
postbox = directory["postboxes"][0]
missing = self.client.request(
"DELETE",
f"/api/v1/postbox/admin/postboxes/{self.postbox_id}",
json={"base_revision": postbox["resource_revision"]},
)
archived = self.client.request(
"DELETE",
f"/api/v1/postbox/admin/postboxes/{self.postbox_id}",
json={"base_revision": postbox["resource_revision"]},
headers={"If-Match": postbox["etag"]},
)
self.assertEqual(428, missing.status_code, missing.text)
self.assertEqual(200, archived.status_code, archived.text)
self.assertEqual(2, archived.json()["resource_revision"])
if __name__ == "__main__":
unittest.main()
+6
View File
@@ -616,6 +616,7 @@ class PostboxServiceTests(unittest.TestCase):
template_id=target_template.id,
revision_number=None,
actor_id="admin-1",
expected_revision=target_template.resource_revision,
)
source_template = service.create_template(
session,
@@ -644,6 +645,7 @@ class PostboxServiceTests(unittest.TestCase):
template_id=source_template.id,
revision_number=None,
actor_id="admin-1",
expected_revision=source_template.resource_revision,
)
source = service.materialize_template(
session,
@@ -846,6 +848,7 @@ class PostboxServiceTests(unittest.TestCase):
template_id=template.id,
revision_number=1,
actor_id="admin-1",
expected_revision=template.resource_revision,
)
first = self.service.materialize_template(
session,
@@ -877,6 +880,7 @@ class PostboxServiceTests(unittest.TestCase):
classification="restricted",
allow_vacant_delivery=True,
actor_id="admin-1",
expected_revision=template.resource_revision,
)
self.assertEqual(first.id, second.id)
@@ -916,6 +920,7 @@ class PostboxServiceTests(unittest.TestCase):
template_id=template.id,
revision_number=None,
actor_id="admin-1",
expected_revision=template.resource_revision,
)
session.commit()
@@ -1474,6 +1479,7 @@ class PostboxServiceTests(unittest.TestCase):
tenant_id="tenant-1",
actor=self.actor,
grouping_id=grouping_id,
expected_revision=grouping.resource_revision,
name="Assigned work",
is_default=True,
postbox_ids=(parent.id,),