"""Unit tests for key_resolution_service.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.services.key_resolution_service import ( _extract_key_from_link, _extract_year_from_folder, _normalize_for_comparison, _strip_year_from_folder, resolve_key_for_folder, ) class TestStripYearFromFolder: """Tests for _strip_year_from_folder.""" def test_removes_year_suffix(self): assert _strip_year_from_folder("Rent-A-Girlfriend (2020)") == "Rent-A-Girlfriend" def test_removes_year_suffix_with_spaces(self): assert _strip_year_from_folder("Attack on Titan (2013)") == "Attack on Titan" def test_no_year_returns_original(self): assert _strip_year_from_folder("Naruto") == "Naruto" def test_year_in_middle_not_stripped(self): assert _strip_year_from_folder("2024 Anime (2024)") == "2024 Anime" def test_empty_string(self): assert _strip_year_from_folder("") == "" def test_only_year(self): assert _strip_year_from_folder("(2020)") == "" class TestExtractYearFromFolder: """Tests for _extract_year_from_folder.""" def test_extracts_year(self): assert _extract_year_from_folder("Rent-A-Girlfriend (2020)") == 2020 def test_no_year_returns_none(self): assert _extract_year_from_folder("Naruto") is None def test_year_in_middle_not_extracted(self): # Only trailing year is extracted assert _extract_year_from_folder("2024 Anime") is None class TestExtractKeyFromLink: """Tests for _extract_key_from_link.""" def test_relative_link(self): assert _extract_key_from_link("/anime/stream/rent-a-girlfriend") == "rent-a-girlfriend" def test_full_url(self): assert ( _extract_key_from_link("https://aniworld.to/anime/stream/attack-on-titan") == "attack-on-titan" ) def test_link_with_trailing_slash(self): assert _extract_key_from_link("/anime/stream/naruto/") == "naruto" def test_empty_link(self): assert _extract_key_from_link("") is None def test_none_link(self): assert _extract_key_from_link(None) is None def test_slug_only(self): assert _extract_key_from_link("one-piece") == "one-piece" class TestNormalizeForComparison: """Tests for _normalize_for_comparison.""" def test_case_insensitive(self): assert _normalize_for_comparison("Rent-A-Girlfriend") == _normalize_for_comparison( "rent-a-girlfriend" ) def test_strips_whitespace(self): assert _normalize_for_comparison(" Naruto ") == "naruto" def test_normalizes_dashes(self): assert _normalize_for_comparison("Rent-A-Girlfriend") == "rent a girlfriend" def test_collapses_spaces(self): assert _normalize_for_comparison("Attack on Titan") == "attack on titan" class TestResolveKeyForFolder: """Tests for resolve_key_for_folder.""" @pytest.mark.asyncio async def test_single_exact_match_returns_key(self): """When provider returns exactly one exact-name match, key is resolved.""" search_results = [ {"link": "/anime/stream/rent-a-girlfriend", "title": "Rent-A-Girlfriend"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("Rent-A-Girlfriend (2020)") assert key == "rent-a-girlfriend" @pytest.mark.asyncio async def test_no_results_returns_none(self): """When provider returns no results, returns None.""" with patch( "src.server.services.key_resolution_service._search_provider", return_value=[], ): key = await resolve_key_for_folder("Unknown Anime (2020)") assert key is None @pytest.mark.asyncio async def test_multiple_exact_matches_returns_none(self): """When multiple results match the same name exactly, returns None.""" search_results = [ {"link": "/anime/stream/my-anime", "title": "My Anime"}, {"link": "/anime/stream/my-anime-2", "title": "My Anime"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("My Anime (2022)") assert key is None @pytest.mark.asyncio async def test_no_exact_match_returns_none(self): """When results exist but none match the folder name, returns None.""" search_results = [ {"link": "/anime/stream/rent-a-girlfriend-2", "title": "Rent-A-Girlfriend 2nd Season"}, {"link": "/anime/stream/rent-a-girlfriend-3", "title": "Rent-A-Girlfriend 3rd Season"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("Rent-A-Girlfriend (2020)") assert key is None @pytest.mark.asyncio async def test_case_insensitive_match(self): """Matching is case-insensitive.""" search_results = [ {"link": "/anime/stream/naruto", "title": "NARUTO"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("Naruto (2002)") assert key == "naruto" @pytest.mark.asyncio async def test_provider_error_returns_none(self): """When provider search raises an exception, returns None gracefully.""" with patch( "src.server.services.key_resolution_service._search_provider", side_effect=RuntimeError("Network error"), ): key = await resolve_key_for_folder("Some Anime (2020)") assert key is None @pytest.mark.asyncio async def test_result_with_name_field_instead_of_title(self): """Search results using 'name' field instead of 'title' work.""" search_results = [ {"link": "/anime/stream/one-piece", "name": "One Piece"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("One Piece (1999)") assert key == "one-piece" @pytest.mark.asyncio async def test_folder_without_year(self): """Folders without year suffix still work.""" search_results = [ {"link": "/anime/stream/naruto", "title": "Naruto"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("Naruto") assert key == "naruto" @pytest.mark.asyncio async def test_exact_match_among_partial_matches(self): """Only exact matches count, partial matches are ignored.""" search_results = [ {"link": "/anime/stream/dororo", "title": "Dororo"}, {"link": "/anime/stream/dororo-to-hyakkimaru", "title": "Dororo to Hyakkimaru"}, ] with patch( "src.server.services.key_resolution_service._search_provider", return_value=search_results, ): key = await resolve_key_for_folder("Dororo (2019)") assert key == "dororo"