Add Postbox message authoring and concurrency
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,),
|
||||
|
||||
Reference in New Issue
Block a user