Expand test coverage: ~188 new tests across 6 critical files

- Fix failing test_authenticated_request_succeeds (dependency override)
- Expand test_anime_service.py (+35 tests: status events, DB, broadcasts)
- Create test_queue_repository.py (27 tests: CRUD, model conversion)
- Expand test_enhanced_provider.py (+24 tests: fetch, download, redirect)
- Expand test_serie_scanner.py (+25 tests: events, year extract, mp4 scan)
- Create test_database_connection.py (38 tests: sessions, transactions)
- Expand test_anime_endpoints.py (+39 tests: status, search, loading)
- Clean up docs/instructions.md TODO list
This commit is contained in:
2026-02-15 17:44:27 +01:00
parent d7ab689fe1
commit e84a220f55
8 changed files with 3254 additions and 115 deletions

View File

@@ -317,3 +317,338 @@ class TestSerieScannerSingleSeries:
# Should only show missing episodes
assert result == {1: [4, 5, 6]}
# ══════════════════════════════════════════════════════════════════════════════
# New coverage tests events, year extraction, find_mp4, read_data
# ══════════════════════════════════════════════════════════════════════════════
class TestEventSubscription:
"""Test subscribe/unsubscribe for all event types."""
def test_subscribe_on_progress(self, temp_directory, mock_loader):
"""Should add handler to on_progress."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_progress(handler)
assert handler in scanner.events.on_progress
def test_unsubscribe_on_progress(self, temp_directory, mock_loader):
"""Should remove handler from on_progress."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_progress(handler)
scanner.unsubscribe_on_progress(handler)
assert handler not in scanner.events.on_progress
def test_subscribe_duplicate_ignored(self, temp_directory, mock_loader):
"""Subscribing same handler twice should not duplicate."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_progress(handler)
scanner.subscribe_on_progress(handler)
assert scanner.events.on_progress.count(handler) == 1
def test_unsubscribe_missing_handler_noop(
self, temp_directory, mock_loader
):
"""Unsubscribing unknown handler should not raise."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.unsubscribe_on_progress(handler) # should not raise
def test_subscribe_on_error(self, temp_directory, mock_loader):
"""Should add handler to on_error."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_error(handler)
assert handler in scanner.events.on_error
def test_unsubscribe_on_error(self, temp_directory, mock_loader):
"""Should remove handler from on_error."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_error(handler)
scanner.unsubscribe_on_error(handler)
assert handler not in scanner.events.on_error
def test_subscribe_on_completion(self, temp_directory, mock_loader):
"""Should add handler to on_completion."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_completion(handler)
assert handler in scanner.events.on_completion
def test_unsubscribe_on_completion(self, temp_directory, mock_loader):
"""Should remove handler from on_completion."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.subscribe_on_completion(handler)
scanner.unsubscribe_on_completion(handler)
assert handler not in scanner.events.on_completion
class TestExtractYearFromFolderName:
"""Test _extract_year_from_folder_name."""
def test_extracts_year(self, temp_directory, mock_loader):
"""Should extract year from folder like 'Title (2025)'."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._extract_year_from_folder_name("Dororo (2025)") == 2025
def test_no_year_returns_none(self, temp_directory, mock_loader):
"""Folder without year returns None."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._extract_year_from_folder_name("Dororo") is None
def test_empty_string_returns_none(self, temp_directory, mock_loader):
"""Empty string returns None."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._extract_year_from_folder_name("") is None
def test_none_returns_none(self, temp_directory, mock_loader):
"""None input returns None."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._extract_year_from_folder_name(None) is None
def test_year_out_of_range_returns_none(
self, temp_directory, mock_loader
):
"""Year outside 1900-2100 returns None."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._extract_year_from_folder_name("Title (1800)") is None
assert scanner._extract_year_from_folder_name("Title (2200)") is None
def test_year_in_middle(self, temp_directory, mock_loader):
"""Year in the middle of folder name should be extracted."""
scanner = SerieScanner(temp_directory, mock_loader)
assert (
scanner._extract_year_from_folder_name("Title (2020) - Extra")
== 2020
)
class TestSafeCallEvent:
"""Test _safe_call_event method."""
def test_calls_handler(self, temp_directory, mock_loader):
"""Handler should be called with data."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock()
scanner.events.on_progress = [handler]
scanner._safe_call_event(scanner.events.on_progress, {"test": True})
handler.assert_called_once_with({"test": True})
def test_handler_error_swallowed(self, temp_directory, mock_loader):
"""Handler exceptions should be swallowed."""
scanner = SerieScanner(temp_directory, mock_loader)
handler = MagicMock(side_effect=Exception("boom"))
scanner.events.on_progress = [handler]
# Should not raise
scanner._safe_call_event(scanner.events.on_progress, {"test": True})
def test_empty_handler_list_noop(self, temp_directory, mock_loader):
"""Empty handler list should not raise."""
scanner = SerieScanner(temp_directory, mock_loader)
scanner.events.on_progress = []
scanner._safe_call_event(scanner.events.on_progress, {"test": True})
class TestFindMp4Files:
"""Test __find_mp4_files method."""
def test_finds_mp4_files(self, temp_directory, mock_loader):
"""Should yield folders with mp4 files."""
scanner = SerieScanner(temp_directory, mock_loader)
result = list(scanner._SerieScanner__find_mp4_files())
# temp_directory has "Attack on Titan (2013)" with one mp4
assert len(result) >= 1
folder, mp4s = result[0]
assert folder == "Attack on Titan (2013)"
assert len(mp4s) == 1
def test_empty_directory(self, mock_loader):
"""Should yield nothing for empty directory."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
scanner = SerieScanner(tmpdir, mock_loader)
result = list(scanner._SerieScanner__find_mp4_files())
assert len(result) == 0
def test_nested_mp4_files(self, mock_loader):
"""Should find mp4 files in subdirectories."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
# Create nested structure
anime = os.path.join(tmpdir, "Naruto")
season = os.path.join(anime, "Season 1")
os.makedirs(season)
with open(os.path.join(season, "ep1.mp4"), "w") as f:
f.write("dummy")
scanner = SerieScanner(tmpdir, mock_loader)
result = list(scanner._SerieScanner__find_mp4_files())
assert len(result) == 1
assert "Naruto" == result[0][0]
assert len(result[0][1]) == 1
def test_non_mp4_ignored(self, mock_loader):
"""Should ignore non-mp4 files."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime = os.path.join(tmpdir, "TestAnime")
os.makedirs(anime)
with open(os.path.join(anime, "readme.txt"), "w") as f:
f.write("not a video")
scanner = SerieScanner(tmpdir, mock_loader)
result = list(scanner._SerieScanner__find_mp4_files())
# The folder is yielded but with empty mp4 list
assert len(result) == 1
assert result[0][1] == []
class TestReadDataFromFile:
"""Test __read_data_from_file method."""
def test_reads_key_file(self, mock_loader):
"""Should read key from 'key' file."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "SomeAnime")
os.makedirs(anime_folder)
with open(os.path.join(anime_folder, "key"), "w") as f:
f.write("some-key")
scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("SomeAnime")
assert result is not None
assert result.key == "some-key"
def test_reads_data_file(self, mock_loader):
"""Should read Serie from 'data' file when no 'key' file."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "SomeAnime")
os.makedirs(anime_folder)
# Create a data file
serie = Serie("test-key", "Test", "aniworld.to", "SomeAnime", {})
data_path = os.path.join(anime_folder, "data")
serie.save_to_file(data_path)
scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("SomeAnime")
assert result is not None
assert result.key == "test-key"
def test_no_files_returns_none(self, mock_loader):
"""Should return None when no key or data file exists."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "Empty")
os.makedirs(anime_folder)
scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Empty")
assert result is None
class TestReinit:
"""Test reinit method."""
def test_clears_keydict(self, temp_directory, mock_loader):
"""reinit should clear the keyDict."""
scanner = SerieScanner(temp_directory, mock_loader)
scanner.keyDict["test"] = MagicMock()
scanner.reinit()
assert scanner.keyDict == {}
class TestGetTotalToScan:
"""Test get_total_to_scan."""
def test_counts_folders(self, temp_directory, mock_loader):
"""Should count number of folders."""
scanner = SerieScanner(temp_directory, mock_loader)
count = scanner.get_total_to_scan()
assert count >= 1
def test_empty_directory(self, mock_loader):
"""Should return 0 for empty directory."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
scanner = SerieScanner(tmpdir, mock_loader)
assert scanner.get_total_to_scan() == 0
class TestScanProgressEvents:
"""Test that scan emits progress and completion events."""
def test_scan_emits_progress(self, temp_directory, mock_loader):
"""Should emit on_progress during scan."""
scanner = SerieScanner(temp_directory, mock_loader)
progress_handler = MagicMock()
scanner.subscribe_on_progress(progress_handler)
with patch.object(scanner, 'get_total_to_scan', return_value=0), \
patch.object(
scanner, '_SerieScanner__find_mp4_files',
return_value=iter([])
):
scanner.scan()
# At minimum, STARTING event should fire
assert progress_handler.call_count >= 1
first_call = progress_handler.call_args_list[0][0][0]
assert first_call["phase"] == "STARTING"
def test_scan_emits_completion(self, temp_directory, mock_loader):
"""Should emit on_completion after scan."""
scanner = SerieScanner(temp_directory, mock_loader)
completion_handler = MagicMock()
scanner.subscribe_on_completion(completion_handler)
with patch.object(scanner, 'get_total_to_scan', return_value=0), \
patch.object(
scanner, '_SerieScanner__find_mp4_files',
return_value=iter([])
):
scanner.scan()
completion_handler.assert_called_once()
call_data = completion_handler.call_args[0][0]
assert call_data["success"] is True
def test_scan_emits_error_on_no_key(
self, temp_directory, mock_loader
):
"""Should emit on_error when NoKeyFoundException occurs."""
from src.core.exceptions.Exceptions import NoKeyFoundException
scanner = SerieScanner(temp_directory, mock_loader)
error_handler = MagicMock()
scanner.subscribe_on_error(error_handler)
with patch.object(scanner, 'get_total_to_scan', return_value=1), \
patch.object(
scanner, '_SerieScanner__find_mp4_files',
return_value=iter([("BadFolder", ["e1.mp4"])])
), \
patch.object(
scanner, '_SerieScanner__read_data_from_file',
side_effect=NoKeyFoundException("no key"),
):
scanner.scan()
error_handler.assert_called_once()
call_data = error_handler.call_args[0][0]
assert call_data["recoverable"] is True