# 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

```bash
# Unit tests (no database needed)
bun test tests/unit

# Integration tests (requires TEST_DATABASE_URL)
TEST_DATABASE_URL=postgres://user:pass@localhost:5432/supervin_test bun test tests/integration

# E2E tests (requires a dedicated test database and a running Typesense instance)
TEST_DATABASE_URL=postgres://user:pass@localhost:5432/supervin_test ENABLE_E2E_TEST_MODE=true bun test tests/e2e

# All tests
bun test
```

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

```bash
docker compose up -d postgres typesense
```

## Test Database Setup

Integration tests and Bun-driven e2e suites that use `tests/helpers/db.ts` need
a dedicated PostgreSQL test database with the schema applied:

```bash
# Run migrations against the dedicated test database
TEST_DATABASE_URL=postgres://user:pass@localhost:5432/supervin_test bun run db:migrate

# Run seed (optional — tests seed their own data)
TEST_DATABASE_URL=postgres://user:pass@localhost:5432/supervin_test bun run db:seed
```

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, requires
the database name to contain `test`, and rejects URLs that point at the same
database as `DATABASE_URL`. Several suites call `cleanAllTables()`, so this is a
hard guard against truncating the primary development database.

If the variable is missing, the helper now prints a concrete example such as
`TEST_DATABASE_URL=postgres://user:pass@localhost:5432/supervin_test bun test tests/integration`
and reminds you that runtime request-scoped e2e must also use a dedicated
`TEST_DATABASE_URL` when `ENABLE_E2E_TEST_MODE=true`.

E2E tests start and stop the application server automatically in each test suite — no need to start it manually.

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:

```dotenv
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=supervin-typesense-dev-key
```

These defaults match `docker-compose.yml`, where Typesense is published on
`localhost:8108`.

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.
