feat(migrations): T11 setup Alembic and initial schema migration

- Initialize Alembic with alembic init alembic
- Configure alembic.ini to use DATABASE_URL from environment
- Configure alembic/env.py to import Base and models metadata
- Generate initial migration: c92fc544a483_initial_schema
- Migration creates all 4 tables: users, api_keys, api_tokens, usage_stats
- Migration includes all indexes, constraints, and foreign keys
- Test upgrade/downgrade cycle works correctly

Alembic commands:
- alembic upgrade head
- alembic downgrade -1
- alembic revision --autogenerate -m 'message'

Tests: 13 migration tests pass
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 11:14:45 +02:00
parent ea198e8b0d
commit abe9fc166b
6 changed files with 603 additions and 2 deletions

View File

@@ -0,0 +1,321 @@
"""Tests for Alembic migrations (T11).
T11: Setup Alembic e migrazione iniziale
"""
import pytest
import os
import tempfile
from pathlib import Path
@pytest.mark.unit
class TestAlembicInitialization:
"""Test Alembic initialization and configuration."""
def test_alembic_ini_exists(self):
"""Test that alembic.ini file exists."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
alembic_ini_path = project_root / "alembic.ini"
# Assert
assert alembic_ini_path.exists(), "alembic.ini should exist"
def test_alembic_directory_exists(self):
"""Test that alembic directory exists."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
alembic_dir = project_root / "alembic"
# Assert
assert alembic_dir.exists(), "alembic directory should exist"
assert alembic_dir.is_dir(), "alembic should be a directory"
def test_alembic_env_py_exists(self):
"""Test that alembic/env.py file exists."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
env_py_path = project_root / "alembic" / "env.py"
# Assert
assert env_py_path.exists(), "alembic/env.py should exist"
def test_alembic_versions_directory_exists(self):
"""Test that alembic/versions directory exists."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
versions_dir = project_root / "alembic" / "versions"
# Assert
assert versions_dir.exists(), "alembic/versions directory should exist"
def test_alembic_ini_contains_database_url(self):
"""Test that alembic.ini contains DATABASE_URL configuration."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
alembic_ini_path = project_root / "alembic.ini"
# Act
with open(alembic_ini_path, 'r') as f:
content = f.read()
# Assert
assert "sqlalchemy.url" in content, "alembic.ini should contain sqlalchemy.url"
def test_alembic_env_py_imports_base(self):
"""Test that alembic/env.py imports Base from models."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
env_py_path = project_root / "alembic" / "env.py"
# Act
with open(env_py_path, 'r') as f:
content = f.read()
# Assert
assert "Base" in content or "target_metadata" in content, \
"alembic/env.py should reference Base or target_metadata"
@pytest.mark.integration
class TestAlembicMigrations:
"""Test Alembic migration functionality."""
def test_migration_file_exists(self):
"""Test that at least one migration file exists."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
versions_dir = project_root / "alembic" / "versions"
# Act
migration_files = list(versions_dir.glob("*.py"))
# Assert
assert len(migration_files) > 0, "At least one migration file should exist"
def test_migration_contains_create_tables(self):
"""Test that migration contains table creation commands."""
# Arrange
project_root = Path(__file__).parent.parent.parent.parent
versions_dir = project_root / "alembic" / "versions"
# Get the first migration file
migration_files = list(versions_dir.glob("*.py"))
if not migration_files:
pytest.skip("No migration files found")
migration_file = migration_files[0]
# Act
with open(migration_file, 'r') as f:
content = f.read()
# Assert
assert "upgrade" in content, "Migration should contain upgrade function"
assert "downgrade" in content, "Migration should contain downgrade function"
def test_alembic_upgrade_creates_tables(self, tmp_path):
"""Test that alembic upgrade creates all required tables."""
# Arrange
import subprocess
import sys
# Create a temporary database
db_path = tmp_path / "test_alembic.db"
# Set up environment with test database
env = os.environ.copy()
env['DATABASE_URL'] = f"sqlite:///{db_path}"
env['SECRET_KEY'] = "test-secret-key-min-32-characters-long"
env['ENCRYPTION_KEY'] = "test-32-byte-encryption-key!!"
# Change to project root
project_root = Path(__file__).parent.parent.parent.parent
# Act - Run alembic upgrade
result = subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Assert
assert result.returncode == 0, f"Alembic upgrade failed: {result.stderr}"
# Verify database file exists
assert db_path.exists(), "Database file should be created"
def test_alembic_downgrade_removes_tables(self, tmp_path):
"""Test that alembic downgrade removes tables."""
# Arrange
import subprocess
import sys
# Create a temporary database
db_path = tmp_path / "test_alembic_downgrade.db"
# Set up environment with test database
env = os.environ.copy()
env['DATABASE_URL'] = f"sqlite:///{db_path}"
env['SECRET_KEY'] = "test-secret-key-min-32-characters-long"
env['ENCRYPTION_KEY'] = "test-32-byte-encryption-key!!"
# Change to project root
project_root = Path(__file__).parent.parent.parent.parent
# Act - First upgrade
subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Then downgrade
result = subprocess.run(
[sys.executable, "-m", "alembic", "downgrade", "-1"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Assert
assert result.returncode == 0, f"Alembic downgrade failed: {result.stderr}"
def test_alembic_upgrade_downgrade_cycle(self, tmp_path):
"""Test that upgrade followed by downgrade and upgrade again works."""
# Arrange
import subprocess
import sys
# Create a temporary database
db_path = tmp_path / "test_alembic_cycle.db"
# Set up environment with test database
env = os.environ.copy()
env['DATABASE_URL'] = f"sqlite:///{db_path}"
env['SECRET_KEY'] = "test-secret-key-min-32-characters-long"
env['ENCRYPTION_KEY'] = "test-32-byte-encryption-key!!"
# Change to project root
project_root = Path(__file__).parent.parent.parent.parent
# Act - Upgrade
result1 = subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Downgrade
result2 = subprocess.run(
[sys.executable, "-m", "alembic", "downgrade", "-1"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Upgrade again
result3 = subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Assert
assert result1.returncode == 0, "First upgrade failed"
assert result2.returncode == 0, "Downgrade failed"
assert result3.returncode == 0, "Second upgrade failed"
@pytest.mark.integration
class TestDatabaseTables:
"""Test that database tables are created correctly."""
def test_users_table_created(self, tmp_path):
"""Test that users table is created by migration."""
# Arrange
import subprocess
import sys
from sqlalchemy import create_engine, inspect
# Create a temporary database
db_path = tmp_path / "test_tables.db"
# Set up environment with test database
env = os.environ.copy()
env['DATABASE_URL'] = f"sqlite:///{db_path}"
env['SECRET_KEY'] = "test-secret-key-min-32-characters-long"
env['ENCRYPTION_KEY'] = "test-32-byte-encryption-key!!"
# Change to project root
project_root = Path(__file__).parent.parent.parent.parent
# Act - Run alembic upgrade
subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Verify tables
engine = create_engine(f"sqlite:///{db_path}")
inspector = inspect(engine)
tables = inspector.get_table_names()
# Assert
assert "users" in tables, "users table should be created"
assert "api_keys" in tables, "api_keys table should be created"
assert "usage_stats" in tables, "usage_stats table should be created"
assert "api_tokens" in tables, "api_tokens table should be created"
engine.dispose()
def test_alembic_version_table_created(self, tmp_path):
"""Test that alembic_version table is created."""
# Arrange
import subprocess
import sys
from sqlalchemy import create_engine, inspect
# Create a temporary database
db_path = tmp_path / "test_version.db"
# Set up environment with test database
env = os.environ.copy()
env['DATABASE_URL'] = f"sqlite:///{db_path}"
env['SECRET_KEY'] = "test-secret-key-min-32-characters-long"
env['ENCRYPTION_KEY'] = "test-32-byte-encryption-key!!"
# Change to project root
project_root = Path(__file__).parent.parent.parent.parent
# Act - Run alembic upgrade
subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
env=env,
capture_output=True,
text=True
)
# Verify tables
engine = create_engine(f"sqlite:///{db_path}")
inspector = inspect(engine)
tables = inspector.get_table_names()
# Assert
assert "alembic_version" in tables, "alembic_version table should be created"
engine.dispose()