Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js and pairing-failure.service.js (new); rewritten pairing.service.js (payment-gated blast + targeted "Curhat lagi" + cancel + fallback); rewritten extension.service.js (data-driven auto-approve with offline safeguard, charge-at-approval); pricing.service.js (extension tiers without free trial); mitra-status.service.js (countAvailableMitras cached path); 60s sweeper for stale payment sessions - Backend routes: client.payment.routes, client.mitra-availability.routes, internal/failed-pairings.routes; client.chat.routes rewritten for payment-gated start + /returning + /cancel + /fallback-to-blast; internal/config.routes adds 4 new keys with Valkey invalidate publish - client_app: mitra-availability poll, payment screen + notifier, pairing notifier rewrite (PairingTargetedWaiting + PairingFailed states), targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi" CTA, failed-pairing terminal, extension via payment-session - mitra_app: PairingRequestType enum, returning-chat 20s countdown auto-dismiss, extension card "otomatis disetujui" copy - control_center: 4 new config rows in Settings, Failed Pairings page (filter + paginate + action menu), sidebar + route registered - Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4 pass), Maestro mobile scaffold (CLI install pending) - Bugs found via Playwright + fixed: LoginPage labels not associated with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE in allow-methods (silent settings breakage in browsers since Stage 4) - Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A), phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md (today's run results) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
backend/test/README.md
Normal file
118
backend/test/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user