"""Unit tests for database integrity checker module. Tests database integrity validation, orphaned record detection, duplicate key checks, and data consistency verification. NOTE: The database_integrity.py module has bugs in raw SQL queries: - _check_invalid_references uses table 'episode' but actual name is 'episodes' - _check_invalid_references uses table 'download_queue_item' but actual is 'download_queue' - _check_duplicate_keys uses column 'anime_key' but actual column is 'key' These bugs cause OperationalError at runtime. Tests document this behavior. """ from unittest.mock import MagicMock, PropertyMock, patch import pytest from sqlalchemy import create_engine, text from sqlalchemy.orm import Session, sessionmaker from src.server.database.base import Base from src.server.database.models import AnimeSeries, DownloadQueueItem, Episode @pytest.fixture def engine(): """Create an in-memory SQLite database engine with schema.""" eng = create_engine("sqlite:///:memory:") Base.metadata.create_all(eng) return eng @pytest.fixture def session(engine): """Create a database session for testing.""" SessionLocal = sessionmaker(bind=engine) sess = SessionLocal() yield sess sess.close() def _make_series(session: Session, key: str = "test-anime", name: str = "Test Anime") -> AnimeSeries: """Helper to create and persist an AnimeSeries record.""" series = AnimeSeries( key=key, name=name, site="https://aniworld.to/anime/stream/test-anime", folder="Test Anime (2024)", ) session.add(series) session.commit() session.refresh(series) return series def _make_episode(session: Session, series_id: int, season: int = 1, ep: int = 1) -> Episode: """Helper to create and persist an Episode record.""" episode = Episode( series_id=series_id, season=season, episode_number=ep, title=f"Episode {ep}", ) session.add(episode) session.commit() session.refresh(episode) return episode class TestDatabaseIntegrityCheckerInit: """Tests for DatabaseIntegrityChecker initialization.""" def test_init_with_session(self, session: Session): """Checker initializes with provided session.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) assert checker.session is session assert checker.issues == [] def test_init_without_session(self): """Checker can be created without a session.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker() assert checker.session is None def test_check_all_requires_session(self): """check_all raises ValueError when no session is set.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker() with pytest.raises(ValueError, match="Session required"): checker.check_all() class TestOrphanedEpisodes: """Tests for orphaned episode detection.""" def test_no_orphaned_episodes_clean_db(self, session: Session): """Returns 0 when all episodes have parent series.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) _make_episode(session, series.id) checker = DatabaseIntegrityChecker(session=session) count = checker._check_orphaned_episodes() assert count == 0 assert len(checker.issues) == 0 def test_no_episodes_returns_zero(self, session: Session): """Returns 0 when no episodes exist at all.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) count = checker._check_orphaned_episodes() assert count == 0 def test_detects_orphaned_episodes(self, session: Session): """Detects episodes whose series_id references nonexistent series.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) _make_episode(session, series.id, season=1, ep=1) _make_episode(session, series.id, season=1, ep=2) # Delete the series directly to create orphans session.execute( text("DELETE FROM anime_series WHERE id = :id"), {"id": series.id}, ) session.commit() checker = DatabaseIntegrityChecker(session=session) count = checker._check_orphaned_episodes() assert count == 2 assert any("orphaned episodes" in issue for issue in checker.issues) class TestOrphanedQueueItems: """Tests for orphaned download queue item detection.""" def test_no_orphaned_queue_items(self, session: Session): """Returns 0 when all queue items have parent series.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) episode = _make_episode(session, series.id) item = DownloadQueueItem( series_id=series.id, episode_id=episode.id, ) session.add(item) session.commit() checker = DatabaseIntegrityChecker(session=session) count = checker._check_orphaned_queue_items() assert count == 0 def test_detects_orphaned_queue_items(self, session: Session): """Detects queue items whose series references no longer exist.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) episode = _make_episode(session, series.id) item = DownloadQueueItem( series_id=series.id, episode_id=episode.id, ) session.add(item) session.commit() # Remove series but keep orphaned items via raw SQL session.execute(text("DELETE FROM anime_series WHERE id = :id"), {"id": series.id}) session.commit() checker = DatabaseIntegrityChecker(session=session) count = checker._check_orphaned_queue_items() assert count == 1 assert any("orphaned queue" in issue for issue in checker.issues) class TestInvalidReferences: """Tests for invalid foreign key reference detection. NOTE: The raw SQL in _check_invalid_references uses wrong table names: - 'episode' instead of 'episodes' - 'download_queue_item' instead of 'download_queue' This causes OperationalError in SQLite. """ def test_invalid_references_raw_sql_uses_wrong_table_names( self, session: Session ): """BUG: Raw SQL references 'episode' and 'download_queue_item' but actual table names are 'episodes' and 'download_queue'. This causes the check to error and return -1.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) result = checker._check_invalid_references() # Returns -1 because the SQL fails with OperationalError assert result == -1 assert any("Error checking invalid references" in i for i in checker.issues) class TestDuplicateKeys: """Tests for duplicate primary key detection. NOTE: The raw SQL in _check_duplicate_keys references column 'anime_key' but the actual column name is 'key'. This causes OperationalError. """ def test_duplicate_keys_raw_sql_uses_wrong_column_name( self, session: Session ): """BUG: Raw SQL references 'anime_key' column but actual column is 'key'. This causes the check to error and return -1.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) result = checker._check_duplicate_keys() # Returns -1 because the SQL fails on nonexistent column assert result == -1 assert any("Error checking duplicate keys" in i for i in checker.issues) class TestDataConsistency: """Tests for data consistency validation.""" def test_no_consistency_issues_clean_data(self, session: Session): """Returns 0 for valid episode data.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) _make_episode(session, series.id, season=1, ep=1) checker = DatabaseIntegrityChecker(session=session) count = checker._check_data_consistency() assert count == 0 def test_detects_negative_season_number(self, session: Session): """Detects episodes with negative season numbers.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) # Insert invalid episode bypassing ORM validation session.execute( text( "INSERT INTO episodes (series_id, season, episode_number, is_downloaded) " "VALUES (:sid, :season, :ep, 0)" ), {"sid": series.id, "season": -1, "ep": 1}, ) session.commit() checker = DatabaseIntegrityChecker(session=session) count = checker._check_data_consistency() assert count == 1 assert any("invalid" in i.lower() for i in checker.issues) def test_detects_negative_episode_number(self, session: Session): """Detects episodes with negative episode numbers.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) session.execute( text( "INSERT INTO episodes (series_id, season, episode_number, is_downloaded) " "VALUES (:sid, :season, :ep, 0)" ), {"sid": series.id, "season": 1, "ep": -5}, ) session.commit() checker = DatabaseIntegrityChecker(session=session) count = checker._check_data_consistency() assert count == 1 def test_empty_database_no_issues(self, session: Session): """Empty database reports no consistency issues.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) count = checker._check_data_consistency() assert count == 0 class TestCheckAll: """Tests for the check_all aggregation method.""" def test_check_all_returns_dict_with_expected_keys(self, session: Session): """check_all returns result dict with all expected keys.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) results = checker.check_all() expected_keys = { "orphaned_episodes", "orphaned_queue_items", "invalid_references", "duplicate_keys", "data_consistency", "total_issues", "issues", } assert set(results.keys()) == expected_keys def test_check_all_aggregates_issues(self, session: Session): """check_all total_issues reflects all discovered issues.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) results = checker.check_all() # At minimum, invalid_references and duplicate_keys fail due to SQL bugs assert results["total_issues"] >= 2 assert len(results["issues"]) >= 2 def test_check_all_resets_issues_list(self, session: Session): """check_all clears previous issues before running.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker(session=session) checker.issues = ["leftover issue"] checker.check_all() # Issues list should not contain the old "leftover issue" assert "leftover issue" not in checker.issues class TestRepairOrphanedRecords: """Tests for repair_orphaned_records method.""" def test_repair_requires_session(self): """repair_orphaned_records raises ValueError without session.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker checker = DatabaseIntegrityChecker() with pytest.raises(ValueError, match="Session required"): checker.repair_orphaned_records() def test_repair_removes_orphaned_episodes(self, session: Session): """repair_orphaned_records removes episodes without parent series.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) _make_episode(session, series.id, season=1, ep=1) _make_episode(session, series.id, season=1, ep=2) # Create orphans session.execute( text("DELETE FROM anime_series WHERE id = :id"), {"id": series.id}, ) session.commit() checker = DatabaseIntegrityChecker(session=session) removed = checker.repair_orphaned_records() assert removed == 2 # Verify episodes are gone count = session.execute(text("SELECT COUNT(*) FROM episodes")).scalar() assert count == 0 def test_repair_removes_orphaned_queue_items(self, session: Session): """repair_orphaned_records removes queue items without parent series.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) episode = _make_episode(session, series.id) item = DownloadQueueItem(series_id=series.id, episode_id=episode.id) session.add(item) session.commit() session.execute( text("DELETE FROM anime_series WHERE id = :id"), {"id": series.id}, ) session.commit() checker = DatabaseIntegrityChecker(session=session) removed = checker.repair_orphaned_records() assert removed >= 1 def test_repair_no_orphans_returns_zero(self, session: Session): """repair_orphaned_records returns 0 when no orphans exist.""" from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker series = _make_series(session) _make_episode(session, series.id) checker = DatabaseIntegrityChecker(session=session) removed = checker.repair_orphaned_records() assert removed == 0 class TestConvenienceFunction: """Tests for check_database_integrity convenience function.""" def test_check_database_integrity_returns_results(self, session: Session): """Convenience function returns check results dict.""" from src.infrastructure.security.database_integrity import check_database_integrity results = check_database_integrity(session) assert isinstance(results, dict) assert "total_issues" in results assert "issues" in results