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:
110
backend/test/setup.js
Normal file
110
backend/test/setup.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Vitest global setup. Runs once per test file before any test bodies.
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Load .env.test (falls back to env vars already in process.env if missing).
|
||||
* 2. Override DATABASE_URL / VALKEY_URL with the *test* equivalents BEFORE any
|
||||
* backend service modules are imported — services capture `getDb()` at module
|
||||
* load, so this rewrite must happen first.
|
||||
* 3. Run the migration against the test schema (idempotent — single migrate.js).
|
||||
* 4. Provide a `beforeEach` truncate hook for any test that imports the helper.
|
||||
*
|
||||
* Why this file is small: helpers live under test/helpers/* and are imported lazily
|
||||
* by individual test files. This file is intentionally just env setup + migrate.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { config as loadDotenv } from 'dotenv'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { afterAll, beforeAll } from 'vitest'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const backendRoot = resolve(__dirname, '..')
|
||||
|
||||
// 1. Load .env.test if it exists
|
||||
const envTestPath = resolve(backendRoot, '.env.test')
|
||||
if (existsSync(envTestPath)) {
|
||||
loadDotenv({ path: envTestPath })
|
||||
}
|
||||
|
||||
// 2. Validate required env (fail fast — silent fallback to dev DB would be catastrophic)
|
||||
const required = ['TEST_DATABASE_URL', 'TEST_VALKEY_URL', 'AUTH_JWT_SECRET']
|
||||
for (const key of required) {
|
||||
if (!process.env[key]) {
|
||||
throw new Error(
|
||||
`Missing required test env var: ${key}. Copy .env.test.example → .env.test or set it in your shell.`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (process.env.AUTH_JWT_SECRET.length < 32) {
|
||||
throw new Error('AUTH_JWT_SECRET must be at least 32 chars')
|
||||
}
|
||||
|
||||
const TEST_SCHEMA = process.env.TEST_DB_SCHEMA || 'halobestie_test'
|
||||
if (TEST_SCHEMA === 'public') {
|
||||
// Hard guard: schema isolation only works if we don't reuse the dev `public` schema.
|
||||
throw new Error(
|
||||
`TEST_DB_SCHEMA must not be "public" (would clobber dev tables). ` +
|
||||
`Set TEST_DB_SCHEMA to something like "halobestie_test".`
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Build the schema-scoped URL used by getDb() in services.
|
||||
// The `?options=-c search_path=...` query param tells Postgres to set search_path
|
||||
// on every new connection. All CREATE TABLE / INSERT / SELECT then default to the
|
||||
// test schema, leaving the dev `public` schema untouched.
|
||||
const baseTestUrl = process.env.TEST_DATABASE_URL
|
||||
const sep = baseTestUrl.includes('?') ? '&' : '?'
|
||||
const scopedTestUrl = `${baseTestUrl}${sep}options=${encodeURIComponent(`-c search_path=${TEST_SCHEMA},public`)}`
|
||||
|
||||
// CRITICAL: rewrite the env vars services read at module load time. Must happen before
|
||||
// any `import { ... } from '../src/services/...'` in a test or helper.
|
||||
process.env.DATABASE_URL = scopedTestUrl
|
||||
process.env.VALKEY_URL = process.env.TEST_VALKEY_URL
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure the schema exists. Use a one-shot connection that's NOT the singleton.
|
||||
const { default: postgres } = await import('postgres')
|
||||
const bootstrap = postgres(process.env.TEST_DATABASE_URL)
|
||||
try {
|
||||
await bootstrap`CREATE SCHEMA IF NOT EXISTS ${bootstrap(TEST_SCHEMA)}`
|
||||
} finally {
|
||||
await bootstrap.end()
|
||||
}
|
||||
|
||||
// Run the migration via a child process so we don't conflict with the singleton
|
||||
// sql client this test process will use. migrate.js calls sql.end() at the bottom,
|
||||
// which would tear down the shared client if invoked in-process.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[resolve(backendRoot, 'src/db/migrate.js')],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: scopedTestUrl,
|
||||
},
|
||||
cwd: backendRoot,
|
||||
encoding: 'utf8',
|
||||
}
|
||||
)
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Test migration failed (exit ${result.status}):\n` +
|
||||
`stdout: ${result.stdout}\nstderr: ${result.stderr}`
|
||||
)
|
||||
}
|
||||
}, 60_000)
|
||||
|
||||
afterAll(async () => {
|
||||
// Best-effort cleanup of any singletons opened by the test process. Each helper that
|
||||
// opens a connection registers its own teardown; this is a safety net.
|
||||
try {
|
||||
const { getDb } = await import('../src/db/client.js')
|
||||
const sql = getDb()
|
||||
await sql.end({ timeout: 5 })
|
||||
} catch {
|
||||
// Singleton was never created — fine.
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user