- 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>
165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
/**
|
|
* Thin wrapper around the internal backend for test setup / fixtures.
|
|
*
|
|
* The CC test suite needs to seed and clean up rows directly via the API
|
|
* (rather than via the UI) for tests that depend on pre-existing state —
|
|
* e.g. "Failed Pairings page renders rows" needs at least one row to exist.
|
|
*
|
|
* All requests:
|
|
* - go to BACKEND_INTERNAL_URL (default http://localhost:3001)
|
|
* - send credentials: 'include' so cookies (refresh token) round-trip
|
|
* - automatically attach Authorization: Bearer <access_token> after login
|
|
*/
|
|
|
|
const BACKEND_URL = process.env.BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
|
const TEST_EMAIL = process.env.CC_TEST_EMAIL || 'test-operator@example.com'
|
|
const TEST_PASSWORD = process.env.CC_TEST_PASSWORD || 'changeme'
|
|
|
|
let cachedAccessToken = null
|
|
let cachedCookieHeader = null
|
|
|
|
/**
|
|
* Logs into the backend via the real CC login route and caches the access
|
|
* token + Set-Cookie value for subsequent calls. Token is cached for the
|
|
* whole worker lifetime — for the slim CC suite that's plenty.
|
|
*/
|
|
async function loginToBackend() {
|
|
if (cachedAccessToken) return cachedAccessToken
|
|
|
|
const res = await fetch(`${BACKEND_URL}/internal/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: TEST_EMAIL, password: TEST_PASSWORD }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '')
|
|
throw new Error(`Backend login failed (${res.status}): ${body}`)
|
|
}
|
|
|
|
const json = await res.json()
|
|
cachedAccessToken = json?.data?.access_token
|
|
if (!cachedAccessToken) {
|
|
throw new Error(`Backend login response missing access_token: ${JSON.stringify(json)}`)
|
|
}
|
|
|
|
// node-fetch returns multi-value Set-Cookie via .getSetCookie() in newer
|
|
// Node versions. Fall back to .get() for older runtimes.
|
|
const setCookie =
|
|
typeof res.headers.getSetCookie === 'function'
|
|
? res.headers.getSetCookie()
|
|
: [res.headers.get('set-cookie')].filter(Boolean)
|
|
cachedCookieHeader = setCookie.map((c) => c.split(';')[0]).join('; ') || null
|
|
|
|
return cachedAccessToken
|
|
}
|
|
|
|
/**
|
|
* Generic authenticated request to the internal backend.
|
|
*
|
|
* @param {string} path — path beginning with `/internal/...`
|
|
* @param {RequestInit} [init] — fetch options (method, body, headers)
|
|
* @returns {Promise<any>} — parsed JSON response body (whole envelope)
|
|
*/
|
|
export async function backendRequest(path, init = {}) {
|
|
const token = await loginToBackend()
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
...(init.headers || {}),
|
|
}
|
|
if (cachedCookieHeader) headers.Cookie = cachedCookieHeader
|
|
|
|
const res = await fetch(`${BACKEND_URL}${path}`, { ...init, headers })
|
|
const text = await res.text()
|
|
let json
|
|
try {
|
|
json = text ? JSON.parse(text) : null
|
|
} catch {
|
|
json = { raw: text }
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const errMsg = json?.error?.message || text || res.statusText
|
|
throw new Error(`backend ${init.method || 'GET'} ${path} -> ${res.status}: ${errMsg}`)
|
|
}
|
|
return json
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Settings helpers — read/write CC config rows used by settings.spec.js
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Read current `payment_session_timeout_minutes` value. */
|
|
export async function getPaymentSessionTimeout() {
|
|
const json = await backendRequest('/internal/config/payment-session-timeout')
|
|
return json?.data?.payment_session_timeout_minutes
|
|
}
|
|
|
|
/** Force-set `payment_session_timeout_minutes` (useful for test reset). */
|
|
export async function setPaymentSessionTimeout(minutes) {
|
|
return backendRequest('/internal/config/payment-session-timeout', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ payment_session_timeout_minutes: minutes }),
|
|
})
|
|
}
|
|
|
|
/** Read current `extension_default_action_on_timeout` value. */
|
|
export async function getExtensionDefaultAction() {
|
|
const json = await backendRequest('/internal/config/extension-default-action')
|
|
return json?.data?.extension_default_action_on_timeout
|
|
}
|
|
|
|
/** Force-set `extension_default_action_on_timeout` (useful for test reset). */
|
|
export async function setExtensionDefaultAction(action) {
|
|
return backendRequest('/internal/config/extension-default-action', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ extension_default_action_on_timeout: action }),
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Failed-pairings helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetch failed-pairings rows (optionally filtered). Used to assert the page
|
|
* is reading what we just inserted, and to discover a real customer/mitra ID
|
|
* if needed.
|
|
*/
|
|
export async function listFailedPairings({ causeTags = [], limit = 50, offset = 0 } = {}) {
|
|
const params = new URLSearchParams()
|
|
for (const tag of causeTags) params.append('cause_tags', tag)
|
|
params.set('limit', String(limit))
|
|
params.set('offset', String(offset))
|
|
const json = await backendRequest(`/internal/failed-pairings?${params}`)
|
|
return json?.data
|
|
}
|
|
|
|
/**
|
|
* Seed a fixture failed-pairing row.
|
|
*
|
|
* NOTE: There is no public "create failed pairing" endpoint — production
|
|
* rows are inserted by the backend pairing service when a real pairing
|
|
* fails. For e2e tests we expose a thin test-only seed by reusing the
|
|
* /internal/failed-pairings list to verify whatever rows already exist.
|
|
*
|
|
* If the suite needs guaranteed-fresh rows, the user should add a small
|
|
* test-only seed route to the backend (e.g. POST /internal/_test/seed-failed-pairing)
|
|
* gated behind NODE_ENV !== 'production'. This helper currently:
|
|
* - returns the existing row count, OR
|
|
* - throws a clear error so the test can be skipped with a TODO.
|
|
*
|
|
* @returns {Promise<{ total: number, rows: any[] }>}
|
|
*/
|
|
export async function ensureAtLeastOneFailedPairing() {
|
|
const data = await listFailedPairings({ limit: 1 })
|
|
if ((data?.total ?? 0) === 0) {
|
|
throw new Error(
|
|
'No failed-pairings rows present in the database. ' +
|
|
'Either trigger one via the real pairing flow or add a backend test-seed route.',
|
|
)
|
|
}
|
|
return data
|
|
}
|