Testing Guide
Testing Guide
Section titled “Testing Guide”Overview
Section titled “Overview”Backend: 458 Vitest tests across 40+ files, ~22s run time.
Frontend: Vitest unit tests in frontend/src/__tests__/.
Running Tests
Section titled “Running Tests”Backend
Section titled “Backend”cd backendnpm test # run all tests oncenpm run test:watch # watch mode — re-runs on file savenpm run test:cover # with Istanbul coverage reportFrontend
Section titled “Frontend”cd frontendnpm testBackend Test Layout
Section titled “Backend Test Layout”backend/src/__tests__/├── helpers/│ └── testDb.js # Supabase mock factory├── analytics.trend.test.js├── appointments.test.js├── authMiddleware.test.js├── auth.test.js├── authRegister.test.js├── encryption.test.js├── inviteCodes.test.js├── lemonsqueezyWebhook.test.js├── phoneRateLimit.test.js├── requireActiveSubscription.test.js├── subscriptionLimits.test.js├── superadmin_schema.test.js├── tenantIsolation.test.js├── tenantScopingGuard.test.js├── validators.test.js├── attendance/│ ├── aiVerificationService.test.js│ ├── checkinToken.test.js│ ├── confirmationDispatcher.test.js│ ├── noShowTracker.test.js│ ├── qrCheckinService.test.js│ └── staffSummaryService.test.js├── chatbot/│ ├── bookingFlow.test.js│ ├── cancellationFlow.test.js│ ├── concurrency.test.js│ ├── confirmationCard.test.js│ ├── conversationEngine.test.js│ ├── messageHandler.test.js│ ├── sessionStore.test.js│ └── welcomeMessage.test.js├── middleware/│ └── requireDentalFeature.test.js├── routes/│ ├── auth.passwordReset.test.js│ ├── auth.test.js│ ├── superadmin.test.js│ └── dental/│ ├── paymentPlans.test.js│ ├── recallCampaigns.test.js│ └── treatmentPlans.test.js└── whatsapp/ ├── adapter.test.js ├── metaProvisioning.test.js ├── metaSend.test.js ├── webhook.test.js └── whatsappOnboarding.test.jsWriting a New Test
Section titled “Writing a New Test”Route test pattern
Section titled “Route test pattern”import { describe, it, expect, vi, beforeEach } from 'vitest';import request from 'supertest';import app from '../../app.js';import jwt from 'jsonwebtoken';
// Mock Supabase so tests never hit the real DBvi.mock('../../db/supabase.js', () => ({ supabaseAdmin: { from: vi.fn().mockReturnThis(), select: vi.fn().mockReturnThis(), eq: vi.fn().mockReturnThis(), single: vi.fn().mockResolvedValue({ data: { id: 'clinic-1', is_active: true, clinics: { is_active: true } }, error: null }), insert: vi.fn().mockReturnThis(), update: vi.fn().mockReturnThis(), }, supabase: { from: vi.fn().mockReturnThis(), select: vi.fn().mockReturnThis(), },}));
function makeToken(overrides = {}) { return jwt.sign( { userId: 'user-1', clinicId: 'clinic-1', role: 'admin', subscriptionPlan: 'pro', subscriptionExpiresAt: null, ...overrides, }, process.env.JWT_SECRET || 'test-secret-at-least-32-characters-long' );}
describe('GET /api/doctors', () => { it('returns 200 for authenticated admin', async () => { const res = await request(app) .get('/api/doctors') .set('Authorization', `Bearer ${makeToken()}`); expect(res.status).toBe(200); });
it('returns 401 with no token', async () => { const res = await request(app).get('/api/doctors'); expect(res.status).toBe(401); });});Webhook signature test pattern
Section titled “Webhook signature test pattern”import crypto from 'node:crypto';import request from 'supertest';import app from '../../app.js';
function signBody(body, secret) { return 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');}
it('returns 200 on invalid signature (Meta must never get non-200)', async () => { const body = JSON.stringify({ entry: [] }); const res = await request(app) .post('/webhooks/whatsapp') .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', 'sha256=bad') .send(body); // Meta retries any non-200. Return 200 even on rejection. expect(res.status).toBe(200);});
it('processes valid signed webhook', async () => { const body = JSON.stringify({ entry: [{ changes: [] }] }); const sig = signBody(body, process.env.WHATSAPP_APP_SECRET || 'test-secret'); const res = await request(app) .post('/webhooks/whatsapp') .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', sig) .send(body); expect(res.status).toBe(200);});Environment for Tests
Section titled “Environment for Tests”Tests run with .env.test loaded automatically via --env-file=.env.test in npm test.
Required .env.test values:
JWT_SECRET=test-secret-at-least-32-characters-longENCRYPTION_KEY=test-encryption-key-exactly-32-charsSUPER_ADMIN_KEY=test-super-admin-key-16chRESEND_API_KEY=re_test_placeholderNODE_ENV=testTest Design Principles
Section titled “Test Design Principles”Test behavior, not implementation. Test what the route returns, not which Supabase methods it calls. A refactor shouldn’t break tests unless behavior changed.
Happy path + at least one failure path. For any new route: test 200 success, test 401 unauthenticated, and test one domain-specific error (missing required field, resource not found, or plan gate).
Mock at the boundary. Mock Supabase and Redis (external systems). Don’t mock Express middleware or service functions — let those run so you’re testing real integration.
One assertion per test case. If two things need to be true, write two tests. A test named “records payment and updates balance” is really two tests.
Avoid testing framework code. Don’t test that authenticateToken works — trust it. Test that your route rejects unauthenticated requests.
Coverage
Section titled “Coverage”Run npm run test:cover to generate an Istanbul report. Focus coverage on:
- Route handlers — all status code branches (200, 400, 401, 403, 404)
- Service functions with real logic (availability engine, conversation engine)
- Idempotency-critical paths (payment reminders, recall sends)
- Webhook signature validation
Low-value coverage targets (don’t chase these):
- Simple pass-through getter routes with no branching
- Logger calls
- Environment variable loading in
index.js