"""PostgreSQL integration coverage for the control-plane persistence boundary.""" from __future__ import annotations import asyncio import sys from pathlib import Path from uuid import uuid4 import pytest import pytest_asyncio from alembic import command from alembic.config import Config from sqlalchemy import text from sqlalchemy.exc import IntegrityError from ragcore.application.authorization import AuthorizationService from ragcore.application.control.api import ControlPlaneApplication from ragcore.config import Settings from ragcore.domain.authorization import Principal, PrincipalType from ragcore.domain.control import DataPolicy, Grant, KnowledgeSpace, Policy, Role, Scope, Workspace from ragcore.infrastructure.database import ( create_database_engine, create_session_factory, session_scope, ) from ragcore.infrastructure.database.control import ControlPlaneRepository REPOSITORY_ROOT = Path(__file__).resolve().parents[3] @pytest.fixture(scope="session") def event_loop_policy() -> asyncio.AbstractEventLoopPolicy: if sys.platform == "win32": return asyncio.WindowsSelectorEventLoopPolicy() return asyncio.get_event_loop_policy() @pytest_asyncio.fixture async def repository() -> ControlPlaneRepository: engine = create_database_engine(Settings(env="test")) factory = create_session_factory(engine) async with session_scope(factory) as session: # Deliberately does not truncate control.workspaces: audit.audit_events # has a foreign key to workspace_id, and TRUNCATE ... CASCADE follows # that FK regardless of its ON DELETE action, tripping audit_events' # own unconditional append-only trigger (BEFORE TRUNCATE, migration # 0005) and erroring out this whole fixture. Every test using this # fixture already creates its own uniquely-id'd (uuid4()) workspace # and only reads back rows scoped to that id, so leaving prior test # runs' workspace rows in place is harmless and does not affect # correctness. await session.execute( text( "TRUNCATE control.grants, control.policies, control.applications, " "control.spaces CASCADE" ) ) await session.commit() yield ControlPlaneRepository(session) await session.rollback() await engine.dispose() @pytest.mark.integration async def test_workspace_slug_and_space_slug_are_unique(repository: ControlPlaneRepository) -> None: first = Workspace(uuid4(), "First", "shared") second = Workspace(uuid4(), "Second", "shared") await repository.create_workspace(first) with pytest.raises(IntegrityError): await repository.create_workspace(second) await repository._session.rollback() workspace = Workspace(uuid4(), "Spaces", "spaces") await repository.create_workspace(workspace) await repository.create_space( workspace.create_knowledge_space(id=uuid4(), name="One", slug="docs") ) with pytest.raises(IntegrityError): await repository.create_space( workspace.create_knowledge_space(id=uuid4(), name="Two", slug="docs") ) @pytest.mark.integration async def test_repository_reads_are_workspace_scoped(repository: ControlPlaneRepository) -> None: first = Workspace(uuid4(), "First", "first") second = Workspace(uuid4(), "Second", "second") await repository.create_workspace(first) await repository.create_workspace(second) await repository.create_application( first.create_application(id=uuid4(), name="Client", scopes={Scope.SEARCH}) ) await repository.create_application( second.create_application(id=uuid4(), name="Client", scopes={Scope.SEARCH}) ) assert len(await repository.list_applications(first.id)) == 1 assert len(await repository.list_applications(second.id)) == 1 assert (await repository.list_applications(first.id))[0].workspace_id == first.id @pytest.mark.integration async def test_composite_foreign_keys_reject_cross_workspace_grants( repository: ControlPlaneRepository, ) -> None: first = Workspace(uuid4(), "First", "first") second = Workspace(uuid4(), "Second", "second") await repository.create_workspace(first) await repository.create_workspace(second) application = first.create_application(id=uuid4(), name="Client", scopes={Scope.SEARCH}) space = second.create_knowledge_space(id=uuid4(), name="Private", slug="private") await repository.create_application(application) await repository.create_space(space) cross_workspace_grant = Grant( id=uuid4(), workspace_id=second.id, application_id=application.id, knowledge_space_id=space.id, role=Role.VIEWER, ) with pytest.raises(IntegrityError): await repository.create_grant(cross_workspace_grant) @pytest.mark.integration async def test_application_and_workspace_policy_uniqueness( repository: ControlPlaneRepository, ) -> None: workspace = Workspace(uuid4(), "Control", "control") await repository.create_workspace(workspace) await repository.create_application( workspace.create_application(id=uuid4(), name="One", slug="client", scopes={Scope.SEARCH}) ) await repository._session.commit() with pytest.raises(IntegrityError): await repository.create_application( workspace.create_application( id=uuid4(), name="Two", slug="client", scopes={Scope.SEARCH} ) ) await repository._session.rollback() await repository.create_policy(workspace.create_policy(id=uuid4(), operation="search")) await repository._session.commit() with pytest.raises(IntegrityError): await repository.create_policy(workspace.create_policy(id=uuid4(), operation="search")) await repository._session.rollback() @pytest.mark.integration async def test_space_and_policy_reads_are_workspace_scoped( repository: ControlPlaneRepository, ) -> None: first = Workspace(uuid4(), "First", "first") second = Workspace(uuid4(), "Second", "second") await repository.create_workspace(first) await repository.create_workspace(second) await repository.create_space( first.create_knowledge_space(id=uuid4(), name="First space", slug="first-space") ) await repository.create_space( second.create_knowledge_space(id=uuid4(), name="Second space", slug="second-space") ) await repository.create_policy(first.create_policy(id=uuid4(), operation="search")) await repository.create_policy(second.create_policy(id=uuid4(), operation="search")) assert len(await repository.list_spaces(first.id)) == 1 assert len(await repository.list_spaces(second.id)) == 1 assert len(await repository.list_policies(first.id)) == 1 assert len(await repository.list_policies(second.id)) == 1 @pytest.mark.integration async def test_composite_foreign_keys_reject_cross_workspace_spaces_and_policies( repository: ControlPlaneRepository, ) -> None: first = Workspace(uuid4(), "First", "first") second = Workspace(uuid4(), "Second", "second") await repository.create_workspace(first) await repository.create_workspace(second) await repository._session.commit() with pytest.raises(IntegrityError): await repository.create_space( KnowledgeSpace(id=uuid4(), workspace_id=uuid4(), name="Orphan", slug="orphan") ) await repository._session.rollback() second_space = second.create_knowledge_space(id=uuid4(), name="Private", slug="private") await repository.create_space(second_space) with pytest.raises(IntegrityError): await repository.create_policy( Policy( id=uuid4(), workspace_id=first.id, knowledge_space_id=second_space.id, operation="search", ) ) await repository._session.rollback() def test_empty_database_migration_round_trip() -> None: config = Config(str(REPOSITORY_ROOT / "alembic.ini")) command.downgrade(config, "base") command.upgrade(config, "head") async def check_schema() -> None: engine = create_database_engine(Settings(env="test")) async with engine.connect() as connection: result = await connection.execute( text( "SELECT table_name FROM information_schema.tables " "WHERE table_schema = 'control'" ) ) assert {row[0] for row in result} == { "workspaces", "applications", "application_default_spaces", "spaces", "grants", "policies", "service_accounts", "user_grants", } await engine.dispose() asyncio.run(check_schema()) @pytest.mark.integration @pytest.mark.parametrize("data_policy", list(DataPolicy)) async def test_space_data_policy_round_trips_through_repository( repository: ControlPlaneRepository, data_policy: DataPolicy ) -> None: workspace = Workspace(uuid4(), "Policies", "policies") await repository.create_workspace(workspace) space = workspace.create_knowledge_space( id=uuid4(), name="Policy space", slug="policy-space", data_policy=data_policy ) await repository.create_space(space) stored = (await repository.list_spaces(workspace.id))[0] assert stored.data_policy is data_policy @pytest.mark.integration async def test_default_knowledge_space_ids_round_trip_through_a_new_session( repository: ControlPlaneRepository, ) -> None: workspace = Workspace(uuid4(), "Defaults", "defaults") await repository.create_workspace(workspace) space_one = workspace.create_knowledge_space(id=uuid4(), name="Space One", slug="space-one") space_two = workspace.create_knowledge_space(id=uuid4(), name="Space Two", slug="space-two") await repository.create_space(space_one) await repository.create_space(space_two) application = workspace.create_application( id=uuid4(), name="Defaults Client", slug="defaults-client", default_knowledge_space_ids=frozenset({space_one.id, space_two.id}), ) created = await repository.create_application(application) assert created.default_knowledge_space_ids == frozenset({space_one.id, space_two.id}) # This repository is Core (not ORM): every select() below is a real # round trip with no identity-map caching, so re-reading through the # same session already proves the value is genuinely in the database, # not merely held on the Python-side entity that created() returned. # The shared fixture session never commits mid-test on its own (see its # own docstring) -- commit explicitly so this row would also survive # into a genuinely separate connection, not just this transaction. await repository._session.commit() reloaded = await repository.get_application(application.id, workspace.id) assert reloaded is not None assert reloaded.default_knowledge_space_ids == frozenset({space_one.id, space_two.id}) listed = await repository.list_applications(workspace.id) (listed_application,) = [app for app in listed if app.id == application.id] assert listed_application.default_knowledge_space_ids == frozenset( {space_one.id, space_two.id} ) @pytest.mark.integration async def test_application_use_case_rejects_an_unauthorized_default_space( repository: ControlPlaneRepository, ) -> None: workspace = Workspace(uuid4(), "Defaults Reject", "defaults-reject") await repository.create_workspace(workspace) allowed_space = workspace.create_knowledge_space( id=uuid4(), name="Allowed", slug="allowed-space" ) await repository.create_space(allowed_space) foreign_space_id = uuid4() # belongs to no workspace at all control_application = ControlPlaneApplication(repository, AuthorizationService()) principal = Principal( uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.WORKSPACE_ADMIN}) ) with pytest.raises(ValueError, match="default knowledge spaces must belong"): await control_application.create_application( principal, slug="rejected-client", display_name="Rejected Client", default_space_ids=frozenset({allowed_space.id, foreign_space_id}), ) assert await repository.list_applications(workspace.id) == ()