feat: implement API versioning /api/v1/
- All backend routers moved to /api/v1/ prefix
- Frontend BASE_URL updated to /api/v1
- Setup redirect middleware updated to redirect to /api/v1/setup
- Health router path fixed: prefix=/api/v1/health, @router.get('')
- conftest.py: set server_status=online for test fixture
- Created Docs/API_VERSIONING.md with deprecation policy
- Updated Docs/Backend-Development.md with versioning section
- Updated Instructions.md curl examples
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -129,11 +129,11 @@ async def bl_client(tmp_path: Path) -> AsyncClient: # type: ignore[misc]
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
resp = await ac.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
resp = await ac.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
assert resp.status_code == 201
|
||||
|
||||
login_resp = await ac.post(
|
||||
"/api/auth/login",
|
||||
"/api/v1/auth/login",
|
||||
json={"password": _SETUP_PAYLOAD["master_password"]},
|
||||
)
|
||||
assert login_resp.status_code == 200
|
||||
@@ -155,13 +155,13 @@ class TestListBlocklists:
|
||||
"app.routers.blocklist.blocklist_service.list_sources",
|
||||
new=AsyncMock(return_value=_make_source_list().sources),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists")
|
||||
resp = await bl_client.get("/api/v1/blocklists")
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_returns_401_unauthenticated(self, client: AsyncClient) -> None:
|
||||
"""Unauthenticated request returns 401."""
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
resp = await client.get("/api/blocklists")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
resp = await client.get("/api/v1/blocklists")
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_response_contains_sources_key(self, bl_client: AsyncClient) -> None:
|
||||
@@ -170,7 +170,7 @@ class TestListBlocklists:
|
||||
"app.routers.blocklist.blocklist_service.list_sources",
|
||||
new=AsyncMock(return_value=[_make_source()]),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists")
|
||||
resp = await bl_client.get("/api/v1/blocklists")
|
||||
body = resp.json()
|
||||
assert "sources" in body
|
||||
assert isinstance(body["sources"], list)
|
||||
@@ -191,7 +191,7 @@ class TestCreateBlocklist:
|
||||
new=AsyncMock(return_value=_make_source()),
|
||||
):
|
||||
resp = await bl_client.post(
|
||||
"/api/blocklists",
|
||||
"/api/v1/blocklists",
|
||||
json={"name": "Test", "url": "https://test.test/", "enabled": True},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
@@ -205,7 +205,7 @@ class TestCreateBlocklist:
|
||||
new=AsyncMock(return_value=_make_source(42)),
|
||||
):
|
||||
resp = await bl_client.post(
|
||||
"/api/blocklists",
|
||||
"/api/v1/blocklists",
|
||||
json={"name": "Test", "url": "https://test.test/", "enabled": True},
|
||||
)
|
||||
assert resp.json()["id"] == 42
|
||||
@@ -226,7 +226,7 @@ class TestUpdateBlocklist:
|
||||
new=AsyncMock(return_value=updated),
|
||||
):
|
||||
resp = await bl_client.put(
|
||||
"/api/blocklists/1",
|
||||
"/api/v1/blocklists/1",
|
||||
json={"enabled": False},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -238,7 +238,7 @@ class TestUpdateBlocklist:
|
||||
new=AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await bl_client.put(
|
||||
"/api/blocklists/999",
|
||||
"/api/v1/blocklists/999",
|
||||
json={"enabled": False},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
@@ -256,7 +256,7 @@ class TestDeleteBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.delete_source",
|
||||
new=AsyncMock(return_value=True),
|
||||
):
|
||||
resp = await bl_client.delete("/api/blocklists/1")
|
||||
resp = await bl_client.delete("/api/v1/blocklists/1")
|
||||
assert resp.status_code == 204
|
||||
|
||||
async def test_delete_returns_404_for_missing(self, bl_client: AsyncClient) -> None:
|
||||
@@ -265,7 +265,7 @@ class TestDeleteBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.delete_source",
|
||||
new=AsyncMock(return_value=False),
|
||||
):
|
||||
resp = await bl_client.delete("/api/blocklists/999")
|
||||
resp = await bl_client.delete("/api/v1/blocklists/999")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ class TestPreviewBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.preview_source",
|
||||
new=AsyncMock(return_value=_make_preview()),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/1/preview")
|
||||
resp = await bl_client.get("/api/v1/blocklists/1/preview")
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_preview_returns_404_for_missing(self, bl_client: AsyncClient) -> None:
|
||||
@@ -293,7 +293,7 @@ class TestPreviewBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.get_source",
|
||||
new=AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/999/preview")
|
||||
resp = await bl_client.get("/api/v1/blocklists/999/preview")
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_preview_returns_502_on_download_error(
|
||||
@@ -307,7 +307,7 @@ class TestPreviewBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.preview_source",
|
||||
new=AsyncMock(side_effect=ValueError("Connection refused")),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/1/preview")
|
||||
resp = await bl_client.get("/api/v1/blocklists/1/preview")
|
||||
assert resp.status_code == 502
|
||||
|
||||
async def test_preview_response_shape(self, bl_client: AsyncClient) -> None:
|
||||
@@ -319,7 +319,7 @@ class TestPreviewBlocklist:
|
||||
"app.routers.blocklist.blocklist_service.preview_source",
|
||||
new=AsyncMock(return_value=_make_preview()),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/1/preview")
|
||||
resp = await bl_client.get("/api/v1/blocklists/1/preview")
|
||||
body = resp.json()
|
||||
assert "entries" in body
|
||||
assert "valid_count" in body
|
||||
@@ -339,7 +339,7 @@ class TestRunImport:
|
||||
"app.routers.blocklist.blocklist_service.import_all",
|
||||
new=AsyncMock(return_value=_make_import_result()),
|
||||
):
|
||||
resp = await bl_client.post("/api/blocklists/import")
|
||||
resp = await bl_client.post("/api/v1/blocklists/import")
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_import_response_shape(self, bl_client: AsyncClient) -> None:
|
||||
@@ -348,7 +348,7 @@ class TestRunImport:
|
||||
"app.routers.blocklist.blocklist_service.import_all",
|
||||
new=AsyncMock(return_value=_make_import_result()),
|
||||
):
|
||||
resp = await bl_client.post("/api/blocklists/import")
|
||||
resp = await bl_client.post("/api/v1/blocklists/import")
|
||||
body = resp.json()
|
||||
assert "total_imported" in body
|
||||
assert "total_skipped" in body
|
||||
@@ -368,7 +368,7 @@ class TestGetSchedule:
|
||||
"app.routers.blocklist.blocklist_service.get_schedule_info_with_runtime",
|
||||
new=AsyncMock(return_value=_make_schedule_info()),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/schedule")
|
||||
resp = await bl_client.get("/api/v1/blocklists/schedule")
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_schedule_response_has_config(self, bl_client: AsyncClient) -> None:
|
||||
@@ -377,7 +377,7 @@ class TestGetSchedule:
|
||||
"app.routers.blocklist.blocklist_service.get_schedule_info_with_runtime",
|
||||
new=AsyncMock(return_value=_make_schedule_info()),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/schedule")
|
||||
resp = await bl_client.get("/api/v1/blocklists/schedule")
|
||||
body = resp.json()
|
||||
assert "config" in body
|
||||
assert "next_run_at" in body
|
||||
@@ -403,7 +403,7 @@ class TestGetSchedule:
|
||||
"app.routers.blocklist.blocklist_service.get_schedule_info_with_runtime",
|
||||
new=AsyncMock(return_value=info_with_errors),
|
||||
):
|
||||
resp = await bl_client.get("/api/blocklists/schedule")
|
||||
resp = await bl_client.get("/api/v1/blocklists/schedule")
|
||||
body = resp.json()
|
||||
assert "last_run_errors" in body
|
||||
assert body["last_run_errors"] is True
|
||||
@@ -433,7 +433,7 @@ class TestUpdateSchedule:
|
||||
new=AsyncMock(return_value=new_info),
|
||||
):
|
||||
resp = await bl_client.put(
|
||||
"/api/blocklists/schedule",
|
||||
"/api/v1/blocklists/schedule",
|
||||
json={
|
||||
"frequency": "hourly",
|
||||
"interval_hours": 12,
|
||||
@@ -453,19 +453,19 @@ class TestUpdateSchedule:
|
||||
class TestImportLog:
|
||||
async def test_log_returns_200(self, bl_client: AsyncClient) -> None:
|
||||
"""GET /api/blocklists/log returns 200."""
|
||||
resp = await bl_client.get("/api/blocklists/log")
|
||||
resp = await bl_client.get("/api/v1/blocklists/log")
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_log_response_shape(self, bl_client: AsyncClient) -> None:
|
||||
"""Log response has items, total, page, page_size."""
|
||||
resp = await bl_client.get("/api/blocklists/log")
|
||||
resp = await bl_client.get("/api/v1/blocklists/log")
|
||||
body = resp.json()
|
||||
for key in ("items", "total", "page", "page_size"):
|
||||
assert key in body
|
||||
|
||||
async def test_log_empty_when_no_runs(self, bl_client: AsyncClient) -> None:
|
||||
"""Log returns empty items list when no import runs have occurred."""
|
||||
resp = await bl_client.get("/api/blocklists/log")
|
||||
resp = await bl_client.get("/api/v1/blocklists/log")
|
||||
body = resp.json()
|
||||
assert body["total"] == 0
|
||||
assert body["items"] == []
|
||||
|
||||
Reference in New Issue
Block a user