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:
curo1305
2026-06-05 19:08:27 +02:00
parent 097cdcadf8
commit eb1647293f
+72 -1
View File
@@ -26,6 +26,7 @@ from sqlalchemy import select
from deps.auth import get_current_admin
from deps.db import get_db
from services.auth import hash_password
from tests.test_auth_api import FakeRedis
# ── Helpers ───────────────────────────────────────────────────────────────────
@@ -70,17 +71,26 @@ async def make_regular_user(session: AsyncSession) -> User:
@pytest_asyncio.fixture
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
admin = await make_admin_user(db_session)
app.dependency_overrides[get_db] = lambda: db_session
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:
yield c, admin, db_session
app.dependency_overrides.clear()
# Reset redis to avoid leaking state between test files (mirrors authed_client teardown)
app.state.redis = None
# ── Tests ─────────────────────────────────────────────────────────────────────
@@ -429,3 +439,64 @@ async def test_delete_user_no_body(admin_client):
resp = await client.delete(f"/api/admin/users/{target.id}")
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"