from __future__ import annotations from dataclasses import replace import unittest from sqlalchemy import event, select from sqlalchemy.dialects import postgresql import test_case_lifecycle as fixture from govoplan_cases.backend import service from govoplan_cases.backend.db.models import CaseAccessGrant, CaseRecordRevision class CaseListQueryTests(unittest.TestCase): def setUp(self): self.fixture = fixture.CaseLifecycleTests() self.fixture.setUp() def tearDown(self): self.fixture.tearDown() def seed(self, count=50, *, restricted=False): for index in range(count): original = fixture.record() reference = replace(original.reference, object_id=f"case-{index:03}") record = replace( original, reference=reference, case_number=f"CASE-{index:03}", context=replace(original.context, case_ref=reference), access_mode="restricted" if restricted else "tenant", ) service.create_case(self.fixture.session, self.fixture.principal, record=record, idempotency_key=f"fixture-{index}") self.fixture.session.add(CaseAccessGrant( tenant_id="tenant-1", case_id=reference.object_id, subject_kind="account", subject_id="other-account", permissions=["read"], allowed_purposes=["cases.casework"], source="manual", active=True, source_revision=1, )) self.fixture.session.commit() self.fixture.session.expunge_all() def test_filtered_empty_page_does_not_materialize_grants_or_case_ids(self): self.seed() loaded = [] queries = [] def row_loaded(target, context): loaded.append(target.id) def executed(conn, cursor, statement, parameters, context, executemany): if statement.lstrip().upper().startswith("SELECT"): queries.append((statement, len(parameters))) event.listen(CaseAccessGrant, "load", row_loaded) event.listen(self.fixture.engine, "before_cursor_execute", executed) try: result = service.list_cases( self.fixture.session, self.fixture.principal, query="no-fixture-matches-this", purpose="cases.casework", limit=1, ) self.assertEqual(((), 0), result) self.assertEqual([], loaded) self.assertEqual(2, len(queries)) self.assertTrue(all(count < 30 for _, count in queries)) self.assertIn("LIMIT", queries[-1][0]) finally: event.remove(CaseAccessGrant, "load", row_loaded) event.remove(self.fixture.engine, "before_cursor_execute", executed) def test_current_grants_exact_purpose_and_permissions_govern_total(self): self.seed(4, restricted=True) reader = fixture.Principal(account_id="other-account") session = self.fixture.session self.assertEqual(((), 0), service.list_cases(session, reader, limit=1)) self.assertEqual(((), 0), service.list_cases(session, reader, purpose="cases.case", limit=1)) page, total = service.list_cases(session, reader, purpose="cases.casework", offset=1, limit=1) self.assertEqual(4, total) self.assertEqual("case-001", page[0].reference.object_id) grants = session.query(CaseAccessGrant).filter(CaseAccessGrant.subject_id == "other-account").order_by(CaseAccessGrant.case_id).all() grants[0].active = False grants[1].permissions = ["reader"] grants[2].allowed_purposes = ["cases.casework.extra"] grants[3].permissions = ["update"] session.commit() self.assertEqual(1, service.list_cases(session, reader, purpose="cases.casework", limit=1)[1]) grants[3].allowed_purposes = {"not_an_array": "cases.casework"} session.commit() self.assertEqual(((), 0), service.list_cases(session, reader, purpose="cases.casework", limit=1)) self.assertEqual(((), 0), service.list_cases(session, fixture.Principal(tenant_id="tenant-2", account_id="other-account"), purpose="cases.casework", limit=1)) def test_access_predicate_compiles_for_postgresql_without_case_id_lists(self): compiled = select(CaseRecordRevision.case_id).where( service._case_access_predicate(self.fixture.principal, permission="read", purpose="cases.casework"), ).compile(dialect=postgresql.dialect()) self.assertIn("EXISTS", str(compiled)) self.assertIn("json_array_elements", str(compiled)) self.assertNotIn("cases.casework", str(compiled))