test(07.2-01): add FakeRedis to admin_client + NBF-write xfail stubs for deactivation
- Import FakeRedis from tests.test_auth_api (no redefinition) - Mount app.state.redis = FakeRedis() in admin_client fixture before yield - Reset app.state.redis = None in teardown (mirrors authed_client convention) - Add test_deactivate_user_writes_user_nbf_to_redis (xfail strict=False) - Add test_activate_user_does_not_write_user_nbf (negative-guard, xfail strict=False) - All 22 existing admin tests unaffected (zero regressions)
This commit is contained in:
@@ -26,6 +26,7 @@ from sqlalchemy import select
|
|||||||
from deps.auth import get_current_admin
|
from deps.auth import get_current_admin
|
||||||
from deps.db import get_db
|
from deps.db import get_db
|
||||||
from services.auth import hash_password
|
from services.auth import hash_password
|
||||||
|
from tests.test_auth_api import FakeRedis
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
@@ -70,17 +71,26 @@ async def make_regular_user(session: AsyncSession) -> User:
|
|||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def admin_client(db_session: AsyncSession):
|
async def admin_client(db_session: AsyncSession):
|
||||||
"""Async client with get_current_admin overridden to an admin user."""
|
"""Async client with get_current_admin overridden to an admin user.
|
||||||
|
|
||||||
|
Phase 7.2: app.state.redis is set to a FakeRedis() instance so that
|
||||||
|
the admin deactivation handler (post-Wave 2) can call
|
||||||
|
request.app.state.redis.set(...) without AttributeError.
|
||||||
|
"""
|
||||||
from main import app
|
from main import app
|
||||||
|
|
||||||
admin = await make_admin_user(db_session)
|
admin = await make_admin_user(db_session)
|
||||||
app.dependency_overrides[get_db] = lambda: db_session
|
app.dependency_overrides[get_db] = lambda: db_session
|
||||||
app.dependency_overrides[get_current_admin] = lambda: admin
|
app.dependency_overrides[get_current_admin] = lambda: admin
|
||||||
|
# Phase 7.2: mount FakeRedis for deactivation handler NBF write
|
||||||
|
app.state.redis = FakeRedis()
|
||||||
|
|
||||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||||||
yield c, admin, db_session
|
yield c, admin, db_session
|
||||||
|
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
# Reset redis to avoid leaking state between test files (mirrors authed_client teardown)
|
||||||
|
app.state.redis = None
|
||||||
|
|
||||||
|
|
||||||
# ── Tests ─────────────────────────────────────────────────────────────────────
|
# ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -429,3 +439,64 @@ async def test_delete_user_no_body(admin_client):
|
|||||||
|
|
||||||
resp = await client.delete(f"/api/admin/users/{target.id}")
|
resp = await client.delete(f"/api/admin/users/{target.id}")
|
||||||
assert resp.status_code == 422
|
assert resp.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 7.2 Wave 0 stubs: NBF-write on admin deactivation ──────────────────
|
||||||
|
# These two tests are xfailed with strict=False. When Wave 2 (Plan 03) adds
|
||||||
|
# user_nbf write to the admin deactivation handler, these stubs are promoted
|
||||||
|
# by replacing `assert False, "stub"` with real Redis key assertions.
|
||||||
|
#
|
||||||
|
# Invariant preserved in Wave 2: activation (is_active=True) must NOT write
|
||||||
|
# user_nbf — only deactivation writes it (RESEARCH.md anti-pattern).
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to admin deactivation handler")
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_deactivate_user_writes_user_nbf_to_redis(admin_client):
|
||||||
|
"""PATCH /api/admin/users/{id}/status {is_active: false} must write user_nbf:{id} to Redis."""
|
||||||
|
client, _admin, session = admin_client
|
||||||
|
target = await make_regular_user(session)
|
||||||
|
|
||||||
|
resp = await client.patch(
|
||||||
|
f"/api/admin/users/{target.id}/status",
|
||||||
|
json={"is_active": False},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Wave 2 will add the user_nbf write; this is the target assertion:
|
||||||
|
assert False, "stub — Wave 2 will fill in: nbf_bytes = await app.state.redis.get(f'user_nbf:{target.id}'); assert nbf_bytes is not None"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — activation must NOT write user_nbf (invariant guard)")
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_activate_user_does_not_write_user_nbf(admin_client):
|
||||||
|
"""PATCH /api/admin/users/{id}/status {is_active: true} must NOT write user_nbf:{id} to Redis.
|
||||||
|
|
||||||
|
Anti-pattern from RESEARCH.md: do not write user_nbf for successful activation.
|
||||||
|
This negative-guard test ensures Wave 2 only writes on deactivation, not activation.
|
||||||
|
"""
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
client, _admin, session = admin_client
|
||||||
|
target = await make_regular_user(session)
|
||||||
|
|
||||||
|
# Deactivate first (sets is_active=False)
|
||||||
|
await client.patch(
|
||||||
|
f"/api/admin/users/{target.id}/status",
|
||||||
|
json={"is_active": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear any nbf that may have been set by deactivation
|
||||||
|
fake_redis = app.state.redis
|
||||||
|
if hasattr(fake_redis, "_store"):
|
||||||
|
fake_redis._store.pop(f"user_nbf:{target.id}", None)
|
||||||
|
|
||||||
|
# Now activate (is_active=True) — must NOT set user_nbf
|
||||||
|
resp = await client.patch(
|
||||||
|
f"/api/admin/users/{target.id}/status",
|
||||||
|
json={"is_active": True},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Wave 2 must preserve this invariant; this is the target assertion:
|
||||||
|
assert False, "stub — Wave 2 will fill in: nbf_bytes = await app.state.redis.get(f'user_nbf:{target.id}'); assert nbf_bytes is None"
|
||||||
|
|||||||
Reference in New Issue
Block a user