From ace8c7905132066ea16b786b4311476458997f10 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 20:10:30 +0300 Subject: [PATCH] test(auth): cover single-session revoke behavior --- docs/api-reference.md | 3 ++- tests/test_auth_flow.py | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 1660ae9..8bd1f94 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -546,7 +546,8 @@ Note: list includes refresh sessions and a synthetic current access-token sessio ### DELETE `/api/v1/auth/sessions/{jti}` Auth required. -Response: `204` +Response: `204` +Behavior: revokes only the specified refresh session token (`jti`); current access token remains valid. ### DELETE `/api/v1/auth/sessions` diff --git a/tests/test_auth_flow.py b/tests/test_auth_flow.py index 26050ff..18b7b9c 100644 --- a/tests/test_auth_flow.py +++ b/tests/test_auth_flow.py @@ -118,6 +118,52 @@ async def test_revoke_all_sessions_invalidates_access_and_refresh(client, db_ses assert refresh_response.status_code == 401 +async def test_revoke_single_session_invalidates_only_target_refresh_token(client, db_session): + payload = { + "email": "frank@example.com", + "name": "Frank", + "username": "frank", + "password": "strongpass123", + } + await client.post("/api/v1/auth/register", json=payload) + token_row = await db_session.execute(select(EmailVerificationToken).order_by(EmailVerificationToken.id.desc())) + verify_token = token_row.scalar_one().token + await client.post("/api/v1/auth/verify-email", json={"token": verify_token}) + + login_response = await client.post( + "/api/v1/auth/login", + json={"email": payload["email"], "password": payload["password"]}, + ) + assert login_response.status_code == 200 + tokens = login_response.json() + access_token = tokens["access_token"] + refresh_token = tokens["refresh_token"] + + sessions_response = await client.get( + "/api/v1/auth/sessions", + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert sessions_response.status_code == 200 + refresh_sessions = [item for item in sessions_response.json() if item.get("token_type") == "refresh"] + assert len(refresh_sessions) >= 1 + target_jti = refresh_sessions[0]["jti"] + + revoke_response = await client.delete( + f"/api/v1/auth/sessions/{target_jti}", + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert revoke_response.status_code == 204 + + refresh_after_revoke = await client.post("/api/v1/auth/refresh", json={"refresh_token": refresh_token}) + assert refresh_after_revoke.status_code == 401 + + me_after_revoke = await client.get( + "/api/v1/auth/me", + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert me_after_revoke.status_code == 200 + + async def test_twofa_setup_is_blocked_when_already_enabled(client, db_session): payload = { "email": "dana@example.com",