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:
2026-05-03 23:02:49 +08:00
parent f3766813f3
commit d09e50af55
92 changed files with 9579 additions and 437 deletions

118
backend/test/README.md Normal file
View 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.