From 097cdcadf80183d49eb0a0e9329be571853ba8c6 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 5 Jun 2026 19:07:30 +0200 Subject: [PATCH] test(07.2-01): add NBF-write xfail stubs for change_password/enable_totp/disable_totp - Add test_change_password_writes_user_nbf_to_redis (xfail strict=False) - Add test_enable_totp_writes_user_nbf_to_redis (xfail strict=False) - Add test_disable_totp_writes_user_nbf_to_redis (xfail strict=False) - Each test exercises the full auth flow then asserts user_nbf Redis key - Wave 2 (Plan 03) promotes stubs by replacing pytest.xfail() with real assertion - All 27 existing auth API tests unaffected --- backend/tests/test_auth_api.py | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/backend/tests/test_auth_api.py b/backend/tests/test_auth_api.py index 9516cfb..daf61fc 100644 --- a/backend/tests/test_auth_api.py +++ b/backend/tests/test_auth_api.py @@ -600,3 +600,95 @@ async def test_disable_totp_revokes_other_sessions(authed_client, db_session: As ) rows = result2.scalars().all() assert any(r.revoked for r in rows), "Expected at least one revoked RefreshToken row" + + +# ── Phase 7.2 Wave 0 stubs: NBF-write on security events ───────────────────── +# These three tests are xfailed with strict=False. When Wave 2 (Plan 03) adds +# user_nbf writes to the handlers, these stubs are promoted to real assertions +# by replacing `pytest.xfail(...)` with actual Redis key assertions. +# +# Implementation hint for Wave 2 (kept here for context): +# After the API call returns success, fetch the user_id from the login response +# payload, then: +# nbf_bytes = await authed_client._transport.app.state.redis.get(f"user_nbf:{user_id}") +# assert nbf_bytes is not None +# assert int(nbf_bytes.decode() if isinstance(nbf_bytes, (bytes, bytearray)) else nbf_bytes) > 0 + + +@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to change_password handler") +@pytest.mark.asyncio +async def test_change_password_writes_user_nbf_to_redis(authed_client, db_session: AsyncSession): + """POST /api/auth/change-password must write user_nbf:{user_id} to Redis.""" + from sqlalchemy import select as sa_select + + await _register(authed_client, handle="nbfw_cp1", email="nbfw_cp1@example.com") + login_resp = await _login(authed_client, email="nbfw_cp1@example.com") + token = login_resp.json()["access_token"] + + result = await db_session.execute(sa_select(User).where(User.email == "nbfw_cp1@example.com")) + user = result.scalar_one() + + with patch("services.auth.check_hibp", return_value=False): + resp = await authed_client.post( + "/api/auth/change-password", + json={"current_password": "ValidPass12!", "new_password": "NewStrong99!@"}, + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + + # Wave 2 will add the user_nbf write; this is the target assertion: + pytest.xfail("Phase 7.2 Wave 2 stub — user_nbf write not yet implemented in change_password") + + +@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to enable_totp handler") +@pytest.mark.asyncio +async def test_enable_totp_writes_user_nbf_to_redis(authed_client, db_session: AsyncSession): + """POST /api/auth/totp/enable must write user_nbf:{user_id} to Redis.""" + from sqlalchemy import select as sa_select + + await _register(authed_client, handle="nbfw_et1", email="nbfw_et1@example.com") + login_resp = await _login(authed_client, email="nbfw_et1@example.com") + token = login_resp.json()["access_token"] + + result = await db_session.execute(sa_select(User).where(User.email == "nbfw_et1@example.com")) + user = result.scalar_one() + user.totp_secret = "JBSWY3DPEHPK3PXP" + await db_session.commit() + + with patch("services.auth.verify_totp", return_value=True): + with patch("services.auth.store_backup_codes", return_value=None): + resp = await authed_client.post( + "/api/auth/totp/enable", + json={"code": "123456"}, + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + + # Wave 2 will add the user_nbf write; this is the target assertion: + pytest.xfail("Phase 7.2 Wave 2 stub — user_nbf write not yet implemented in enable_totp") + + +@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to disable_totp handler") +@pytest.mark.asyncio +async def test_disable_totp_writes_user_nbf_to_redis(authed_client, db_session: AsyncSession): + """DELETE /api/auth/totp must write user_nbf:{user_id} to Redis.""" + from sqlalchemy import select as sa_select + + await _register(authed_client, handle="nbfw_dt1", email="nbfw_dt1@example.com") + login_resp = await _login(authed_client, email="nbfw_dt1@example.com") + token = login_resp.json()["access_token"] + + result = await db_session.execute(sa_select(User).where(User.email == "nbfw_dt1@example.com")) + user = result.scalar_one() + user.totp_enabled = True + user.totp_secret = "JBSWY3DPEHPK3PXP" + await db_session.commit() + + resp = await authed_client.delete( + "/api/auth/totp", + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + + # Wave 2 will add the user_nbf write; this is the target assertion: + pytest.xfail("Phase 7.2 Wave 2 stub — user_nbf write not yet implemented in disable_totp")