"""Tests for NFO API endpoints. This module tests all NFO management REST API endpoints. """ import pytest from httpx import ASGITransport, AsyncClient from unittest.mock import AsyncMock, Mock, patch from src.server.fastapi_app import app from src.server.services.auth_service import auth_service from src.server.models.nfo import ( MediaFilesStatus, NFOCheckResponse, NFOCreateResponse, ) @pytest.fixture(autouse=True) def reset_auth(): """Reset authentication state before each test.""" original_hash = auth_service._hash auth_service._hash = None auth_service._failed.clear() yield auth_service._hash = original_hash auth_service._failed.clear() @pytest.fixture async def client(): """Create an 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): """Create an authenticated test client with token.""" # Setup master password await client.post( "/api/auth/setup", json={"master_password": "TestPassword123!"} ) # Login to get token response = await client.post( "/api/auth/login", json={"password": "TestPassword123!"} ) token = response.json()["access_token"] # Add token to default headers client.headers.update({"Authorization": f"Bearer {token}"}) yield client @pytest.fixture def mock_anime_service(): """Create mock anime service.""" service = Mock() serie = Mock() serie.key = "test-anime" serie.folder = "Test Anime (2024)" serie.name = "Test Anime" service.get_series_list = Mock(return_value=[serie]) return service @pytest.fixture def mock_nfo_service(): """Create mock NFO service.""" service = Mock() service.check_nfo_exists = AsyncMock(return_value=False) service.create_tvshow_nfo = AsyncMock(return_value="/path/to/tvshow.nfo") service.update_tvshow_nfo = AsyncMock(return_value="/path/to/tvshow.nfo") return service class TestNFOCheckEndpoint: """Tests for GET /api/nfo/{serie_id}/check endpoint.""" @pytest.mark.asyncio async def test_check_nfo_requires_auth(self, client): """Test that check endpoint requires authentication.""" response = await client.get("/api/nfo/test-anime/check") assert response.status_code == 401 @pytest.mark.asyncio async def test_check_nfo_series_not_found( self, authenticated_client, mock_anime_service, mock_nfo_service ): """Test check endpoint with non-existent series.""" mock_anime_service.get_series_list = Mock(return_value=[]) with patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): response = await authenticated_client.get( "/api/nfo/nonexistent/check" ) assert response.status_code == 404 @pytest.mark.asyncio async def test_check_nfo_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test successful NFO check.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.get( "/api/nfo/test-anime/check" ) assert response.status_code == 200 data = response.json() assert data["serie_id"] == "test-anime" assert data["serie_folder"] == "Test Anime (2024)" assert data["has_nfo"] is False class TestNFOCreateEndpoint: """Tests for POST /api/nfo/{serie_id}/create endpoint.""" @pytest.mark.asyncio async def test_create_nfo_requires_auth(self, client): """Test that create endpoint requires authentication.""" response = await client.post( "/api/nfo/test-anime/create", json={} ) assert response.status_code == 401 @pytest.mark.asyncio async def test_create_nfo_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test successful NFO creation.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.post( "/api/nfo/test-anime/create", json={ "download_poster": True, "download_logo": True, "download_fanart": True } ) assert response.status_code == 200 data = response.json() assert data["serie_id"] == "test-anime" assert "NFO and media files created successfully" in data["message"] @pytest.mark.asyncio async def test_create_nfo_already_exists( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test NFO creation when NFO already exists.""" mock_nfo_service.check_nfo_exists = AsyncMock(return_value=True) with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.post( "/api/nfo/test-anime/create", json={"overwrite_existing": False} ) assert response.status_code == 409 @pytest.mark.asyncio async def test_create_nfo_with_year( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test NFO creation with year parameter.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.post( "/api/nfo/test-anime/create", json={ "year": 2024, "download_poster": True } ) assert response.status_code == 200 # Verify year was passed to service mock_nfo_service.create_tvshow_nfo.assert_called_once() call_kwargs = mock_nfo_service.create_tvshow_nfo.call_args[1] assert call_kwargs["year"] == 2024 class TestNFOUpdateEndpoint: """Tests for PUT /api/nfo/{serie_id}/update endpoint.""" @pytest.mark.asyncio async def test_update_nfo_requires_auth(self, client): """Test that update endpoint requires authentication.""" response = await client.put("/api/nfo/test-anime/update") assert response.status_code == 401 @pytest.mark.asyncio async def test_update_nfo_not_found( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test update when NFO doesn't exist.""" mock_nfo_service.check_nfo_exists = AsyncMock(return_value=False) with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.put( "/api/nfo/test-anime/update" ) assert response.status_code == 404 @pytest.mark.asyncio async def test_update_nfo_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test successful NFO update.""" mock_nfo_service.check_nfo_exists = AsyncMock(return_value=True) with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.put( "/api/nfo/test-anime/update?download_media=true" ) assert response.status_code == 200 data = response.json() assert "NFO updated successfully" in data["message"] class TestNFOContentEndpoint: """Tests for GET /api/nfo/{serie_id}/content endpoint.""" @pytest.mark.asyncio async def test_get_content_requires_auth(self, client): """Test that content endpoint requires authentication.""" response = await client.get("/api/nfo/test-anime/content") assert response.status_code == 401 @pytest.mark.asyncio async def test_get_content_nfo_not_found( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test get content when NFO doesn't exist.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.get( "/api/nfo/test-anime/content" ) assert response.status_code == 404 @pytest.mark.asyncio async def test_get_content_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test successful content retrieval.""" # Create NFO file anime_dir = tmp_path / "Test Anime (2024)" anime_dir.mkdir() nfo_file = anime_dir / "tvshow.nfo" nfo_file.write_text("Test") with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.get( "/api/nfo/test-anime/content" ) assert response.status_code == 200 data = response.json() assert "" in data["content"] assert data["file_size"] > 0 class TestNFOMissingEndpoint: """Tests for GET /api/nfo/missing endpoint.""" @pytest.mark.asyncio async def test_get_missing_requires_auth(self, client): """Test that missing endpoint requires authentication.""" response = await client.get("/api/nfo/missing") assert response.status_code == 401 @pytest.mark.asyncio async def test_get_missing_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test getting list of series without NFO.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.get("/api/nfo/missing") assert response.status_code == 200 data = response.json() assert "total_series" in data assert "missing_nfo_count" in data assert "series" in data class TestNFOBatchCreateEndpoint: """Tests for POST /api/nfo/batch/create endpoint.""" @pytest.mark.asyncio async def test_batch_create_requires_auth(self, client): """Test that batch create endpoint requires authentication.""" response = await client.post( "/api/nfo/batch/create", json={"serie_ids": ["test1", "test2"]} ) assert response.status_code == 401 @pytest.mark.asyncio async def test_batch_create_success( self, authenticated_client, mock_anime_service, mock_nfo_service, tmp_path ): """Test successful batch NFO creation.""" with patch('src.server.api.nfo.settings') as mock_settings, \ patch( 'src.server.api.nfo.get_anime_service', return_value=mock_anime_service ), \ patch( 'src.server.api.nfo.get_nfo_service', return_value=mock_nfo_service ): mock_settings.anime_directory = str(tmp_path) response = await authenticated_client.post( "/api/nfo/batch/create", json={ "serie_ids": ["test-anime"], "download_media": True, "skip_existing": False, "max_concurrent": 3 } ) assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert "successful" in data assert "results" in data class TestNFOServiceDependency: """Tests for NFO service dependency.""" @pytest.mark.asyncio async def test_nfo_service_unavailable_without_api_key( self, authenticated_client ): """Test NFO endpoints fail gracefully without TMDB API key.""" with patch('src.server.api.nfo.settings') as mock_settings: mock_settings.tmdb_api_key = None response = await authenticated_client.get( "/api/nfo/test-anime/check" ) assert response.status_code == 503 assert "not configured" in response.json()["detail"]