"""Tests for anime metadata edit (PUT /api/anime/{anime_key}) endpoint.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from httpx import ASGITransport, AsyncClient from src.server.fastapi_app import app from src.server.services.auth_service import auth_service @pytest.fixture def reset_auth(): """Reset auth state before each test.""" auth_service._hash = None auth_service._failed = {} @pytest.fixture async def client(): """Create async test client.""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac @pytest.fixture async def authenticated_client(client): """Get authenticated client with Bearer token.""" # Setup auth await client.post("/api/auth/setup", json={"master_password": "TestPass123!"}) response = await client.post( "/api/auth/login", json={"password": "TestPass123!"} ) token = response.json()["access_token"] client.headers["Authorization"] = f"Bearer {token}" return client @pytest.fixture def mock_db_session(): """Create a mock async database session.""" session = AsyncMock() session.commit = AsyncMock() session.flush = AsyncMock() session.refresh = AsyncMock() return session @pytest.fixture def mock_series_in_db(): """Create a mock AnimeSeries DB record.""" series = MagicMock() series.id = 1 series.key = "test-anime" series.name = "Test Anime" series.tmdb_id = 1234 series.tvdb_id = 5678 series.folder = "Test Anime (2023)" return series @pytest.fixture def override_db_dependency(mock_db_session): """Override database session dependency.""" from src.server.utils.dependencies import get_database_session app.dependency_overrides[get_database_session] = lambda: mock_db_session yield mock_db_session app.dependency_overrides.pop(get_database_session, None) class TestUpdateAnimeMetadata: """Tests for PUT /api/anime/{anime_key}.""" @pytest.mark.asyncio async def test_update_tmdb_id_success( self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db ): """Test successful tmdb_id update.""" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, return_value=mock_series_in_db, ), patch( "src.server.api.anime.AnimeSeriesService.update", new_callable=AsyncMock, ) as mock_update: mock_series_in_db.tmdb_id = 9999 mock_update.return_value = mock_series_in_db response = await authenticated_client.put( "/api/anime/test-anime", json={"tmdb_id": 9999}, ) assert response.status_code == 200 data = response.json() assert data["tmdb_id"] == 9999 assert data["message"] == "Metadata updated successfully" @pytest.mark.asyncio async def test_update_tvdb_id_success( self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db ): """Test successful tvdb_id update.""" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, return_value=mock_series_in_db, ), patch( "src.server.api.anime.AnimeSeriesService.update", new_callable=AsyncMock, ) as mock_update: mock_series_in_db.tvdb_id = 7777 mock_update.return_value = mock_series_in_db response = await authenticated_client.put( "/api/anime/test-anime", json={"tvdb_id": 7777}, ) assert response.status_code == 200 data = response.json() assert data["tvdb_id"] == 7777 @pytest.mark.asyncio async def test_update_key_success( self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db ): """Test successful key rename.""" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, ) as mock_get: # First call finds the series, second call checks uniqueness (returns None) mock_get.side_effect = [mock_series_in_db, None] mock_series_in_db.key = "new-anime-key" with patch( "src.server.api.anime.AnimeSeriesService.update", new_callable=AsyncMock, return_value=mock_series_in_db, ): response = await authenticated_client.put( "/api/anime/test-anime", json={"key": "new-anime-key"}, ) assert response.status_code == 200 data = response.json() assert data["key"] == "new-anime-key" @pytest.mark.asyncio async def test_update_key_conflict_409( self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db ): """Test key rename conflict returns 409.""" existing_series = MagicMock() existing_series.key = "existing-key" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, ) as mock_get: # First call finds original series, second call finds conflict mock_get.side_effect = [mock_series_in_db, existing_series] response = await authenticated_client.put( "/api/anime/test-anime", json={"key": "existing-key"}, ) assert response.status_code == 409 assert "already exists" in response.json()["detail"] @pytest.mark.asyncio async def test_update_key_invalid_chars_422( self, reset_auth, authenticated_client, override_db_dependency ): """Test key with invalid characters returns 422.""" response = await authenticated_client.put( "/api/anime/test-anime", json={"key": "Invalid Key With Spaces!"}, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_update_key_empty_422( self, reset_auth, authenticated_client, override_db_dependency ): """Test empty key returns 422.""" response = await authenticated_client.put( "/api/anime/test-anime", json={"key": ""}, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_update_unauthenticated_401(self, reset_auth, client): """Test unauthenticated access returns 401.""" response = await client.put( "/api/anime/test-anime", json={"tmdb_id": 1234}, ) assert response.status_code == 401 @pytest.mark.asyncio async def test_update_nonexistent_anime_404( self, reset_auth, authenticated_client, override_db_dependency ): """Test update of non-existent anime returns 404.""" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, return_value=None, ): response = await authenticated_client.put( "/api/anime/nonexistent-key", json={"tmdb_id": 1234}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_update_no_changes( self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db ): """Test sending empty body returns no-op response.""" with patch( "src.server.api.anime.AnimeSeriesService.get_by_key", new_callable=AsyncMock, return_value=mock_series_in_db, ): response = await authenticated_client.put( "/api/anime/test-anime", json={}, ) assert response.status_code == 200 assert response.json()["message"] == "No changes" @pytest.mark.asyncio async def test_update_negative_tmdb_id_422( self, reset_auth, authenticated_client, override_db_dependency ): """Test negative TMDB ID returns 422.""" response = await authenticated_client.put( "/api/anime/test-anime", json={"tmdb_id": -5}, ) assert response.status_code == 422