from __future__ import annotations import unittest from sqlalchemy import create_engine, event from sqlalchemy.orm import Session from govoplan_core.db.base import Base from govoplan_notifications.backend.db.models import ( NotificationDeliveryAttempt, NotificationMessage, ) from govoplan_notifications.backend.schemas import NotificationCreateRequest from govoplan_notifications.backend.service import ( create_notification, list_notifications, notification_response, ) class NotificationListEfficiencyTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all( self.engine, tables=[ NotificationMessage.__table__, NotificationDeliveryAttempt.__table__, ], ) def tearDown(self) -> None: self.engine.dispose() def seed(self, count: int) -> str: with Session(self.engine) as session: first_id = "" for index in range(count + 2): row = create_notification( session, tenant_id="tenant-other" if index == count else "tenant-one", payload=NotificationCreateRequest( source_module="test", source_resource_type="record", event_kind="changed", enqueue_delivery=False, recipient_id="other-recipient" if index == count + 1 else "reader", subject=f"Fixture {index}", ), ) if not first_id: first_id = row.id session.add( NotificationDeliveryAttempt( notification_id=row.id, tenant_id=row.tenant_id, attempt_no=1, channel="inbox", status="failed", ) ) session.commit() return first_id def test_list_and_full_attempt_projection_use_two_queries_independent_of_page_size( self, ) -> None: for count in (1, 40): with self.subTest(count=count): first_id = self.seed(count) statements: list[str] = [] def count_selects( _connection, _cursor, statement, _parameters, _context, _many ): if statement.lstrip().upper().startswith("SELECT"): statements.append(statement) event.listen(self.engine, "before_cursor_execute", count_selects) try: with Session(self.engine) as session: rows = list_notifications( session, tenant_id="tenant-one", recipient_ids=("reader",), limit=count, ) payloads = [notification_response(row) for row in rows] self.assertEqual(count, len(payloads)) self.assertTrue( all(len(item["attempts"]) == 1 for item in payloads) ) self.assertTrue( all( item["tenant_id"] == "tenant-one" and item["recipient_id"] == "reader" for item in payloads ) ) self.assertEqual( 2, len(statements), "The list and attempt projection must not add a query per message.", ) finally: event.remove(self.engine, "before_cursor_execute", count_selects) self.assertTrue(first_id) def test_attempts_with_inconsistent_tenant_are_never_projected_even_from_a_loaded_relationship( self, ) -> None: first_id = self.seed(1) with Session(self.engine) as session: session.add( NotificationDeliveryAttempt( id="foreign-attempt", notification_id=first_id, tenant_id="tenant-other", attempt_no=2, channel="mail", status="failed", error="Foreign tenant evidence", ) ) session.commit() with Session(self.engine) as session: row = session.get(NotificationMessage, first_id) self.assertEqual(2, len(row.attempts)) self.assertEqual(1, len(notification_response(row)["attempts"])) with Session(self.engine) as session: rows = list_notifications( session, tenant_id="tenant-one", recipient_ids=("reader",) ) self.assertEqual([1], [len(row.attempts) for row in rows]) if __name__ == "__main__": unittest.main()