"""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