"""Tests for the session-expiration-policy series. See docs/plans/2026-05-13-session-expiration-policy.md. Test numbers below correspond to the cases listed in §6 of the plan. This file grows across commits — commit 2 lands the error-detail taxonomy tests (#11 + a wrong-type case + a bad-signature case). """ import uuid from datetime import datetime, timedelta, timezone import pytest from httpx import AsyncClient from jose import jwt from app.core.config import settings def _encode_refresh_token( *, sub: str, exp: datetime, token_type: str = "refresh", secret: str | None = None, ) -> str: """Build a refresh JWT with arbitrary `exp` for testing. Bypasses create_refresh_token so tests can produce already-expired tokens, wrong-type tokens, or wrong-signature tokens. """ return jwt.encode( { "sub": sub, "type": token_type, "jti": str(uuid.uuid4()), "exp": exp, }, secret or settings.SECRET_KEY, algorithm=settings.ALGORITHM, ) class TestRefreshTokenErrorTaxonomy: """§6 test #11 — refresh-token error-detail taxonomy. `/auth/refresh` distinguishes idle expiry from generic invalid-token failures via `detail`, so the frontend can choose between the "session ended for security" banner and a plain logout redirect. """ @pytest.mark.asyncio async def test_idle_expired_refresh_returns_session_expired_idle( self, client: AsyncClient, test_user: dict ): token = _encode_refresh_token( sub=test_user["user_data"]["id"], exp=datetime.now(timezone.utc) - timedelta(seconds=1), ) response = await client.post( "/api/v1/auth/refresh", headers={"Authorization": f"Bearer {token}"}, ) assert response.status_code == 401 assert response.json()["detail"] == "session_expired_idle" @pytest.mark.asyncio async def test_wrong_type_token_returns_invalid_refresh_token( self, client: AsyncClient, test_user: dict ): token = _encode_refresh_token( sub=test_user["user_data"]["id"], exp=datetime.now(timezone.utc) + timedelta(minutes=5), token_type="access", ) response = await client.post( "/api/v1/auth/refresh", headers={"Authorization": f"Bearer {token}"}, ) assert response.status_code == 401 assert response.json()["detail"] == "invalid_refresh_token" @pytest.mark.asyncio async def test_bad_signature_returns_invalid_refresh_token( self, client: AsyncClient, test_user: dict ): token = _encode_refresh_token( sub=test_user["user_data"]["id"], exp=datetime.now(timezone.utc) + timedelta(minutes=5), secret="not-the-real-secret-key", ) response = await client.post( "/api/v1/auth/refresh", headers={"Authorization": f"Bearer {token}"}, ) assert response.status_code == 401 assert response.json()["detail"] == "invalid_refresh_token"