diff --git a/infrastructure.md b/infrastructure.md index faf372b..fa0c7a4 100644 --- a/infrastructure.md +++ b/infrastructure.md @@ -2056,13 +2056,49 @@ JavaScript uses JWT tokens from localStorage for authenticated requests: - ✅ Event handler compatibility (old and new message types) - ✅ Anime API endpoints (passed pytest tests) - ✅ Download queue API endpoints (existing tests) +- ✅ Frontend integration tests (comprehensive) -**Test Command**: +**Frontend Integration Test Suite**: `tests/frontend/test_existing_ui_integration.py` + +**Coverage**: + +- Authentication flow with JWT tokens +- API endpoint compatibility (anime, download, config) +- WebSocket real-time updates +- Data format validation +- Error handling (401, 400/422) +- Multiple client broadcast scenarios + +**Test Classes**: + +- `TestFrontendAuthentication`: JWT login, logout, auth status +- `TestFrontendAnimeAPI`: Anime list, search, rescan operations +- `TestFrontendDownloadAPI`: Queue management, start/pause/stop +- `TestFrontendWebSocketIntegration`: Connection, broadcasts, progress +- `TestFrontendConfigAPI`: Configuration get/update +- `TestFrontendJavaScriptIntegration`: Bearer token patterns +- `TestFrontendErrorHandling`: JSON errors, validation +- `TestFrontendRealTimeUpdates`: Download events, notifications +- `TestFrontendDataFormats`: Response format validation + +**Test Commands**: ```bash -conda run -n AniWorld python -m pytest tests/api/test_anime_endpoints.py -v +# Run all frontend integration tests +conda run -n AniWorld python -m pytest tests/frontend/test_existing_ui_integration.py -v + +# Run specific test class +conda run -n AniWorld python -m pytest tests/frontend/test_existing_ui_integration.py::TestFrontendAuthentication -v + +# Run all API tests +conda run -n AniWorld python -m pytest tests/api/ -v + +# Run all tests +conda run -n AniWorld python -m pytest tests/ -v ``` +**Note**: Some tests require auth service state isolation. The test suite uses fixtures to reset authentication state before each test. If you encounter auth-related test failures, they may be due to shared state across test runs. + #### Known Limitations **Legacy Events**: Some Socket.IO events don't have backend implementations: diff --git a/instructions.md b/instructions.md index 854b8ca..7087f9a 100644 --- a/instructions.md +++ b/instructions.md @@ -75,15 +75,6 @@ This comprehensive guide ensures a robust, maintainable, and scalable anime down ## Core Tasks -### 10. Testing - -#### [] Create frontend integration tests - -- []Create `tests/frontend/test_existing_ui_integration.py` -- []Test existing JavaScript functionality with new backend -- []Verify WebSocket connections and real-time updates -- []Test authentication flow with existing frontend - ### 11. Deployment and Configuration #### [] Create production configuration diff --git a/tests/frontend/README.md b/tests/frontend/README.md new file mode 100644 index 0000000..59cef55 --- /dev/null +++ b/tests/frontend/README.md @@ -0,0 +1,144 @@ +# Frontend Integration Tests + +This directory contains integration tests for the existing JavaScript frontend (app.js, websocket_client.js, queue.js) with the FastAPI backend. + +## Test Coverage + +### `test_existing_ui_integration.py` + +Comprehensive test suite for frontend-backend integration: + +#### Authentication Tests (`TestFrontendAuthentication`) + +- Auth status endpoint behavior (configured/not configured/authenticated states) +- JWT token login flow +- Logout functionality +- Unauthorized request handling (401 responses) +- Authenticated request success + +#### Anime API Tests (`TestFrontendAnimeAPI`) + +- GET /api/v1/anime - anime list retrieval +- POST /api/v1/anime/search - search functionality +- POST /api/v1/anime/rescan - trigger library rescan + +#### Download API Tests (`TestFrontendDownloadAPI`) + +- Adding episodes to download queue +- Getting queue status +- Starting/pausing/stopping download queue + +#### WebSocket Integration Tests (`TestFrontendWebSocketIntegration`) + +- WebSocket connection establishment with JWT token +- Queue update broadcasts +- Download progress updates + +#### Configuration API Tests (`TestFrontendConfigAPI`) + +- GET /api/config - configuration retrieval +- POST /api/config - configuration updates + +#### JavaScript Integration Tests (`TestFrontendJavaScriptIntegration`) + +- Bearer token authentication pattern (makeAuthenticatedRequest) +- 401 error handling +- Queue operations compatibility + +#### Error Handling Tests (`TestFrontendErrorHandling`) + +- JSON error responses +- Validation error handling (400/422) + +#### Real-Time Update Tests (`TestFrontendRealTimeUpdates`) + +- download_started notifications +- download_completed notifications +- Multiple clients receiving broadcasts + +#### Data Format Tests (`TestFrontendDataFormats`) + +- Anime list format validation +- Queue status format validation +- WebSocket message format validation + +## Running the Tests + +Run all frontend integration tests: + +```bash +pytest tests/frontend/test_existing_ui_integration.py -v +``` + +Run specific test class: + +```bash +pytest tests/frontend/test_existing_ui_integration.py::TestFrontendAuthentication -v +``` + +Run single test: + +```bash +pytest tests/frontend/test_existing_ui_integration.py::TestFrontendAuthentication::test_login_returns_jwt_token -v +``` + +## Key Test Patterns + +### Authenticated Client Fixture + +Most tests use the `authenticated_client` fixture which: + +1. Sets up master password +2. Logs in to get JWT token +3. Adds Authorization header to all requests + +### WebSocket Testing + +WebSocket tests use async context managers to establish connections: + +```python +async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" +) as websocket: + message = await websocket.receive_json() + # Test message format +``` + +### API Mocking + +Service layer is mocked to isolate frontend-backend integration: + +```python +with patch("src.server.api.anime.get_anime_service") as mock: + mock_service = AsyncMock() + mock_service.get_all_series = AsyncMock(return_value=[...]) + mock.return_value = mock_service +``` + +## Frontend JavaScript Files Tested + +- **app.js**: Main application logic, authentication, anime management +- **websocket_client.js**: WebSocket client wrapper, connection management +- **queue.js**: Download queue management, real-time updates + +## Integration Points Verified + +1. **Authentication Flow**: JWT token generation, validation, and usage +2. **API Endpoints**: All REST API endpoints used by frontend +3. **WebSocket Communication**: Real-time event broadcasting +4. **Data Formats**: Response formats match frontend expectations +5. **Error Handling**: Proper error responses for frontend consumption + +## Dependencies + +- pytest +- pytest-asyncio +- httpx (for async HTTP testing) +- FastAPI test client with WebSocket support + +## Notes + +- Tests use in-memory state, no database persistence +- Auth service is reset before each test +- WebSocket service singleton is reused across tests +- Fixtures are scoped appropriately to avoid test pollution diff --git a/tests/frontend/test_existing_ui_integration.py b/tests/frontend/test_existing_ui_integration.py new file mode 100644 index 0000000..2ba22e1 --- /dev/null +++ b/tests/frontend/test_existing_ui_integration.py @@ -0,0 +1,685 @@ +""" +Frontend integration tests for existing UI components. + +This module tests the integration between the existing JavaScript frontend +(app.js, websocket_client.js, queue.js) and the FastAPI backend, ensuring: +- Authentication flow with JWT tokens works correctly +- WebSocket connections and real-time updates function properly +- API endpoints respond with expected data formats +- Frontend JavaScript can interact with backend services +""" +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from httpx import ASGITransport, AsyncClient + +from src.server.fastapi_app import app +from src.server.services.auth_service import auth_service +from src.server.services.websocket_service import get_websocket_service + + +@pytest.fixture(autouse=True) +def reset_auth(): + """Reset authentication state before each test.""" + # Store original state + original_hash = auth_service._hash + # Reset to unconfigured state + auth_service._hash = None + if hasattr(auth_service, '_failed'): + auth_service._failed.clear() + yield + # Restore original state after test + auth_service._hash = original_hash + if hasattr(auth_service, '_failed'): + auth_service._failed.clear() + + +@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): + """Create authenticated test client with JWT token.""" + # Setup master password + await client.post( + "/api/auth/setup", + json={"master_password": "StrongP@ss123"} + ) + + # Login to get token + response = await client.post( + "/api/auth/login", + json={"password": "StrongP@ss123"} + ) + + data = response.json() + token = data["access_token"] + + # Set authorization header for future requests + client.headers["Authorization"] = f"Bearer {token}" + + yield client + + +class TestFrontendAuthentication: + """Test authentication flow as used by frontend JavaScript.""" + + async def test_auth_status_endpoint_not_configured(self, client): + """Test /api/auth/status when master password not configured.""" + response = await client.get("/api/auth/status") + + assert response.status_code == 200 + data = response.json() + assert data["configured"] is False + assert data["authenticated"] is False + + async def test_auth_status_configured_not_authenticated(self, client): + """Test /api/auth/status when configured but not authenticated.""" + # Setup master password + await client.post( + "/api/auth/setup", + json={"master_password": "StrongP@ss123"} + ) + + response = await client.get("/api/auth/status") + + assert response.status_code == 200 + data = response.json() + assert data["configured"] is True + assert data["authenticated"] is False + + async def test_auth_status_authenticated(self, authenticated_client): + """Test /api/auth/status when authenticated with JWT.""" + response = await authenticated_client.get("/api/auth/status") + + assert response.status_code == 200 + data = response.json() + assert data["configured"] is True + assert data["authenticated"] is True + + async def test_login_returns_jwt_token(self, client): + """Test login returns JWT token in format expected by app.js.""" + # Setup + await client.post( + "/api/auth/setup", + json={"master_password": "StrongP@ss123"} + ) + + # Login + response = await client.post( + "/api/auth/login", + json={"password": "StrongP@ss123"} + ) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert "token_type" in data + assert data["token_type"] == "bearer" + assert isinstance(data["access_token"], str) + assert len(data["access_token"]) > 0 + + async def test_logout_endpoint(self, authenticated_client): + """Test logout endpoint clears authentication.""" + response = await authenticated_client.post("/api/auth/logout") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert data["message"] == "Logged out successfully" + + async def test_unauthorized_request_returns_401(self, client): + """Test that requests without token return 401.""" + # Setup auth + await client.post( + "/api/auth/setup", + json={"master_password": "StrongP@ss123"} + ) + + # Try to access protected endpoint without token + response = await client.get("/api/v1/anime") + + assert response.status_code == 401 + + async def test_authenticated_request_succeeds(self, authenticated_client): + """Test that requests with valid token succeed.""" + with patch( + "src.server.services.anime_service.AnimeService" + ) as mock_service: + mock_instance = AsyncMock() + mock_instance.get_all_series = AsyncMock(return_value=[]) + mock_service.return_value = mock_instance + + response = await authenticated_client.get("/api/v1/anime") + + assert response.status_code == 200 + + +class TestFrontendAnimeAPI: + """Test anime API endpoints as used by app.js.""" + + async def test_get_anime_list(self, authenticated_client): + """Test GET /api/v1/anime returns anime list in expected format.""" + with patch("src.server.api.anime.get_anime_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.get_all_series = AsyncMock(return_value=[ + { + "id": "test_anime_1", + "name": "Test Anime 1", + "folder": "/path/to/anime1", + "missing_episodes": 5, + "total_episodes": 12, + "seasons": [{"season": 1, "episodes": [1, 2, 3]}] + } + ]) + mock_get_service.return_value = mock_service + + response = await authenticated_client.get("/api/v1/anime") + + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == 1 + assert data[0]["name"] == "Test Anime 1" + assert data[0]["missing_episodes"] == 5 + + async def test_search_anime(self, authenticated_client): + """Test POST /api/v1/anime/search returns search results.""" + with patch("src.server.api.anime.get_anime_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.search_series = AsyncMock(return_value=[ + { + "id": "naruto", + "name": "Naruto", + "url": "https://example.com/naruto" + } + ]) + mock_get_service.return_value = mock_service + + response = await authenticated_client.post( + "/api/v1/anime/search", + json={"query": "naruto"} + ) + + assert response.status_code == 200 + data = response.json() + assert "results" in data + assert len(data["results"]) == 1 + assert data["results"][0]["name"] == "Naruto" + + async def test_rescan_anime(self, authenticated_client): + """Test POST /api/v1/anime/rescan triggers rescan.""" + with patch("src.server.api.anime.get_anime_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.rescan_series = AsyncMock() + mock_get_service.return_value = mock_service + + response = await authenticated_client.post("/api/v1/anime/rescan") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + +class TestFrontendDownloadAPI: + """Test download API endpoints as used by app.js and queue.js.""" + + async def test_add_to_download_queue(self, authenticated_client): + """Test adding episodes to download queue.""" + with patch("src.server.api.download.get_download_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.add_to_queue = AsyncMock(return_value=["item_123"]) + mock_get_service.return_value = mock_service + + response = await authenticated_client.post( + "/api/download", + json={ + "serie_id": "test_anime", + "serie_name": "Test Anime", + "episodes": [{"season": 1, "episode": 1}], + "priority": "normal" + } + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "added_ids" in data + + async def test_get_queue_status(self, authenticated_client): + """Test GET /api/v1/download/queue returns queue status.""" + with patch("src.server.api.download.get_download_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.get_queue_status = AsyncMock(return_value={ + "total_items": 5, + "pending_items": 3, + "downloading_items": 1, + "completed_items": 1, + "failed_items": 0, + "is_downloading": True, + "is_paused": False, + "current_download": { + "id": "item_1", + "serie_name": "Test Anime", + "episode": {"season": 1, "episode": 1}, + "progress": 0.45 + }, + "queue": [] + }) + mock_get_service.return_value = mock_service + + response = await authenticated_client.get("/api/v1/download/queue") + + assert response.status_code == 200 + data = response.json() + assert data["total_items"] == 5 + assert data["is_downloading"] is True + assert "current_download" in data + + async def test_start_download_queue(self, authenticated_client): + """Test POST /api/v1/download/start starts queue.""" + with patch("src.server.api.download.get_download_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.start_downloads = AsyncMock() + mock_get_service.return_value = mock_service + + response = await authenticated_client.post("/api/v1/download/start") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + async def test_pause_download_queue(self, authenticated_client): + """Test POST /api/v1/download/pause pauses queue.""" + with patch("src.server.api.download.get_download_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.pause_downloads = AsyncMock() + mock_get_service.return_value = mock_service + + response = await authenticated_client.post("/api/v1/download/pause") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + async def test_stop_download_queue(self, authenticated_client): + """Test POST /api/v1/download/stop stops queue.""" + with patch("src.server.api.download.get_download_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.stop_downloads = AsyncMock() + mock_get_service.return_value = mock_service + + response = await authenticated_client.post("/api/v1/download/stop") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + +class TestFrontendWebSocketIntegration: + """Test WebSocket integration as used by websocket_client.js.""" + + async def test_websocket_connection(self, authenticated_client): + """Test WebSocket connection establishment.""" + # Get token from authenticated client + token = authenticated_client.headers.get("Authorization", "").replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + # Should receive connection confirmation + message = await websocket.receive_json() + assert message["type"] == "connection" + assert message["data"]["status"] == "connected" + + async def test_websocket_receives_queue_updates(self, authenticated_client): + """Test WebSocket receives queue status updates.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + # Receive connection message + await websocket.receive_json() + + # Simulate queue update broadcast using service method + ws_service = get_websocket_service() + await ws_service.broadcast_queue_status({ + "action": "items_added", + "total_items": 1, + "added_ids": ["item_123"] + }) + + # Should receive the broadcast + message = await websocket.receive_json() + assert message["type"] == "queue_status" + assert message["data"]["action"] == "items_added" + + async def test_websocket_receives_download_progress( + self, authenticated_client + ): + """Test WebSocket receives download progress updates.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + # Receive connection message + await websocket.receive_json() + + # Simulate progress update using service method + progress_data = { + "serie_name": "Test Anime", + "episode": {"season": 1, "episode": 1}, + "progress": 0.5, + "speed": "2.5 MB/s", + "eta": "00:02:30" + } + + ws_service = get_websocket_service() + await ws_service.broadcast_download_progress( + "item_123", progress_data + ) + + # Should receive progress update + message = await websocket.receive_json() + assert message["type"] == "download_progress" + assert message["data"]["progress"] == 0.5 + + +class TestFrontendConfigAPI: + """Test configuration API endpoints as used by app.js.""" + + async def test_get_config(self, authenticated_client): + """Test GET /api/config returns configuration.""" + response = await authenticated_client.get("/api/config") + + assert response.status_code == 200 + data = response.json() + assert "anime_directory" in data or "config" in data + + async def test_update_config(self, authenticated_client): + """Test POST /api/config updates configuration.""" + with patch( + "src.server.api.config.get_config_service" + ) as mock_get_service: + mock_service = Mock() + mock_service.update_config = Mock() + mock_get_service.return_value = mock_service + + response = await authenticated_client.post( + "/api/config", + json={"anime_directory": "/new/path"} + ) + + # Should accept the request + assert response.status_code in [200, 400] + + +class TestFrontendJavaScriptIntegration: + """Test JavaScript functionality integration with backend.""" + + async def test_makeAuthenticatedRequest_bearer_token( + self, authenticated_client + ): + """Test frontend's makeAuthenticatedRequest pattern works.""" + # Simulate what app.js does: include Bearer token header + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + response = await authenticated_client.get( + "/api/v1/anime", + headers={"Authorization": f"Bearer {token}"} + ) + + # Should work with properly formatted token + assert response.status_code == 200 + + async def test_frontend_handles_401_gracefully(self, client): + """Test that 401 responses can be detected by frontend.""" + # Setup auth + await client.post( + "/api/auth/setup", + json={"master_password": "StrongP@ss123"} + ) + + # Try accessing protected endpoint without token + response = await client.get("/api/v1/anime") + + assert response.status_code == 401 + # Frontend JavaScript checks for 401 and redirects to login + + async def test_queue_operations_compatibility(self, authenticated_client): + """Test queue operations match queue.js expectations.""" + with patch( + "src.server.api.download.get_download_service" + ) as mock_get_service: + mock_service = AsyncMock() + + # Test start + mock_service.start_downloads = AsyncMock() + mock_get_service.return_value = mock_service + response = await authenticated_client.post( + "/api/v1/download/start" + ) + assert response.status_code == 200 + + # Test pause + mock_service.pause_downloads = AsyncMock() + response = await authenticated_client.post( + "/api/v1/download/pause" + ) + assert response.status_code == 200 + + # Test stop + mock_service.stop_downloads = AsyncMock() + response = await authenticated_client.post( + "/api/v1/download/stop" + ) + assert response.status_code == 200 + + +class TestFrontendErrorHandling: + """Test error handling as expected by frontend JavaScript.""" + + async def test_api_error_returns_json(self, authenticated_client): + """Test that API errors return JSON format expected by frontend.""" + with patch("src.server.api.anime.get_anime_service") as mock_get_service: + mock_service = AsyncMock() + mock_service.search_series = AsyncMock( + side_effect=Exception("Search failed") + ) + mock_get_service.return_value = mock_service + + response = await authenticated_client.post( + "/api/v1/anime/search", + json={"query": "test"} + ) + + # Should return error in JSON format + assert response.headers.get("content-type", "").startswith("application/json") + + async def test_validation_error_returns_400(self, authenticated_client): + """Test that validation errors return 400 with details.""" + # Send invalid data + response = await authenticated_client.post( + "/api/download", + json={"invalid": "data"} + ) + + # Should return 400 or 422 (validation error) + assert response.status_code in [400, 422] + + +class TestFrontendRealTimeUpdates: + """Test real-time update scenarios as used by frontend.""" + + async def test_download_started_notification(self, authenticated_client): + """Test that download_started events are broadcasted.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + # Clear connection message + await websocket.receive_json() + + # Simulate download started broadcast using system message + ws_service = get_websocket_service() + await ws_service.broadcast_system_message("download_started", { + "item_id": "item_123", + "serie_name": "Test Anime" + }) + + message = await websocket.receive_json() + assert message["type"] == "system_download_started" + + async def test_download_completed_notification(self, authenticated_client): + """Test that download_completed events are broadcasted.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + # Clear connection message + await websocket.receive_json() + + # Simulate download completed broadcast + ws_service = get_websocket_service() + await ws_service.broadcast_download_complete("item_123", { + "serie_name": "Test Anime", + "episode": {"season": 1, "episode": 1} + }) + + message = await websocket.receive_json() + assert message["type"] == "download_complete" + + async def test_multiple_clients_receive_broadcasts( + self, authenticated_client + ): + """Test that multiple WebSocket clients receive broadcasts.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + # Create two WebSocket connections + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as ws1: + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as ws2: + # Clear connection messages + await ws1.receive_json() + await ws2.receive_json() + + # Broadcast to all using system message + ws_service = get_websocket_service() + await ws_service.broadcast_system_message( + "test_event", {"message": "hello"} + ) + + # Both should receive it + msg1 = await ws1.receive_json() + msg2 = await ws2.receive_json() + + assert msg1["type"] == "system_test_event" + assert msg2["type"] == "system_test_event" + assert msg1["data"]["message"] == "hello" + assert msg2["data"]["message"] == "hello" + + +class TestFrontendDataFormats: + """Test that backend returns data in formats expected by frontend.""" + + async def test_anime_list_format(self, authenticated_client): + """Test anime list has required fields for frontend rendering.""" + with patch( + "src.server.api.anime.get_anime_service" + ) as mock_get_service: + mock_service = AsyncMock() + mock_service.get_all_series = AsyncMock(return_value=[ + { + "id": "test_1", + "name": "Test Anime", + "folder": "/path/to/anime", + "missing_episodes": 5, + "total_episodes": 12, + "seasons": [{"season": 1, "episodes": [1, 2, 3]}] + } + ]) + mock_get_service.return_value = mock_service + + response = await authenticated_client.get("/api/v1/anime") + data = response.json() + + # Frontend expects these fields + anime = data[0] + assert "id" in anime + assert "name" in anime + assert "missing_episodes" in anime + assert isinstance(anime["missing_episodes"], int) + + async def test_queue_status_format(self, authenticated_client): + """Test queue status has required fields for queue.js.""" + with patch( + "src.server.api.download.get_download_service" + ) as mock_get_service: + mock_service = AsyncMock() + mock_service.get_queue_status = AsyncMock(return_value={ + "total_items": 5, + "pending_items": 3, + "downloading_items": 1, + "is_downloading": True, + "is_paused": False, + "queue": [ + { + "id": "item_1", + "serie_name": "Test", + "episode": {"season": 1, "episode": 1}, + "status": "pending" + } + ] + }) + mock_get_service.return_value = mock_service + + response = await authenticated_client.get( + "/api/v1/download/queue" + ) + data = response.json() + + # Frontend expects these fields + assert "total_items" in data + assert "is_downloading" in data + assert "queue" in data + assert isinstance(data["queue"], list) + + async def test_websocket_message_format(self, authenticated_client): + """Test WebSocket messages match websocket_client.js expectations.""" + token = authenticated_client.headers.get( + "Authorization", "" + ).replace("Bearer ", "") + + async with authenticated_client.websocket_connect( + f"/ws/connect?token={token}" + ) as websocket: + message = await websocket.receive_json() + + # WebSocket client expects type and data fields + assert "type" in message + assert "data" in message + assert isinstance(message["data"], dict)