Files
BanGUI/backend/tests/test_routers/test_setup.py
Lukas 1cdc97a729 Stage 11: polish, cross-cutting concerns & hardening
- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline
- 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone
- 11.3 Responsive sidebar: auto-collapse <640px, media query listener
- 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated
- 11.5 prefers-reduced-motion: disable sidebar transition
- 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries
- 11.7 Health transition logging (fail2ban_came_online/went_offline)
- 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502
- 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean
- Timezone endpoint: setup_service.get_timezone, 5 new tests
2026-03-01 15:59:06 +01:00

159 lines
6.0 KiB
Python

"""Tests for the setup router (POST /api/setup, GET /api/setup, GET /api/setup/timezone)."""
from __future__ import annotations
from httpx import AsyncClient
class TestGetSetupStatus:
"""GET /api/setup — check setup completion state."""
async def test_returns_not_completed_on_fresh_db(self, client: AsyncClient) -> None:
"""Status endpoint reports setup not done on a fresh database."""
response = await client.get("/api/setup")
assert response.status_code == 200
assert response.json() == {"completed": False}
async def test_returns_completed_after_setup(self, client: AsyncClient) -> None:
"""Status endpoint reports setup done after POST /api/setup."""
await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
"session_duration_minutes": 60,
},
)
response = await client.get("/api/setup")
assert response.status_code == 200
assert response.json() == {"completed": True}
class TestPostSetup:
"""POST /api/setup — run the first-run configuration wizard."""
async def test_accepts_valid_payload(self, client: AsyncClient) -> None:
"""Setup endpoint returns 201 for a valid first-run payload."""
response = await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
"session_duration_minutes": 60,
},
)
assert response.status_code == 201
body = response.json()
assert "message" in body
async def test_rejects_short_password(self, client: AsyncClient) -> None:
"""Setup endpoint rejects passwords shorter than 8 characters."""
response = await client.post(
"/api/setup",
json={"master_password": "short"},
)
assert response.status_code == 422
async def test_rejects_second_call(self, client: AsyncClient) -> None:
"""Setup endpoint returns 409 if setup has already been completed."""
payload = {
"master_password": "supersecret123",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
"session_duration_minutes": 60,
}
first = await client.post("/api/setup", json=payload)
assert first.status_code == 201
second = await client.post("/api/setup", json=payload)
assert second.status_code == 409
async def test_accepts_defaults_for_optional_fields(
self, client: AsyncClient
) -> None:
"""Setup endpoint uses defaults when optional fields are omitted."""
response = await client.post(
"/api/setup",
json={"master_password": "supersecret123"},
)
assert response.status_code == 201
class TestSetupRedirectMiddleware:
"""Verify that the setup-redirect middleware enforces setup-first."""
async def test_protected_endpoint_redirects_before_setup(
self, client: AsyncClient
) -> None:
"""Non-setup API requests redirect to /api/setup on a fresh instance."""
response = await client.get(
"/api/auth/login",
follow_redirects=False,
)
# Middleware issues 307 redirect to /api/setup
assert response.status_code == 307
assert response.headers["location"] == "/api/setup"
async def test_health_always_reachable_before_setup(
self, client: AsyncClient
) -> None:
"""Health endpoint is always reachable even before setup."""
response = await client.get("/api/health")
assert response.status_code == 200
async def test_no_redirect_after_setup(self, client: AsyncClient) -> None:
"""Protected endpoints are reachable (no redirect) after setup."""
await client.post(
"/api/setup",
json={"master_password": "supersecret123"},
)
# /api/auth/login should now be reachable (returns 405 GET not allowed,
# not a setup redirect)
response = await client.post(
"/api/auth/login",
json={"password": "wrong"},
follow_redirects=False,
)
# 401 wrong password — not a 307 redirect
assert response.status_code == 401
class TestGetTimezone:
"""GET /api/setup/timezone — return the configured IANA timezone."""
async def test_returns_utc_before_setup(self, client: AsyncClient) -> None:
"""Timezone endpoint returns 'UTC' on a fresh database (no setup yet)."""
response = await client.get("/api/setup/timezone")
assert response.status_code == 200
assert response.json() == {"timezone": "UTC"}
async def test_returns_configured_timezone(self, client: AsyncClient) -> None:
"""Timezone endpoint returns the value set during setup."""
await client.post(
"/api/setup",
json={
"master_password": "supersecret123",
"timezone": "Europe/Berlin",
},
)
response = await client.get("/api/setup/timezone")
assert response.status_code == 200
assert response.json() == {"timezone": "Europe/Berlin"}
async def test_endpoint_always_reachable_before_setup(
self, client: AsyncClient
) -> None:
"""Timezone endpoint is reachable before setup (no redirect)."""
response = await client.get(
"/api/setup/timezone",
follow_redirects=False,
)
# Should return 200, not a 307 redirect, because /api/setup paths
# are always allowed by the SetupRedirectMiddleware.
assert response.status_code == 200