docs: add comprehensive test behavior documentation for all pages

Add behavior documentation covering all SSR and legacy SPA pages:
- Testing strategy and type definitions (unit/integration/UI)
- Dashboard, Invoice, Payment, Transaction, Ledger pages
- Company/Settings, POS, Admin, Search, Auth pages
- Legacy SPA behavior docs (no UI tests until migrated)
- Edge cases, test data requirements, and dependencies per subsystem

Total: 3,600+ lines of behavior documentation to guide test authorship.
This commit is contained in:
2026-05-04 12:15:20 -07:00
parent 2993da5c82
commit b499d460f3
13 changed files with 3785 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
# Authentication Behaviors
## Overview
Authentication in Integreat uses Google OAuth 2.0 as the primary identity provider. Users authenticate via Google, receive a JWT token, and establish a server-side session. The system supports role-based access control (`admin`, `user`, `read-only`), client-scoped permissions, session versioning for SSR rollout, and admin impersonation via signed JWT tokens.
## Routes & Pages
| Route | Method | Handler | Purpose |
|-------|--------|---------|---------|
| `/login` | GET | `:login` (SPA) | Legacy login page |
| `/needs-activation` | GET | `:needs-activation` (SPA) | Account pending activation page |
| `/api/oauth` | GET | `:oauth` | Google OAuth callback, creates/updates user, establishes session |
| `/logout` | GET | `:logout` | Clears session, redirects to `/login` |
| `/impersonate` | GET | `:impersonate` | Admin-only: assumes another user's identity via JWT |
## Behaviors
### Unit Tests
- `user->jwt` returns nil when user or oauth-token is nil
- `user->jwt` includes `:user`, `:exp` (30 days), `:db/id`, `:user/role`, `:user/name` for all users
- `user->jwt` includes `:gz-clients` (gzip-compressed client list with code, id, locations) for admin and read-only users
- `user->jwt` includes `:user/clients` (plain client list) for non-admin, non-read-only users
- `user->jwt` excludes `:exp` from the returned JWT payload (expiration is handled by buddy.sign.jwt)
- `gzip` compresses and base64-encodes a Clojure data structure
- `gunzip` reverses `gzip`, restoring the original data structure
- `make-api-token` creates a JWT with `:user "API"`, `:user/role "admin"`, `:user/name "API"`, expiring in 1000 days
- `logout` returns 301 redirect to `/login` with empty session map
- `impersonate` unsigns JWT from query param, dissoc's `:exp`, and returns 200 with new session
- `impersonate` requires `wrap-secure` and `wrap-admin` middleware (enforced in route registration)
- `wrap-secure` allows authenticated requests to proceed
- `wrap-secure` returns 401 with `hx-redirect` to `/login` for HTMX requests without authentication
- `wrap-secure` returns 302 redirect to `/login` with `redirect-to` param for normal requests without authentication
- `wrap-admin` allows admin requests to proceed
- `wrap-admin` returns 302 redirect to `/login` for non-admin requests
- `wrap-client-redirect-unauthenticated` converts 401 responses to HTMX redirects for unauthenticated users
- `wrap-session-version` invalidates sessions with mismatched version, redirecting to `/login`
- `wrap-session-version` allows valid sessions to proceed
- `wrap-session-version` returns 401 for GraphQL requests with invalid session version
### Integration Tests
- `GET /api/oauth` with valid Google `code` exchanges code for access token
- OAuth handler fetches Google user profile using access token
- `users/find-or-insert!` creates new user if not exists (Google provider, id, email, name, picture)
- `users/find-or-insert!` returns existing user if already present
- Successful OAuth redirects to `state` param (or `/`) with JWT in query string
- Successful OAuth establishes server session with `:identity` and `:version`
- Failed OAuth (invalid code, network error) returns 401 with error message
- Failed OAuth logs warning via mulog
- `GET /logout` clears session and redirects to `/login`
- `GET /impersonate?jwt=<token>` with admin session unsigns JWT and sets `:identity` in session
- Impersonation JWT must be signed with `:jwt-secret` and `:hs512` algorithm
- Non-admin requests to `/impersonate` receive 302 redirect to `/login`
- Unauthenticated requests to `/impersonate` receive redirect to `/login`
- Session includes `:version` matching `session-version/current-session-version`
- Outdated session version triggers redirect to `/login` on normal routes
- Outdated session version triggers `hx-redirect` to `/login` on HTMX routes
- Outdated session version returns 401 for GraphQL routes
### UI Test Behaviors (SSR)
#### Happy Path: OAuth Login
1. Unauthenticated user navigates to `/login`
2. User clicks "Sign in with Google" button
3. Browser redirects to Google OAuth consent screen
4. User consents and Google redirects to `/api/oauth?code=<code>&state=<state>`
5. Server exchanges code for token, fetches profile, finds/creates user
6. Server redirects to original page (or `/`) with `?jwt=<token>`
7. Client page reads JWT and establishes session
8. User is authenticated and sees their clients/data
#### Logout
1. Authenticated user clicks logout link
2. Browser navigates to `/logout`
3. Session is cleared
4. Browser redirects to `/login`
5. User is unauthenticated; subsequent requests redirect to login
#### Impersonation (Admin)
1. Admin user navigates to `/impersonate?jwt=<signed-jwt>`
2. Server validates JWT signature and extracts identity
3. Session is updated to impersonated user's identity
4. Admin sees the application as the impersonated user
5. Admin's original session is replaced (no stack)
#### Unauthenticated Access Attempt
1. Unauthenticated user navigates to a protected page (e.g., `/`)
2. `wrap-secure` detects missing authentication
3. Browser redirects to `/login?redirect-to=<original-path>`
4. After successful OAuth, user is redirected back to original page
#### HTMX Unauthenticated Request
1. Authenticated user's session expires
2. HTMX request fires (e.g., card refresh)
3. `wrap-secure` detects missing authentication
4. Response includes `hx-redirect: /login`
5. HTMX redirects the browser to login page
#### Session Version Mismatch
1. User has old session (version != current)
2. User navigates to any SSR route
3. `wrap-session-version` detects mismatch
4. Session is invalidated
5. User is redirected to `/login`
## Edge Cases
### OAuth
- **Missing code parameter**: Returns 401 "Couldn't authenticate"
- **Invalid/expired code**: Google token endpoint returns error; handler catches exception and returns 401
- **Network failure to Google**: Exception caught, logged, 401 returned
- **Missing state parameter**: Redirects to `/` (fallback)
- **User without email**: Google profile may lack email; `find-or-insert!` handles via provider-id
- **Large client list for admin**: `:gz-clients` compresses client data to fit in JWT
### Session & Permissions
- **Session without version**: Treated as outdated by `wrap-session-version`
- **Session with nil identity**: Treated as unauthenticated by `wrap-secure`
- **Admin with no clients**: Still gets `:gz-clients` (empty list compressed)
- **User role with no clients**: Gets `:user/clients` as empty vector
- **Read-only user**: Gets `:gz-clients` like admin, but role is `"read-only"`
- **JWT tampering**: `jwt/unsign` fails with invalid signature; impersonation fails
- **Expired impersonation JWT**: `jwt/unsign` rejects expired token
### Logout
- **Logout without active session**: Still clears session and redirects to login
- **Double logout**: Idempotent; session remains empty
## Test Data Requirements
### Users
- User with `:user/role :user.role/admin`, multiple clients
- User with `:user/role :user.role/user`, subset of clients
- User with `:user/role :user.role/read-only`, multiple clients
- New user (not yet in database) with Google provider details
- Existing user with Google provider details
### Clients
- Multiple clients with `:client/code`, `:client/name`, `:client/locations`
- Client associations on users
### OAuth Mock
- Mock Google token endpoint responses (success and failure)
- Mock Google userinfo endpoint responses
## Dependencies
### External Services
- **Google OAuth 2.0**: Authorization code exchange and userinfo retrieval
- **Buddy (JWT)**: Token signing/unsigning with HS512
- **Datomic**: User lookup and creation via `users/find-or-insert!`
### Frontend Libraries
- **Legacy SPA**: Login and needs-activation pages are legacy SPA routes (no SSR tests)
### Middleware Stack
- `wrap-secure`: Authentication gate
- `wrap-admin`: Admin-only gate
- `wrap-client-redirect-unauthenticated`: Converts 401 to redirects
- `wrap-session-version`: Invalidates outdated sessions during SSR rollout
### Related Subsystems
- **Users**: `auto-ap.datomic.users` handles user CRUD
- **All SSR Pages**: Every protected route depends on auth middleware