""" Tests for Alembic database migrations. Verifies that migrations can be applied and rolled back correctly. These tests use the actual database and should be run with caution on shared environments. What: Tests for database migration integrity Why: Ensures migrations can be applied cleanly on fresh databases and rolled back safely for disaster recovery """ import pytest from alembic import command from alembic.config import Config from sqlalchemy import create_engine, inspect, text from app.config import get_settings @pytest.fixture def alembic_config(): """Create Alembic config pointing to the backend directory.""" config = Config("/mnt/NV2/Development/strat-gameplay-webapp/backend/alembic.ini") return config @pytest.fixture def sync_engine(): """Create synchronous engine for migration testing.""" settings = get_settings() sync_url = settings.database_url.replace("+asyncpg", "+psycopg2") return create_engine(sync_url) class TestMigrationHistory: """Tests for migration chain integrity.""" def test_migration_history_valid(self, alembic_config): """ Verify migration chain is valid and has no gaps. What: Checks that all migrations reference existing predecessors Why: Broken migration chains cause 'revision not found' errors """ from alembic.script import ScriptDirectory script = ScriptDirectory.from_config(alembic_config) revisions = list(script.walk_revisions()) # Should have at least 2 migrations (001 and 004) assert len(revisions) >= 2, "Expected at least 2 migrations" # All revisions except base should have a down_revision that exists for rev in revisions: if rev.down_revision is not None: # The down_revision should be in our revisions list down_revs = ( rev.down_revision if isinstance(rev.down_revision, tuple) else (rev.down_revision,) ) for down_rev in down_revs: assert script.get_revision(down_rev) is not None, ( f"Migration {rev.revision} references non-existent " f"down_revision {down_rev}" ) def test_head_is_reachable(self, alembic_config): """ Verify we can traverse from base to head. What: Ensures migration path from base to head is unbroken Why: Broken path means new databases cannot be fully initialized """ from alembic.script import ScriptDirectory script = ScriptDirectory.from_config(alembic_config) heads = script.get_heads() assert len(heads) == 1, f"Expected single head, got {heads}" assert heads[0] == "004", f"Expected head to be 004, got {heads[0]}" class TestMigrationContent: """Tests for migration content correctness.""" def test_initial_migration_creates_all_tables(self, alembic_config): """ Verify initial migration (001) creates all expected tables. What: Checks that 001 migration creates the full schema Why: Missing tables would cause application failures """ from alembic.script import ScriptDirectory script = ScriptDirectory.from_config(alembic_config) rev_001 = script.get_revision("001") # Read the migration file import importlib.util spec = importlib.util.spec_from_file_location("001", rev_001.path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # Check upgrade function exists assert hasattr(module, "upgrade"), "Migration 001 missing upgrade function" assert hasattr(module, "downgrade"), "Migration 001 missing downgrade function" # Get source code to verify table creation import inspect as py_inspect source = py_inspect.getsource(module.upgrade) expected_tables = [ "games", "plays", "lineups", "game_sessions", "rolls", "roster_links", "game_cardset_links", ] for table in expected_tables: assert ( f"'{table}'" in source ), f"Migration 001 missing table creation for '{table}'" def test_materialized_views_migration_content(self, alembic_config): """ Verify materialized views migration (004) creates expected views. What: Checks that 004 migration creates statistics views Why: Missing views would break box score functionality """ from alembic.script import ScriptDirectory script = ScriptDirectory.from_config(alembic_config) rev_004 = script.get_revision("004") import importlib.util spec = importlib.util.spec_from_file_location("004", rev_004.path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) import inspect as py_inspect source = py_inspect.getsource(module.upgrade) expected_views = [ "batting_game_stats", "pitching_game_stats", "game_stats", ] for view in expected_views: assert ( view in source ), f"Migration 004 missing materialized view '{view}'" class TestCurrentDatabaseState: """Tests for current database migration state.""" def test_database_at_head(self, sync_engine): """ Verify database is at the latest migration revision. What: Checks alembic_version table shows current head Why: Ensures database schema is up to date """ with sync_engine.connect() as conn: result = conn.execute(text("SELECT version_num FROM alembic_version")) versions = list(result) assert len(versions) == 1, f"Expected 1 version row, got {len(versions)}" assert versions[0][0] == "004", f"Expected version 004, got {versions[0][0]}" def test_all_tables_exist(self, sync_engine): """ Verify all expected tables exist in the database. What: Inspects database to confirm table presence Why: Validates migrations were applied correctly """ inspector = inspect(sync_engine) tables = set(inspector.get_table_names()) expected_tables = { "games", "plays", "lineups", "game_sessions", "rolls", "roster_links", "game_cardset_links", "alembic_version", # Created by Alembic } missing = expected_tables - tables assert not missing, f"Missing tables: {missing}" def test_materialized_views_exist(self, sync_engine): """ Verify all materialized views exist in the database. What: Checks for statistics views created by migration 004 Why: Box score functionality depends on these views """ with sync_engine.connect() as conn: result = conn.execute( text( """ SELECT matviewname FROM pg_matviews WHERE schemaname = 'public' ORDER BY matviewname """ ) ) views = {row[0] for row in result} expected_views = { "batting_game_stats", "pitching_game_stats", "game_stats", } missing = expected_views - views assert not missing, f"Missing materialized views: {missing}" class TestMigrationDowngrade: """Tests for migration downgrade safety (non-destructive checks only).""" def test_downgrade_functions_exist(self, alembic_config): """ Verify all migrations have downgrade functions. What: Checks each migration has a downgrade() implementation Why: Rollback capability is critical for disaster recovery """ from alembic.script import ScriptDirectory script = ScriptDirectory.from_config(alembic_config) for rev in script.walk_revisions(): import importlib.util spec = importlib.util.spec_from_file_location(rev.revision, rev.path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) assert hasattr( module, "downgrade" ), f"Migration {rev.revision} missing downgrade function" import inspect as py_inspect source = py_inspect.getsource(module.downgrade) # Downgrade should not just be 'pass' for data-creating migrations if rev.revision in ["001", "004"]: assert "pass" not in source.replace( " ", "" ).replace("\n", ""), ( f"Migration {rev.revision} downgrade should not be empty" )