Testing Guide
Testing Guide
This document covers the test framework, patterns, and conventions used in the Groundtruth Platform.
Framework
The web app uses Vitest as its test framework. Vitest is compatible with Jest APIs but significantly faster due to native ESM support and Vite-based transforms.
Configuration is in apps/web/vitest.config.ts (or equivalent in package.json).
Running Tests
All commands are run from the apps/web directory:
# Watch mode -- re-runs affected tests on file changes
npm test
# Single run (CI mode) -- runs all tests once and exits
npm test -- --run
# With coverage report
npm test -- --coverage
# Run a specific test file
npm test -- --run src/__tests__/api-keys/api-key-management.test.ts
# Run tests in a specific directory
npm test -- --run src/__tests__/enterprise/
# Run tests matching a name pattern
npm test -- --run -t "should reject unauthorized"Test Count and Organization
The codebase contains 299 tests across 21 test directories in apps/web/src/__tests__/:
| Directory | Tests | Coverage Area |
|---|---|---|
analytics/ | Quality analytics, pattern analysis | |
api-keys/ | API key management, API key auth | |
attachments/ | Upload, management, manifest generation, storage limits | |
cache/ | Redis caching behavior | |
deliverables/ | Approval workflows | |
email/ | Email dispatch | |
enterprise/ | RBAC enforcement, team management, tenant config (SSO, branding) | |
export/ | PDF generation, markdown parsing, branding in exports, portal export | |
gdpr/ | Data export, account deletion, data retention | |
helpers/ | Test utility functions | |
integration/ | Multi-step workflow tests | |
isolation/ | Tenant isolation (billing, engagement, run routes) | |
observability/ | Health endpoint | |
portal/ | Portal auth, portal comments | |
quality/ | Quality scores | |
streaming/ | SSE proxy, event parsing | |
mission-control/ | Mission control types, constants, state derivation, event parsing | |
templates/ | Template management | |
webhooks/ | Webhook dispatch, webhook management | |
| Root | Rate limiting |
A shared test setup file at src/__tests__/setup.ts configures global mocks and test utilities.
Test Patterns
1. API Route Tests
The most common test type. These test Next.js API route handlers by constructing mock Request objects and asserting on the Response.
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before importing the route
vi.mock('@/lib/prisma', () => ({
default: {
engagement: {
findMany: vi.fn(),
create: vi.fn(),
},
},
}));
vi.mock('@/lib/auth', () => ({
getAuthenticatedUser: vi.fn(),
}));
import { GET, POST } from '@/app/api/engagements/route';
import prisma from '@/lib/prisma';
import { getAuthenticatedUser } from '@/lib/auth';
describe('GET /api/engagements', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return engagements for the authenticated tenant', async () => {
// Arrange
(getAuthenticatedUser as any).mockResolvedValue({
id: 'user-1',
tenantId: 'tenant-1',
role: 'admin',
});
(prisma.engagement.findMany as any).mockResolvedValue([
{ id: 'eng-1', name: 'Test Engagement', tenantId: 'tenant-1' },
]);
const request = new Request('http://localhost:3000/api/engagements');
// Act
const response = await GET(request);
const data = await response.json();
// Assert
expect(response.status).toBe(200);
expect(data).toHaveLength(1);
expect(prisma.engagement.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { tenantId: 'tenant-1' },
})
);
});
it('should return 401 when not authenticated', async () => {
(getAuthenticatedUser as any).mockResolvedValue(null);
const request = new Request('http://localhost:3000/api/engagements');
const response = await GET(request);
expect(response.status).toBe(401);
});
});Key points:
- Mock
@/lib/prismato control database responses - Mock
@/lib/authto simulate authenticated/unauthenticated states - Test both success and error paths
- Verify that tenant scoping is applied to all queries
2. Auth and Tenant Isolation Tests
These verify that requests are properly scoped to the authenticated tenant and that cross-tenant access is blocked.
it('should not return engagements from other tenants', async () => {
(getAuthenticatedUser as any).mockResolvedValue({
id: 'user-1',
tenantId: 'tenant-1',
role: 'admin',
});
const request = new Request('http://localhost:3000/api/engagements/eng-other');
const response = await GET(request, { params: { id: 'eng-other' } });
// The engagement belongs to tenant-2, so tenant-1 should get 404
expect(response.status).toBe(404);
});3. RBAC Tests
Verify that role-based access control is enforced per route.
it('should reject member trying to manage team', async () => {
(getAuthenticatedUser as any).mockResolvedValue({
id: 'user-1',
tenantId: 'tenant-1',
role: 'member', // Members cannot manage team
});
const request = new Request('http://localhost:3000/api/team', {
method: 'POST',
body: JSON.stringify({ email: 'new@example.com', role: 'member' }),
});
const response = await POST(request);
expect(response.status).toBe(403);
});4. Integration Tests
Multi-step workflow tests that verify end-to-end behavior across multiple API calls.
Located in src/__tests__/integration/, these tests simulate real user workflows:
- Create an engagement, then start a run, then check status
- Create a template, then create an engagement from that template
- Upload an attachment, then verify manifest generation
5. Webhook and Event Tests
Test webhook dispatch (HMAC-SHA256 signed payloads) and event handling.
it('should sign webhook payload with HMAC-SHA256', async () => {
// Verify the signature header matches the expected HMAC
const payload = JSON.stringify({ event: 'engagement.completed', data: {...} });
const signature = createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
expect(deliveryRecord.signature).toBe(`sha256=${signature}`);
});Mocking Strategy
Prisma (Database)
Every test file that touches the database mocks @/lib/prisma:
vi.mock('@/lib/prisma', () => ({
default: {
engagement: { findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn(), update: vi.fn() },
run: { findMany: vi.fn(), create: vi.fn(), update: vi.fn() },
deliverable: { findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn() },
// ... other models as needed
},
}));Supabase Auth
vi.mock('@/lib/supabase', () => ({
createClient: vi.fn(() => ({
auth: {
getUser: vi.fn(),
signInWithPassword: vi.fn(),
},
})),
}));Stripe
vi.mock('@/lib/stripe', () => ({
stripe: {
checkout: { sessions: { create: vi.fn() } },
subscriptions: { retrieve: vi.fn(), cancel: vi.fn() },
webhooks: { constructEvent: vi.fn() },
},
}));Engine Client
vi.mock('@/lib/engine', () => ({
engineClient: {
startRun: vi.fn(),
getRunStatus: vi.fn(),
stopRun: vi.fn(),
pauseRun: vi.fn(),
resumeRun: vi.fn(),
},
}));Redis
Redis is not mocked in tests. The @/lib/cache.ts and @/lib/rate-limit.ts modules are designed with graceful degradation -- when Redis is unavailable (which it always is in tests), they fall back to:
- Cache: No caching (every call passes through)
- Rate limiting: In-memory token bucket
This means tests exercise the real fallback behavior without needing a Redis instance or mock.
Test Utilities
The src/__tests__/helpers/ directory contains shared test utilities:
- Functions to create mock authenticated user contexts with specific roles and tenant IDs
- Helpers for constructing
Requestobjects with proper headers - Factories for creating mock database records (engagements, runs, deliverables)
The src/__tests__/setup.ts file runs before all tests and configures:
- Global mock resets between tests
- Environment variable defaults for test mode
- Any global polyfills needed for the test environment
RLS (Row-Level Security) Tests
Phase 1 included 29 dedicated RLS tests in the isolation/ directory. These verify tenant isolation at the database level:
- Billing routes only return data for the authenticated tenant
- Engagement routes scope all queries by
tenantId - Run routes prevent cross-tenant access to run data
These tests mock the auth layer to simulate different tenant contexts and verify that Prisma queries always include the tenantId filter.
Writing New Tests
When adding tests for a new feature:
- Create a new file in the appropriate
src/__tests__/subdirectory - Mock all external dependencies (
prisma,auth, service clients) - Test the happy path, auth failures (401), permission failures (403), not-found (404), and validation errors (400)
- For tenant-scoped resources, verify the
tenantIdfilter is present in all queries - For role-restricted routes, test each role level
- Run the full suite to check for regressions:
npm test -- --run
Related Documentation
- Local Development -- setting up the test environment
- Architecture Overview -- understanding the components being tested