Test strategy
Source of truth: ../plan/standards.md §1. This file expands the strategy and is the doc to extend as the test surface grows.
Layers
| Layer | Tooling | Scope |
|---|---|---|
| Unit | Vitest | Pure utilities, validators, schema parsers, scope checkers, rate-limit math, signed-URL helpers, phone normalisers, Meta error mappers |
| Integration | Vitest + testcontainers Postgres | Tool handlers, Inngest functions, webhook routes, auth middleware. Real ephemeral Postgres, not DB mocks |
| End-to-end (selective) | Vitest + supertest against a running app | Critical flows: webhook → message persisted → notification routed; send → mocked Meta API → row written |
| Cross-tenant isolation | Vitest + testcontainers | Two clients with disjoint grants; assert client B cannot read/write client A’s rows via any tool, scope, or grant path |
| Webhook signature | Vitest | Valid signature passes; tampered body → 404; missing header → 404; replay (same wamid) → idempotent no-op |
Coverage gates
Enforced in vitest.config.ts.
| Directory / file | Minimum |
|---|---|
src/ (overall) | 80% |
src/auth/ | 95% |
src/webhook/verify-signature.ts | 100% |
src/db/scoped.ts | 95% |
src/audit/ | 95% |
src/media/signed-url.ts | 100% |
A PR that drops coverage below threshold fails CI.
What we never mock
- Postgres. Use testcontainers. Mocks of DBs have a long history of letting integration bugs reach production.
- HMAC / crypto primitives. Test the real
crypto.timingSafeEqualand friends; the wrappers around them are the security boundary. - Meta error code → typed error mapping. Real strings, real integers, real shapes.
What we always mock
- Meta Cloud API HTTP boundary.
mswinterceptor at thefetchlayer. The wire calls are slow, rate-limited, and have real-world side effects. - Filesystem writes outside
MEDIA_ROOT.memfsfor anything that touches arbitrary host paths. - Time for sliding-window tests.
vi.useFakeTimers().
When to write what
| Change | Required test |
|---|---|
| New function | At least one unit test before the function ships |
| New tool | Integration test against testcontainers Postgres: auth, scope, grant, rate-limit interaction, happy path |
| New Inngest function | Integration test asserting idempotency and retry semantics |
| Bug fix | A failing test that reproduces the bug first, then the fix that turns it green |
Running tests
| Command | What it runs |
|---|---|
pnpm test | unit tests only — fast, no Docker |
pnpm test:integration | integration tests; testcontainers Postgres |
pnpm test:ci | everything + coverage |
pnpm test:watch | unit tests, watch mode |
CI
GitHub Actions runs pnpm test:ci on every PR. The coverage gate is reported per-PR and blocks merge if any threshold drops. docs/reference/ is gitignored generated output — no CI regen gate; a future docs project will own it.