Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
733 lines
29 KiB
Markdown
733 lines
29 KiB
Markdown
# Testing Standards
|
||
|
||
This document outlines testing conventions, file organization, and naming patterns for the Alga PSA codebase.
|
||
|
||
## Table of Contents
|
||
|
||
1. [Test Directory Structure](#test-directory-structure)
|
||
2. [Test Type Guidelines](#test-type-guidelines)
|
||
3. [File Naming Patterns](#file-naming-patterns)
|
||
4. [Test Organization Decision Tree](#test-organization-decision-tree)
|
||
5. [Running Tests](#running-tests)
|
||
6. [Test File Templates](#test-file-templates)
|
||
7. [Key Principles](#key-principles)
|
||
|
||
## Test Directory Structure
|
||
|
||
```
|
||
server/src/test/
|
||
├── unit/ # Isolated tests with mocked dependencies
|
||
│ ├── components/ # React component tests
|
||
│ ├── app/ # App-level tests (mirroring app structure)
|
||
│ ├── project-actions/ # Domain-specific action tests
|
||
│ └── workflow/ # Workflow-related tests
|
||
├── integration/ # Multi-component tests with real database
|
||
├── infrastructure/ # Full system scenarios (billing, invoicing, etc.)
|
||
├── e2e/ # End-to-end API and workflow tests
|
||
│ ├── api/ # REST API endpoint tests
|
||
│ ├── email-settings/ # Email workflow tests
|
||
│ ├── factories/ # Test data factories
|
||
│ ├── fixtures/ # Test fixtures
|
||
│ └── utils/ # E2E test utilities
|
||
├── mocks/ # Shared mocks
|
||
└── utils/ # General test utilities
|
||
|
||
ee/temporal-workflows/src/__tests__/
|
||
├── e2e/ # Temporal workflow E2E tests
|
||
└── activities/__tests__/ # Temporal activity tests
|
||
```
|
||
|
||
## Test Type Guidelines
|
||
|
||
### 1. Unit Tests (`server/src/test/unit/`)
|
||
|
||
**When to use:** Testing individual functions, classes, or components in isolation
|
||
|
||
**Naming convention:** `<feature>.test.ts` or `<ComponentName>.test.tsx`
|
||
|
||
**Location options:**
|
||
- **Centralized** (preferred): `server/src/test/unit/`
|
||
- **Colocated**: Next to source file (e.g., `lib/auth/sessionCookies.test.ts`)
|
||
- Only use for utilities that are deeply coupled to their implementation
|
||
|
||
**Examples:**
|
||
- `bucketUsageService.test.ts` - Service function tests
|
||
- `auth.test.ts` - Authentication logic tests
|
||
- `EmailProviderConfiguration.test.tsx` - Component tests
|
||
|
||
**Characteristics:**
|
||
- Heavy use of mocks and stubs via Vitest's `vi.mock()`
|
||
- No real database connections
|
||
- Fast execution (< 1 second per test)
|
||
- Test single responsibility/function
|
||
- Use `@vitest-environment jsdom` comment for React component tests
|
||
|
||
**Subdirectory organization:**
|
||
- `components/` - UI component tests
|
||
- `app/` - Mirror app directory structure for page/route tests
|
||
- Domain-specific folders (e.g., `project-actions/`, `workflow/`)
|
||
|
||
### 2. Integration Tests (`server/src/test/integration/`)
|
||
|
||
**When to use:** Testing multiple components working together with real dependencies
|
||
|
||
**Naming convention:** `<feature>Integration.test.ts` or `<feature><Purpose>.test.ts`
|
||
|
||
**Examples:**
|
||
- `bucketUsageIntegration.test.ts`
|
||
- `emailProviderIntegration.test.ts`
|
||
- `googleProviderDatabase.test.ts`
|
||
|
||
**Characteristics:**
|
||
- Use real database connections via `createTenantKnex()`
|
||
- Test interactions between services, actions, and data layer
|
||
- Test data persistence and retrieval
|
||
- May use `TestContext` for setup
|
||
- Moderate execution time (1-5 seconds per test)
|
||
|
||
### 3. Infrastructure Tests (`server/src/test/infrastructure/`)
|
||
|
||
**When to use:** Testing complete business workflows and complex system scenarios
|
||
|
||
**Naming convention:**
|
||
- Simple features: `<feature>.test.ts`
|
||
- Complex features: `<feature>_<aspect>.test.ts` (split by concern)
|
||
|
||
**Examples:**
|
||
- `projectManagement.test.ts`
|
||
- `billingInvoiceGeneration_tax.test.ts`
|
||
- `billingInvoiceGeneration_discounts.test.ts`
|
||
- `billingInvoiceGeneration_edgeCases.test.ts`
|
||
- `creditExpirationCore.test.ts`
|
||
|
||
**Characteristics:**
|
||
- Full system testing with real database
|
||
- Test complete business workflows (billing cycles, invoice generation, etc.)
|
||
- Use `TestContext.createHelpers()` for comprehensive setup
|
||
- Long-running tests (5-30 seconds per test)
|
||
- Often require seed data and complex fixtures
|
||
- **Split large test suites** by aspect using underscore notation
|
||
|
||
**Database bring-up pattern (Billing suites):**
|
||
- Override pgbouncer defaults to connect directly to PostgreSQL in test runs (`process.env.DB_PORT = '5432'` and remap `DB_HOST` to `localhost` when necessary).
|
||
- Use the shared context harness: `const { beforeAll, beforeEach, afterEach, afterAll } = TestContext.createHelpers();`
|
||
- In `beforeAll`, call `setupContext({ runSeeds: true, cleanupTables: [...] })` to provision the tenant, preload reference data, and register table cleanups.
|
||
- Refresh the scoped context in `beforeEach` via `resetContext()`, then reseed tenant-scoped data (e.g., tax regions, numbering seeds) needed for each test.
|
||
- Roll back with `rollbackContext()` in `afterEach` and tear everything down with `cleanupContext()` in `afterAll` so temporary schemas/tables are dropped cleanly.
|
||
|
||
**When to split tests:**
|
||
- Test file exceeds 500 lines
|
||
- Multiple distinct concerns (tax, discounts, edge cases, etc.)
|
||
- Different setup requirements per concern
|
||
- Example: Invoice generation split into `_tax`, `_discounts`, `_subtotal`, `_edgeCases`, `_consistency`
|
||
|
||
### 4. E2E Tests (`server/src/test/e2e/`)
|
||
|
||
**When to use:** Testing complete user workflows, API endpoints, and system integration
|
||
|
||
**Naming convention:** `<feature>.e2e.test.ts`
|
||
|
||
**Examples:**
|
||
- `companies.e2e.test.ts` - Company API endpoints
|
||
- `email-only.e2e.test.ts` - Email workflow
|
||
- `oauth-flow.test.ts` - OAuth integration
|
||
|
||
**Subdirectory organization:**
|
||
- `api/` - REST API endpoint tests
|
||
- Follow pattern: `<resource>.e2e.test.ts`
|
||
- Example: `companies.e2e.test.ts`, `tickets.e2e.test.ts`
|
||
- `email-settings/` - Email-specific workflow tests
|
||
- `utils/` - E2E utilities (e.g., `e2eTestSetup.ts`, `apiTestHelpers.ts`)
|
||
- `factories/` - Test data factories for creating realistic test data
|
||
- `fixtures/` - Static test data files
|
||
|
||
**Characteristics:**
|
||
- Test complete API request/response cycles
|
||
- Authenticate with real API keys (via `x-api-key` header)
|
||
- Test full workflows end-to-end
|
||
- Use `setupE2ETestEnvironment()` helper
|
||
- Moderate to long execution time (5-30 seconds per test)
|
||
- Test authentication, authorization, validation, error handling
|
||
|
||
### 5. Temporal Workflow Tests (`ee/temporal-workflows/src/__tests__/`)
|
||
|
||
**When to use:** Testing Temporal workflows and activities (Enterprise Edition only)
|
||
|
||
**Naming convention:**
|
||
- E2E: `<workflow>.e2e.test.ts`
|
||
- Activities: `<activity>.test.ts` or `<activity>.temporal.test.ts`
|
||
- Standalone: `<activity>-standalone.test.ts`
|
||
|
||
**Examples:**
|
||
- `email-only.e2e.test.ts`
|
||
- `tenant-creation-workflow.e2e.test.ts`
|
||
- `email-activities.temporal.test.ts`
|
||
- `email-activities-standalone.test.ts`
|
||
|
||
**Subdirectory organization:**
|
||
- `e2e/` - Full workflow integration tests
|
||
- `activities/__tests__/` - Activity unit and integration tests
|
||
|
||
### 6. Playwright Tests (Browser E2E)
|
||
|
||
**When to use:** Testing browser-based user interactions (currently limited use)
|
||
|
||
**Location:** `ee/server/src/__tests__/integration/`
|
||
|
||
**Configuration:** `playwright.config.ts`
|
||
|
||
**Note:** Playwright is configured but not widely used. Most E2E testing uses Vitest.
|
||
|
||
### Playwright E2E (EE) With Fresh DB
|
||
|
||
For Enterprise Playwright browser tests (VS Code runner and CLI), we keep a clean, migrated, and seeded database at test-session startup. This avoids flakiness and makes tests deterministic.
|
||
|
||
- Where: `ee/server/src/__tests__/integration/**`
|
||
- Config: `ee/server/playwright.config.ts`, `ee/server/playwright.global-setup.ts`
|
||
- Bootstrap: `scripts/bootstrap-playwright-db.ts`
|
||
|
||
How It Works
|
||
- Fresh DB per Playwright session, before the web server starts:
|
||
- `ee/server/playwright.config.ts` runs the DB bootstrap script in `webServer.command` before `npm run dev`:
|
||
- `cd ../../ && node --import tsx/esm scripts/bootstrap-playwright-db.ts && NEXT_PUBLIC_EDITION=enterprise npm run dev`
|
||
- In VS Code Playwright UI mode this runs once per “Restart” of the runner (session start). Click “Restart” in the Playwright panel to rebuild the DB.
|
||
- The bootstrap script:
|
||
- Drops and recreates the Playwright test database, runs all migrations and seeds.
|
||
- Provisions `app_user` and grants privileges.
|
||
- Reads credentials from `server/.env` (override with `PLAYWRIGHT_DB_*`).
|
||
- globalSetup applies Playwright DB env so the dev server connects to the right DB.
|
||
|
||
Authentication in Playwright (avoiding login flakiness)
|
||
- Problem: Interactive login during Playwright runs is slow and brittle (middleware redirects, cookie domain/host, edge-runtime session token decoding, etc.).
|
||
- Solution: For UI automation we bypass interactive auth and keep routes accessible in test runs:
|
||
- Add a targeted bypass in middleware for MSP routes when `E2E_AUTH_BYPASS=true`.
|
||
- Code: `server/src/middleware.ts` guards `/msp/*` with `if (process.env.E2E_AUTH_BYPASS === 'true') return NextResponse.next()`.
|
||
- Enable the bypass only for Playwright’s dev server:
|
||
- `ee/server/playwright.config.ts` sets `env: { E2E_AUTH_BYPASS: 'true' }` in `webServer.env`.
|
||
- Keep prod/staging secure: do not set `E2E_AUTH_BYPASS` outside of test/dev.
|
||
- Why this approach:
|
||
- Eliminates cookie/host mismatch issues between `localhost` vs. `canonical.localhost`.
|
||
- Avoids edge vs. node token decoding differences.
|
||
- Focuses Playwright on UI correctness instead of auth.
|
||
|
||
Notes
|
||
- If a specific test must exercise auth flows, disable bypass for that test run (unset `E2E_AUTH_BYPASS`) and use a test-only auth helper to seed a valid session cookie on the expected host.
|
||
- For API E2E tests prefer `x-api-key` with per-test keys rather than browser sessions.
|
||
- When you need bespoke data, prefer hitting the actual product route and seed mocks through Playwright (for example via `page.addInitScript` to populate `window.__ALGA_PLAYWRIGHT_*`) instead of building a dedicated UI harness page.
|
||
- To bypass the MSP login wall, reuse `applyPlaywrightAuthEnvDefaults` and `setupAuthenticatedSession` (or mint your own cookie with `@auth/core/jwt`) from `ee/server/src/__tests__/integration/helpers/playwrightAuthSessionHelper.ts`.
|
||
|
||
Test File Pattern (seeded data; no DB writes)
|
||
- Use an admin Knex connection to query seed data for test setup (bypasses RLS/ACL).
|
||
- Do not create tenants/users in tests. Pick a seeded user and mint a valid Auth.js cookie.
|
||
|
||
Template
|
||
```ts
|
||
// ee/server/src/__tests__/integration/my-feature.playwright.test.ts
|
||
import { test, expect } from '@playwright/test';
|
||
import { encode } from '@auth/core/jwt';
|
||
import { knex as createKnex } from 'knex';
|
||
import { PLAYWRIGHT_DB_CONFIG } from './utils/playwrightDatabaseConfig';
|
||
|
||
function adminDb() {
|
||
return createKnex({
|
||
client: 'pg',
|
||
connection: {
|
||
host: PLAYWRIGHT_DB_CONFIG.host,
|
||
port: PLAYWRIGHT_DB_CONFIG.port,
|
||
database: PLAYWRIGHT_DB_CONFIG.database,
|
||
user: PLAYWRIGHT_DB_CONFIG.adminUser, // admin for read access to seeds
|
||
password: PLAYWRIGHT_DB_CONFIG.adminPassword,
|
||
},
|
||
pool: { min: 0, max: 5 },
|
||
});
|
||
}
|
||
|
||
async function getSeededUser(db, email?: string) {
|
||
if (email) {
|
||
const row = await db('users').where({ email: email.toLowerCase() }).first();
|
||
if (row) return row;
|
||
}
|
||
const any = await db('users').first();
|
||
if (!any) throw new Error('No seeded users found. Check seeds.');
|
||
return any;
|
||
}
|
||
|
||
async function setSessionCookie(page, user) {
|
||
const token = await encode({
|
||
token: {
|
||
sub: user.user_id,
|
||
id: user.user_id,
|
||
email: user.email,
|
||
tenant: user.tenant,
|
||
user_type: user.user_type || 'client',
|
||
},
|
||
secret: process.env.NEXTAUTH_SECRET!,
|
||
maxAge: 60 * 60,
|
||
salt: 'authjs.session-token',
|
||
});
|
||
await page.context().addCookies([{
|
||
name: 'authjs.session-token',
|
||
value: token,
|
||
url: 'http://localhost:3000',
|
||
httpOnly: true,
|
||
secure: false,
|
||
sameSite: 'Lax',
|
||
}]);
|
||
}
|
||
|
||
test.describe('My Feature', () => {
|
||
test.setTimeout(180_000); // allow time for first-run migrations
|
||
let db;
|
||
test.beforeAll(async () => { db = adminDb(); });
|
||
test.afterAll(async () => { await db?.destroy().catch(() => undefined); });
|
||
|
||
test('happy path', async ({ page }) => {
|
||
const seeded = await getSeededUser(db, process.env.CLIENT_PORTAL_TEST_EMAIL);
|
||
await setSessionCookie(page, seeded);
|
||
await page.goto('http://localhost:3000/some/route');
|
||
await expect(page.getByText('Welcome')).toBeVisible();
|
||
});
|
||
});
|
||
```
|
||
|
||
Running
|
||
- VS Code Playwright panel:
|
||
- Click “Restart” to start a new session; the Runner executes the DB bootstrap and then starts Next. Subsequent runs reuse the server/DB until you restart.
|
||
- CLI:
|
||
- `npx playwright test ee/server/src/__tests__/integration/<file>.playwright.test.ts`
|
||
- The DB bootstrap runs before `npm run dev` (per session).
|
||
|
||
Variations
|
||
- Reset every run: set `reuseExistingServer: false` in `ee/server/playwright.config.ts`; the server restarts and the DB bootstrap re-runs on every test run.
|
||
- Per‑fixture reset (advanced): add a dev-only reset endpoint and POST it from `beforeAll`. Use only if you need frequent resets while keeping the server running.
|
||
## File Naming Patterns
|
||
|
||
| Test Type | Pattern | Example |
|
||
|-----------|---------|---------|
|
||
| Unit | `<feature>.test.ts` | `bucketUsageService.test.ts` |
|
||
| Unit (Component) | `<ComponentName>.test.tsx` | `EmailProviderConfiguration.test.tsx` |
|
||
| Integration | `<feature>Integration.test.ts` | `bucketUsageIntegration.test.ts` |
|
||
| Integration (Specific) | `<feature><Purpose>.test.ts` | `googleProviderDatabase.test.ts` |
|
||
| Infrastructure | `<feature>.test.ts` | `projectManagement.test.ts` |
|
||
| Infrastructure (Split) | `<feature>_<aspect>.test.ts` | `billingInvoiceGeneration_tax.test.ts` |
|
||
| E2E | `<feature>.e2e.test.ts` | `companies.e2e.test.ts` |
|
||
| Temporal E2E | `<workflow>.e2e.test.ts` | `email-only.e2e.test.ts` |
|
||
| Temporal Activity | `<activity>.temporal.test.ts` | `email-activities.temporal.test.ts` |
|
||
|
||
## Test Organization Decision Tree
|
||
|
||
```
|
||
Is it testing a single function/class/component in isolation?
|
||
├─ YES → Unit Test (server/src/test/unit/)
|
||
│ ├─ Component? → server/src/test/unit/components/<Component>.test.tsx
|
||
│ ├─ App route? → server/src/test/unit/app/<path>/page.test.ts
|
||
│ └─ Service/Action? → server/src/test/unit/<feature>.test.ts
|
||
│
|
||
└─ NO → Does it test multiple components together?
|
||
├─ YES → Integration Test (server/src/test/integration/)
|
||
│ └─ server/src/test/integration/<feature>Integration.test.ts
|
||
│
|
||
└─ NO → Does it test complete business workflows?
|
||
├─ YES → Infrastructure Test (server/src/test/infrastructure/)
|
||
│ ├─ Simple? → server/src/test/infrastructure/<feature>.test.ts
|
||
│ └─ Complex? → Split by aspect:
|
||
│ ├─ server/src/test/infrastructure/<feature>_<aspect1>.test.ts
|
||
│ └─ server/src/test/infrastructure/<feature>_<aspect2>.test.ts
|
||
│
|
||
└─ NO → Does it test API endpoints or complete user flows?
|
||
└─ YES → E2E Test (server/src/test/e2e/)
|
||
├─ API? → server/src/test/e2e/api/<resource>.e2e.test.ts
|
||
└─ Workflow? → server/src/test/e2e/<workflow-name>/<feature>.test.ts
|
||
```
|
||
|
||
## Continuous Integration
|
||
|
||
Test execution in CI is tiered to keep PR feedback fast while still exercising
|
||
the expensive suites regularly:
|
||
|
||
| Workflow | Trigger | What runs | Database |
|
||
|---|---|---|---|
|
||
| `unit-tests.yml` | every PR, push to main | `nx affected -t test` (DB-free unit suites across projects with a `test` target, Nx-cached, two-pass to avoid runner OOM) + skipped-test budget guard | none |
|
||
| `unit-tests.yml` (coverage job) | push to main only | server unit suite with coverage, lcov uploaded as artifact (informational) | none |
|
||
| `integration-tests.yml` | PRs touching server/packages/shared/ee | Tier-1 subset: `server npm run test:integration:tier1` (billing, accounting, authorization) | Postgres service container (pgvector) |
|
||
| `integration-tests.yml` | nightly cron, manual dispatch (`suite: full`) | full `server/src/test/integration` suite | Postgres service container |
|
||
| `e2e-fresh-install-tests.yaml` | PRs (path-filtered), push to main | full docker-compose stack + Playwright | full stack |
|
||
|
||
Notes:
|
||
- CI sets `REQUIRE_DB=1`, which makes `describeWithDb()` (server/test-utils/requireDb.ts)
|
||
**throw** instead of skipping when Postgres is unreachable. Never reintroduce
|
||
silent `dbReachable ? describe : describe.skip` probes — a DB outage in CI must fail.
|
||
- CI pins the vitest shuffle seed via `VITEST_SEED` so order-dependent failures
|
||
reproduce across reruns.
|
||
- Skipped tests are budgeted: `scripts/check-skip-budget.mjs` fails CI when the
|
||
number of `.skip`/`xit`/`xdescribe` markers exceeds `skip-budget.json`. Lower the
|
||
budget when you fix skips; raising it requires editing the budget file in your PR.
|
||
- New workflows start as non-required checks; they are promoted to required in
|
||
branch protection after a green shakeout period.
|
||
|
||
## Coverage Roadmap (priority order)
|
||
|
||
Seeded suites exist for authorization, tenancy, job handlers, inbound webhooks,
|
||
and the billing money paths. Highest-value areas still needing depth:
|
||
|
||
1. Invoice generation & recurring billing (`packages/billing/src/actions/`) — extend the seeded integration suites
|
||
2. Payments (`paymentActions.ts`, `paymentWebhookHelpers.ts`, `ee/server/src/lib/payments`)
|
||
3. Tax (`taxService.ts`, tax actions) and accounting export (`accountingExportService.ts`, companySync adapters)
|
||
4. Integrations mappers (QuickBooks, Xero, CSV in `packages/integrations`)
|
||
5. API route contract tests (`server/src/app/api/v1/*`) — auth required, input validation, error shapes
|
||
6. `shared/billingClients` (54 files, ~1 test)
|
||
7. Email (`packages/email`), reports (`server/src/lib/reports`), EE integrations (Tactical RMM, NinjaOne)
|
||
|
||
When adding tests to a package that has none, give it a `vitest.config.ts`
|
||
(model: `packages/tickets/vitest.config.ts`) and a `"test": "vitest run"` script
|
||
so `nx affected -t test` picks it up in CI.
|
||
|
||
## Running Tests
|
||
|
||
### NPM Scripts
|
||
|
||
```bash
|
||
# All tests
|
||
npm test
|
||
|
||
# By type
|
||
npm run test:unit # Unit tests only
|
||
npm run test:integration # Integration tests only
|
||
npm run test:infrastructure # Infrastructure tests only
|
||
npm run test:e2e # E2E tests only
|
||
|
||
# Specific suites
|
||
npm run test:e2e:email-settings # Email E2E tests
|
||
|
||
# Watch mode
|
||
npm run test:watch # Run tests in watch mode
|
||
|
||
# Local with config
|
||
npm run test:local # Run with local config
|
||
|
||
### Runner backend smoke tests
|
||
|
||
```bash
|
||
# Validate runner backend selection logic (Knative vs Docker)
|
||
npx vitest run ee/server/src/lib/extensions/__tests__/runner/backend.test.ts
|
||
|
||
# Spin up the local Docker runner (requires Docker)
|
||
npm run runner:up
|
||
|
||
# Check runner health and bundle-store wiring
|
||
curl http://localhost:${RUNNER_DOCKER_PORT:-8085}/healthz
|
||
# Fetch a static asset once a bundle is staged
|
||
curl -I "http://localhost:${RUNNER_DOCKER_PORT:-8085}/ext-ui/<extensionId>/<contentHash>/index.html"
|
||
|
||
# Tear down the runner stack
|
||
npm run runner:down
|
||
```
|
||
|
||
Use `.env.runner` (see `.env.runner.example`) to point the runner container at your bundle storage (MinIO/S3) and registry endpoints when validating the compose stack.
|
||
```
|
||
|
||
### Vitest Configuration
|
||
|
||
Tests use Vitest as the primary test runner, configured in `server/vitest.config.ts`:
|
||
|
||
- **Environment:** Node (default), jsdom (for React components)
|
||
- **Setup files:** `./src/test/setup.ts`
|
||
- **Global setup:** `./vitest.globalSetup.js`
|
||
- **Execution:** Single fork mode (for database isolation)
|
||
- **Timeout:** 20 seconds default
|
||
|
||
## Test File Templates
|
||
|
||
### Unit Test Template
|
||
|
||
```typescript
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { functionUnderTest } from '@/lib/services/myService';
|
||
|
||
describe('MyService Unit Tests', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
describe('functionUnderTest', () => {
|
||
it('should handle expected input correctly', () => {
|
||
// Arrange
|
||
const input = 'test';
|
||
|
||
// Act
|
||
const result = functionUnderTest(input);
|
||
|
||
// Assert
|
||
expect(result).toBe('expected');
|
||
});
|
||
|
||
it('should throw error for invalid input', () => {
|
||
// Arrange & Act & Assert
|
||
expect(() => functionUnderTest(null)).toThrow();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### React Component Unit Test Template
|
||
|
||
```typescript
|
||
/**
|
||
* @vitest-environment jsdom
|
||
*/
|
||
import React from 'react';
|
||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||
import '@testing-library/jest-dom';
|
||
import { MyComponent } from '../../../components/MyComponent';
|
||
|
||
// Mock dependencies
|
||
vi.mock('../../../lib/actions/myActions', () => ({
|
||
myAction: vi.fn(),
|
||
}));
|
||
|
||
describe('MyComponent', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
it('should render with default props', () => {
|
||
render(<MyComponent />);
|
||
expect(screen.getByText('Expected Text')).toBeInTheDocument();
|
||
});
|
||
|
||
it('should handle user interaction', async () => {
|
||
const user = userEvent.setup();
|
||
render(<MyComponent />);
|
||
|
||
await user.click(screen.getByRole('button', { name: /click me/i }));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('Success')).toBeInTheDocument();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### Integration Test Template
|
||
|
||
```typescript
|
||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||
import { createTenantKnex } from 'server/src/lib/db';
|
||
import { Knex } from 'knex';
|
||
import { myService } from 'server/src/lib/services/myService';
|
||
|
||
describe('MyService Integration Tests', () => {
|
||
let knex: Knex;
|
||
let tenant: string;
|
||
|
||
beforeAll(async () => {
|
||
const { knex: testKnex, tenant: testTenant } = await createTenantKnex();
|
||
knex = testKnex;
|
||
tenant = testTenant || 'default-test-tenant';
|
||
});
|
||
|
||
afterAll(async () => {
|
||
if (knex) {
|
||
await knex.destroy();
|
||
}
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
// Clean up test data
|
||
await knex('test_table').where('tenant', tenant).del();
|
||
});
|
||
|
||
describe('myService function', () => {
|
||
it('should persist data to database correctly', async () => {
|
||
// Arrange
|
||
const testData = { name: 'test' };
|
||
|
||
// Act
|
||
await myService.create(testData);
|
||
|
||
// Assert
|
||
const result = await knex('test_table')
|
||
.where('tenant', tenant)
|
||
.first();
|
||
expect(result.name).toBe('test');
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### Infrastructure Test Template
|
||
|
||
```typescript
|
||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||
import { TestContext } from '../../test-utils/testContext';
|
||
import { generateInvoice } from 'server/src/lib/actions/invoiceGeneration';
|
||
|
||
describe('Invoice Generation Infrastructure Tests', () => {
|
||
const testHelpers = TestContext.createHelpers();
|
||
let context: TestContext;
|
||
|
||
beforeAll(async () => {
|
||
context = await testHelpers.beforeAll({
|
||
runSeeds: true,
|
||
cleanupTables: [
|
||
'invoice_items',
|
||
'invoices',
|
||
'time_entries',
|
||
'company_contract_lines'
|
||
],
|
||
companyName: 'Test Company',
|
||
userType: 'internal'
|
||
});
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await testHelpers.afterAll();
|
||
});
|
||
|
||
describe('Complete Invoice Generation Workflow', () => {
|
||
it('should generate invoice with correct tax calculations', async () => {
|
||
// Arrange - Create complete test scenario
|
||
const company = await context.createEntity('companies', {
|
||
company_name: 'Test Company'
|
||
});
|
||
|
||
// Act - Execute business workflow
|
||
const invoice = await generateInvoice(company.company_id);
|
||
|
||
// Assert - Verify complete workflow results
|
||
expect(invoice.total).toBeGreaterThan(0);
|
||
expect(invoice.tax_amount).toBeDefined();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### E2E Test Template
|
||
|
||
```typescript
|
||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||
import { setupE2ETestEnvironment, E2ETestEnvironment } from '../utils/e2eTestSetup';
|
||
|
||
describe('Companies API E2E Tests', () => {
|
||
let env: E2ETestEnvironment;
|
||
let createdCompanyIds: string[] = [];
|
||
|
||
beforeAll(async () => {
|
||
env = await setupE2ETestEnvironment({
|
||
companyName: 'E2E Test Company',
|
||
userName: 'e2e_test_user'
|
||
});
|
||
});
|
||
|
||
afterAll(async () => {
|
||
// Clean up created resources
|
||
for (const id of createdCompanyIds) {
|
||
try {
|
||
await env.apiClient.delete(`/api/v1/companies/${id}`);
|
||
} catch (error) {
|
||
// Ignore cleanup errors
|
||
}
|
||
}
|
||
await env.cleanup();
|
||
});
|
||
|
||
describe('Authentication', () => {
|
||
it('should reject requests without API key', async () => {
|
||
const response = await env.apiClient.get('/api/v1/companies');
|
||
expect(response.status).toBe(401);
|
||
});
|
||
});
|
||
|
||
describe('CRUD Operations', () => {
|
||
it('should create a company', async () => {
|
||
const response = await env.apiClient.post('/api/v1/companies', {
|
||
company_name: 'New Company'
|
||
});
|
||
|
||
expect(response.status).toBe(201);
|
||
expect(response.data.company_name).toBe('New Company');
|
||
|
||
createdCompanyIds.push(response.data.company_id);
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
## Key Principles
|
||
|
||
### 1. Tests Should NOT Be Colocated by Default
|
||
- Use centralized test directories (`server/src/test/`)
|
||
- Only colocate in rare cases where test is deeply coupled to implementation
|
||
- Exception: Small utility functions that are rarely changed
|
||
|
||
### 2. Mirror Source Structure Within Test Directories
|
||
- Use subdirectories to organize by feature area
|
||
- Example: `test/unit/components/` for component tests
|
||
- Example: `test/e2e/api/` for API endpoint tests
|
||
|
||
### 3. Use Clear, Descriptive File Names
|
||
- Include `.e2e` suffix for E2E tests
|
||
- Include `Integration` or `<Purpose>` for integration tests
|
||
- Split complex features with underscore: `feature_aspect.test.ts`
|
||
|
||
### 4. Split Large Test Suites
|
||
- When test file exceeds 500 lines, split by concern
|
||
- Use underscore naming: `billingInvoiceGeneration_tax.test.ts`
|
||
- Keep related tests together: `feature_core.test.ts`, `feature_edgeCases.test.ts`
|
||
|
||
### 5. Follow the Test Type Hierarchy
|
||
- Start with unit tests (fast, isolated)
|
||
- Add integration tests (multi-component)
|
||
- Use infrastructure tests for complete workflows
|
||
- E2E tests for full API/user flows
|
||
|
||
### 6. Use Appropriate Test Utilities
|
||
- **Unit:** `vi.mock()` for mocking dependencies
|
||
- **Integration/Infrastructure:** `TestContext.createHelpers()` for database setup
|
||
- **E2E:** `setupE2ETestEnvironment()` for full environment setup
|
||
|
||
### 7. Database Testing Best Practices
|
||
- Always filter by tenant in all queries
|
||
- Clean up test data in `afterEach` or `afterAll`
|
||
- Use transactions where possible for isolation
|
||
- Ensure tests can run in any order
|
||
- For REST/E2E suites that need real HTTP semantics:
|
||
- Spin up a lightweight HTTP server (e.g. Node's `http.createServer`) that delegates incoming requests to the Next.js route handlers using `NextRequest`.
|
||
- Use `ApiTestClient` (or similar API helper) to exercise the endpoints the same way the product does, including headers, auth, and query parameters.
|
||
- Manage the server lifecycle in `beforeAll`/`afterAll` so the listener is available across tests and shut down cleanly.
|
||
- When the test flow requires overlaid storage/migration behavior without a running application:
|
||
- Ensure required tables exist by inspecting the schema and creating or altering tables as needed inside the test setup.
|
||
- Create or update database roles (e.g. `app_user`) and grant privileges so the real connection pool can authenticate exactly as the application would.
|
||
- For isolation between tests while still hitting the live handlers:
|
||
- Use `TestContext.createHelpers()` to wrap each test in a database transaction; perform per-test seeding after `beforeEach()` so every test sees a clean, consistent state.
|
||
- If tests need to override service configuration (quotas, limits, etc.), use `vi.spyOn` to swap implementations temporarily, and restore them in `finally` blocks.
|
||
|
||
### 8. Mock External Dependencies
|
||
- Mock external APIs (email providers, payment gateways, etc.)
|
||
- Use `vi.mock()` for module-level mocks
|
||
- Create reusable mock fixtures in `test/mocks/`
|
||
|
||
### 9. Test Naming Conventions
|
||
- Describe what is being tested: `describe('MyService')`
|
||
- Use "should" statements: `it('should return correct value')`
|
||
- Be specific about scenarios: `it('should throw error for invalid input')`
|
||
|
||
### 10. Assertion Best Practices
|
||
- Use descriptive assertions: `expect(result.name).toBe('expected')`
|
||
- Test both happy path and error cases
|
||
- Verify side effects (database changes, API calls, etc.)
|
||
- Use `toThrow()` for error testing
|
||
|
||
### 11. Use Updated Billing Terminology
|
||
- Refer to billing `contract lines` and `contracts` instead of the legacy `plans` and `bundles`.
|
||
- When bringing in legacy helpers (e.g., `createFixedPlanAssignment`), alias them to the new naming in your test files so intent stays aligned with the schema.
|
||
|
||
## Additional Resources
|
||
|
||
- [Vitest Documentation](https://vitest.dev/)
|
||
- [Testing Library Documentation](https://testing-library.com/)
|
||
- [Contact API E2E Test Plan](../archive/contact-api-e2e-test-plan.md) - Example E2E test implementation
|
||
- [Inbound Email Testing Guide](../inbound-email/development/testing.md) - Email workflow testing examples
|