""" Tests for Alembic async support and env - AC3, AC5, and migration integration (M3). """ import asyncio import os import subprocess import sys from pathlib import Path import pytest # Project root (parent of tests/) PROJECT_ROOT = Path(__file__).resolve().parent.parent class TestAlembicAsyncConfig: """AC3: Alembic configured for async migrations.""" def test_env_has_async_migrations_runner(self): """run_async_migrations and run_migrations_online exist and are callable.""" # Load project's alembic/env.py (not the alembic package) and inspect env_py = PROJECT_ROOT / "alembic" / "env.py" assert env_py.exists(), "alembic/env.py not found" code = """ import asyncio import sys import importlib.util sys.path.insert(0, %r) spec = importlib.util.spec_from_file_location("env", %r) e = importlib.util.module_from_spec(spec) # Avoid running migrations on load: spec.loader.exec_module(e) would run env # So we only check that the file defines the symbols (compile + ast or exec with mock) with open(%r) as f: src = f.read() assert "run_async_migrations" in src and "async def run_async_migrations" in src assert "run_migrations_online" in src and "asyncio.run" in src assert "create_async_engine" in src print("ok") """ % (str(PROJECT_ROOT), str(env_py), str(env_py)) result = subprocess.run( [sys.executable, "-c", code], cwd=PROJECT_ROOT, env=os.environ.copy(), capture_output=True, text=True, timeout=15, ) assert result.returncode == 0, (result.stderr or result.stdout or "subprocess failed") assert "ok" in (result.stdout or "") def test_env_uses_convert_to_async_url(self): """Alembic env uses shared convert_to_async_url for async URL.""" from database.utils import convert_to_async_url # Just ensure the helper is used by env (env imports it) assert callable(convert_to_async_url) assert "asyncpg" in convert_to_async_url("postgresql://localhost/db") assert "aiosqlite" in convert_to_async_url("sqlite:///./foo.db") class TestSecretsFromEnvironment: """AC5: All secrets (e.g. DATABASE_URL) loaded from environment.""" def test_alembic_env_reads_database_url_from_env(self): """Alembic env.py reads DATABASE_URL from os.getenv.""" env_py = PROJECT_ROOT / "alembic" / "env.py" code = """ with open(%r) as f: src = f.read() assert "os.getenv" in src and "DATABASE_URL" in src assert "SQLITE_PATH" in src or "sqlite" in src.lower() print("ok") """ % str(env_py) result = subprocess.run( [sys.executable, "-c", code], cwd=PROJECT_ROOT, env=os.environ.copy(), capture_output=True, text=True, timeout=15, ) assert result.returncode == 0, (result.stderr or result.stdout or "subprocess failed") def test_connection_module_uses_env_for_url(self): """database.connection uses os.getenv for DATABASE_URL.""" import database.connection as conn_module # The module reads DATABASE_URL at import time assert hasattr(conn_module, "DATABASE_URL") assert isinstance(conn_module.DATABASE_URL, str) or conn_module.DATABASE_URL == "" class TestAlembicMigrationIntegration: """M3: Integration test - alembic upgrade head and downgrade -1.""" @pytest.fixture def temp_db_env(self, tmp_path): """Set env to use a temp SQLite file for migrations (absolute path).""" db_file = tmp_path / "test_migration.db" # SQLite needs absolute path when cwd differs; use 3 slashes + abs path url = "sqlite:///" + str(db_file.resolve()).replace("\\", "/") env = os.environ.copy() env["DATABASE_URL"] = url return env def test_alembic_upgrade_head_succeeds(self, temp_db_env): """Task 6.1: alembic upgrade head runs successfully.""" result = subprocess.run( [sys.executable, "-m", "alembic", "upgrade", "head"], cwd=PROJECT_ROOT, env=temp_db_env, capture_output=True, text=True, timeout=30, ) assert result.returncode == 0, ( f"alembic upgrade head failed: {result.stderr or result.stdout}" ) def test_alembic_downgrade_one_succeeds(self, temp_db_env): """Task 6.2: alembic downgrade -1 runs after upgrade.""" # First upgrade to head subprocess.run( [sys.executable, "-m", "alembic", "upgrade", "head"], cwd=PROJECT_ROOT, env=temp_db_env, capture_output=True, timeout=30, ) # Then downgrade one revision result = subprocess.run( [sys.executable, "-m", "alembic", "downgrade", "-1"], cwd=PROJECT_ROOT, env=temp_db_env, capture_output=True, text=True, timeout=30, ) assert result.returncode == 0, ( f"alembic downgrade -1 failed: {result.stderr or result.stdout}" )