Integration Tests
API-level integration tests for AI Software Factory. Every test spins up the
real stack — NestJS API + PostgreSQL + Redis (+ OpenSearch / MinIO per test)
— via Testcontainers, then drives the NestJS application through its
HTTP surface (supertest-style Request from Nest's testing module).
What "integration" means here
- Real infrastructure, not mocks: the NestJS app under test uses a
Testcontainers-managed Postgres (Prisma schema applied via
prisma migrate deploy), a real Redis (BullMQ + rate-limit state), and — where a spec needs it — a real MinIO (S3) and OpenSearch. - Full HTTP contract: specs hit
app.get(...)/app.post(...)etc. on the compiled Nest application, so routing, guards (authN + authZ), interceptors, validation pipes, and the domain layer all run. - No browser (that's
tests/e2e) and no unit-level isolation (that'spackages/*/src/**/*.unit.test.ts).
Running
# From the repo root (after pnpm install):
pnpm --dir tests/integration test # all specs, one shared DB
pnpm --dir tests/integration test -- auth # filter by filename substring
pnpm --dir tests/integration test:watch # watch mode (local dev)
Docker must be available (Testcontainers). In CI the e2e job's service containers are NOT used — integration tests manage their own containers.
Test environment convention
helpers/containers.ts is a shared module-level singleton: all specs in
one vitest run share the same Postgres + Redis containers (started once in
beforeAll via Vitest's setup in each file, deduped by the module cache).
Isolation between specs is at the data level, not the container level:
- Each test file creates its own workspace(s) and users via the API.
- The shared app instance is created once per test file by
helpers/api.ts(createTestApp()), with the Prisma client pointed at the Testcontainers DB URL.
Per-spec env overrides (e.g. a different S3 bucket) are read from process.env
in the same .env.example variable names:
| Var | Purpose |
|---|---|
DATABASE_URL |
set automatically by helpers/containers.ts |
REDIS_URL |
set automatically by helpers/containers.ts |
S3_ENDPOINT |
optional; set when a spec starts MinIO |
SEARCH_URL |
optional; set when a spec starts OpenSearch |
JWT_SECRET |
random per run (generated by helpers/api.ts) |
MASTER_ENCRYPTION_KEY |
random 32-byte base64 per run |
Modules covered (per AGENTS.md §Testing)
| Spec | Covers |
|---|---|
auth.integration.test.ts |
register, login, refresh rotation, reuse detection, rate limit |
workspace.integration.test.ts |
CRUD, roles/invitations, tenant isolation (A cannot see B) |
pages.integration.test.ts |
create, nesting, move, trash/restore/purge, fractional position, authZ |
blocks.integration.test.ts |
batch insert/update/move/delete, version bump, position invariants |
databases.integration.test.ts |
records CRUD, filter tree, sort/group, 10k-record query (soft) |
comments.integration.test.ts |
threads, block anchors, replies, mentions → notification rows |
files.integration.test.ts |
upload session → complete → status transitions (READY), storage mocked |
webhooks.integration.test.ts |
webhook create, event emit, delivery attempt, HMAC signature verify |
permissions.integration.test.ts |
grant matrix (view/edit/full), inheritance, denial paths |
Adding a new integration spec
- Drop
<name>.integration.test.tsin this directory. - Get the app via
createTestApp()from./helpers/api(it handles container startup, schema deploy, and teardown). - Create your fixtures through the API (register users, create workspaces) — never write raw SQL, so the spec exercises the real authorization layer.
- Assert on HTTP status + response body, plus DB state through the exported Prisma client when you need to verify a side effect (e.g. an AuditLog row, a WebhookDelivery row).
- Clean up in
afterAllwhere practical (delete workspaces you created). The container is destroyed at process exit regardless.
Notes
- Files spec mocks the storage layer (MinIO) at the repository boundary:
the API contract (session → presigned → complete → status machine) is what
is under test, not the object store. A real-MinIO variant lives in
tests/e2e(the e2e stack runs MinIO). - OpenSearch is not started by default (slow to boot); the search paths
are covered in e2e against the dev stack. If a future spec needs it, start
a container and export
SEARCH_URLbeforecreateTestApp(). - The Prisma client is generated at build time (
pnpm db:generate); specs import@prisma/clientand reconfigure its datasource at runtime via theDATABASE_URLenv var (Testcontainers' JDBC-style URL maps 1:1 to a postgres:// DSN).