test script and pnpm test at the repo root runs them all through Turbo, after building dependencies (dependsOn: ["^build"]).
Where tests live
The older notes describe
tests/unit/ and tests/integration/ subfolders. Those folders do not exist. Tests are named after the feature they cover, for example autofit-import-worker.test.ts, app-trust-proxy.test.ts, assistant.test.ts.
Commands
test script is vitest run --passWithNoTests.
Unit testing a service
Services are factories that take their dependencies as arguments, so a test passes plain objects..js, the same as in source.
What to cover for a service:
- The happy path.
- Every branch that throws an
AppError, asserting thecode. - Tenancy: the same id under a different
studioIdis not found. - Role or permission dependent behaviour.
Test seams built into the code
The code is written to be tested without infrastructure. Use these before reaching for a mock library.
When you write new code, keep this property. Take the clock, the network and the model as arguments.
Testing the Express shell
For behaviour that lives inapp.ts or a middleware, build the app with createApp and drive it with supertest. The modules behind /v1 pull in everything, so tests replace them with stubs using vi.mock before importing the app:
tests/app-trust-proxy.test.ts, which checks that the rate limiter keys on the forwarded client address. createApp takes a context object, so a test passes only the env fields the shell reads.
To test a single router, mount it on a bare Express app with the error middleware, set req.auth in a small middleware in front of it, and give the router a context whose prisma is a fake.
What must be tested
This list combinesbackend/docs/TESTING.md with what the existing test files cover:
- Auth guards. Missing token, bad token, wrong role, each lane.
- Tenancy. A principal of studio A cannot read or change studio B.
- Money and limits. Plan limits, seat counts, subscription state changes, anything that calls the payment provider.
- Message sending. Anything that pushes, sends WhatsApp or email. Assert that silent paths stay silent.
- Webhooks. A bad token or signature is rejected, and a valid delivery repeated twice is applied once.
- Idempotent jobs. Running a scheduler function twice with the same
nowchanges nothing the second time. - A regression test for a fixed bug. Several files in the folder exist for one specific failure, for example
app-trust-proxy.test.tsandarchived-studio-quiet.test.ts.
Database migrations
Migrations are hand written SQL and are applied withprisma migrate deploy. Before applying one to a shared environment, replay the whole migration folder against a throwaway local Postgres to prove it applies cleanly from scratch. pnpm db:migrate:status shows what a database has applied.
Smoke scripts
A few scripts exercise a running API or a real provider. They are not part ofpnpm test.
Gates
ESLint enforces a few project rules through the local
perform-internal plugin: no-direct-process-env, no-em-dash, and no-loose-version-pins, which flags caret and tilde ranges in the package.json files it is configured for. Some dependencies in apps/core-api/package.json still use a caret range.
Typecheck and tests depend on the built
dist of workspace packages. After pulling changes to @repo/database, @perform/queue or @perform/smartsend, errors such as a missing Prisma model or queue name usually mean a stale build. Rebuild the package and try again.