""" Unit tests for FavoritesManager """ import pytest import json from pathlib import Path from unittest.mock import patch, AsyncMock, mock_open from app.favorites import FavoritesManager, get_favorites_manager class TestFavoritesManagerInit: """Tests for FavoritesManager initialization""" def test_init_default_path(self, temp_dir): """Test FavoritesManager initialization with default path""" manager = FavoritesManager(storage_path=str(temp_dir / "favorites.json")) assert manager.storage_path == temp_dir / "favorites.json" assert manager._favorites == {} def test_init_creates_directory(self, temp_dir): """Test that initialization creates the parent directory""" storage_path = temp_dir / "subdir" / "favorites.json" assert not storage_path.parent.exists() manager = FavoritesManager(storage_path=str(storage_path)) assert storage_path.parent.exists() assert storage_path.parent.is_dir() def test_global_manager_singleton(self): """Test that get_favorites_manager returns singleton""" manager1 = get_favorites_manager() manager2 = get_favorites_manager() assert manager1 is manager2 class TestFavoritesManagerLoad: """Tests for loading favorites from disk""" @pytest.mark.asyncio async def test_load_empty_file(self, favorites_manager): """Test loading from empty file""" await favorites_manager._load() assert favorites_manager._favorites == {} @pytest.mark.asyncio async def test_load_with_data(self, temp_dir): """Test loading favorites with data""" storage_path = temp_dir / "test_favorites.json" test_data = { "anime-1": { "id": "anime-1", "title": "Test Anime 1", "url": "https://example.com/anime1", "provider": "anime-sama" } } storage_path.write_text(json.dumps(test_data)) manager = FavoritesManager(storage_path=str(storage_path)) await manager._load() assert len(manager._favorites) == 1 assert "anime-1" in manager._favorites assert manager._favorites["anime-1"]["title"] == "Test Anime 1" @pytest.mark.asyncio async def test_load_invalid_json(self, temp_dir): """Test loading invalid JSON""" storage_path = temp_dir / "invalid.json" storage_path.write_text("invalid json content {") manager = FavoritesManager(storage_path=str(storage_path)) await manager._load() # Should default to empty dict on error assert manager._favorites == {} @pytest.mark.asyncio async def test_load_nonexistent_file(self, temp_dir): """Test loading when file doesn't exist""" storage_path = temp_dir / "nonexistent.json" assert not storage_path.exists() manager = FavoritesManager(storage_path=str(storage_path)) await manager._load() assert manager._favorites == {} class TestFavoritesManagerSave: """Tests for saving favorites to disk""" @pytest.mark.asyncio async def test_save_favorites(self, temp_dir): """Test saving favorites to disk""" storage_path = temp_dir / "test_save.json" manager = FavoritesManager(storage_path=str(storage_path)) manager._favorites = { "anime-1": {"id": "anime-1", "title": "Test Anime"} } await manager._save() assert storage_path.exists() content = storage_path.read_text() saved_data = json.loads(content) assert saved_data == {"anime-1": {"id": "anime-1", "title": "Test Anime"}} class TestFavoritesManagerAdd: """Tests for adding favorites""" @pytest.mark.asyncio async def test_add_favorite_new(self, favorites_manager): """Test adding a new favorite""" result = await favorites_manager.add_favorite( anime_id="anime-123", title="Test Anime", url="https://example.com/anime", provider="anime-sama", metadata={"genres": ["Action"]}, poster_url="https://example.com/poster.jpg" ) assert result["id"] == "anime-123" assert result["title"] == "Test Anime" assert result["url"] == "https://example.com/anime" assert result["provider"] == "anime-sama" assert result["metadata"]["genres"] == ["Action"] assert result["poster_url"] == "https://example.com/poster.jpg" assert "created_at" in result assert "updated_at" in result # Verify it was saved await favorites_manager._load() assert "anime-123" in favorites_manager._favorites @pytest.mark.asyncio async def test_add_favorite_minimal(self, favorites_manager): """Test adding favorite with minimal data""" result = await favorites_manager.add_favorite( anime_id="anime-456", title="Minimal Anime", url="https://example.com/minimal", provider="neko-sama" ) assert result["id"] == "anime-456" assert result["metadata"] == {} assert result["poster_url"] is None @pytest.mark.asyncio async def test_add_favorite_update_existing(self, favorites_manager): """Test updating an existing favorite""" # Add initial favorite await favorites_manager.add_favorite( anime_id="anime-789", title="Original Title", url="https://example.com/anime", provider="anime-sama", metadata={"rating": "7.0/10"} ) # Update with new metadata result = await favorites_manager.add_favorite( anime_id="anime-789", title="Updated Title", # This won't update url="https://example.com/anime", provider="anime-sama", metadata={"rating": "8.5/10"}, poster_url="https://example.com/new_poster.jpg" ) # Title should remain the same, metadata and poster should update assert result["title"] == "Original Title" assert result["metadata"]["rating"] == "8.5/10" assert result["poster_url"] == "https://example.com/new_poster.jpg" @pytest.mark.asyncio async def test_add_favorite_preserves_created_at(self, favorites_manager): """Test that created_at is preserved on update""" await favorites_manager.add_favorite( anime_id="anime-time", title="Time Test", url="https://example.com", provider="anime-sama" ) import time await asyncio.sleep(0.1) # Small delay result = await favorites_manager.add_favorite( anime_id="anime-time", title="Time Test", url="https://example.com", provider="anime-sama", metadata={"new": "data"} ) # updated_at should be newer than created_at from datetime import datetime created = datetime.fromisoformat(result["created_at"]) updated = datetime.fromisoformat(result["updated_at"]) assert updated > created class TestFavoritesManagerRemove: """Tests for removing favorites""" @pytest.mark.asyncio async def test_remove_favorite_success(self, favorites_manager): """Test successfully removing a favorite""" # Add favorite first await favorites_manager.add_favorite( anime_id="anime-remove", title="Remove Me", url="https://example.com", provider="anime-sama" ) # Remove it result = await favorites_manager.remove_favorite("anime-remove") assert result is True # Verify it's gone await favorites_manager._load() assert "anime-remove" not in favorites_manager._favorites @pytest.mark.asyncio async def test_remove_favorite_nonexistent(self, favorites_manager): """Test removing a non-existent favorite""" result = await favorites_manager.remove_favorite("nonexistent-id") assert result is False @pytest.mark.asyncio async def test_remove_multiple_favorites(self, favorites_manager): """Test removing multiple favorites""" # Add multiple favorites for i in range(5): await favorites_manager.add_favorite( anime_id=f"anime-{i}", title=f"Anime {i}", url=f"https://example.com/anime{i}", provider="anime-sama" ) # Remove some await favorites_manager.remove_favorite("anime-1") await favorites_manager.remove_favorite("anime-3") await favorites_manager._load() assert len(favorites_manager._favorites) == 3 assert "anime-1" not in favorites_manager._favorites assert "anime-3" not in favorites_manager._favorites class TestFavoritesManagerGet: """Tests for getting favorites""" @pytest.mark.asyncio async def test_get_favorite_success(self, favorites_manager): """Test getting an existing favorite""" await favorites_manager.add_favorite( anime_id="anime-get", title="Get Test", url="https://example.com", provider="anime-sama" ) result = await favorites_manager.get_favorite("anime-get") assert result is not None assert result["id"] == "anime-get" assert result["title"] == "Get Test" @pytest.mark.asyncio async def test_get_favorite_nonexistent(self, favorites_manager): """Test getting a non-existent favorite""" result = await favorites_manager.get_favorite("nonexistent-id") assert result is None class TestFavoritesManagerList: """Tests for listing favorites""" @pytest.mark.asyncio async def test_list_favorites_empty(self, favorites_manager): """Test listing when no favorites exist""" result = await favorites_manager.list_favorites() assert result == [] @pytest.mark.asyncio async def test_list_favorites_default_sort(self, favorites_manager): """Test listing favorites with default sorting""" # Add favorites with different timestamps import time for i in range(3): await favorites_manager.add_favorite( anime_id=f"anime-list-{i}", title=f"Anime {i}", url=f"https://example.com/{i}", provider="anime-sama" ) await asyncio.sleep(0.05) # Small delay for different timestamps result = await favorites_manager.list_favorites() assert len(result) == 3 # Default is sort by created_at desc, so last added should be first assert result[0]["id"] == "anime-list-2" @pytest.mark.asyncio async def test_list_favorites_sort_by_title(self, favorites_manager): """Test sorting by title""" await favorites_manager.add_favorite("z-anime", "Z Anime", "https://example.com/z", "anime-sama") await asyncio.sleep(0.05) await favorites_manager.add_favorite("a-anime", "A Anime", "https://example.com/a", "anime-sama") result_asc = await favorites_manager.list_favorites(sort_by="title", order="asc") assert result_asc[0]["title"] == "A Anime" result_desc = await favorites_manager.list_favorites(sort_by="title", order="desc") assert result_desc[0]["title"] == "Z Anime" @pytest.mark.asyncio async def test_list_favorites_filter_by_provider(self, favorites_manager): """Test filtering by provider""" await favorites_manager.add_favorite("anime-1", "Anime 1", "https://example.com/1", "anime-sama") await favorites_manager.add_favorite("anime-2", "Anime 2", "https://example.com/2", "neko-sama") result = await favorites_manager.list_favorites(filter_provider="anime-sama") assert len(result) == 1 assert result[0]["provider"] == "anime-sama" @pytest.mark.asyncio async def test_list_favorites_filter_by_genre(self, favorites_manager): """Test filtering by genre""" await favorites_manager.add_favorite( "anime-action", "Action Anime", "https://example.com/action", "anime-sama", metadata={"genres": ["Action", "Adventure"]} ) await favorites_manager.add_favorite( "anime-drama", "Drama Anime", "https://example.com/drama", "anime-sama", metadata={"genres": ["Drama", "Romance"]} ) result = await favorites_manager.list_favorites(filter_genre="Action") assert len(result) == 1 assert result[0]["id"] == "anime-action" @pytest.mark.asyncio async def test_list_favorites_combined_filters(self, favorites_manager): """Test combining filters""" await favorites_manager.add_favorite( "anime-1", "Anime 1", "https://example.com/1", "anime-sama", metadata={"genres": ["Action"]} ) await favorites_manager.add_favorite( "anime-2", "Anime 2", "https://example.com/2", "neko-sama", metadata={"genres": ["Action"]} ) result = await favorites_manager.list_favorites( filter_provider="anime-sama", filter_genre="Action" ) assert len(result) == 1 assert result[0]["id"] == "anime-1" class TestFavoritesManagerIsFavorite: """Tests for is_favorite method""" @pytest.mark.asyncio async def test_is_favorite_true(self, favorites_manager): """Test checking if anime is a favorite (true)""" await favorites_manager.add_favorite( "anime-check", "Check Anime", "https://example.com", "anime-sama" ) result = await favorites_manager.is_favorite("anime-check") assert result is True @pytest.mark.asyncio async def test_is_favorite_false(self, favorites_manager): """Test checking if anime is a favorite (false)""" result = await favorites_manager.is_favorite("nonexistent-id") assert result is False class TestFavoritesManagerToggle: """Tests for toggle_favorite method""" @pytest.mark.asyncio async def test_toggle_favorite_add(self, favorites_manager): """Test toggling to add a favorite""" result = await favorites_manager.toggle_favorite( anime_id="anime-toggle-add", title="Toggle Add", url="https://example.com", provider="anime-sama" ) assert result["action"] == "added" assert result["anime_id"] == "anime-toggle-add" assert "favorite" in result # Verify it was added is_fav = await favorites_manager.is_favorite("anime-toggle-add") assert is_fav is True @pytest.mark.asyncio async def test_toggle_favorite_remove(self, favorites_manager): """Test toggling to remove a favorite""" # Add first await favorites_manager.add_favorite( "anime-toggle-remove", title="Toggle Remove", url="https://example.com", provider="anime-sama" ) # Toggle to remove result = await favorites_manager.toggle_favorite( anime_id="anime-toggle-remove", title="Toggle Remove", url="https://example.com", provider="anime-sama" ) assert result["action"] == "removed" assert result["anime_id"] == "anime-toggle-remove" # Verify it was removed is_fav = await favorites_manager.is_favorite("anime-toggle-remove") assert is_fav is False class TestFavoritesManagerStats: """Tests for get_stats method""" @pytest.mark.asyncio async def test_get_stats_empty(self, favorites_manager): """Test getting stats with no favorites""" stats = await favorites_manager.get_stats() assert stats["total"] == 0 assert stats["by_provider"] == {} assert stats["by_genre"] == {} @pytest.mark.asyncio async def test_get_stats_with_data(self, favorites_manager): """Test getting stats with favorites""" # Add favorites with different providers and genres await favorites_manager.add_favorite( "anime-1", "Anime 1", "https://example.com/1", "anime-sama", metadata={"genres": ["Action", "Adventure"]} ) await favorites_manager.add_favorite( "anime-2", "Anime 2", "https://example.com/2", "anime-sama", metadata={"genres": ["Action"]} ) await favorites_manager.add_favorite( "anime-3", "Anime 3", "https://example.com/3", "neko-sama", metadata={"genres": ["Drama"]} ) stats = await favorites_manager.get_stats() assert stats["total"] == 3 assert stats["by_provider"]["anime-sama"] == 2 assert stats["by_provider"]["neko-sama"] == 1 assert stats["by_genre"]["Action"] == 2 assert stats["by_genre"]["Adventure"] == 1 assert stats["by_genre"]["Drama"] == 1 @pytest.mark.asyncio async def test_get_stats_multiple_genres(self, favorites_manager): """Test stats with anime having multiple genres""" await favorites_manager.add_favorite( "anime-multi", "Multi Genre Anime", "https://example.com", "anime-sama", metadata={"genres": ["Action", "Adventure", "Fantasy", "Comedy"]} ) stats = await favorites_manager.get_stats() assert stats["by_genre"]["Action"] == 1 assert stats["by_genre"]["Adventure"] == 1 assert stats["by_genre"]["Fantasy"] == 1 assert stats["by_genre"]["Comedy"] == 1 class TestFavoritesManagerConcurrency: """Tests for concurrent operations""" @pytest.mark.asyncio async def test_concurrent_add(self, favorites_manager): """Test adding favorites concurrently""" async def add_favorite(i): return await favorites_manager.add_favorite( f"concurrent-{i}", f"Concurrent {i}", f"https://example.com/{i}", "anime-sama" ) tasks = [add_favorite(i) for i in range(10)] results = await asyncio.gather(*tasks) assert len(results) == 10 # Verify all were added await favorites_manager._load() assert len(favorites_manager._favorites) == 10 @pytest.mark.asyncio async def test_concurrent_read_write(self, favorites_manager): """Test concurrent reads and writes""" await favorites_manager.add_favorite("base", "Base", "https://example.com", "anime-sama") async def operations(): tasks = [] for i in range(5): tasks.append(favorites_manager.add_favorite(f"rw-{i}", f"RW {i}", "https://example.com", "anime-sama")) tasks.append(favorites_manager.list_favorites()) results = await asyncio.gather(*tasks) return results results = await operations() assert len(results) == 10