Skip to Content
TestingAPI Testing Guide

API Testing Guide

Scope: apps/api — the Fastify REST + WebSocket server.


Table of Contents

  1. Overview
  2. Test Types
  3. Test Infrastructure
  4. How to Run
  5. Current Coverage
  6. Coverage Target
  7. Known Issues
  8. 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:

TierConfigCountLocationNeeds DB?Speed
Unitvitest.unit.config.ts18 filessrc/__tests__/*.test.tsNoFast (~1–2 min)
Integrationvitest.config.ts43 filessrc/__tests__/**/*.test.tsYesSlow (~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.

FileWhat it covers
jwt.test.tssignAccessToken, verifyAccessToken, appAccessFromRole, edge cases (expired, tampered, wrong secret)
lib-auth.test.tsbearerToken(), apiError(), requireTenantUser(), requireSuperAdmin(), requireDMAccess()
password.test.tshashPassword(), verifyPassword() — PBKDF2 rounds, salt uniqueness, unicode passwords
cors.test.tsCORS allowlist driven by CORS_ORIGINS env var; preflight handling
redis-plugin.test.tsFastify Redis plugin lifecycle — single IORedis instance, decoration, quit on close
socket-plugin.test.tsSocket.IO Fastify plugin — IORedis instance count, namespace registration
socket-auth-middleware.test.tsapplyTenantAuthMiddleware, applyManageAuthMiddleware — JWT validation on socket handshake
socket-chat-handlers.test.tsregisterChatHandlers — chat join/leave/message events with mocked socket and DB
socket-tenant-namespace.test.tsTenant namespace connection, event handler routing
socket-manage-namespace.test.tsManage namespace — tenant watch/unwatch, presence heartbeat, disconnect cleanup
socket-routing.test.tseventsSub routing — agent_events, notifications, rag:status → correct namespaces
manage-presence.test.tsPresence 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)
FileRoutes covered
register.test.tsPOST /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.tsPOST /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.tsTenant list/detail, user CRUD, plans, context, subscriptions, agent runs, invoice reminders, retrigger endpoints
dm.test.tsAll 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.tsPOST /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.tsCross-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

SettingValue
EnginePostgreSQL
Host127.0.0.1:5434
Databaseleadmetrics_api_test
ORMPrisma (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 push

cleanDb() in helpers truncates all tables between test files in dependency order (child tables first).

Helpers

src/__tests__/integration/helpers.ts

ExportPurpose
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

DependencyApproach
@leadmetrics/queuevi.mock() — all enqueue functions return undefined
@leadmetrics/nosqldbvi.mock()writeAuditLog is a no-op
@leadmetrics/provider-razorpayvi.mock() — provider constructor + verifyWebhookSignature
DigitalOcean Spaces (SpacesProvider)vi.mock() — upload/delete/sign return fake URLs
Redisvi.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 APIsvi.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=false

Do not run npx vitest run without --sequence.concurrent=false for 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):

MetricValue
Test files18
Tests passing263
Failing0

Integration tests (pnpm test:integration — requires PostgreSQL):

MetricValue
Test files43
Tests passing842 (99.0%)
Failing0
Skipped9 (media.test.ts — real API keys required)

Route coverage by router:

RouterPrefixIntegration test fileCoverage
auth.ts/auth/v1register.test.ts, auth-profile.test.tsFull
admin/tenants.ts/admin/v1/tenants/**admin-create-tenant, admin-extended, admin-miscFull
admin/users.ts/admin/v1/users/**admin-users, admin-extendedFull
admin/agents.ts/admin/v1/agents/**, /skills/**admin-agents-skills, admin-extendedFull
admin/billing.ts/admin/v1/plans, /invoices/**admin-billing, admin-extendedFull
admin/templates.ts/admin/v1/templates/**admin-email-templates, admin-telegram-templatesFull
admin/audit.ts/admin/v1/audit-logs, /analytics/**admin-audit-logs, admin-miscFull
admin/notifications.ts/admin/v1/notificationsadmin-miscFull
dm//dm/v1/**dm.test.tsFull
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-paymentFull
docs.ts/tenant/v1/docsdocs.test.tsFull
knowledge.ts/tenant/v1/knowledgeknowledge.test.tsFull
media-library.ts/tenant/v1/mediamedia-library.test.tsFull
media.ts/media/v1media.test.tsSkipped (real API keys)
rag-config.ts/admin/v1/rag-configrag-config.test.tsFull
pgcallbacks.ts/pg/v1/webhooks/razorpaypgcallbacks.test.tsFull
blog.ts/tenant/v1/blogblog.test.tsFull
social.ts/tenant/v1/socialsocial.test.tsFull

Unit coverage:

LayerCovered
JWT signing/verificationYes
Auth guards (bearer, requireX)Yes
Password hashingYes
CORS allowlistYes
Redis pluginYes
Socket.IO pluginYes
Socket auth middlewareYes
Socket namespaces (tenant, manage)Yes (unit mocks)
Socket event routingYes
Presence pipelineYes
Media provider routingYes (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:

  1. media.ts real-path — 9 integration tests all skipped due to external API dependencies
  2. Socket namespace edge cases — some event handler branches not exercised
  3. Error branches in billing — Razorpay order creation failure paths
  4. 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) on activity.create() Root cause: Multiple test files reuse the same user/tenant IDs (e.g., "test-super-admin", "test-tenant-user"). The cleanDb() helper truncates tables between files, but when files run concurrently the truncates race with inserts. Mitigation: Always use pnpm test (which runs serially) or pass --sequence.concurrent=false for 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

#GapFile(s) to create/updateNotes
T-1Install and configure @vitest/coverage-v8vitest.config.ts, package.jsonAdd coverage block with thresholds (statements: 90, branches: 85, functions: 90, lines: 90); fail CI if below threshold
T-2Enable coverage CI gate.github/workflows/ or CI configRun pnpm test:coverage on every PR; block merge if below target

Medium Priority

#GapFile(s)Notes
T-3media.ts routes with mocked providersintegration/media-mocked.test.tsMirror media-router.test.ts style (unit mocks) so /media/v1/search and /media/v1/pick are covered in CI without real API keys
T-4handleSubscriptionCharged DB pathintegration/pgcallbacks.test.tsOnce razorpaySubId is stored in paymentGatewayInfo, add a test that verifies the subscription nextBillingDate is updated
T-5handleSubscriptionCancelled DB pathintegration/pgcallbacks.test.tsSame — once the local DB lookup is implemented, add a test that marks the subscription as cancelled
T-6Razorpay order creation error branch in tenant/billing.tsintegration/tenant-misc.test.tsTest POST /billing/invoices/:id/pay when RazorpayProvider throws on createOrder() — should return 500
T-7POST /admin/v1/tenants/:id/retrigger-setup failure pathsintegration/admin-extended.test.tsTest when tenant not found (404), when no tenant member found (email domain = empty string is ok — test boundary)
T-8SSE audit log stream (/admin/v1/audit-logs/live) contentintegration/admin-audit-logs.test.tsCurrently only tests the 200 header; add a test that emits an audit event and verifies it appears in the stream

Low Priority

#GapNotes
T-9Socket 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-10GET /admin/v1/analytics/agents period bucketingAdd tests for day, month, 3month periods to verify correct date window calculation
T-11Subscription lockout audit log contentadmin-billing.test.ts — verify writeAuditLog was called with correct action: "subscription.locked"
T-12Multi-subscription PATCH/DELETE/lockoutadmin-extended.test.ts — the 5 new subscription CRUD routes added in v3 have schema but need behavioural coverage
T-13dm/calendar.ts overview-running contentdm.test.ts — verify correct in_progress activities are returned for the accessible tenant filter
T-14Concurrent cleanDb isolationExtract tenant/user IDs per test file into unique namespaces (e.g., admin-billing- prefix) to safely enable fileParallelism: true in future

© 2026 Leadmetrics — Internal use only