# Testing Guide

## Test Strategy

The project uses `bun test` (Bun's built-in test runner) with three test tiers:

| Tier | Location | Database | What it tests |
|------|----------|----------|---------------|
| Unit | `tests/unit/` | None | Pure logic: permission registry, context guards, error classes, schema validation |
| Integration | `tests/integration/` | Real PostgreSQL | Database operations: seed, RBAC joins, audit writes/queries |
| E2E | `tests/e2e/` | Real PostgreSQL + real Typesense | Full lifecycle: invitation → activation → RBAC → audit, plus request-scoped search sync. Server starts automatically. |

## Running Tests

Backend tests are only supported through the Nx targets below, which resolve
to `bun run tests/run-profiles.ts <target>` (`apps/backend/tests/run-profiles.ts`).
That launcher is the sole supported entrypoint: it applies the correct,
fully-isolated environment profile for each tier, forwards only an explicit
allowlist of host environment variables (never a blind copy of your shell),
bootstraps and health-checks the dedicated E2E infrastructure before the "e2e"
tier runs, and validates its own arguments before spawning anything.

### Locally: the unit tier, and individual files

```bash
# Unit tests (no database needed)
bun nx run supervin-core-backend:test

# One file from the integration or e2e tier — pass the path to the launcher.
# Running a handful of files this way is the normal loop while writing a test.
bun run tests/run-profiles.ts integration ./tests/integration/visma-product-cases.test.ts
bun run tests/run-profiles.ts e2e ./tests/e2e/orders.test.ts
```

The launcher routes each requested path to its matching profile, so a file only
runs under the tier it belongs to.

### In CI: the whole integration and e2e tiers

```bash
# Integration tests (requires TEST_DATABASE_URL against a dedicated test database)
bun nx run supervin-core-backend:test:integration

# E2E tests — bootstraps the isolated E2E infrastructure (docker-compose.e2e.yml)
# automatically, then requires a dedicated test database and a running Typesense
# instance
bun nx run supervin-core-backend:e2e

# All tiers, run sequentially through the same launcher
bun run test:all
```

**These three are CI commands, not local ones.** CI shards integration 2× and
e2e 3× across their own runners and finishes the whole pipeline in about four
and a half minutes. A developer machine runs several worktrees at once and
cannot; a full local tier is slower than CI *and* produces failures caused by
contention rather than by the change under test. Trigger a run with
`gh workflow run ci.yml --ref <branch>` and read it with `gh run view --log-failed`
— see [Validation](../../../../AGENTS.md#validation).

A full local run is still the right tool for debugging the E2E infrastructure
itself. Stop this worktree's dev servers first (`bun run dev:stop`); the tier
allows each test 90 seconds, and with dev servers competing for the machine that
budget goes on waiting instead.

Do **not** invoke `bun test tests/e2e` (or any other test directory) directly.
It is not a supported entrypoint: it skips the environment profile, the host
environment allowlist, the E2E infrastructure preflight, and queue
provisioning that `run-profiles.ts` performs. As defense in depth,
`tests/e2e/server.ts` independently refuses to start unless
`ENABLE_E2E_TEST_MODE=true` and `TEST_DATABASE_URL` resolves to a verified,
dedicated test database (see `tests/config/e2e-db-guard.ts`), so a direct
invocation fails fast instead of quietly touching the wrong database — but it
is still an unsupported way to run these suites.

If you use the local Docker Compose setup, start dependencies first:

```bash
bun run dev:start
```

`bun run dev:start` provisions the standard local development stack and a
dedicated Playwright/test sidecar stack backed by the physically isolated E2E
infrastructure (`docker-compose.e2e.yml`, started via `bun run e2e:up`):

| Purpose | Frontend | Backend | Database |
|---------|----------|---------|----------|
| Normal development | `http://localhost:43173` | `http://localhost:34045` | `supervin` (dev PostgreSQL) |
| Playwright/runtime e2e | `http://localhost:43174` | `http://localhost:34046` | Dedicated E2E PostgreSQL instance (see `docker-compose.e2e.yml`) |

It also runs migrations against the dedicated E2E database. You should not
need to export `TEST_DATABASE_URL`, `PLAYWRIGHT_API_URL`, or
`PLAYWRIGHT_BASE_URL` for the standard local workflow, and Playwright itself
now refuses to start against any target other than the isolated E2E sidecar
above (see `apps/frontend/playwright.config.cts`).

## Database Selection Invariant

The runtime database is chosen by `ENABLE_E2E_TEST_MODE`, **not** by whether
`TEST_DATABASE_URL` happens to be set. This matters because the shared
`apps/backend/.env` defines *both* `DATABASE_URL` and `TEST_DATABASE_URL`, so a
naive `TEST_DATABASE_URL ?? DATABASE_URL` fallback would silently point the dev
backend at the test database that e2e suites truncate.

| Mode | When | Database used |
|------|------|---------------|
| `ENABLE_E2E_TEST_MODE=false` | Dev backend (port `34045`), prod | `DATABASE_URL` (`supervin`) |
| `ENABLE_E2E_TEST_MODE=true` | E2e sidecar (port `34046`), Bun e2e/integration suites | `TEST_DATABASE_URL` (dedicated E2E database `supervin_e2e_test`, see `docker-compose.e2e.yml`) |

Rules:

- `src/db/client.ts` (`getBaseDatabaseUrl`) and `drizzle.config.ts` both key off
	`ENABLE_E2E_TEST_MODE`. They are the source of truth — keep them in sync and
	never reintroduce a `TEST_DATABASE_URL ?? DATABASE_URL` fallback.
- `ENABLE_E2E_TEST_MODE=true` requires `TEST_DATABASE_URL`; the backend fails fast
	otherwise instead of falling back to the dev database.
- `TEST_DATABASE_URL` must target the dedicated E2E PostgreSQL stack exactly:
	loopback host, port `5434` (never the shared dev PostgreSQL's port `5432`,
	and never the fixed port `5433` of an unrelated Compose project),
	role `supervin_e2e`, and a database name starting with `supervin_e2e` (see
	`tests/config/e2e-db-guard.ts`). A `localhost:5432`/`supervin`-role target,
	or a merely "test"-named database on the dev instance, is rejected — not
	just accepted-then-warned-about.
- Integration tests preload `tests/integration/setup-env.ts`, which forces
	`ENABLE_E2E_TEST_MODE=true` so the eager `db` export resolves to the test
	database at import time.

This is why e2e runs no longer wipe dev data or sessions: dev (`supervin`,
port `5432`) and the dedicated E2E database (`supervin_e2e_test`, port `5434`,
`docker-compose.e2e.yml`) are physically separate PostgreSQL instances and can
run side by side.

## Test Database Setup

Integration tests and Bun-driven e2e suites that use `tests/helpers/db.ts` need
the dedicated E2E PostgreSQL stack (`docker-compose.e2e.yml`) running, with the
schema applied. `bun run e2e:up` (`tools/scripts/e2e-up.sh`) does both — it
starts the stack and migrates it using an explicit, minimal environment (no
inherited shell variables, no auto-loaded `.env` file), and is the only
supported way to provision it:

```bash
bun run e2e:up
```

Do not rely on `DATABASE_URL` fallback for Bun test suites. The helper in
`tests/helpers/db.ts` refuses to run unless `TEST_DATABASE_URL` is set, and
requires it to be the exact dedicated E2E identity described above. Several
suites call `cleanAllTables()`, so this is a hard guard against truncating the
primary development database — or any other database that merely looks
test-like.

If the variable is missing or malformed, the helper prints a concrete error
explaining the exact expected identity and reminds you to use
`bun nx run supervin-core-backend:test:integration` (which resolves the
dedicated E2E profile automatically via `tests/config/environment-profiles.ts`)
rather than setting `TEST_DATABASE_URL` by hand.

E2E tests start and stop the application server automatically in each test suite — no need to start it manually.
When running through the local dev workflow, the Playwright backend is already
running on port `34046` with `ENABLE_E2E_TEST_MODE=true` and a dedicated
`TEST_DATABASE_URL`.

This is separate from request-scoped frontend e2e against a dedicated test server:

- Bun test suites use `tests/helpers/db.ts` and now require `TEST_DATABASE_URL`.
- Runtime request-scoped e2e uses `X-Test-run-id` and provisions isolated
	PostgreSQL schemas inside the configured test database.
- That runtime flow is enabled by `ENABLE_E2E_TEST_MODE=true`, which now requires
	`TEST_DATABASE_URL`; it must not run against the normal development backend.

## Typesense Test Setup

The search-related e2e tests use a real Typesense instance. Nothing is mocked.

Expected local settings (see `docker-compose.e2e.yml`, started via `bun run e2e:up`):

```dotenv
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8119
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=supervin-typesense-e2e-key
```

These defaults match the dedicated E2E stack (`docker-compose.e2e.yml`), where
Typesense is published on `localhost:8119` — a separate instance from the dev
stack's Typesense on `localhost:8108` (`docker-compose.yml`).

The backend itself runs in-process during e2e tests, but it talks to real
PostgreSQL and real Typesense using the configured environment variables.

## Test Helpers

Located in `tests/helpers/db.ts`:

- `getTestDb()` — returns a Drizzle database instance for test use (requires `TEST_DATABASE_URL`)
- `cleanAllTables(db)` — truncates all tables in FK-safe order
- `closeTestDb()` — closes the connection pool

E2E tests also use `tests/e2e/server.ts`:

- `setupE2eServer()` — starts the Elysia app in-process on a random port, returns the base URL
- `teardownE2eServer()` — stops the server
- `getBaseUrl()` — returns the base URL of the running test server

Search-related e2e tests also use `tests/e2e/helpers.ts`:

- `gql()` — shared GraphQL request helper with optional bearer token and `X-Test-run-id`
- `typesenseGet()` — direct Typesense HTTP helper for asserting indexed state

## What the Tests Cover

### Unit Tests
- Permission keys follow `domain.action` format
- All permissions have descriptions
- No duplicate permission keys
- GraphQL schema contains all required types and fields
- `requireAuth` / `requirePerm` guards behave correctly
- Custom error classes have correct properties

### Integration Tests
- Seed creates all permissions and Superadmin role
- Superadmin gets all permissions bound
- Custom roles can be created with permission bindings
- Users inherit permissions through role assignment
- Audit log entries can be written and queried
- JSONB before/after diffs work correctly

### E2E Tests
- Admin can create an invitation
- Invited user is inactive before first login
- OAuth activation flow works correctly
- Activated user gets the correct permissions
- Security operations create audit entries
- Superadmin role cannot be deleted
- Product create, update, and delete sync to Typesense
- `searchConfig(type: "products")` returns frontend Typesense configuration
- Scoped Typesense keys can query the indexed collection directly
- `X-Test-run-id` isolates both PostgreSQL data and Typesense collection names
- Bun test helpers refuse `DATABASE_URL` fallback and same-DB `TEST_DATABASE_URL` values to avoid truncating the primary database

## Frontend E2E Login Shortcut

Frontend e2e tests do not need to go through Google or Microsoft OAuth.
Use the `devLogin` GraphQL mutation to mint a session token directly for an
existing active user.

This is intended for local development and automated tests only.
It is blocked in production.

### Typical flow

1. Ensure the test user exists.
2. Ensure the test user is active.
3. Call `devLogin(email: String!)`.
4. Store the returned token in the frontend test runner.
5. Send it as `Authorization: Bearer <token>` on subsequent GraphQL requests.

### Example mutation

```graphql
mutation DevLogin($email: String!) {
	devLogin(email: $email) {
		token
		user {
			id
			email
		}
	}
}
```

Example variables:

```json
{
	"email": "admin@supervin.dk"
}
```

### Example response

```json
{
	"data": {
		"devLogin": {
			"token": "<session-token>",
			"user": {
				"id": "...",
				"email": "admin@supervin.dk"
			}
		}
	}
}
```

### Important constraints

- `devLogin` only works outside production.
- The user must already exist.
- The user must already be active.
- Invited users created via `createInvitation` start as inactive, so activate
	them before using `devLogin` if your test depends on that user.
- `bootstrapFirstUser` is useful for the first admin in a clean environment,
	while `devLogin` is the normal shortcut for repeated e2e logins.

## Request-Scoped Frontend E2E Runs

The backend supports request-scoped frontend e2e isolation via the
`X-Test-run-id` header when `ENABLE_E2E_TEST_MODE=true` and `TEST_DATABASE_URL`
points at a dedicated test database.

### Purpose

- Each unique `X-Test-run-id` value maps to its own isolated PostgreSQL schema.
- Each unique `X-Test-run-id` value also maps to its own isolated Typesense collection.
- The first request for a new test run provisions the schema automatically.
- Provisioning runs migrations, seeds permissions and the Superadmin role, and
	ensures the active baseline superadmin user `cra@supervin.dk` exists.

### Safety guard

- If `ENABLE_E2E_TEST_MODE` is not set to `true`, any request carrying
	`X-Test-run-id` is rejected with a 400 error.
- If `ENABLE_E2E_TEST_MODE=true` is set without `TEST_DATABASE_URL`, backend
	startup fails so test schemas cannot be created inside the development
	database.
- Frontend Playwright defaults use a separate frontend port (`43174`) and API
	port (`34046`) and reject the normal development ports (`43173` and `34045`).
- This applies before GraphQL execution, so browser and test-runner requests
	fail fast instead of silently hitting shared data.

### Typical frontend flow

1. Choose a unique `X-Test-run-id`, for example `e2e_<commit>_<timestamp>`.
2. Send GraphQL requests with that header on every request in the run.
3. Call `devLogin(email: "cra@supervin.dk")` to get a bearer token for the
	baseline superadmin in that isolated scope.
4. Run the suite using the same header and bearer token.
5. Call `cleanupTestRun` with the same header to drop the isolated schema and
	delete the isolated Typesense collection when the suite is done.

### Example request headers

```http
Content-Type: application/json
Authorization: Bearer <token>
X-Test-run-id: frontend_run_alpha
```

### Example cleanup mutation

```graphql
mutation CleanupCurrentTestRun {
	cleanupTestRun
}
```

### Notes

- `X-Test-run-id` is included in CORS allow-headers so browser-based e2e tests
	can send it.
- The isolated schema is created lazily on first use, so there is no separate
	setup endpoint required for baseline provisioning.
- Search writes go to `products_e2e_<normalized-test-run-id>` during isolation,
	so test traffic does not pollute the shared `products` index.
- `cleanupTestRun` requires the `X-Test-run-id` header and only removes the
	current isolated test scope.
