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:
180
tests/test_cli_migrations.py
Normal file
180
tests/test_cli_migrations.py
Normal 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
|
||||
Reference in New Issue
Block a user