download re implemented

This commit is contained in:
2025-10-30 22:06:41 +01:00
parent 6ebc2ed2ea
commit 3be175522f
16 changed files with 359 additions and 1335 deletions

View File

@@ -92,14 +92,9 @@ def mock_download_service():
# Mock remove_from_queue
service.remove_from_queue = AsyncMock(return_value=["item-id-1"])
# Mock reorder_queue
service.reorder_queue = AsyncMock(return_value=True)
# Mock start/stop/pause/resume
service.start = AsyncMock()
service.stop = AsyncMock()
service.pause_queue = AsyncMock()
service.resume_queue = AsyncMock()
# Mock start/stop
service.start_next_download = AsyncMock(return_value="item-id-1")
service.stop_downloads = AsyncMock()
# Mock clear_completed and retry_failed
service.clear_completed = AsyncMock(return_value=5)
@@ -259,54 +254,56 @@ async def test_remove_from_queue_not_found(
@pytest.mark.asyncio
async def test_remove_multiple_from_queue(
async def test_start_download_success(
authenticated_client, mock_download_service
):
"""Test DELETE /api/queue/ with multiple items."""
request_data = {"item_ids": ["item-id-1", "item-id-2"]}
response = await authenticated_client.request(
"DELETE", "/api/queue/", json=request_data
)
assert response.status_code == 204
mock_download_service.remove_from_queue.assert_called_once_with(
["item-id-1", "item-id-2"]
)
@pytest.mark.asyncio
async def test_remove_multiple_empty_list(
authenticated_client, mock_download_service
):
"""Test removing with empty item list returns 400."""
request_data = {"item_ids": []}
response = await authenticated_client.request(
"DELETE", "/api/queue/", json=request_data
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_start_queue(authenticated_client, mock_download_service):
"""Test POST /api/queue/start endpoint."""
"""Test POST /api/queue/start starts first pending download."""
response = await authenticated_client.post("/api/queue/start")
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert "started" in data["message"].lower()
assert "item_id" in data
assert data["item_id"] == "item-id-1"
mock_download_service.start.assert_called_once()
mock_download_service.start_next_download.assert_called_once()
@pytest.mark.asyncio
async def test_stop_queue(authenticated_client, mock_download_service):
"""Test POST /api/queue/stop endpoint."""
async def test_start_download_empty_queue(
authenticated_client, mock_download_service
):
"""Test starting download with empty queue returns 400."""
mock_download_service.start_next_download.return_value = None
response = await authenticated_client.post("/api/queue/start")
assert response.status_code == 400
data = response.json()
detail = data["detail"].lower()
assert "empty" in detail or "no pending" in detail
@pytest.mark.asyncio
async def test_start_download_already_active(
authenticated_client, mock_download_service
):
"""Test starting download while one is active returns 400."""
mock_download_service.start_next_download.side_effect = (
DownloadServiceError("A download is already in progress")
)
response = await authenticated_client.post("/api/queue/start")
assert response.status_code == 400
data = response.json()
assert "already" in data["detail"].lower()
@pytest.mark.asyncio
async def test_stop_downloads(authenticated_client, mock_download_service):
"""Test POST /api/queue/stop stops queue processing."""
response = await authenticated_client.post("/api/queue/stop")
assert response.status_code == 200
@@ -315,70 +312,7 @@ async def test_stop_queue(authenticated_client, mock_download_service):
assert data["status"] == "success"
assert "stopped" in data["message"].lower()
mock_download_service.stop.assert_called_once()
@pytest.mark.asyncio
async def test_pause_queue(authenticated_client, mock_download_service):
"""Test POST /api/queue/pause endpoint."""
response = await authenticated_client.post("/api/queue/pause")
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert "paused" in data["message"].lower()
mock_download_service.pause_queue.assert_called_once()
@pytest.mark.asyncio
async def test_resume_queue(authenticated_client, mock_download_service):
"""Test POST /api/queue/resume endpoint."""
response = await authenticated_client.post("/api/queue/resume")
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert "resumed" in data["message"].lower()
mock_download_service.resume_queue.assert_called_once()
@pytest.mark.asyncio
async def test_reorder_queue(authenticated_client, mock_download_service):
"""Test POST /api/queue/reorder endpoint."""
request_data = {"item_id": "item-id-1", "new_position": 0}
response = await authenticated_client.post(
"/api/queue/reorder", json=request_data
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
mock_download_service.reorder_queue.assert_called_once_with(
item_id="item-id-1", new_position=0
)
@pytest.mark.asyncio
async def test_reorder_queue_not_found(
authenticated_client, mock_download_service
):
"""Test reordering non-existent item returns 404."""
mock_download_service.reorder_queue.return_value = False
request_data = {"item_id": "non-existent", "new_position": 0}
response = await authenticated_client.post(
"/api/queue/reorder", json=request_data
)
assert response.status_code == 404
mock_download_service.stop_downloads.assert_called_once()
@pytest.mark.asyncio
@@ -444,8 +378,6 @@ async def test_queue_endpoints_require_auth(mock_download_service):
("DELETE", "/api/queue/item-1"),
("POST", "/api/queue/start"),
("POST", "/api/queue/stop"),
("POST", "/api/queue/pause"),
("POST", "/api/queue/resume"),
]
for method, url in endpoints:
@@ -456,7 +388,8 @@ async def test_queue_endpoints_require_auth(mock_download_service):
elif method == "DELETE":
response = await client.delete(url)
# Should return 401 or 503 (503 if service not available)
# Should return 401 or 503 (503 if service unavailable)
assert response.status_code in (401, 503), (
f"{method} {url} should require auth, got {response.status_code}"
f"{method} {url} should require auth, "
f"got {response.status_code}"
)

View File

@@ -247,23 +247,17 @@ class TestFrontendDownloadAPI:
assert "status" in data or "statistics" in data
async def test_start_download_queue(self, authenticated_client):
"""Test POST /api/queue/start starts queue."""
"""Test POST /api/queue/start starts next download."""
response = await authenticated_client.post("/api/queue/start")
assert response.status_code == 200
# Should return 200 with item_id, or 400 if queue is empty
assert response.status_code in [200, 400]
data = response.json()
assert "message" in data or "status" in data
async def test_pause_download_queue(self, authenticated_client):
"""Test POST /api/queue/pause pauses queue."""
response = await authenticated_client.post("/api/queue/pause")
assert response.status_code == 200
data = response.json()
assert "message" in data or "status" in data
if response.status_code == 200:
assert "item_id" in data
async def test_stop_download_queue(self, authenticated_client):
"""Test POST /api/queue/stop stops queue."""
"""Test POST /api/queue/stop stops processing new downloads."""
response = await authenticated_client.post("/api/queue/stop")
assert response.status_code == 200

View File

@@ -224,35 +224,6 @@ class TestQueueControlOperations:
data = response.json()
assert data["status"] == "success"
async def test_pause_queue_processing(self, authenticated_client):
"""Test pausing the queue processor."""
# Start first
await authenticated_client.post("/api/queue/start")
# Then pause
response = await authenticated_client.post("/api/queue/pause")
assert response.status_code in [200, 503]
if response.status_code == 200:
data = response.json()
assert data["status"] == "success"
async def test_resume_queue_processing(self, authenticated_client):
"""Test resuming the queue processor."""
# Start and pause first
await authenticated_client.post("/api/queue/start")
await authenticated_client.post("/api/queue/pause")
# Then resume
response = await authenticated_client.post("/api/queue/resume")
assert response.status_code in [200, 503]
if response.status_code == 200:
data = response.json()
assert data["status"] == "success"
async def test_clear_completed_downloads(self, authenticated_client):
"""Test clearing completed downloads from the queue."""
response = await authenticated_client.delete("/api/queue/completed")
@@ -294,36 +265,9 @@ class TestQueueItemOperations:
# For now, test the endpoint with a dummy ID
response = await authenticated_client.post("/api/queue/items/dummy-id/retry")
# Should return 404 if item doesn't exist, or 503 if service unavailable
# Should return 404 if item doesn't exist, or 503 if unavailable
assert response.status_code in [200, 404, 503]
async def test_reorder_queue_items(self, authenticated_client):
"""Test reordering queue items."""
# Add multiple items
item_ids = []
for i in range(3):
add_response = await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": f"series-{i}",
"serie_name": f"Series {i}",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
if add_response.status_code == 201:
item_ids.extend(add_response.json()["item_ids"])
if len(item_ids) >= 2:
# Reorder items
response = await authenticated_client.post(
"/api/queue/reorder",
json={"item_order": list(reversed(item_ids))}
)
assert response.status_code in [200, 503]
class TestDownloadProgressTracking:
"""Test progress tracking during downloads."""
@@ -598,33 +542,7 @@ class TestCompleteDownloadWorkflow:
assert progress_response.status_code in [200, 503]
# 5. Verify final state (completed or still processing)
final_response = await authenticated_client.get("/api/queue/status")
final_response = await authenticated_client.get(
"/api/queue/status"
)
assert final_response.status_code in [200, 503]
async def test_workflow_with_pause_and_resume(self, authenticated_client):
"""Test download workflow with pause and resume."""
# Add items
await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": "pause-test",
"serie_name": "Pause Test Series",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
# Start processing
await authenticated_client.post("/api/queue/control/start")
# Pause
pause_response = await authenticated_client.post("/api/queue/control/pause")
assert pause_response.status_code in [200, 503]
# Resume
resume_response = await authenticated_client.post("/api/queue/control/resume")
assert resume_response.status_code in [200, 503]
# Verify queue status
status_response = await authenticated_client.get("/api/queue/status")
assert status_response.status_code in [200, 503]

View File

@@ -60,7 +60,6 @@ async def download_service(anime_service, progress_service):
"""Create a DownloadService with dependencies."""
service = DownloadService(
anime_service=anime_service,
max_concurrent_downloads=2,
progress_service=progress_service,
persistence_path="/tmp/test_queue.json",
)
@@ -173,40 +172,6 @@ class TestWebSocketDownloadIntegration:
assert stop_broadcast is not None
assert stop_broadcast["data"]["is_running"] is False
@pytest.mark.asyncio
async def test_queue_pause_resume_broadcast(
self, download_service
):
"""Test that pause/resume operations broadcast updates."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
# Pause queue
await download_service.pause_queue()
# Resume queue
await download_service.resume_queue()
# Find pause/resume broadcasts
pause_broadcast = next(
(b for b in broadcasts if b["type"] == "queue_paused"),
None,
)
resume_broadcast = next(
(b for b in broadcasts if b["type"] == "queue_resumed"),
None,
)
assert pause_broadcast is not None
assert pause_broadcast["data"]["is_paused"] is True
assert resume_broadcast is not None
assert resume_broadcast["data"]["is_paused"] is False
@pytest.mark.asyncio
async def test_clear_completed_broadcast(
self, download_service

View File

@@ -1,7 +1,7 @@
"""Unit tests for the download queue service.
Tests cover queue management, priority handling, persistence,
concurrent downloads, and error scenarios.
Tests cover queue management, manual download control, persistence,
and error scenarios for the simplified download service.
"""
from __future__ import annotations
@@ -42,7 +42,6 @@ def download_service(mock_anime_service, temp_persistence_path):
"""Create a DownloadService instance for testing."""
return DownloadService(
anime_service=mock_anime_service,
max_concurrent_downloads=2,
max_retries=3,
persistence_path=temp_persistence_path,
)
@@ -61,11 +60,10 @@ class TestDownloadServiceInitialization:
)
assert len(service._pending_queue) == 0
assert len(service._active_downloads) == 0
assert service._active_download is None
assert len(service._completed_items) == 0
assert len(service._failed_items) == 0
assert service._is_running is False
assert service._is_paused is False
assert service._is_stopped is True
def test_initialization_loads_persisted_queue(
self, mock_anime_service, temp_persistence_path
@@ -152,29 +150,6 @@ class TestQueueManagement:
assert len(item_ids) == 3
assert len(download_service._pending_queue) == 3
@pytest.mark.asyncio
async def test_add_high_priority_to_front(self, download_service):
"""Test that high priority items are added to front of queue."""
# Add normal priority item
await download_service.add_to_queue(
serie_id="series-1",
serie_name="Test Series",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
# Add high priority item
await download_service.add_to_queue(
serie_id="series-2",
serie_name="Priority Series",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.HIGH,
)
# High priority should be at front
assert download_service._pending_queue[0].serie_id == "series-2"
assert download_service._pending_queue[1].serie_id == "series-1"
@pytest.mark.asyncio
async def test_remove_from_pending_queue(self, download_service):
"""Test removing items from pending queue."""
@@ -191,32 +166,108 @@ class TestQueueManagement:
assert len(download_service._pending_queue) == 0
@pytest.mark.asyncio
async def test_reorder_queue(self, download_service):
"""Test reordering items in queue."""
# Add three items
async def test_start_next_download(self, download_service):
"""Test starting the next download from queue."""
# Add items to queue
item_ids = await download_service.add_to_queue(
serie_id="series-1",
serie_name="Test Series",
episodes=[
EpisodeIdentifier(season=1, episode=1),
EpisodeIdentifier(season=1, episode=2),
],
)
# Start next download
started_id = await download_service.start_next_download()
assert started_id is not None
assert started_id == item_ids[0]
assert len(download_service._pending_queue) == 1
assert download_service._is_stopped is False
@pytest.mark.asyncio
async def test_start_next_download_empty_queue(self, download_service):
"""Test starting download with empty queue returns None."""
result = await download_service.start_next_download()
assert result is None
@pytest.mark.asyncio
async def test_start_next_download_already_active(
self, download_service, mock_anime_service
):
"""Test that starting download while one is active raises error."""
# Add items and start one
await download_service.add_to_queue(
serie_id="series-1",
serie_name="Series 1",
episodes=[EpisodeIdentifier(season=1, episode=1)],
serie_name="Test Series",
episodes=[
EpisodeIdentifier(season=1, episode=1),
EpisodeIdentifier(season=1, episode=2),
],
)
# Make download slow so it stays active
async def slow_download(**kwargs):
await asyncio.sleep(10)
mock_anime_service.download = AsyncMock(side_effect=slow_download)
# Start first download (will block for 10s in background)
item_id = await download_service.start_next_download()
assert item_id is not None
await asyncio.sleep(0.1) # Let it start processing
# Try to start another - should fail because one is active
with pytest.raises(DownloadServiceError, match="already in progress"):
await download_service.start_next_download()
@pytest.mark.asyncio
async def test_stop_downloads(self, download_service):
"""Test stopping queue processing."""
await download_service.stop_downloads()
assert download_service._is_stopped is True
@pytest.mark.asyncio
async def test_download_completion_moves_to_list(
self, download_service, mock_anime_service
):
"""Test successful download moves item to completed list."""
# Add item
await download_service.add_to_queue(
serie_id="series-2",
serie_name="Series 2",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.add_to_queue(
serie_id="series-3",
serie_name="Series 3",
serie_id="series-1",
serie_name="Test Series",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
# Move last item to position 0
item_to_move = download_service._pending_queue[2].id
success = await download_service.reorder_queue(item_to_move, 0)
# Start and wait for completion
await download_service.start_next_download()
await asyncio.sleep(0.2) # Wait for download to complete
assert success is True
assert download_service._pending_queue[0].id == item_to_move
assert download_service._pending_queue[0].serie_id == "series-3"
assert len(download_service._completed_items) == 1
assert download_service._active_download is None
@pytest.mark.asyncio
async def test_download_failure_moves_to_list(
self, download_service, mock_anime_service
):
"""Test failed download moves item to failed list."""
# Make download fail
mock_anime_service.download = AsyncMock(return_value=False)
# Add item
await download_service.add_to_queue(
serie_id="series-1",
serie_name="Test Series",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
# Start and wait for failure
await download_service.start_next_download()
await asyncio.sleep(0.2) # Wait for download to fail
assert len(download_service._failed_items) == 1
assert download_service._active_download is None
class TestQueueStatus:
@@ -237,6 +288,7 @@ class TestQueueStatus:
status = await download_service.get_queue_status()
# Queue is stopped until start_next_download() is called
assert status.is_running is False
assert status.is_paused is False
assert len(status.pending_queue) == 2
@@ -270,19 +322,6 @@ class TestQueueStatus:
class TestQueueControl:
"""Test queue control operations."""
@pytest.mark.asyncio
async def test_pause_queue(self, download_service):
"""Test pausing the queue."""
await download_service.pause_queue()
assert download_service._is_paused is True
@pytest.mark.asyncio
async def test_resume_queue(self, download_service):
"""Test resuming the queue."""
await download_service.pause_queue()
await download_service.resume_queue()
assert download_service._is_paused is False
@pytest.mark.asyncio
async def test_clear_completed(self, download_service):
"""Test clearing completed downloads."""
@@ -438,33 +477,29 @@ class TestServiceLifecycle:
@pytest.mark.asyncio
async def test_start_service(self, download_service):
"""Test starting the service."""
# start() is now just for initialization/compatibility
await download_service.start()
assert download_service._is_running is True
# No _is_running attribute - simplified service doesn't track this
@pytest.mark.asyncio
async def test_stop_service(self, download_service):
"""Test stopping the service."""
await download_service.start()
await download_service.stop()
assert download_service._is_running is False
# Verifies service can be stopped without errors
# No _is_running attribute in simplified service
@pytest.mark.asyncio
async def test_start_already_running(self, download_service):
"""Test starting service when already running."""
await download_service.start()
await download_service.start() # Should not raise error
assert download_service._is_running is True
# No _is_running attribute in simplified service
class TestErrorHandling:
"""Test error handling in download service."""
@pytest.mark.asyncio
async def test_reorder_nonexistent_item(self, download_service):
"""Test reordering non-existent item raises error."""
with pytest.raises(DownloadServiceError):
await download_service.reorder_queue("nonexistent-id", 0)
@pytest.mark.asyncio
async def test_download_failure_moves_to_failed(self, download_service):
"""Test that download failures are handled correctly."""