""" Tests for database migration system. This module tests the migration runner, validator, and base classes. """ from datetime import datetime from pathlib import Path from unittest.mock import AsyncMock, Mock, patch import pytest from src.server.database.migrations.base import ( Migration, MigrationError, MigrationHistory, ) from src.server.database.migrations.runner import MigrationRunner from src.server.database.migrations.validator import MigrationValidator class TestMigration: """Tests for base Migration class.""" def test_migration_initialization(self): """Test migration can be initialized with basic attributes.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig( version="20250124_001", description="Test migration" ) assert mig.version == "20250124_001" assert mig.description == "Test migration" assert isinstance(mig.created_at, datetime) def test_migration_equality(self): """Test migrations are equal based on version.""" class TestMig1(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None class TestMig2(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig1 = TestMig1(version="20250124_001", description="Test 1") mig2 = TestMig2(version="20250124_001", description="Test 2") mig3 = TestMig1(version="20250124_002", description="Test 3") assert mig1 == mig2 assert mig1 != mig3 assert hash(mig1) == hash(mig2) assert hash(mig1) != hash(mig3) def test_migration_repr(self): """Test migration string representation.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig( version="20250124_001", description="Test migration" ) assert "20250124_001" in repr(mig) assert "Test migration" in repr(mig) class TestMigrationHistory: """Tests for MigrationHistory class.""" def test_history_initialization(self): """Test migration history record can be created.""" history = MigrationHistory( version="20250124_001", description="Test migration", applied_at=datetime.now(), execution_time_ms=1500, success=True, ) assert history.version == "20250124_001" assert history.description == "Test migration" assert history.execution_time_ms == 1500 assert history.success is True assert history.error_message is None def test_history_with_error(self): """Test migration history with error message.""" history = MigrationHistory( version="20250124_001", description="Failed migration", applied_at=datetime.now(), execution_time_ms=500, success=False, error_message="Test error", ) assert history.success is False assert history.error_message == "Test error" class TestMigrationValidator: """Tests for MigrationValidator class.""" def test_validator_initialization(self): """Test validator can be initialized.""" validator = MigrationValidator() assert isinstance(validator.errors, list) assert isinstance(validator.warnings, list) assert len(validator.errors) == 0 def test_validate_version_format_valid(self): """Test validation of valid version formats.""" validator = MigrationValidator() assert validator._validate_version_format("20250124_001") assert validator._validate_version_format("20231201_099") assert validator._validate_version_format("20250124_001_description") def test_validate_version_format_invalid(self): """Test validation of invalid version formats.""" validator = MigrationValidator() assert not validator._validate_version_format("") assert not validator._validate_version_format("20250124") assert not validator._validate_version_format("invalid_001") assert not validator._validate_version_format("202501_001") def test_validate_migration_valid(self): """Test validation of valid migration.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig( version="20250124_001", description="Valid test migration", ) validator = MigrationValidator() assert validator.validate_migration(mig) is True assert len(validator.errors) == 0 def test_validate_migration_invalid_version(self): """Test validation fails for invalid version.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig( version="invalid", description="Valid description", ) validator = MigrationValidator() assert validator.validate_migration(mig) is False assert len(validator.errors) > 0 def test_validate_migration_missing_description(self): """Test validation fails for missing description.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig(version="20250124_001", description="") validator = MigrationValidator() assert validator.validate_migration(mig) is False assert any("description" in e.lower() for e in validator.errors) def test_validate_migrations_duplicate_version(self): """Test validation detects duplicate versions.""" class TestMig1(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None class TestMig2(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig1 = TestMig1(version="20250124_001", description="First") mig2 = TestMig2(version="20250124_001", description="Duplicate") validator = MigrationValidator() assert validator.validate_migrations([mig1, mig2]) is False assert any("duplicate" in e.lower() for e in validator.errors) def test_check_migration_conflicts(self): """Test detection of migration conflicts.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None old_mig = TestMig(version="20250101_001", description="Old") new_mig = TestMig(version="20250124_001", description="New") validator = MigrationValidator() # No conflict when pending is newer conflict = validator.check_migration_conflicts( [new_mig], ["20250101_001"] ) assert conflict is None # Conflict when pending is older conflict = validator.check_migration_conflicts( [old_mig], ["20250124_001"] ) assert conflict is not None assert "older" in conflict.lower() def test_get_validation_report(self): """Test validation report generation.""" validator = MigrationValidator() validator.errors.append("Test error") validator.warnings.append("Test warning") report = validator.get_validation_report() assert "Test error" in report assert "Test warning" in report assert "Validation Errors:" in report assert "Validation Warnings:" in report def test_raise_if_invalid(self): """Test exception raising on validation failure.""" validator = MigrationValidator() validator.errors.append("Test error") with pytest.raises(MigrationError): validator.raise_if_invalid() @pytest.mark.asyncio class TestMigrationRunner: """Tests for MigrationRunner class.""" @pytest.fixture def mock_session(self): """Create mock database session.""" session = AsyncMock() session.execute = AsyncMock() session.commit = AsyncMock() session.rollback = AsyncMock() return session @pytest.fixture def migrations_dir(self, tmp_path): """Create temporary migrations directory.""" return tmp_path / "migrations" async def test_runner_initialization( self, migrations_dir, mock_session ): """Test migration runner can be initialized.""" runner = MigrationRunner(migrations_dir, mock_session) assert runner.migrations_dir == migrations_dir assert runner.session == mock_session assert isinstance(runner._migrations, list) async def test_initialize_creates_table( self, migrations_dir, mock_session ): """Test initialization creates migration_history table.""" runner = MigrationRunner(migrations_dir, mock_session) await runner.initialize() mock_session.execute.assert_called() mock_session.commit.assert_called() async def test_load_migrations_empty_dir( self, migrations_dir, mock_session ): """Test loading migrations from empty directory.""" runner = MigrationRunner(migrations_dir, mock_session) runner.load_migrations() assert len(runner._migrations) == 0 async def test_get_applied_migrations( self, migrations_dir, mock_session ): """Test retrieving list of applied migrations.""" # Mock database response mock_result = Mock() mock_result.fetchall.return_value = [ ("20250124_001",), ("20250124_002",), ] mock_session.execute.return_value = mock_result runner = MigrationRunner(migrations_dir, mock_session) applied = await runner.get_applied_migrations() assert len(applied) == 2 assert "20250124_001" in applied assert "20250124_002" in applied async def test_apply_migration_success( self, migrations_dir, mock_session ): """Test successful migration application.""" class TestMig(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig = TestMig(version="20250124_001", description="Test") runner = MigrationRunner(migrations_dir, mock_session) await runner.apply_migration(mig) mock_session.commit.assert_called() async def test_apply_migration_failure( self, migrations_dir, mock_session ): """Test migration application handles failures.""" class FailingMig(Migration): async def upgrade(self, session): raise Exception("Test failure") async def downgrade(self, session): return None mig = FailingMig(version="20250124_001", description="Failing") runner = MigrationRunner(migrations_dir, mock_session) with pytest.raises(MigrationError): await runner.apply_migration(mig) mock_session.rollback.assert_called() async def test_get_pending_migrations( self, migrations_dir, mock_session ): """Test retrieving pending migrations.""" class TestMig1(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None class TestMig2(Migration): async def upgrade(self, session): return None async def downgrade(self, session): return None mig1 = TestMig1(version="20250124_001", description="Applied") mig2 = TestMig2(version="20250124_002", description="Pending") runner = MigrationRunner(migrations_dir, mock_session) runner._migrations = [mig1, mig2] # Mock only mig1 as applied mock_result = Mock() mock_result.fetchall.return_value = [("20250124_001",)] mock_session.execute.return_value = mock_result pending = await runner.get_pending_migrations() assert len(pending) == 1 assert pending[0].version == "20250124_002"