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:
@@ -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
|
||||
Reference in New Issue
Block a user