Commit 7e618f

2026-03-20 22:17:10 Claude (MCP): [mcp] Update E2E_Testing: reflect completed implementation (23 tests on main)
Design/E2E_Testing.md ..
@@ 1,7 1,7 @@
---
category: design
tags: [testing, playwright, e2e, infrastructure]
- last_updated: 2026-03-19
+ last_updated: 2026-03-20
confidence: high
---
@@ 9,111 9,89 @@
End-to-end testing for robot.wtf using Playwright and a mock ATProto PDS.
+ **Status: COMPLETE** — 23 tests on `main`, deployed 2026-03-20.
+
## Current State
- **Branch**: `feat/e2e-tests` on `robot.wtf` repo (4 commits)
+ **23 passing E2E tests** on `main` across 4 files, plus 294 unit tests.
+
+ ### Test files
- **4 passing tests** in `tests/e2e/test_login_flow.py`:
+ **`test_login_flow.py`** (5 tests):
1. Login page loads
2. Client metadata endpoint serves valid ATProto OAuth metadata
3. Full OAuth login flow (mock PDS → PAR → consent → callback → cookie)
4. Logout clears cookie
-
- **Infrastructure built**:
- - `tests/e2e/mock_pds.py` — In-process mock ATProto PDS (OAuth endpoints, account creation, DID docs)
- - `tests/e2e/conftest.py` — Fixtures for PDS (mock or Docker), test account, key generation, auth server in background thread, Playwright browser context
- - `tests/e2e/docker-compose.yml` — Real PDS for CI environments with Docker
- - `.github/workflows/e2e.yml` — CI workflow
- - Production code changes gated by `ALLOW_HTTP_PDS` env var (requires `FLASK_ENV=testing`, raises RuntimeError otherwise)
-
- **Partially written test files** (on branch, need fixtures):
- - `tests/e2e/test_wiki_lifecycle.py` — 4 tests written by Agent B
- - `tests/e2e/test_account.py` — 4 tests written by Agent C (MCP consent test marked skip)
-
- ## Blocked On
-
- Server consolidation (see [[Design/Server_Consolidation]]). The auth_server and api_server are separate Flask apps on different ports. This causes:
- - Cookie cross-port sharing failures
- - SQLite threading issues with two in-process Flask servers
- - An implementation agent burned its entire context trying to work around this
-
- Once auth + api are merged into a single Flask app, the E2E fixtures simplify dramatically.
-
- ## Plan After Consolidation
-
- ### Step 1: Simplify conftest.py
-
- The `auth_server` fixture currently starts only the auth Flask app. Post-consolidation, it starts the single platform app, which serves both `/auth/*` and `/app/*` routes. Rename to `platform_server` or just `server`.
-
- No `management_server` fixture needed — it's the same app.
-
- No cross-port cookie injection needed — same origin.
-
- ### Step 2: Add new fixtures
-
- **`authenticated_page`** (function-scoped):
- - Logs in via mock PDS OAuth flow
- - Returns a Playwright page with valid `platform_token` cookie
- - Cookie works for all routes (same origin)
-
- **`wiki_fixture`** (function-scoped):
- - Creates a wiki directly in DB + filesystem (bypasses route to avoid tier limits)
- - Calls `WikiModel.create()` + `_init_wiki_repo()` + `_init_wiki_db()`
- - Cleans up wiki dir + DB row after test
-
- **`destructive_page`** (function-scoped):
- - Separate browser context for tests that destroy state (account deletion, wiki deletion)
- - Prevents cookie/state pollution to other tests
-
- ### Step 3: Implement 11 additional tests
-
- #### Auth flows (`test_auth_flows.py`, 3 tests)
- - **Auto-redirect when authenticated**: Visit `/auth/login` with valid cookie → redirects to `/app/`
- - **Return-to URL preservation**: `return_to` parameter survives the full OAuth redirect chain
- - **Login with DID**: Login using DID instead of handle (may be redundant with existing test_oauth_login — check)
-
- #### Wiki lifecycle (`test_wiki_lifecycle.py`, 4 tests — already written)
- - **Wiki creation form**: Fill slug/name → submit → redirect to settings → MCP token visible
- - **Wiki settings update**: Change display_name → flash message → persists on reload
- - **Wiki deletion with confirmation**: Expand danger zone → confirm slug → delete → flash
- - **MCP token regeneration**: Click regen → JS confirm dialog → new token in flash
-
- #### Account management (`test_account.py`, 3 tests — already written)
- - **Account page renders**: Displays DID, handle, created_at from JWT claims
- - **Account deletion**: Confirm handle → cookie cleared → cascading wiki delete
- - **Account deletion wrong confirmation**: Wrong handle → stays on page → error flash
-
- #### MCP consent (`test_account.py`, 1 test — already written, marked skip)
- - **MCP consent page renders**: Consent page shows client info, wiki name, approve/deny buttons
-
- ### Step 4: Verify existing test files
-
- Agent B's `test_wiki_lifecycle.py` and Agent C's `test_account.py` were written against fixture signatures that may differ from the simplified post-consolidation conftest. Review and update selectors/fixture names before running.
-
- ### Step 5: Run full suite
-
- All 15 tests (4 existing + 11 new) should pass. Run unit tests too to verify no regressions.
+ 5. OAuth callback error shows flash (not 500)
+
+ **`test_auth_flows.py`** (4 tests):
+ 1. Auto-redirect when authenticated (visit `/auth/login` with valid cookie → `/app/`)
+ 2. Return-to URL preservation across OAuth redirect chain
+ 3. Login with handle (tests error handling for unresolvable mock handles)
+ 4. Unauthenticated access redirects to login with `return_to`
+
+ **`test_wiki_lifecycle.py`** (8 tests):
+ 1. Wiki creation form (slug/name → submit → redirect → MCP token visible)
+ 2. Wiki settings update (change display_name → flash → persists on reload)
+ 3. Wiki deletion with confirmation (expand danger zone → confirm slug → delete)
+ 4. MCP token regeneration (click regen → JS confirm → new token in flash)
+ 5. Dashboard redirects to existing wiki
+ 6. Wiki deletion wrong slug rejected (confirm mismatch → flash error → wiki survives)
+ 7. Wiki creation duplicate slug rejected
+ 8. Wiki creation invalid slug rejected (bypasses browser validation, tests server-side)
+ 9. Wiki settings steady state (no token flash, regenerate button present)
+
+ **`test_account.py`** (6 tests):
+ 1. Account page renders (displays DID, handle)
+ 2. Account deletion wrong confirmation (wrong handle → error flash)
+ 3. MCP consent page renders (client info, wiki name, approve/deny buttons)
+ 4. Account deletion (correct handle → cookie cleared → redirected)
+ 5. MCP consent deny redirects with error
+ 6. Wiki settings steady state page elements
+
+ ### Infrastructure
+
+ - `tests/e2e/mock_pds.py` — In-process mock ATProto PDS with PKCE verification, thread-safe state
+ - `tests/e2e/conftest.py` — Fixtures for single platform server, authenticated pages, wiki creation
+
+ ### Fixtures
+
+ - **`platform_server`** (session): Starts consolidated Flask app in daemon thread on a free port
+ - **`authenticated_page`** (function): Fresh browser context with valid `platform_token` cookie via direct JWT minting
+ - **`wiki_fixture`** (function): Creates wiki directly in DB + filesystem, cleans up after test
+ - **`destructive_page`** (function): Separate browser context for tests that destroy state
+ - **`pds`** (session): Mock PDS in daemon thread
+ - **`test_account`** (session): Test account on mock PDS
+
+ ### Production code changes for test mode
+
+ Gated by `ALLOW_HTTP_PDS=true` + `FLASK_ENV=testing` (RuntimeError at import if either is wrong):
+
+ - `app/auth/atproto_security.py` — `_ALLOW_HTTP_PDS` flag, loopback SSRF relaxation
+ - `app/auth/atproto_identity.py` — `PLC_DIRECTORY_URL` read at request time, skip bidirectional handle verification
+ - `app/auth/atproto_oauth.py` — Relax HTTPS/port assertions on auth server metadata
+ - `app/platform_server.py` — `_SCHEME` variable, conditional `SESSION_COOKIE_SECURE`, conditional cookie `secure` flag, rate limiter disabled in test mode, limiter GC strong-reference fix
+ - `app/db.py` — `check_same_thread=False` scoped to `FLASK_ENV=testing`
+
+ ### Bug fixes discovered during E2E work
+
+ - `resolve_did()` SSRF: Upgraded from plain `requests.get` to `hardened_http` (pre-existing vulnerability, elevated by injectable `PLC_DIRECTORY_URL`)
+ - Flask-Limiter GC: `Limiter` object garbage collected after `create_app()` returned due to weak references. Fixed with strong ref in `app.config["_LIMITER"]`
## Architecture Notes
### Mock PDS
- The mock PDS (`tests/e2e/mock_pds.py`) implements:
- - `POST /xrpc/com.atproto.server.createAccount` — creates test accounts with `did:plc:` DIDs
- - `POST /xrpc/com.atproto.server.createSession` — handles re-use of existing accounts
- - `GET /.well-known/oauth-authorization-server` — AS metadata
- - `GET /.well-known/oauth-protected-resource` — protected resource metadata
- - `POST /oauth/par` — Pushed Authorization Request
- - `GET/POST /oauth/authorize` — Login + consent form (simple HTML, not React SPA)
- - `POST /oauth/token` — Token exchange (skips PKCE verification)
- - `GET /did:plc:*` — DID document serving (acts as PLC directory)
+ The mock PDS (`tests/e2e/mock_pds.py`) implements the full ATProto OAuth flow:
+ - Account creation/session management
+ - OAuth AS metadata, protected resource metadata
+ - PAR, authorize (HTML form), token exchange with PKCE S256 verification
+ - DID document serving (acts as PLC directory)
+ - Thread-safe global state with `threading.Lock`
All on `127.0.0.1` to avoid IPv6 resolution issues.
### Test mode env vars
- `ALLOW_HTTP_PDS=true` — relaxes SSRF protections for loopback HTTP (guarded by `FLASK_ENV=testing`)
- - `PLC_DIRECTORY_URL` — points at mock PDS for DID resolution
+ - `PLC_DIRECTORY_URL` — points at mock PDS for DID resolution (read at request time in `resolve_did()`)
- `PLATFORM_DOMAIN=127.0.0.1:{port}` — makes CLIENT_ID/REDIRECT_URI use HTTP
- `WIKI_TEMPLATE_DIR` — pointed at nonexistent path for predictable fallback behavior
-
- ### Docker vs mock
- Conftest has a 3-tier fallback: external PDS already running → Docker Compose → in-process mock. CI with Docker gets the real PDS; devcontainers without Docker get the mock. The mock is sufficient for all current tests.
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9