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()