Files
ss-tools/backend/tests/test_alembic_migrations.py

87 lines
4.2 KiB
Python

# #region Test.Alembic.Migrations [C:3] [TYPE Module] [SEMANTICS test,alembic,migration,schema,postgres]
# @BRIEF Verifies the single baseline Alembic upgrade.
# @RELATION BINDS_TO -> [Alembic.AddDeploymentRecords]
# @INVARIANT Running `alembic upgrade head` twice is idempotent (no-op on second run) on PostgreSQL.
# @PRE Alembic is installed, migration files exist in alembic/versions/.
# @POST Schema matches expected state after upgrade.
# @TEST_INVARIANT: alembic_fresh_upgrade -> VERIFIED_BY: [Test.AlembicMigrations.TestFreshUpgradeCreatesTables]
# @REJECTED Full migration chain idempotency on SQLite — initial migration uses
# op.create_index() which conflicts with SQLite auto-created indexes
# for UNIQUE constraints. Migrations are designed for PostgreSQL.
import os
from pathlib import Path
import pytest
import sys
# Ensure backend/src is importable for model metadata
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
# Alembic migrations use PostgreSQL-specific features (CREATE INDEX may conflict
# with SQLite auto-indexes for UNIQUE constraints). Skip full-chain tests on SQLite.
_DB_BACKEND = os.environ.get("DATABASE_URL", "sqlite://").split(":")[0]
_REQUIRES_PG = pytest.mark.skipif(
_DB_BACKEND != "postgresql",
reason="Alembic migrations are PostgreSQL-specific; use postgresql:// DATABASE_URL to test",
)
# #region Test.AlembicMigrations.TestFreshUpgradeCreatesTables [C:3] [TYPE Function]
# @BRIEF Run alembic upgrade head on a fresh PostgreSQL DB and verify key tables exist.
# @POST All expected tables exist after upgrade.
# @TEST_INVARIANT: alembic_fresh_upgrade -> VERIFIED_BY: [Test.AlembicMigrations.TestFreshUpgradeCreatesTables]
@_REQUIRES_PG
def test_fresh_upgrade_creates_tables() -> None:
"""Fresh Alembic upgrade creates all expected tables (PostgreSQL only)."""
from sqlalchemy import create_engine, inspect, text as sa_text
_run_alembic_upgrade()
engine = create_engine(os.environ["DATABASE_URL"])
inspector = inspect(engine)
tables = set(inspector.get_table_names())
from src.models.mapping import Base
expected_tables = set(Base.metadata.tables) | {"alembic_version"}
missing = expected_tables - tables
assert not missing, f"Missing tables after Alembic upgrade: {missing}"
with engine.connect() as conn:
result = conn.execute(sa_text("SELECT version_num FROM alembic_version"))
version = result.scalar_one()
assert version is not None and len(version) > 0
roles_cols = {c["name"] for c in inspector.get_columns("roles")}
assert "is_admin" in roles_cols, "roles.is_admin column missing after Alembic upgrade"
_run_alembic_upgrade()
assert set(inspect(engine).get_table_names()) == expected_tables
engine.dispose()
# #endregion Test.AlembicMigrations.TestFreshUpgradeCreatesTables
def test_baseline_refuses_incomplete_existing_schema(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A legacy table cannot be silently stamped as the reset baseline."""
from sqlalchemy import create_engine, text
database_url = f"sqlite:///{tmp_path / 'legacy.db'}"
engine = create_engine(database_url)
with engine.begin() as connection:
connection.execute(text("CREATE TABLE legacy_data (id INTEGER PRIMARY KEY)"))
engine.dispose()
monkeypatch.setenv("DATABASE_URL", database_url)
with pytest.raises(RuntimeError, match="Refusing to stamp an incomplete existing schema"):
_run_alembic_upgrade()
# #region Test.AlembicMigrations.RunAlembicUpgrade [C:1] [TYPE Function]
def _run_alembic_upgrade(revision: str = "head") -> None:
"""Run a named Alembic upgrade programmatically against DATABASE_URL."""
from alembic.config import Config as AlembicConfig
from alembic import command as alembic_command
backend_dir = Path(__file__).parent.parent
alembic_cfg = AlembicConfig(str(backend_dir / "alembic.ini"))
alembic_cfg.set_main_option("script_location", str(backend_dir / "alembic"))
alembic_cfg.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
alembic_command.upgrade(alembic_cfg, revision)
# #endregion Test.AlembicMigrations.RunAlembicUpgrade
# #endregion Test.Alembic.Migrations