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:
@@ -69,12 +69,12 @@ async def dashboard_client(tmp_path: Path) -> AsyncClient: # type: ignore[misc]
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
# Complete setup so the middleware doesn't redirect.
|
||||
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 to get a session cookie.
|
||||
login_resp = await ac.post(
|
||||
"/api/auth/login",
|
||||
"/api/v1/auth/login",
|
||||
json={"password": _SETUP_PAYLOAD["master_password"]},
|
||||
)
|
||||
assert login_resp.status_code == 200
|
||||
@@ -107,11 +107,11 @@ async def offline_dashboard_client(tmp_path: Path) -> AsyncClient: # type: igno
|
||||
|
||||
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
|
||||
@@ -133,7 +133,7 @@ class TestDashboardStatus:
|
||||
self, dashboard_client: AsyncClient
|
||||
) -> None:
|
||||
"""Authenticated request returns HTTP 200."""
|
||||
response = await dashboard_client.get("/api/dashboard/status")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/status")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_returns_401_when_unauthenticated(
|
||||
@@ -141,15 +141,15 @@ class TestDashboardStatus:
|
||||
) -> None:
|
||||
"""Unauthenticated request returns HTTP 401."""
|
||||
# Complete setup so the middleware allows the request through.
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/dashboard/status")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/v1/dashboard/status")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_response_shape_when_online(
|
||||
self, dashboard_client: AsyncClient
|
||||
) -> None:
|
||||
"""Response contains the expected ``status`` object shape."""
|
||||
response = await dashboard_client.get("/api/dashboard/status")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/status")
|
||||
body = response.json()
|
||||
|
||||
assert "status" in body
|
||||
@@ -165,7 +165,7 @@ class TestDashboardStatus:
|
||||
self, dashboard_client: AsyncClient
|
||||
) -> None:
|
||||
"""Endpoint returns the exact values from ``app.state.server_status``."""
|
||||
response = await dashboard_client.get("/api/dashboard/status")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/status")
|
||||
body = response.json()
|
||||
status = body["status"]
|
||||
|
||||
@@ -179,7 +179,7 @@ class TestDashboardStatus:
|
||||
self, offline_dashboard_client: AsyncClient
|
||||
) -> None:
|
||||
"""Endpoint returns online=False when the cache holds an offline snapshot."""
|
||||
response = await offline_dashboard_client.get("/api/dashboard/status")
|
||||
response = await offline_dashboard_client.get("/api/v1/dashboard/status")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
status = body["status"]
|
||||
@@ -195,13 +195,13 @@ class TestDashboardStatus:
|
||||
) -> None:
|
||||
"""Endpoint returns online=False as a safe default if the cache is absent."""
|
||||
# Setup + login so the endpoint is reachable.
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
await client.post(
|
||||
"/api/auth/login",
|
||||
"/api/v1/auth/login",
|
||||
json={"password": _SETUP_PAYLOAD["master_password"]},
|
||||
)
|
||||
# server_status is not set on app.state in the shared `client` fixture.
|
||||
response = await client.get("/api/dashboard/status")
|
||||
response = await client.get("/api/v1/dashboard/status")
|
||||
assert response.status_code == 200
|
||||
status = response.json()["status"]
|
||||
assert status["online"] is False
|
||||
@@ -243,15 +243,15 @@ class TestDashboardBans:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=_make_ban_list_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_returns_401_when_unauthenticated(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Unauthenticated request returns HTTP 401."""
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/dashboard/bans")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/v1/dashboard/bans")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_response_contains_items_and_total(
|
||||
@@ -262,7 +262,7 @@ class TestDashboardBans:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=_make_ban_list_response(3)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
body = response.json()
|
||||
assert "items" in body
|
||||
@@ -274,7 +274,7 @@ class TestDashboardBans:
|
||||
"""If no ``range`` param is provided the default ``24h`` preset is used."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
called_range = mock_list.call_args[0][1]
|
||||
assert called_range == "24h"
|
||||
@@ -285,7 +285,7 @@ class TestDashboardBans:
|
||||
"""The ``range`` query parameter is forwarded to ban_service."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans?range=7d")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans?range=7d")
|
||||
|
||||
called_range = mock_list.call_args[0][1]
|
||||
assert called_range == "7d"
|
||||
@@ -296,7 +296,7 @@ class TestDashboardBans:
|
||||
"""The ``source`` query parameter is forwarded to ban_service."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans?source=archive")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans?source=archive")
|
||||
|
||||
called_source = mock_list.call_args[1]["source"]
|
||||
assert called_source == "archive"
|
||||
@@ -310,7 +310,7 @@ class TestDashboardBans:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=empty),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
body = response.json()
|
||||
assert body["total"] == 0
|
||||
@@ -322,7 +322,7 @@ class TestDashboardBans:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=_make_ban_list_response(1)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
item = response.json()["items"][0]
|
||||
assert "ip" in item
|
||||
@@ -386,15 +386,15 @@ class TestBansByCountry:
|
||||
"app.routers.dashboard.ban_service.bans_by_country",
|
||||
new=AsyncMock(return_value=_make_bans_by_country_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_returns_401_when_unauthenticated(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Unauthenticated request returns HTTP 401."""
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/dashboard/bans/by-country")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/v1/dashboard/bans/by-country")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_response_shape(self, dashboard_client: AsyncClient) -> None:
|
||||
@@ -403,7 +403,7 @@ class TestBansByCountry:
|
||||
"app.routers.dashboard.ban_service.bans_by_country",
|
||||
new=AsyncMock(return_value=_make_bans_by_country_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
|
||||
body = response.json()
|
||||
assert "countries" in body
|
||||
@@ -423,7 +423,7 @@ class TestBansByCountry:
|
||||
with patch(
|
||||
"app.routers.dashboard.ban_service.bans_by_country", new=mock_fn
|
||||
):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-country?range=7d")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-country?range=7d")
|
||||
|
||||
called_range = mock_fn.call_args[0][1]
|
||||
assert called_range == "7d"
|
||||
@@ -433,7 +433,7 @@ class TestBansByCountry:
|
||||
) -> None:
|
||||
"""An invalid source value returns HTTP 422."""
|
||||
response = await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-country?source=invalid"
|
||||
"/api/v1/dashboard/bans/by-country?source=invalid"
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -453,7 +453,7 @@ class TestBansByCountry:
|
||||
"app.routers.dashboard.ban_service.bans_by_country",
|
||||
new=AsyncMock(return_value=empty),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
|
||||
body = response.json()
|
||||
assert body["total"] == 0
|
||||
@@ -477,7 +477,7 @@ class TestDashboardBansOriginField:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=_make_ban_list_response(1)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
item = response.json()["items"][0]
|
||||
assert "origin" in item
|
||||
@@ -491,7 +491,7 @@ class TestDashboardBansOriginField:
|
||||
"app.routers.dashboard.ban_service.list_bans",
|
||||
new=AsyncMock(return_value=_make_ban_list_response(1)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
item = response.json()["items"][0]
|
||||
assert item["jail"] == "sshd"
|
||||
@@ -505,7 +505,7 @@ class TestDashboardBansOriginField:
|
||||
"app.routers.dashboard.ban_service.bans_by_country",
|
||||
new=AsyncMock(return_value=_make_bans_by_country_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
|
||||
bans = response.json()["bans"]
|
||||
assert all("origin" in ban for ban in bans)
|
||||
@@ -518,7 +518,7 @@ class TestDashboardBansOriginField:
|
||||
"""The ``source`` query parameter is forwarded to bans_by_country."""
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_country_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_country", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-country?source=archive")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-country?source=archive")
|
||||
|
||||
assert mock_fn.call_args[1]["source"] == "archive"
|
||||
|
||||
@@ -529,7 +529,7 @@ class TestDashboardBansOriginField:
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_country_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_country", new=mock_fn):
|
||||
await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-country?country_code=DE"
|
||||
"/api/v1/dashboard/bans/by-country?country_code=DE"
|
||||
)
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
@@ -543,7 +543,7 @@ class TestDashboardBansOriginField:
|
||||
"app.routers.dashboard.ban_service.bans_by_country",
|
||||
new=AsyncMock(return_value=_make_bans_by_country_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
|
||||
bans = response.json()["bans"]
|
||||
blocklist_ban = next(b for b in bans if b["jail"] == "blocklist-import")
|
||||
@@ -564,7 +564,7 @@ class TestOriginFilterParam:
|
||||
"""``?origin=blocklist`` is passed to ``ban_service.list_bans``."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans?origin=blocklist")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans?origin=blocklist")
|
||||
|
||||
_, kwargs = mock_list.call_args
|
||||
assert kwargs.get("origin") == "blocklist"
|
||||
@@ -575,7 +575,7 @@ class TestOriginFilterParam:
|
||||
"""``?origin=selfblock`` is passed to ``ban_service.list_bans``."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans?origin=selfblock")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans?origin=selfblock")
|
||||
|
||||
_, kwargs = mock_list.call_args
|
||||
assert kwargs.get("origin") == "selfblock"
|
||||
@@ -586,7 +586,7 @@ class TestOriginFilterParam:
|
||||
"""Omitting ``origin`` passes ``None`` to the service (no filtering)."""
|
||||
mock_list = AsyncMock(return_value=_make_ban_list_response())
|
||||
with patch("app.routers.dashboard.ban_service.list_bans", new=mock_list):
|
||||
await dashboard_client.get("/api/dashboard/bans")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans")
|
||||
|
||||
_, kwargs = mock_list.call_args
|
||||
assert kwargs.get("origin") is None
|
||||
@@ -595,7 +595,7 @@ class TestOriginFilterParam:
|
||||
self, dashboard_client: AsyncClient
|
||||
) -> None:
|
||||
"""An invalid ``origin`` value returns HTTP 422 Unprocessable Entity."""
|
||||
response = await dashboard_client.get("/api/dashboard/bans?origin=invalid")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans?origin=invalid")
|
||||
assert response.status_code == 422
|
||||
|
||||
async def test_by_country_origin_blocklist_forwarded(
|
||||
@@ -607,7 +607,7 @@ class TestOriginFilterParam:
|
||||
"app.routers.dashboard.ban_service.bans_by_country", new=mock_fn
|
||||
):
|
||||
await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-country?origin=blocklist"
|
||||
"/api/v1/dashboard/bans/by-country?origin=blocklist"
|
||||
)
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
@@ -621,7 +621,7 @@ class TestOriginFilterParam:
|
||||
with patch(
|
||||
"app.routers.dashboard.ban_service.bans_by_country", new=mock_fn
|
||||
):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-country")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-country")
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
assert kwargs.get("origin") is None
|
||||
@@ -655,15 +655,15 @@ class TestBanTrend:
|
||||
"app.routers.dashboard.ban_service.ban_trend",
|
||||
new=AsyncMock(return_value=_make_ban_trend_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_returns_401_when_unauthenticated(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Unauthenticated request returns HTTP 401."""
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/dashboard/bans/trend")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/v1/dashboard/bans/trend")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_response_shape(self, dashboard_client: AsyncClient) -> None:
|
||||
@@ -672,7 +672,7 @@ class TestBanTrend:
|
||||
"app.routers.dashboard.ban_service.ban_trend",
|
||||
new=AsyncMock(return_value=_make_ban_trend_response(24)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
|
||||
body = response.json()
|
||||
assert "buckets" in body
|
||||
@@ -688,7 +688,7 @@ class TestBanTrend:
|
||||
"app.routers.dashboard.ban_service.ban_trend",
|
||||
new=AsyncMock(return_value=_make_ban_trend_response(3)),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
|
||||
for bucket in response.json()["buckets"]:
|
||||
assert "timestamp" in bucket
|
||||
@@ -699,7 +699,7 @@ class TestBanTrend:
|
||||
"""Omitting ``range`` defaults to ``24h``."""
|
||||
mock_fn = AsyncMock(return_value=_make_ban_trend_response())
|
||||
with patch("app.routers.dashboard.ban_service.ban_trend", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
|
||||
called_range = mock_fn.call_args[0][1]
|
||||
assert called_range == "24h"
|
||||
@@ -708,7 +708,7 @@ class TestBanTrend:
|
||||
"""The ``range`` query parameter is forwarded to the service."""
|
||||
mock_fn = AsyncMock(return_value=_make_ban_trend_response(28))
|
||||
with patch("app.routers.dashboard.ban_service.ban_trend", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/trend?range=7d")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/trend?range=7d")
|
||||
|
||||
called_range = mock_fn.call_args[0][1]
|
||||
assert called_range == "7d"
|
||||
@@ -718,7 +718,7 @@ class TestBanTrend:
|
||||
mock_fn = AsyncMock(return_value=_make_ban_trend_response())
|
||||
with patch("app.routers.dashboard.ban_service.ban_trend", new=mock_fn):
|
||||
await dashboard_client.get(
|
||||
"/api/dashboard/bans/trend?origin=blocklist"
|
||||
"/api/v1/dashboard/bans/trend?origin=blocklist"
|
||||
)
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
@@ -730,7 +730,7 @@ class TestBanTrend:
|
||||
"""Omitting ``origin`` passes ``None`` to the service."""
|
||||
mock_fn = AsyncMock(return_value=_make_ban_trend_response())
|
||||
with patch("app.routers.dashboard.ban_service.ban_trend", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
assert kwargs.get("origin") is None
|
||||
@@ -740,7 +740,7 @@ class TestBanTrend:
|
||||
) -> None:
|
||||
"""An invalid ``range`` value returns HTTP 422."""
|
||||
response = await dashboard_client.get(
|
||||
"/api/dashboard/bans/trend?range=invalid"
|
||||
"/api/v1/dashboard/bans/trend?range=invalid"
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -749,7 +749,7 @@ class TestBanTrend:
|
||||
) -> None:
|
||||
"""An invalid source value returns HTTP 422."""
|
||||
response = await dashboard_client.get(
|
||||
"/api/dashboard/bans/trend?source=invalid"
|
||||
"/api/v1/dashboard/bans/trend?source=invalid"
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -762,7 +762,7 @@ class TestBanTrend:
|
||||
"app.routers.dashboard.ban_service.ban_trend",
|
||||
new=AsyncMock(return_value=empty),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/trend")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/trend")
|
||||
|
||||
body = response.json()
|
||||
assert body["buckets"] == []
|
||||
@@ -799,15 +799,15 @@ class TestBansByJail:
|
||||
"app.routers.dashboard.ban_service.bans_by_jail",
|
||||
new=AsyncMock(return_value=_make_bans_by_jail_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_returns_401_when_unauthenticated(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Unauthenticated request returns HTTP 401."""
|
||||
await client.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/dashboard/bans/by-jail")
|
||||
await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
response = await client.get("/api/v1/dashboard/bans/by-jail")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_response_shape(self, dashboard_client: AsyncClient) -> None:
|
||||
@@ -816,7 +816,7 @@ class TestBansByJail:
|
||||
"app.routers.dashboard.ban_service.bans_by_jail",
|
||||
new=AsyncMock(return_value=_make_bans_by_jail_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
|
||||
body = response.json()
|
||||
assert "jails" in body
|
||||
@@ -831,7 +831,7 @@ class TestBansByJail:
|
||||
"app.routers.dashboard.ban_service.bans_by_jail",
|
||||
new=AsyncMock(return_value=_make_bans_by_jail_response()),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
|
||||
for entry in response.json()["jails"]:
|
||||
assert "jail" in entry
|
||||
@@ -843,7 +843,7 @@ class TestBansByJail:
|
||||
"""Omitting ``range`` defaults to ``"24h"``."""
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_jail_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_jail", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
|
||||
called_range = mock_fn.call_args[0][1]
|
||||
assert called_range == "24h"
|
||||
@@ -852,7 +852,7 @@ class TestBansByJail:
|
||||
"""The ``range`` query parameter is forwarded to the service."""
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_jail_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_jail", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-jail?range=7d")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-jail?range=7d")
|
||||
|
||||
called_range = mock_fn.call_args[0][1]
|
||||
assert called_range == "7d"
|
||||
@@ -862,7 +862,7 @@ class TestBansByJail:
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_jail_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_jail", new=mock_fn):
|
||||
await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-jail?origin=blocklist"
|
||||
"/api/v1/dashboard/bans/by-jail?origin=blocklist"
|
||||
)
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
@@ -874,7 +874,7 @@ class TestBansByJail:
|
||||
"""Omitting ``origin`` passes ``None`` to the service."""
|
||||
mock_fn = AsyncMock(return_value=_make_bans_by_jail_response())
|
||||
with patch("app.routers.dashboard.ban_service.bans_by_jail", new=mock_fn):
|
||||
await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
|
||||
_, kwargs = mock_fn.call_args
|
||||
assert kwargs.get("origin") is None
|
||||
@@ -884,7 +884,7 @@ class TestBansByJail:
|
||||
) -> None:
|
||||
"""An invalid ``range`` value returns HTTP 422."""
|
||||
response = await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-jail?range=invalid"
|
||||
"/api/v1/dashboard/bans/by-jail?range=invalid"
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -893,7 +893,7 @@ class TestBansByJail:
|
||||
) -> None:
|
||||
"""An invalid source value returns HTTP 422."""
|
||||
response = await dashboard_client.get(
|
||||
"/api/dashboard/bans/by-jail?source=invalid"
|
||||
"/api/v1/dashboard/bans/by-jail?source=invalid"
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -906,7 +906,7 @@ class TestBansByJail:
|
||||
"app.routers.dashboard.ban_service.bans_by_jail",
|
||||
new=AsyncMock(return_value=empty),
|
||||
):
|
||||
response = await dashboard_client.get("/api/dashboard/bans/by-jail")
|
||||
response = await dashboard_client.get("/api/v1/dashboard/bans/by-jail")
|
||||
|
||||
body = response.json()
|
||||
assert body["jails"] == []
|
||||
|
||||
Reference in New Issue
Block a user