API Testing Guide
Scope:
apps/api— the Fastify REST + WebSocket server.
Table of Contents
- Overview
- Test Types
- Test Infrastructure
- How to Run
- Current Coverage
- Coverage Target
- Known Issues
- Backlog — Coverage Gaps
Overview
The API test suite uses Vitest v2 with a Node.js environment. Tests are split into two tiers with separate Vitest configs:
| Tier | Config | Count | Location | Needs DB? | Speed |
|---|---|---|---|---|---|
| Unit | vitest.unit.config.ts | 18 files | src/__tests__/*.test.ts | No | Fast (~1–2 min) |
| Integration | vitest.config.ts | 43 files | src/__tests__/**/*.test.ts | Yes | Slow (~3–30 s per file) |
Unit tests run in CI (pnpm test:unit). Integration tests require a live PostgreSQL instance and are run separately (pnpm test:integration).
Integration files run serially (fileParallelism: false) because they share a single PostgreSQL test database and concurrent writes cause deadlocks.
Test Types
Unit Tests
Pure function tests that mock all external dependencies. No database, no HTTP server, no Redis.
| File | What it covers |
|---|---|
jwt.test.ts | signAccessToken, verifyAccessToken, appAccessFromRole, edge cases (expired, tampered, wrong secret) |
lib-auth.test.ts | bearerToken(), apiError(), requireTenantUser(), requireSuperAdmin(), requireDMAccess() |
password.test.ts | hashPassword(), verifyPassword() — PBKDF2 rounds, salt uniqueness, unicode passwords |
cors.test.ts | CORS allowlist driven by CORS_ORIGINS env var; preflight handling |
redis-plugin.test.ts | Fastify Redis plugin lifecycle — single IORedis instance, decoration, quit on close |
socket-plugin.test.ts | Socket.IO Fastify plugin — IORedis instance count, namespace registration |
socket-auth-middleware.test.ts | applyTenantAuthMiddleware, applyManageAuthMiddleware — JWT validation on socket handshake |
socket-chat-handlers.test.ts | registerChatHandlers — chat join/leave/message events with mocked socket and DB |
socket-tenant-namespace.test.ts | Tenant namespace connection, event handler routing |
socket-manage-namespace.test.ts | Manage namespace — tenant watch/unwatch, presence heartbeat, disconnect cleanup |
socket-routing.test.ts | eventsSub routing — agent_events, notifications, rag:status → correct namespaces |
manage-presence.test.ts | Presence list via Redis pipeline (not sequential EXISTS calls), TTL filtering, UID dedup |
media-router.test.ts | /media/v1/search and /media/v1/pick with mocked Pixabay, Unsplash, Spaces providers |
Integration Tests
Each integration test:
- Spins up a full Fastify app via
createApp() - Sends requests via
app.inject()(no real TCP) - Hits the real PostgreSQL test database (
leadmetrics_api_test) - Mocks only external services (queue, nosqldb, S3/Spaces, Razorpay)
| File | Routes covered |
|---|---|
register.test.ts | POST /auth/v1/register/start, /step/2, /complete, /login |
auth-profile.test.ts | /auth/v1/me, /refresh, /logout, /forgot-password, /reset-password, /me/password |
admin-create-tenant.test.ts | POST /admin/v1/tenants — no plan, predefined plan, custom plan, India tax fields |
admin-users.test.ts | /admin/v1/users CRUD, /users/:id/tenants assign/remove |
admin-agents-skills.test.ts | /admin/v1/agents/**, /skills/**, /agents/:id/skills |
admin-billing.test.ts | /admin/v1/plans, /offerings, /regions, /invoices/**, /tenants/:id/subscription*, lockout |
admin-email-templates.test.ts | /admin/v1/templates/email CRUD |
admin-telegram-templates.test.ts | /admin/v1/templates/telegram CRUD |
admin-audit-logs.test.ts | /admin/v1/audit-logs, /audit-logs/live (SSE) |
admin-activities.test.ts | /admin/v1/activities, /approvals, /overview-stats |
admin-misc.test.ts | /admin/v1/stats, /analytics/agents, /notifications, /tenants/:id detail |
admin-extended.test.ts | Tenant list/detail, user CRUD, plans, context, subscriptions, agent runs, invoice reminders, retrigger endpoints |
dm.test.ts | All 24 DM portal routes (/dm/v1/**) — tenant switch, users, agents, blog, social, calendar, overview |
blog.test.ts | /tenant/v1/blog/** |
social.test.ts | /tenant/v1/social/** |
calendar.test.ts | /tenant/v1/calendar |
brand-assets.test.ts | /tenant/v1/brand-assets/** |
media-library.test.ts | /tenant/v1/media/** |
docs.test.ts | /tenant/v1/docs/** |
knowledge.test.ts | /tenant/v1/knowledge/** (RAG datasets, files, crawl) |
rag-config.test.ts | /admin/v1/rag-config |
tenant-misc.test.ts | /tenant/v1/agents, /billing/**, /company-details, /activity-log, deliverable plan periods |
tenant-pipeline.test.ts | /tenant/v1/context, /strategy, /deliverable-plan — full approval flows |
tenant-notification-prefs.test.ts | /tenant/v1/notification-preferences GET + PUT |
notifications.test.ts | /tenant/v1/notifications — cursor pagination, unread filtering |
pgcallbacks.test.ts | POST /pg/v1/webhooks/razorpay — all event types, signature verification |
billing-payment.test.ts | /tenant/v1/billing/invoices/:id/pay, signature guard (C-1 regression) |
presence.test.ts | /tenant/v1/presence, /chat/unread with Redis mock |
security.test.ts | Cross-cutting security regressions (H-4 country context, H-5 Spaces env, H-6 queue errors, M-5 typed DB, M-8 duplicate email) |
media.test.ts | /media/v1/** with real Pixabay/Unsplash/Spaces — all 9 tests skipped (see Known Issues) |
Test Infrastructure
Database
| Setting | Value |
|---|---|
| Engine | PostgreSQL |
| Host | 127.0.0.1:5434 |
| Database | leadmetrics_api_test |
| ORM | Prisma (via @leadmetrics/db) |
The test database is a separate instance from development. It must be created and migrated before running tests:
# One-time setup (from packages/db/)
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5434/leadmetrics_api_test" \
npx prisma db pushcleanDb() in helpers truncates all tables between test files in dependency order (child tables first).
Helpers
src/__tests__/integration/helpers.ts
| Export | Purpose |
|---|---|
createApp() | Builds a full Fastify instance (all routes registered) |
getTestDb() | Returns a PrismaClient pointed at the test DB |
makeAdminToken(userId?) | JWT for a super_admin; default userId = "test-super-admin" |
makeNonAdminToken(userId?) | JWT for an admin; default userId = "test-regular-admin" |
makeTenantToken(tenantId, userId?) | JWT for a tenant admin; default userId = "test-tenant-user" |
cleanDb(db) | Truncates all tables in safe dependency order |
Mocking Strategy
| Dependency | Approach |
|---|---|
@leadmetrics/queue | vi.mock() — all enqueue functions return undefined |
@leadmetrics/nosqldb | vi.mock() — writeAuditLog is a no-op |
@leadmetrics/provider-razorpay | vi.mock() — provider constructor + verifyWebhookSignature |
DigitalOcean Spaces (SpacesProvider) | vi.mock() — upload/delete/sign return fake URLs |
| Redis | vi.mock() in unit tests; real connection in integration tests (via test Redis) |
Rate-limiting (@fastify/rate-limit) | Bypassed via allowList: () => process.env.NODE_ENV === "test" — prevents shared Redis counter from accumulating across test files and triggering spurious 429s |
| External image/search APIs | vi.mock() in media-router.test.ts; real calls skipped in media.test.ts |
How to Run
All commands from apps/api/ or via turbo from repo root:
# Unit tests only — no DB required (CI)
pnpm test:unit
# or from repo root:
pnpm turbo run test:unit --filter=@leadmetrics/api
# Integration tests — requires PostgreSQL test DB
pnpm test:integration
# Watch mode (unit tests)
pnpm test:watch
# Run a single integration file
npx vitest run src/__tests__/integration/admin-billing.test.ts
# Run multiple files
npx vitest run src/__tests__/integration/dm.test.ts src/__tests__/jwt.test.ts
# Coverage report
pnpm test:coverage
# Force sequential when running ad-hoc integration subsets (avoids DB conflicts)
npx vitest run --sequence.concurrent=falseDo not run
npx vitest runwithout--sequence.concurrent=falsefor integration tests — the default allows file-level parallelism and causes DB constraint violations.
The pnpm test:integration script uses vitest.config.ts which enforces fileParallelism: false.
Current Coverage
As of May 2026:
Unit tests (pnpm test:unit — no DB, runs in CI):
| Metric | Value |
|---|---|
| Test files | 18 |
| Tests passing | 263 |
| Failing | 0 |
Integration tests (pnpm test:integration — requires PostgreSQL):
| Metric | Value |
|---|---|
| Test files | 43 |
| Tests passing | 842 (99.0%) |
| Failing | 0 |
| Skipped | 9 (media.test.ts — real API keys required) |
Route coverage by router:
| Router | Prefix | Integration test file | Coverage |
|---|---|---|---|
| auth.ts | /auth/v1 | register.test.ts, auth-profile.test.ts | Full |
| admin/tenants.ts | /admin/v1/tenants/** | admin-create-tenant, admin-extended, admin-misc | Full |
| admin/users.ts | /admin/v1/users/** | admin-users, admin-extended | Full |
| admin/agents.ts | /admin/v1/agents/**, /skills/** | admin-agents-skills, admin-extended | Full |
| admin/billing.ts | /admin/v1/plans, /invoices/** | admin-billing, admin-extended | Full |
| admin/templates.ts | /admin/v1/templates/** | admin-email-templates, admin-telegram-templates | Full |
| admin/audit.ts | /admin/v1/audit-logs, /analytics/** | admin-audit-logs, admin-misc | Full |
| admin/notifications.ts | /admin/v1/notifications | admin-misc | Full |
| dm/ | /dm/v1/** | dm.test.ts | Full |
| tenant/main.ts | /tenant/v1/** | tenant-misc, tenant-pipeline, blog, social, calendar, brand-assets, etc. | Full |
| tenant/billing.ts | /tenant/v1/billing/** | tenant-misc, billing-payment | Full |
| docs.ts | /tenant/v1/docs | docs.test.ts | Full |
| knowledge.ts | /tenant/v1/knowledge | knowledge.test.ts | Full |
| media-library.ts | /tenant/v1/media | media-library.test.ts | Full |
| media.ts | /media/v1 | media.test.ts | Skipped (real API keys) |
| rag-config.ts | /admin/v1/rag-config | rag-config.test.ts | Full |
| pgcallbacks.ts | /pg/v1/webhooks/razorpay | pgcallbacks.test.ts | Full |
| blog.ts | /tenant/v1/blog | blog.test.ts | Full |
| social.ts | /tenant/v1/social | social.test.ts | Full |
Unit coverage:
| Layer | Covered |
|---|---|
| JWT signing/verification | Yes |
| Auth guards (bearer, requireX) | Yes |
| Password hashing | Yes |
| CORS allowlist | Yes |
| Redis plugin | Yes |
| Socket.IO plugin | Yes |
| Socket auth middleware | Yes |
| Socket namespaces (tenant, manage) | Yes (unit mocks) |
| Socket event routing | Yes |
| Presence pipeline | Yes |
| Media provider routing | Yes (mocked providers) |
Estimated line coverage: ~85–88% (exact figure requires @vitest/coverage-v8 install — see Backlog).
Coverage Target
Goal: 90–95% line/statement coverage across the codebase.
The gap between current (~85–88%) and target (~90–95%) is concentrated in:
- media.ts real-path — 9 integration tests all skipped due to external API dependencies
- Socket namespace edge cases — some event handler branches not exercised
- Error branches in billing — Razorpay order creation failure paths
handleSubscriptionCharged/handleSubscriptionCancelled— DB lookup stub (not yet implemented)
Known Issues
1. Parallel DB conflicts when running integration tests concurrently
Status: Mitigated by fileParallelism: false in vitest.config.ts
Symptom: When integration tests run in parallel (e.g., during ad-hoc npx vitest run without the config), you see:
Unique constraint failed on the fields: (userId, tenantId)deadlock detected (40P01)onactivity.create()Root cause: Multiple test files reuse the same user/tenant IDs (e.g.,"test-super-admin","test-tenant-user"). ThecleanDb()helper truncates tables between files, but when files run concurrently the truncates race with inserts. Mitigation: Always usepnpm test(which runs serially) or pass--sequence.concurrent=falsefor ad-hoc subsets.
2. media.test.ts — 9 tests skipped
Status: By design
Reason: These tests call real Pixabay and Unsplash APIs and upload to a real DigitalOcean Spaces bucket. They require PIXABAY_API_KEY, UNSPLASH_ACCESS_KEY, DO_SPACES_KEY, DO_SPACES_SECRET, and DO_SPACES_ENDPOINT to be set.
How to run locally: Set those env vars and remove the .skip from the describe blocks in media.test.ts.
Backlog — Coverage Gaps
Items tracked here represent missing tests or test infrastructure improvements. They are ordered by impact.
High Priority
| # | Gap | File(s) to create/update | Notes |
|---|---|---|---|
| T-1 | Install and configure @vitest/coverage-v8 | vitest.config.ts, package.json | Add coverage block with thresholds (statements: 90, branches: 85, functions: 90, lines: 90); fail CI if below threshold |
| T-2 | Enable coverage CI gate | .github/workflows/ or CI config | Run pnpm test:coverage on every PR; block merge if below target |
Medium Priority
| # | Gap | File(s) | Notes |
|---|---|---|---|
| T-3 | media.ts routes with mocked providers | integration/media-mocked.test.ts | Mirror media-router.test.ts style (unit mocks) so /media/v1/search and /media/v1/pick are covered in CI without real API keys |
| T-4 | handleSubscriptionCharged DB path | integration/pgcallbacks.test.ts | Once razorpaySubId is stored in paymentGatewayInfo, add a test that verifies the subscription nextBillingDate is updated |
| T-5 | handleSubscriptionCancelled DB path | integration/pgcallbacks.test.ts | Same — once the local DB lookup is implemented, add a test that marks the subscription as cancelled |
| T-6 | Razorpay order creation error branch in tenant/billing.ts | integration/tenant-misc.test.ts | Test POST /billing/invoices/:id/pay when RazorpayProvider throws on createOrder() — should return 500 |
| T-7 | POST /admin/v1/tenants/:id/retrigger-setup failure paths | integration/admin-extended.test.ts | Test when tenant not found (404), when no tenant member found (email domain = empty string is ok — test boundary) |
| T-8 | SSE audit log stream (/admin/v1/audit-logs/live) content | integration/admin-audit-logs.test.ts | Currently only tests the 200 header; add a test that emits an audit event and verifies it appears in the stream |
Low Priority
| # | Gap | Notes |
|---|---|---|
| T-9 | Socket integration tests (end-to-end, not unit) | Spin up a real Socket.IO client against a test server; test handshake → auth → event flow. High effort, high confidence value. |
| T-10 | GET /admin/v1/analytics/agents period bucketing | Add tests for day, month, 3month periods to verify correct date window calculation |
| T-11 | Subscription lockout audit log content | admin-billing.test.ts — verify writeAuditLog was called with correct action: "subscription.locked" |
| T-12 | Multi-subscription PATCH/DELETE/lockout | admin-extended.test.ts — the 5 new subscription CRUD routes added in v3 have schema but need behavioural coverage |
| T-13 | dm/calendar.ts overview-running content | dm.test.ts — verify correct in_progress activities are returned for the accessible tenant filter |
| T-14 | Concurrent cleanDb isolation | Extract tenant/user IDs per test file into unique namespaces (e.g., admin-billing- prefix) to safely enable fileParallelism: true in future |