Skip to content

Testing Guide

Backend: 458 Vitest tests across 40+ files, ~22s run time. Frontend: Vitest unit tests in frontend/src/__tests__/.


Terminal window
cd backend
npm test # run all tests once
npm run test:watch # watch mode — re-runs on file save
npm run test:cover # with Istanbul coverage report
Terminal window
cd frontend
npm test

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.js

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 DB
vi.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);
});
});
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);
});

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-long
ENCRYPTION_KEY=test-encryption-key-exactly-32-chars
SUPER_ADMIN_KEY=test-super-admin-key-16ch
RESEND_API_KEY=re_test_placeholder
NODE_ENV=test

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.


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