Skip to content

Testing

Keelmint is tested at three levels, and CI runs all of them on every push. Read this when you add a feature (what to test, where) or when a run fails and you need to know what it needs.

Unit tests

Terminal window
pnpm nx test <project> # one project
pnpm nx run-many -t test # everything
pnpm nx affected -t test # what your change touches

Spec files sit next to the code (*.spec.ts), with Jest; client components use jest-preset-angular. No database or Docker needed: Prisma is stubbed.

API end-to-end tests

Terminal window
docker compose up -d db mailpit
pnpm nx e2e keelmint-api-e2e

Real HTTP against the built API and a real Postgres, using the separate <product>_test database (TEST_DATABASE_URL), so a run never touches development data. The suite applies migrations, starts the server itself on port 3001 (PORT overrides), truncates tables between tests, reads mail from Mailpit, and runs fake Google and Stripe servers, so it needs no external accounts.

Each product has its own suite: pnpm nx e2e <product>-api-e2e.

Tenant isolation

apps/keelmint/api-e2e/src/api/tenant-isolation.spec.ts creates two organizations with data and holds one table row per organization-scoped route: ids from the other organization must answer 404, and lists must never contain them. A route-coverage guard at the bottom fails when a route the app registers is in neither a row nor an ALLOWED entry with its reason. Add a row for every organization-scoped route you write.

Database integration specs

Specs named *.int.spec.ts (tenant scoping, unit scope, migrations, the seed) run against a real, migrated test database and only when asked:

Terminal window
DB_INTEGRATION=1 pnpm nx run-many -t test -p core-db keelmint-db keelmint-api-todos

DB_INTEGRATION is part of the test cache key, so a cached run without it never counts as a run with it.

Load testing

To measure how the API behaves with tens of thousands of members (the figures in Tenant data), seed one large organization into a throwaway database and run the load script against an API pointed at it. Never use keelmint or keelmint_test.

  1. Create an empty database whose name has load as a token (keelmint_load, load, load_2; not workload) and migrate it:

    Terminal window
    SKIP_PRODUCT_DOTENV=1 DATABASE_URL=postgres://postgres:postgres@localhost:5433/keelmint_load \
    pnpm exec prisma migrate deploy --config libs/keelmint/db/prisma.config.ts
  2. Seed it (same two variables): pnpm nx run keelmint-db:seed-load -- --members=20000 --slug=load. The seed refuses production, non-local hosts and any database whose name lacks the load token, unless LOAD_ALLOW_DB=<exact database name> is set. Options (each also a LOAD_<NAME> variable): --slug (default load, also the email prefix), --members (20000), --share (fraction of members in the big unit subtree, 0.5), --depth and --breadth (4 and 5: 781 units), --todos per member (5), --audit (5000), --tickets (3000). LOAD_PASSWORD sets the accounts’ password (default LoadTest!2026). The accounts are <slug>-0@load.local (organization admin), -1 (unit admin of the big root unit) and -2 (member). It prints the big root unit’s id. A finished slug is left alone; a seed that was interrupted fails on the next run: drop the database and start again.

  3. Start the API on another port against that database, with THROTTLE_LIMIT high (login is rate limited). A unit admin of a large subtree needs NODE_OPTIONS=--max-http-header-size=262144 (see Configuration).

  4. Run tools/load/run.ts (autocannon) with ts-node:

    Terminal window
    ts-node --transpile-only -O '{"module":"commonjs","esModuleInterop":true,"ignoreDeprecations":"6.0"}' \
    tools/load/run.ts --unit=<big root unit id> [--base=http://localhost:3100] [--slug=load] \
    [--seconds=10] [--connections=10] [--session-cap=10] [--out=results.json]

    It logs in as the three accounts and hammers the member list, todos, audit log, chart, helpdesk queue and token refresh, printing requests per second and latencies. It uses one session per connection and the API keeps at most MAX_ACTIVE_SESSIONS_PER_USER (default 10) per user, so --connections may not exceed --session-cap; raise both together with that variable to test more connections. LOAD_PASSWORD must match the seed’s.

After any bulk import (this seed, or your own) run ANALYZE on the database: with stale statistics Postgres can pick a bad plan and a query that should take milliseconds stalls.

CI

.github/workflows/ci.yml runs on every push and pull request: a format check, then lint, test, build, typecheck and e2e through Nx, with Postgres and Mailpit as services, and a job that generates a fresh product and runs its e2e suite. It is configured for Nx Cloud task distribution; remove the nx start-ci-run step if you don’t use Nx Cloud. The release workflow (images to GHCR on a version tag) is described in Deploy.