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:
321
tests/unit/models/test_migrations.py
Normal file
321
tests/unit/models/test_migrations.py
Normal 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()
|
||||
Reference in New Issue
Block a user