# Backend tests (Vitest)

Vitest scaffolding for the Halo Bestie Fastify backend. Three sample tests exist
to demonstrate the patterns; broader coverage will be filled in incrementally.

## Strategy: schema-isolated remote DB (default)

The remote dev role on `omv.sjamsani.id` does **not** have `CREATE DATABASE`
privilege, so the chosen isolation mechanism is a separate **schema** inside the
existing `halobestie_clone` database. The migration runs into a `halobestie_test`
schema (driven by `?options=-c search_path=...` on the test DB URL), leaving the
dev `public` schema untouched.

Valkey isolation uses a separate logical db number (`/1`) on the same instance.

### Why not Docker?

Docker availability could not be verified inside the agent sandbox at scaffold
time. A `docker-compose.test.yml` exists for users who prefer ephemeral local
containers — see "Switching to local Docker" below.

### Why not a separate Postgres database?

The dev role is non-superuser and lacks `CREATE DATABASE`. Schema isolation gives
us the same isolation guarantee (test tables live in their own namespace) without
requiring a privilege bump.

## Setup

1. Copy `.env.test.example` → `.env.test`:
   ```
   cp .env.test.example .env.test
   ```
   Adjust `TEST_DATABASE_URL` / `TEST_VALKEY_URL` if your dev DB is elsewhere.

2. (Optional) Verify connectivity:
   ```
   node -e "import('postgres').then(({default:p})=>{const s=p(process.env.TEST_DATABASE_URL);s\`SELECT 1\`.then(console.log).finally(()=>s.end())})" 
   ```

3. The `halobestie_test` schema and all test tables are created automatically the
   first time `npm test` runs (idempotent — re-running `npm test` is safe).

## Running

```
npm test              # one-shot run
npm run test:watch    # re-run on file change
npm run test:coverage # plus coverage report under coverage/
```

## Required environment variables

| Var | Default | Purpose |
|-----|---------|---------|
| `TEST_DATABASE_URL` | `postgresql://halobestie_clone:halobestie_clone@omv.sjamsani.id:5432/halobestie_clone` | Same as dev — schema isolates |
| `TEST_DB_SCHEMA` | `halobestie_test` | Schema name for test tables. Hard-rejected if set to `public` |
| `TEST_VALKEY_URL` | `redis://omv.sjamsani.id:6379/1` | Note the `/1` — separate logical db from dev |
| `AUTH_JWT_SECRET` | (must be ≥ 32 chars) | Signs JWTs the prod `authenticate` plugin verifies. Test value can differ from dev |
| `ACCESS_TOKEN_TTL_SECONDS` | `3600` | Optional |
| `REFRESH_TOKEN_TTL_DAYS` | `30` | Optional |
| `CC_ORIGIN` | `http://localhost:5173` | Required by the internal app's CORS config |

## Adding a new test

Templates by type:

| Test type | Template | Sample |
|-----------|----------|--------|
| Pure service | uses `db()` + fixtures | `test/services/payment.service.test.js` |
| Service with mocked WS/FCM | `vi.mock('../../src/plugins/websocket.js')` at top | `test/services/pairing.service.test.js` |
| Route (HTTP-free via inject) | `app.inject({ method, url, headers, payload })` | `test/routes/client.payment.routes.test.js` |

Helpers (under `test/helpers/`):
- `db.js` — `db()` returns the shared sql client; `resetDb()` truncates Phase 3.7 + dependent tables; `resetAppConfig()` restores config defaults.
- `valkey.js` — `getTestValkey()` for direct keyspace assertions; `flushTestDb()` to wipe between tests.
- `server.js` — `buildPublic()` / `buildInternal()` for route tests.
- `jwt.js` — `customerJwt(id)`, `mitraJwt(id)`, `ccJwt(id)` mint tokens the prod `authenticate` plugin accepts. `authHeader(token)` builds the header.
- `fixtures.js` — `createCustomer()`, `createMitra({ isOnline })`.

Patterns to follow (from the sample tests):
- Always import status / cause values from `../../src/constants.js` — never hard-code `'pending'`, `'all_mitras_rejected'`, etc. (See project memory: "Use Enums for Fixed Values".)
- Mock `../../src/plugins/websocket.js` and `../../src/services/notification.service.js` for any test that touches pairing / extension / closure — they fan out via WS + FCM and you don't want either to fire on a real socket / Firebase project.
- Call `resetDb()` in `beforeEach`, `resetAppConfig()` once in `beforeAll` (or in `afterEach` if your test mutates config).

## Isolation notes

Tests run **sequentially** (`fileParallelism: false`, `sequence.concurrent: false`)
because they share one DB schema and one Valkey db. If you ever need
parallelism: switch to per-test transactions (`BEGIN` in `beforeEach`, `ROLLBACK`
in `afterEach`) or per-test schemas (`CREATE SCHEMA test_${random}`) and update
`vitest.config.js`.

## Switching to local Docker

If you'd rather run an isolated, throwaway Postgres + Valkey on your machine:

```
docker compose -f docker-compose.test.yml up -d
# In .env.test:
TEST_DATABASE_URL=postgresql://test:test@localhost:55432/halobestie_test
TEST_DB_SCHEMA=public
TEST_VALKEY_URL=redis://localhost:56379/0

npm test
docker compose -f docker-compose.test.yml down -v
```

The non-default ports (55432, 56379) avoid clashing with any local Postgres /
Redis you have running. Note `TEST_DB_SCHEMA=public` is OK in the Docker case
because the whole database is throwaway — schema isolation is only required
when sharing with the dev DB.

## Safety guards

- `setup.js` hard-fails if `TEST_DB_SCHEMA === 'public'` AND `TEST_DATABASE_URL` looks like the dev DB. (Schema reuse on the dev DB would clobber dev tables.)
- `setup.js` hard-fails if any required env var is missing — silent fallback to dev URLs would be catastrophic.
- The migration runs as a **child process** (not in-process) so its `sql.end()` at the bottom doesn't tear down the singleton this test process shares with services.
