# 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.