TASK-004: Bootstrap frontend auth state from backend session check
Validates session on app mount by calling GET /api/auth/session instead of relying solely on cached sessionStorage. This ensures the UI state always reflects server reality — expired or revoked sessions are detected immediately. Changes: - Backend: Add GET /api/auth/session endpoint (requires valid session, returns 200/401) - Frontend: Add useSessionValidation hook for mount-time validation - Frontend: Add SessionValidationLoading component for validation spinner - Frontend: Update AuthProvider to call validation on mount with loading state - Frontend: Add validateSession API function - Docs: Update Features.md with session validation behavior - Docs: Update Web-Development.md with session validation pattern Handles three outcomes: 1. Valid session (200): Proceed with cached state 2. Invalid session (401): Clear sessionStorage and redirect to login 3. Network error: Don't logout (backend may be temporarily unreachable) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -202,6 +202,68 @@ class TestRequireAuth:
|
||||
assert call_count == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Session validation (Task 4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestValidateSession:
|
||||
"""GET /api/auth/session."""
|
||||
|
||||
async def test_validate_session_returns_200_with_valid_token(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Validate session returns 200 for a valid authenticated request."""
|
||||
await _do_setup(client)
|
||||
await _login(client)
|
||||
response = await client.get("/api/auth/session")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"valid": True}
|
||||
|
||||
async def test_validate_session_returns_401_without_token(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Validate session returns 401 when no token is present."""
|
||||
await _do_setup(client)
|
||||
response = await client.get("/api/auth/session")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_validate_session_returns_401_with_invalid_token(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Validate session returns 401 for an invalid or expired token."""
|
||||
await _do_setup(client)
|
||||
response = await client.get(
|
||||
"/api/auth/session",
|
||||
headers={"Authorization": "Bearer invalidtoken"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_validate_session_with_cookie(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Validate session works with cookie-based authentication."""
|
||||
await _do_setup(client)
|
||||
token = await _login(client)
|
||||
# Login sets the cookie on the client automatically via httpx.
|
||||
response = await client.get("/api/auth/session")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"valid": True}
|
||||
|
||||
async def test_validate_session_after_logout(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Validate session returns 401 after logout."""
|
||||
await _do_setup(client)
|
||||
token = await _login(client)
|
||||
await client.post("/api/auth/logout")
|
||||
response = await client.get(
|
||||
"/api/auth/session",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Session-token cache (Task 4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user