303 lines
9.8 KiB
Python
303 lines
9.8 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
|
from govoplan_core.core.access import PrincipalRef
|
|
from govoplan_core.core.identity import IdentityRef
|
|
from govoplan_core.core.idm import (
|
|
OrganizationFunctionAssignmentRef,
|
|
OrganizationFunctionIncumbencyRef,
|
|
)
|
|
from govoplan_core.core.organizations import (
|
|
OrganizationFunctionRef,
|
|
OrganizationUnitRef,
|
|
)
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_postbox.backend.db.models import (
|
|
Postbox,
|
|
PostboxAccessEvent,
|
|
PostboxAddress,
|
|
PostboxAttachmentReference,
|
|
PostboxBinding,
|
|
PostboxDelivery,
|
|
PostboxGrouping,
|
|
PostboxGroupingSource,
|
|
PostboxMessage,
|
|
PostboxMessageReceipt,
|
|
PostboxParticipant,
|
|
PostboxRoute,
|
|
PostboxTemplate,
|
|
PostboxTemplateRevision,
|
|
)
|
|
from govoplan_postbox.backend.router import router
|
|
from govoplan_postbox.backend.service import PostboxService
|
|
|
|
|
|
TABLES = (
|
|
PostboxTemplate.__table__,
|
|
PostboxTemplateRevision.__table__,
|
|
PostboxAddress.__table__,
|
|
Postbox.__table__,
|
|
PostboxBinding.__table__,
|
|
PostboxMessage.__table__,
|
|
PostboxParticipant.__table__,
|
|
PostboxAttachmentReference.__table__,
|
|
PostboxDelivery.__table__,
|
|
PostboxRoute.__table__,
|
|
PostboxMessageReceipt.__table__,
|
|
PostboxGrouping.__table__,
|
|
PostboxGroupingSource.__table__,
|
|
PostboxAccessEvent.__table__,
|
|
)
|
|
|
|
|
|
class FakeIdentityDirectory:
|
|
def get_identity(self, identity_id: str):
|
|
return IdentityRef(id=identity_id, primary_account_id="account-1")
|
|
|
|
def identity_for_account(self, account_id: str):
|
|
return IdentityRef(id="identity-1", primary_account_id=account_id)
|
|
|
|
def identities_for_accounts(self, account_ids):
|
|
return tuple(self.identity_for_account(account_id) for account_id in account_ids)
|
|
|
|
def accounts_for_identity(self, identity_id: str):
|
|
return ()
|
|
|
|
|
|
class FakeIdmDirectory:
|
|
def __init__(self, assignment: OrganizationFunctionAssignmentRef) -> None:
|
|
self.assignment = assignment
|
|
|
|
def get_organization_function_assignment(self, assignment_id: str):
|
|
return self.assignment if assignment_id == self.assignment.id else None
|
|
|
|
def organization_function_assignments_for_identity(
|
|
self,
|
|
identity_id: str,
|
|
*,
|
|
tenant_id: str | None = None,
|
|
):
|
|
return (self.assignment,) if identity_id == self.assignment.identity_id else ()
|
|
|
|
def organization_function_assignments_for_account(
|
|
self,
|
|
account_id: str,
|
|
*,
|
|
tenant_id: str | None = None,
|
|
):
|
|
return (self.assignment,) if account_id == self.assignment.account_id else ()
|
|
|
|
def organization_function_assignments_for_function(
|
|
self,
|
|
function_id: str,
|
|
*,
|
|
tenant_id: str | None = None,
|
|
):
|
|
return (self.assignment,) if function_id == self.assignment.function_id else ()
|
|
|
|
def organization_function_incumbencies(
|
|
self,
|
|
function_ids,
|
|
*,
|
|
tenant_id: str,
|
|
effective_at=None,
|
|
):
|
|
del effective_at
|
|
return {
|
|
function_id: OrganizationFunctionIncumbencyRef(
|
|
tenant_id=tenant_id,
|
|
function_id=function_id,
|
|
assignments=self.organization_function_assignments_for_function(
|
|
function_id,
|
|
tenant_id=tenant_id,
|
|
),
|
|
)
|
|
for function_id in function_ids
|
|
}
|
|
|
|
|
|
class FakeOrganizationDirectory:
|
|
unit = OrganizationUnitRef(
|
|
id="unit-1",
|
|
tenant_id="tenant-1",
|
|
slug="district",
|
|
name="District",
|
|
)
|
|
function = OrganizationFunctionRef(
|
|
id="function-1",
|
|
tenant_id="tenant-1",
|
|
organization_unit_id="unit-1",
|
|
slug="clerk",
|
|
name="Clerk",
|
|
function_type_id="clerk-type",
|
|
)
|
|
|
|
def get_organization_unit(self, organization_unit_id: str):
|
|
return self.unit if organization_unit_id == self.unit.id else None
|
|
|
|
def organization_units_for_tenant(self, tenant_id: str):
|
|
return (self.unit,) if tenant_id == self.unit.tenant_id else ()
|
|
|
|
def get_function(self, function_id: str):
|
|
return self.function if function_id == self.function.id else None
|
|
|
|
def functions_for_organization_unit(
|
|
self,
|
|
organization_unit_id: str,
|
|
*,
|
|
include_subunits: bool = False,
|
|
):
|
|
return (self.function,) if organization_unit_id == self.unit.id else ()
|
|
|
|
|
|
class PostboxRouterTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.engine = create_engine(
|
|
"sqlite://",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
Base.metadata.create_all(self.engine, tables=TABLES)
|
|
assignment = OrganizationFunctionAssignmentRef(
|
|
id="assignment-1",
|
|
tenant_id="tenant-1",
|
|
identity_id="identity-1",
|
|
account_id="account-1",
|
|
function_id="function-1",
|
|
organization_unit_id="unit-1",
|
|
)
|
|
idm = FakeIdmDirectory(assignment)
|
|
self.service = PostboxService(
|
|
identities=FakeIdentityDirectory(), # type: ignore[arg-type]
|
|
idm=idm, # type: ignore[arg-type]
|
|
incumbencies=idm, # type: ignore[arg-type]
|
|
organizations=FakeOrganizationDirectory(), # type: ignore[arg-type]
|
|
)
|
|
with Session(self.engine) as session:
|
|
self.postbox = self.service.create_exact_postbox(
|
|
session,
|
|
tenant_id="tenant-1",
|
|
name="District / Clerk",
|
|
organization_unit_id="unit-1",
|
|
function_id="function-1",
|
|
address_key=None,
|
|
description=None,
|
|
classification="internal",
|
|
actor_id="account-1",
|
|
)
|
|
session.commit()
|
|
self.postbox_id = self.postbox.id
|
|
|
|
principal = ApiPrincipal(
|
|
principal=PrincipalRef(
|
|
account_id="account-1",
|
|
membership_id="membership-1",
|
|
tenant_id="tenant-1",
|
|
identity_id="identity-1",
|
|
scopes=frozenset(
|
|
{
|
|
"postbox:postbox:read",
|
|
"postbox:message:write",
|
|
"postbox:message:acknowledge",
|
|
"postbox:delivery:write",
|
|
"postbox:binding:admin",
|
|
"postbox:template:admin",
|
|
}
|
|
),
|
|
),
|
|
account=SimpleNamespace(id="account-1"),
|
|
user=SimpleNamespace(id="membership-1"),
|
|
)
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api/v1")
|
|
|
|
def session_dependency():
|
|
with Session(self.engine) as session:
|
|
yield session
|
|
|
|
app.dependency_overrides[get_session] = session_dependency
|
|
app.dependency_overrides[get_api_principal] = lambda: principal
|
|
self.patch = patch(
|
|
"govoplan_postbox.backend.router.get_service",
|
|
return_value=self.service,
|
|
)
|
|
self.patch.start()
|
|
self.client = TestClient(app)
|
|
|
|
def tearDown(self) -> None:
|
|
self.client.close()
|
|
self.patch.stop()
|
|
self.engine.dispose()
|
|
|
|
def test_directory_delivery_message_and_receipt_round_trip(self) -> None:
|
|
directory = self.client.get("/api/v1/postbox/directory")
|
|
self.assertEqual(200, directory.status_code, directory.text)
|
|
self.assertEqual(self.postbox_id, directory.json()["postboxes"][0]["id"])
|
|
|
|
delivery = self.client.post(
|
|
"/api/v1/postbox/deliveries",
|
|
json={
|
|
"target": {"postbox_id": self.postbox_id},
|
|
"producer_module": "campaigns",
|
|
"producer_resource_type": "campaign_recipient",
|
|
"producer_resource_id": "recipient-1",
|
|
"idempotency_key": "campaign-1:recipient-1",
|
|
"subject": "Decision",
|
|
"body_text": "The decision is ready.",
|
|
},
|
|
)
|
|
self.assertEqual(201, delivery.status_code, delivery.text)
|
|
message_id = delivery.json()["message_id"]
|
|
|
|
messages = self.client.get(
|
|
"/api/v1/postbox/messages",
|
|
params={"postbox_id": self.postbox_id},
|
|
)
|
|
self.assertEqual(200, messages.status_code, messages.text)
|
|
self.assertEqual(1, messages.json()["total"])
|
|
self.assertEqual(message_id, messages.json()["messages"][0]["id"])
|
|
|
|
filtered = self.client.get(
|
|
"/api/v1/postbox/messages",
|
|
params={
|
|
"postbox_id": self.postbox_id,
|
|
"q": "decision",
|
|
"state": "unread",
|
|
},
|
|
)
|
|
self.assertEqual(200, filtered.status_code, filtered.text)
|
|
self.assertEqual(1, filtered.json()["total"])
|
|
|
|
acknowledged = self.client.patch(
|
|
f"/api/v1/postbox/messages/{message_id}/state",
|
|
json={"state": "acknowledged"},
|
|
)
|
|
self.assertEqual(200, acknowledged.status_code, acknowledged.text)
|
|
self.assertIsNotNone(acknowledged.json()["read_at"])
|
|
self.assertIsNotNone(acknowledged.json()["acknowledged_at"])
|
|
|
|
unread = self.client.get(
|
|
"/api/v1/postbox/messages",
|
|
params={
|
|
"postbox_id": self.postbox_id,
|
|
"state": "unread",
|
|
},
|
|
)
|
|
self.assertEqual(200, unread.status_code, unread.text)
|
|
self.assertEqual(0, unread.json()["total"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|