diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index 0ffc74a5..d9278e4e 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -13,7 +13,7 @@ import uuid import asyncio from pathlib import Path -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse def parse_json_fields(rows: List[Dict], fields: List[str]) -> List[Dict]: """Helper to parse stringified JSON fields from SQLite.""" @@ -899,19 +899,18 @@ async def list_tasks( next_page = page + 1 if page < total_pages else None prev_page = page - 1 if page > 1 else None - # Function to build URL with all query parameters def build_page_url(page_num): if page_num is None: return None - # Start with page and per_page - params_list = [f"page={page_num}", f"per_page={per_page}"] - # Add filters if they exist + query_params = { + "page": page_num, + "per_page": per_page, + } if plugin_id: - params_list.append(f"plugin_id={plugin_id}") + query_params["plugin_id"] = plugin_id if status: - params_list.append(f"status={status}") - # Join with & and return - return f"/api/v1/tasks?{'&'.join(params_list)}" + query_params["status"] = status + return f"/api/v1/tasks?{urlencode(query_params)}" return { "tasks": tasks_list, "pagination": { diff --git a/testing/backend/test_task_pagination.py b/testing/backend/test_task_pagination.py index 000de760..e62b3718 100644 --- a/testing/backend/test_task_pagination.py +++ b/testing/backend/test_task_pagination.py @@ -2,7 +2,31 @@ Tests for pagination metadata in tasks list endpoint. """ +import asyncio + import pytest +from backend.secuscan.database import get_db + + +async def _insert_task(task_id, plugin_id, status, created_at): + db = await get_db() + await db.execute( + """ + INSERT INTO tasks ( + id, plugin_id, tool_name, target, status, created_at, inputs_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + task_id, + plugin_id, + "Test Scanner", + "example.com", + status, + created_at, + "{}", + ), + ) class TestTasksPagination: @@ -79,3 +103,31 @@ def test_next_url_preserves_filters(self, test_client): assert "per_page=5" in next_url assert "status=completed" in next_url assert "plugin_id=nmap" in next_url + + def test_next_url_encodes_filtered_pagination_params(self, test_client): + """Test that filtered pagination links URL-encode query values.""" + plugin_id = "web scanner/alpha" + status = "queued & reviewed" + asyncio.run( + _insert_task("encoded-filter-1", plugin_id, status, "2026-06-02T10:00:00") + ) + asyncio.run( + _insert_task("encoded-filter-2", plugin_id, status, "2026-06-02T09:00:00") + ) + + response = test_client.get( + "/api/v1/tasks", + params={ + "page": 1, + "per_page": 1, + "plugin_id": plugin_id, + "status": status, + }, + ) + assert response.status_code == 200 + + next_url = response.json()["pagination"]["next"] + assert next_url == ( + "/api/v1/tasks?page=2&per_page=1&" + "plugin_id=web+scanner%2Falpha&status=queued+%26+reviewed" + )