/** * 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 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} — 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 }