---
phase: 08-stack-upgrade-backend-decomposition
plan: "01"
subsystem: backend-tests
tags: [tests, session-revocation, xfail, wave-0, cr-01, cr-02, cr-03]
dependency_graph:
  requires: []
  provides:
    - "behavioral contract for CR-01: change_password revokes other sessions"
    - "behavioral contract for CR-02: enable_totp revokes other sessions"
    - "behavioral contract for CR-03: disable_totp revokes other sessions"
  affects:
    - "backend/tests/test_auth.py — new file"
tech_stack:
  added: []
  patterns:
    - "xfail(strict=False) Wave 0 scaffold — tests pass now (xpassed), marker removed in 08-03"
    - "FakeRedis in-memory store for all auth/TOTP tests (established pattern)"
    - "cookies= kwarg for explicit refresh token injection bypassing path restriction"
    - "patch.dict(sys.modules) to prevent Celery broker connection on token replay"
key_files:
  created:
    - path: "backend/tests/test_auth.py"
      description: "Three xfail tests for CR-01/CR-02/CR-03 session revocation contracts"
  modified: []
decisions:
  - "Used fixed User-Agent on revoke_client fixture to match fgp claim in access tokens"
  - "Used DB-direct TOTP enable for CR-03 test to avoid extraneous setup session token"
  - "Patched services.auth.verify_totp for CR-03 TOTP logins to bypass 90s replay prevention"
  - "Patched tasks.email_tasks via patch.dict(sys.modules) to avoid real Celery connection"
metrics:
  duration: "10m 8s"
  completed: "2026-06-08"
  tasks_completed: 1
  tasks_total: 1
  files_created: 1
  files_modified: 0
---

# Phase 8 Plan 01: Wave 0 xfail Stubs for CR-01/CR-02/CR-03 Summary

**One-liner:** Three xfail(strict=False) tests locking session-revocation contracts for change_password, enable_totp, and disable_totp — all xpassed since backend is already complete.

## What Was Built

Created `backend/tests/test_auth.py` with three Wave 0 test stubs:

| Test | Requirement | Status |
|------|------------|--------|
| `test_change_password_revokes_other_sessions` | CR-01 | xpassed |
| `test_enable_totp_revokes_other_sessions` | CR-02 | xpassed |
| `test_disable_totp_revokes_other_sessions` | CR-03 | xpassed |

All three tests pass when run with `--runxfail` (backend already implements the behavior per RESEARCH.md §Wave 1). They show `XPASS` in normal mode since `strict=False`.

## Test Infrastructure

**Fixtures used:**
- `revoke_client` (new, defined in test_auth.py): AsyncClient with FakeRedis, fixed `User-Agent: docuvault-test/1.0`, DB override
- `db_session` (from conftest.py): in-memory SQLite session

**Helper functions:**
- `_register_user(client, handle, email)` — register + assert 201
- `_login_session(client, email)` — login + return (access_token, refresh_cookie)
- `_try_refresh(client, refresh_token)` — POST /api/auth/refresh + return status code

**xfail decorator reason string:** `"Wave 0 stub — promoted to passing in 08-03"`

## Pytest Collection Count

- Pre-change baseline: 0 tests in `test_auth.py` (file did not exist)
- Post-change: 3 tests collected

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] FakeRedis not set on app.state.redis**
- **Found during:** Task 1 implementation
- **Issue:** The plan's existing `authed_client` fixture pattern required FakeRedis injection on `app.state.redis`. Without it, endpoints that call `request.app.state.redis.set(...)` (change_password, enable_totp, disable_totp) would fail.
- **Fix:** Created dedicated `revoke_client` fixture with FakeRedis injection, matching the pattern from `test_auth_api.py`.
- **Files modified:** `backend/tests/test_auth.py`

**2. [Rule 1 - Bug] Token fingerprint mismatch on API calls**
- **Found during:** Task 1 — first test run
- **Issue:** The access token's `fgp` claim is bound to the User-Agent at login time. The plan suggested using distinct User-Agents for session A and B to ensure separate DB rows, but this caused fingerprint mismatch when using token_a in subsequent API calls with a different User-Agent.
- **Fix:** Used a single fixed User-Agent (`"docuvault-test/1.0"`) for the `revoke_client` fixture. Two sequential logins always create two separate RefreshToken rows regardless of User-Agent.
- **Files modified:** `backend/tests/test_auth.py`

**3. [Rule 1 - Bug] TOTP replay prevention blocks second session login in CR-03**
- **Found during:** Task 1 — second test run
- **Issue:** The FakeRedis stores TOTP used-code keys with a 90s TTL. When `_login_with_totp()` was called twice in the same 30-second TOTP window, `pyotp.TOTP(secret).now()` returned the same code which was already marked used in FakeRedis.
- **Fix:** Patched `services.auth.verify_totp` to return `True` for the TOTP login calls in CR-03. The test focuses on session revocation, not TOTP validation.
- **Files modified:** `backend/tests/test_auth.py`

**4. [Rule 1 - Bug] Celery broker connection attempt on revoked token**
- **Found during:** Task 1 — third test run
- **Issue:** When `_try_refresh` is called with session B's revoked token, `rotate_refresh_token` triggers the family-revocation path which calls `send_security_alert_email.delay(...)`. This attempted a real Redis/Celery broker connection (not available in unit tests).
- **Fix:** Patched `tasks.email_tasks` via `patch.dict("sys.modules", {...})` inside `_try_refresh`, matching the pattern from `test_task2_auth_service.py`.
- **Files modified:** `backend/tests/test_auth.py`

## Verification

```
3 xpassed, 10 warnings in 2.02s
```

Full backend suite before this plan (pre-existing): `1 failed (test_extract_docx — ModuleNotFoundError: docx not installed locally), 402 passed`

Full backend suite after this plan: same baseline + 3 xpassed new tests added.

The pre-existing `test_extractor.py::test_extract_docx` failure is a `ModuleNotFoundError: No module named 'docx'` — the `python-docx` package is only installed inside Docker, not in the local Python environment. This is out-of-scope and was pre-existing before Plan 08-01.

## Threat Flags

None — this plan only adds tests, no new network endpoints, auth paths, file access patterns, or schema changes.

## Known Stubs

None — the test bodies contain full assertion logic, not `pass` placeholders.

## Self-Check: PASSED

- [x] `backend/tests/test_auth.py` created and contains 3 test functions
- [x] All three function names match the exact names specified in the plan
- [x] `grep -c "pytest.mark.xfail" backend/tests/test_auth.py` = 4 (3 decorators + 1 in docstring, baseline was 0)
- [x] Commit `f750d30` exists: `git log --oneline | grep f750d30`
- [x] Tests show XPASS status (strict=False xfail)
- [x] No new failures in the full suite
