from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path from urllib.parse import urlsplit from uuid import uuid4 import pytest from alembic.autogenerate import compare_metadata from alembic.config import Config from alembic.migration import MigrationContext from alembic.operations import Operations from alembic.script import ScriptDirectory from sqlalchemy import delete, insert, inspect, text from sqlalchemy.exc import IntegrityError from ai_platform.infrastructure.config.settings import Settings from ai_platform.infrastructure.persistence.database import Base, make_session_factory from ai_platform.infrastructure.persistence.models import ( AppCredential, ScoringRun, ) ROOT = Path(__file__).resolve().parents[1] def test_one_initial_migration_with_no_legacy_chain(): config = Config(str(ROOT / "alembic.ini")) config.set_main_option("path_separator", "os") config.set_main_option("script_location", str(ROOT / "migrations")) scripts = ScriptDirectory.from_config(config) revisions = list(scripts.walk_revisions()) assert len(revisions) == 1 assert revisions[0].revision == "0001_initial" assert revisions[0].down_revision is None assert scripts.get_heads() == ["0001_initial"] async def test_initial_schema_matches_models_constraints_and_round_trip(): settings = Settings.load() if urlsplit(settings.database_url).path.rstrip("/").rsplit("/", 1)[-1] != "ai_platform_test": pytest.fail("Migration tests require isolated ai_platform_test") path = ROOT / "migrations/versions/0001_initial.py" spec = spec_from_file_location(path.stem, path) assert spec is not None and spec.loader is not None migration = module_from_spec(spec) spec.loader.exec_module(migration) factory = make_session_factory(settings.database_url) schema = "migration_test_" + uuid4().hex try: async with factory.kw["bind"].connect() as connection: transaction = await connection.begin() try: await connection.execute(text(f"CREATE SCHEMA {schema}")) await connection.execute(text(f"SET LOCAL search_path TO {schema}")) def apply(sync, direction): with Operations.context(MigrationContext.configure(sync)): getattr(migration, direction)() def assert_schema(sync): context = MigrationContext.configure( sync, opts={"compare_server_default": True} ) assert compare_metadata(context, Base.metadata) == [] assert set(inspect(sync).get_table_names()) == set(Base.metadata.tables) await connection.run_sync(apply, "upgrade") await connection.run_sync(assert_schema) for app_id in ("app-a", "app-b"): await connection.execute( insert(AppCredential).values(app_id=app_id, verifier=app_id) ) request_id, run_id = str(uuid4()), str(uuid4()) run = { "app_id": "app-a", "account_id": "3", "request_id": request_id, "fingerprint": "0" * 64, "request": {}, "profile_version": "example", } await connection.execute(insert(ScoringRun).values(id=run_id, **run)) # The same request identity is valid for a different account or app. await connection.execute(insert(ScoringRun).values(**{**run, "account_id": "4"})) await connection.execute(insert(ScoringRun).values(**{**run, "app_id": "app-b"})) for values in (run, {**run, "app_id": "unknown"}): with pytest.raises(IntegrityError): async with connection.begin_nested(): await connection.execute(insert(ScoringRun).values(**values)) await connection.execute(delete(ScoringRun).where(ScoringRun.id == run_id)) await connection.run_sync(apply, "downgrade") assert await connection.run_sync(lambda sync: inspect(sync).get_table_names()) == [] await connection.run_sync(apply, "upgrade") await connection.run_sync(assert_schema) finally: # Only this test's schema/data are reverted; public data is untouched. await transaction.rollback() finally: await factory.kw["bind"].dispose()