feat: add migration framework with FastMigrate integration

- Add migrations.py with create_migration, run_migrations, get_db_version
- Implement CLI migrate and create-migration commands
- Use FastMigrate's sequential 0001-*.sql naming convention
- Add comprehensive unit, integration, and E2E tests (35 tests)
- Add fresh_db_path and temp_migrations_dir test fixtures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 18:25:28 +00:00
parent 32bb3c01e0
commit 7d7cd2bb57
6 changed files with 756 additions and 11 deletions

View File

@@ -0,0 +1,180 @@
# ABOUTME: End-to-end tests for migration CLI commands.
# ABOUTME: Tests migrate and create-migration commands via subprocess.
import os
import subprocess
import sys
from pathlib import Path
# Get the project root directory (parent of tests/)
PROJECT_ROOT = Path(__file__).parent.parent
class TestMigrateCLI:
"""End-to-end tests for migrate command."""
def test_migrate_command_success(self, tmp_path, temp_migrations_dir):
"""Should run migrations via CLI and exit 0."""
# Use a path that doesn't exist yet
db_path = tmp_path / "test.db"
# Create a migration file
migration_file = temp_migrations_dir / "0001-test.sql"
migration_file.write_text("CREATE TABLE test (id INTEGER PRIMARY KEY);")
# Set required env vars with PYTHONPATH to find the package
# Note: Settings uses no env_prefix, so use DB_PATH not AT_DB_PATH
env = os.environ.copy()
env["DB_PATH"] = str(db_path)
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Run migrate command
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "migrate"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
assert "success" in result.stdout.lower() or "completed" in result.stdout.lower()
def test_migrate_command_no_migrations(self, tmp_path, temp_migrations_dir):
"""Should succeed when no migrations to run."""
# Use a path that doesn't exist yet
db_path = tmp_path / "test.db"
# Set required env vars
env = os.environ.copy()
env["DB_PATH"] = str(db_path)
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Run migrate command with empty migrations dir
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "migrate"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
def test_migrate_command_failure(self, tmp_path, temp_migrations_dir):
"""Should exit 1 on migration failure."""
# Use a path that doesn't exist yet
db_path = tmp_path / "test.db"
# Create a broken migration file
migration_file = temp_migrations_dir / "0001-broken.sql"
migration_file.write_text("THIS IS INVALID SQL;")
# Set required env vars
env = os.environ.copy()
env["DB_PATH"] = str(db_path)
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Run migrate command
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "migrate"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 1
class TestCreateMigrationCLI:
"""End-to-end tests for create-migration command."""
def test_create_migration_command(self, temp_migrations_dir):
"""Should create migration file via CLI."""
# Set required env vars
env = os.environ.copy()
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Run create-migration command
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "create-migration", "add users table"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
# Verify file was created
created_files = list(temp_migrations_dir.glob("0001-*.sql"))
assert len(created_files) == 1
assert "add-users-table" in created_files[0].name
def test_create_migration_shows_path(self, temp_migrations_dir):
"""Should print path to created migration."""
# Set required env vars
env = os.environ.copy()
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Run create-migration command
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "create-migration", "test migration"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 0
assert "0001-test-migration.sql" in result.stdout
def test_create_migration_increments_index(self, temp_migrations_dir):
"""Should create migrations with incrementing indexes."""
# Set required env vars
env = os.environ.copy()
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
# Create first migration
subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "create-migration", "first"],
capture_output=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
# Create second migration
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "create-migration", "second"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
assert result.returncode == 0
assert "0002-second.sql" in result.stdout
def test_create_migration_requires_description(self, temp_migrations_dir):
"""Should fail when description not provided."""
env = os.environ.copy()
env["CSRF_SECRET"] = "test-secret-for-csrf"
env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
result = subprocess.run(
[sys.executable, "-m", "animaltrack.cli", "create-migration"],
capture_output=True,
text=True,
env=env,
cwd=str(temp_migrations_dir.parent),
)
# argparse should reject missing required argument
assert result.returncode != 0