Properties
category: design tags: [testing, playwright, e2e, infrastructure] last_updated: 2026-03-20 confidence: high
E2E Testing
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
23 passing E2E tests on main across 4 files, plus 294 unit tests.
Test files
test_login_flow.py (5 tests):
- Login page loads
- Client metadata endpoint serves valid ATProto OAuth metadata
- Full OAuth login flow (mock PDS → PAR → consent → callback → cookie)
- Logout clears cookie
- OAuth callback error shows flash (not 500)
test_auth_flows.py (4 tests):
- Auto-redirect when authenticated (visit
/auth/loginwith valid cookie →/app/) - Return-to URL preservation across OAuth redirect chain
- Login with handle (tests error handling for unresolvable mock handles)
- Unauthenticated access redirects to login with
return_to
test_wiki_lifecycle.py (8 tests):
- Wiki creation form (slug/name → submit → redirect → MCP token visible)
- Wiki settings update (change display_name → flash → persists on reload)
- Wiki deletion with confirmation (expand danger zone → confirm slug → delete)
- MCP token regeneration (click regen → JS confirm → new token in flash)
- Dashboard redirects to existing wiki
- Wiki deletion wrong slug rejected (confirm mismatch → flash error → wiki survives)
- Wiki creation duplicate slug rejected
- Wiki creation invalid slug rejected (bypasses browser validation, tests server-side)
- Wiki settings steady state (no token flash, regenerate button present)
test_account.py (6 tests):
- Account page renders (displays DID, handle)
- Account deletion wrong confirmation (wrong handle → error flash)
- MCP consent page renders (client info, wiki name, approve/deny buttons)
- Account deletion (correct handle → cookie cleared → redirected)
- MCP consent deny redirects with error
- Wiki settings steady state page elements
Infrastructure
tests/e2e/mock_pds.py— In-process mock ATProto PDS with PKCE verification, thread-safe statetests/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 portauthenticated_page(function): Fresh browser context with validplatform_tokencookie via direct JWT mintingwiki_fixture(function): Creates wiki directly in DB + filesystem, cleans up after testdestructive_page(function): Separate browser context for tests that destroy statepds(session): Mock PDS in daemon threadtest_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_PDSflag, loopback SSRF relaxationapp/auth/atproto_identity.py—PLC_DIRECTORY_URLread at request time, skip bidirectional handle verificationapp/auth/atproto_oauth.py— Relax HTTPS/port assertions on auth server metadataapp/platform_server.py—_SCHEMEvariable, conditionalSESSION_COOKIE_SECURE, conditional cookiesecureflag, rate limiter disabled in test mode, limiter GC strong-reference fixapp/db.py—check_same_thread=Falsescoped toFLASK_ENV=testing
Bug fixes discovered during E2E work
resolve_did()SSRF: Upgraded from plainrequests.gettohardened_http(pre-existing vulnerability, elevated by injectablePLC_DIRECTORY_URL)- Flask-Limiter GC:
Limiterobject garbage collected aftercreate_app()returned due to weak references. Fixed with strong ref inapp.config["_LIMITER"]
Architecture Notes
Mock PDS
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 byFLASK_ENV=testing)PLC_DIRECTORY_URL— points at mock PDS for DID resolution (read at request time inresolve_did())PLATFORM_DOMAIN=127.0.0.1:{port}— makes CLIENT_ID/REDIRECT_URI use HTTPWIKI_TEMPLATE_DIR— pointed at nonexistent path for predictable fallback behavior
