|
|
|
|
@@ -70,20 +70,26 @@ class TestDatabaseIntegrityCheckerInit:
|
|
|
|
|
|
|
|
|
|
def test_init_with_session(self, session: Session):
|
|
|
|
|
"""Checker initializes with provided session."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
checker = DatabaseIntegrityChecker()
|
|
|
|
|
with pytest.raises(ValueError, match="Session required"):
|
|
|
|
|
checker.check_all()
|
|
|
|
|
@@ -94,7 +100,9 @@ class TestOrphanedEpisodes:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
_make_episode(session, series.id)
|
|
|
|
|
checker = DatabaseIntegrityChecker(session=session)
|
|
|
|
|
@@ -104,14 +112,18 @@ class TestOrphanedEpisodes:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
@@ -134,7 +146,9 @@ class TestOrphanedQueueItems:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
episode = _make_episode(session, series.id)
|
|
|
|
|
item = DownloadQueueItem(
|
|
|
|
|
@@ -150,7 +164,9 @@ class TestOrphanedQueueItems:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
episode = _make_episode(session, series.id)
|
|
|
|
|
item = DownloadQueueItem(
|
|
|
|
|
@@ -185,7 +201,9 @@ class TestInvalidReferences:
|
|
|
|
|
"""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
|
|
|
|
|
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
|
|
|
|
|
@@ -205,7 +223,9 @@ class TestDuplicateKeys:
|
|
|
|
|
):
|
|
|
|
|
"""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
|
|
|
|
|
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
|
|
|
|
|
@@ -218,7 +238,9 @@ class TestDataConsistency:
|
|
|
|
|
|
|
|
|
|
def test_no_consistency_issues_clean_data(self, session: Session):
|
|
|
|
|
"""Returns 0 for valid episode data."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
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)
|
|
|
|
|
@@ -227,7 +249,9 @@ class TestDataConsistency:
|
|
|
|
|
|
|
|
|
|
def test_detects_negative_season_number(self, session: Session):
|
|
|
|
|
"""Detects episodes with negative season numbers."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
# Insert invalid episode bypassing ORM validation
|
|
|
|
|
session.execute(
|
|
|
|
|
@@ -246,7 +270,9 @@ class TestDataConsistency:
|
|
|
|
|
|
|
|
|
|
def test_detects_negative_episode_number(self, session: Session):
|
|
|
|
|
"""Detects episodes with negative episode numbers."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
session.execute(
|
|
|
|
|
text(
|
|
|
|
|
@@ -263,7 +289,9 @@ class TestDataConsistency:
|
|
|
|
|
|
|
|
|
|
def test_empty_database_no_issues(self, session: Session):
|
|
|
|
|
"""Empty database reports no consistency issues."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
checker = DatabaseIntegrityChecker(session=session)
|
|
|
|
|
count = checker._check_data_consistency()
|
|
|
|
|
assert count == 0
|
|
|
|
|
@@ -274,7 +302,9 @@ class TestCheckAll:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
checker = DatabaseIntegrityChecker(session=session)
|
|
|
|
|
results = checker.check_all()
|
|
|
|
|
expected_keys = {
|
|
|
|
|
@@ -290,7 +320,9 @@ class TestCheckAll:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
@@ -299,7 +331,9 @@ class TestCheckAll:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
checker = DatabaseIntegrityChecker(session=session)
|
|
|
|
|
checker.issues = ["leftover issue"]
|
|
|
|
|
checker.check_all()
|
|
|
|
|
@@ -312,14 +346,18 @@ class TestRepairOrphanedRecords:
|
|
|
|
|
|
|
|
|
|
def test_repair_requires_session(self):
|
|
|
|
|
"""repair_orphaned_records raises ValueError without session."""
|
|
|
|
|
from src.infrastructure.security.database_integrity import DatabaseIntegrityChecker
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
@@ -341,7 +379,9 @@ class TestRepairOrphanedRecords:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
@@ -360,7 +400,9 @@ class TestRepairOrphanedRecords:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
DatabaseIntegrityChecker,
|
|
|
|
|
)
|
|
|
|
|
series = _make_series(session)
|
|
|
|
|
_make_episode(session, series.id)
|
|
|
|
|
|
|
|
|
|
@@ -374,7 +416,9 @@ class TestConvenienceFunction:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from src.infrastructure.security.database_integrity import (
|
|
|
|
|
check_database_integrity,
|
|
|
|
|
)
|
|
|
|
|
results = check_database_integrity(session)
|
|
|
|
|
assert isinstance(results, dict)
|
|
|
|
|
assert "total_issues" in results
|
|
|
|
|
|