"""Tests for queue management features. This module tests the queue page functionality including: - Display of queued items in organized lists - Drag-and-drop reordering - Starting and stopping queue processing - Filtering completed and failed downloads """ import pytest from httpx import ASGITransport, AsyncClient from src.server.fastapi_app import app @pytest.fixture async def client(): """Create an async test client.""" transport = ASGITransport(app=app) async with AsyncClient( transport=transport, base_url="http://test" ) as client: yield client @pytest.fixture async def auth_headers(client: AsyncClient): """Get authentication headers with valid JWT token.""" # Setup auth await client.post( "/api/auth/setup", json={"master_password": "TestPass123!"} ) # Login response = await client.post( "/api/auth/login", json={"password": "TestPass123!"} ) data = response.json() token = data["access_token"] return {"Authorization": f"Bearer {token}"} @pytest.fixture def sample_download_request(): """Sample download request for testing.""" return { "serie_id": "test-series", "serie_name": "Test Series", "episodes": [ {"season": 1, "episode": 1}, {"season": 1, "episode": 2} ], "priority": "normal" } class TestQueueDisplay: """Test queue display and organization.""" @pytest.mark.asyncio async def test_queue_status_includes_all_sections( self, client: AsyncClient, auth_headers: dict ): """Test queue status includes all sections.""" response = await client.get( "/api/queue/status", headers=auth_headers ) assert response.status_code == 200 data = response.json() # Verify top-level structure assert "status" in data assert "statistics" in data # Verify status nested structure status = data["status"] assert "active_downloads" in status assert "pending_queue" in status assert "completed_downloads" in status assert "failed_downloads" in status assert "is_running" in status assert "is_paused" in status @pytest.mark.asyncio async def test_queue_items_have_required_fields( self, client: AsyncClient, auth_headers: dict, sample_download_request: dict ): """Test queue items have required display fields.""" # Add an item to the queue add_response = await client.post( "/api/queue/add", json=sample_download_request, headers=auth_headers ) assert add_response.status_code == 201 # Get queue status response = await client.get( "/api/queue/status", headers=auth_headers ) assert response.status_code == 200 data = response.json() # Updated for nested status structure pending = data["status"]["pending_queue"] assert len(pending) > 0 item = pending[0] # Verify required fields for display assert "id" in item assert "serie_name" in item assert "episode" in item assert "priority" in item assert "added_at" in item # Verify episode structure episode = item["episode"] assert "season" in episode assert "episode" in episode class TestQueueReordering: """Test queue reordering functionality.""" @pytest.mark.asyncio async def test_reorder_queue_with_item_ids( self, client: AsyncClient, auth_headers: dict ): """Test reordering queue using item_ids array.""" # Clear existing queue first status_response = await client.get( "/api/queue/status", headers=auth_headers ) existing_items = [ item["id"] for item in status_response.json()["status"]["pending_queue"] ] if existing_items: await client.request( "DELETE", "/api/queue/", json={"item_ids": existing_items}, headers=auth_headers ) # Add exactly 3 items added_ids = [] for i in range(3): response = await client.post( "/api/queue/add", json={ "serie_id": f"test-{i}", "serie_name": f"Test Series {i}", "episodes": [{"season": 1, "episode": i+1}], "priority": "normal" }, headers=auth_headers ) if response.status_code == 201: data = response.json() if "added_items" in data and data["added_items"]: added_ids.extend(data["added_items"]) assert len(added_ids) == 3, f"Expected 3 items, got {len(added_ids)}" # Reverse the order new_order = list(reversed(added_ids)) # Reorder reorder_response = await client.post( "/api/queue/reorder", json={"item_ids": new_order}, headers=auth_headers ) assert reorder_response.status_code == 200 assert reorder_response.json()["status"] == "success" # Verify new order status_response = await client.get( "/api/queue/status", headers=auth_headers ) current_order = [ item["id"] for item in status_response.json()["status"]["pending_queue"] ] assert current_order == new_order @pytest.mark.asyncio async def test_reorder_with_invalid_ids( self, client: AsyncClient, auth_headers: dict ): """Test reordering with non-existent IDs succeeds (idempotent).""" response = await client.post( "/api/queue/reorder", json={"item_ids": ["invalid-id-1", "invalid-id-2"]}, headers=auth_headers ) # Bulk reorder is idempotent and succeeds even with invalid IDs # It just ignores items that don't exist assert response.status_code == 200 @pytest.mark.asyncio async def test_reorder_empty_list( self, client: AsyncClient, auth_headers: dict ): """Test reordering with empty list.""" response = await client.post( "/api/queue/reorder", json={"item_ids": []}, headers=auth_headers ) # Should succeed but do nothing assert response.status_code in [200, 404] class TestQueueControl: """Test queue start/stop functionality.""" @pytest.mark.asyncio async def test_start_queue( self, client: AsyncClient, auth_headers: dict ): """Test starting the download queue.""" response = await client.post( "/api/queue/start", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["status"] == "success" @pytest.mark.asyncio async def test_stop_queue( self, client: AsyncClient, auth_headers: dict ): """Test stopping the download queue.""" # Start first await client.post("/api/queue/start", headers=auth_headers) # Then stop response = await client.post( "/api/queue/stop", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["status"] == "success" @pytest.mark.asyncio async def test_queue_status_reflects_running_state( self, client: AsyncClient, auth_headers: dict ): """Test queue status reflects running state.""" # Initially not running status = await client.get( "/api/queue/status", headers=auth_headers ) assert status.json()["status"]["is_running"] is False # Start queue await client.post("/api/queue/start", headers=auth_headers) # Should be running status = await client.get( "/api/queue/status", headers=auth_headers ) assert status.json()["status"]["is_running"] is True # Stop queue await client.post("/api/queue/stop", headers=auth_headers) # Should not be running status = await client.get( "/api/queue/status", headers=auth_headers ) assert status.json()["status"]["is_running"] is False class TestCompletedDownloads: """Test completed downloads management.""" @pytest.mark.asyncio async def test_clear_completed_downloads( self, client: AsyncClient, auth_headers: dict ): """Test clearing completed downloads.""" response = await client.delete( "/api/queue/completed", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "count" in data assert data["status"] == "success" @pytest.mark.asyncio async def test_completed_section_count( self, client: AsyncClient, auth_headers: dict ): """Test that completed count is accurate.""" status = await client.get( "/api/queue/status", headers=auth_headers ) data = status.json() completed_count = data["statistics"]["completed_count"] completed_list = len(data["status"]["completed_downloads"]) # Count should match list length assert completed_count == completed_list class TestFailedDownloads: """Test failed downloads management.""" @pytest.mark.asyncio async def test_clear_failed_downloads( self, client: AsyncClient, auth_headers: dict ): """Test clearing failed downloads.""" response = await client.delete( "/api/queue/failed", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "count" in data assert data["status"] == "success" @pytest.mark.asyncio async def test_retry_failed_downloads( self, client: AsyncClient, auth_headers: dict ): """Test retrying failed downloads.""" response = await client.post( "/api/queue/retry", json={"item_ids": []}, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "retried_count" in data assert data["status"] == "success" @pytest.mark.asyncio async def test_retry_specific_failed_download( self, client: AsyncClient, auth_headers: dict ): """Test retrying a specific failed download.""" # Test the endpoint accepts the format response = await client.post( "/api/queue/retry", json={"item_ids": ["some-id"]}, headers=auth_headers ) # Should succeed even if ID doesn't exist (idempotent) assert response.status_code == 200 @pytest.mark.asyncio async def test_failed_section_count( self, client: AsyncClient, auth_headers: dict ): """Test that failed count is accurate.""" status = await client.get( "/api/queue/status", headers=auth_headers ) data = status.json() failed_count = data["statistics"]["failed_count"] failed_list = len(data["status"]["failed_downloads"]) # Count should match list length assert failed_count == failed_list class TestBulkOperations: """Test bulk queue operations.""" @pytest.mark.asyncio async def test_remove_multiple_items( self, client: AsyncClient, auth_headers: dict ): """Test removing multiple items from queue.""" # Add multiple items item_ids = [] for i in range(3): add_response = await client.post( "/api/queue/add", json={ "serie_id": f"bulk-test-{i}", "serie_name": f"Bulk Test {i}", "episodes": [{"season": 1, "episode": i+1}], "priority": "normal" }, headers=auth_headers ) if add_response.status_code == 201: data = add_response.json() if "added_items" in data and len(data["added_items"]) > 0: item_ids.append(data["added_items"][0]) # Remove all at once if item_ids: response = await client.request( "DELETE", "/api/queue/", json={"item_ids": item_ids}, headers=auth_headers ) assert response.status_code == 204 @pytest.mark.asyncio async def test_clear_entire_pending_queue( self, client: AsyncClient, auth_headers: dict ): """Test clearing entire pending queue.""" # Get all pending items status = await client.get( "/api/queue/status", headers=auth_headers ) pending = status.json()["status"]["pending_queue"] if pending: item_ids = [item["id"] for item in pending] # Remove all response = await client.request( "DELETE", "/api/queue/", json={"item_ids": item_ids}, headers=auth_headers ) assert response.status_code == 204 # Verify queue is empty status = await client.get( "/api/queue/status", headers=auth_headers ) assert len(status.json()["status"]["pending_queue"]) == 0