Calafai Docs

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__/:

DirectoryTestsCoverage 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
RootRate 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/prisma to control database responses
  • Mock @/lib/auth to 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 Request objects 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:

  1. Create a new file in the appropriate src/__tests__/ subdirectory
  2. Mock all external dependencies (prisma, auth, service clients)
  3. Test the happy path, auth failures (401), permission failures (403), not-found (404), and validation errors (400)
  4. For tenant-scoped resources, verify the tenantId filter is present in all queries
  5. For role-restricted routes, test each role level
  6. Run the full suite to check for regressions: npm test -- --run

On this page