import { randomUUID } from 'node:crypto' import { db, resetAppConfig } from './db.js' import { getTestValkey } from './valkey.js' /** * Insert a customer row. Defaults to the schema after the Phase 3.4 auth rewrite * (display_name nullable, is_anonymous defaults true). */ export const createCustomer = async ({ id = randomUUID(), callName = `TestCust-${id.slice(0, 6)}`, phone = null, isAnonymous = false, } = {}) => { const sql = db() const [row] = await sql` INSERT INTO customers (id, display_name, phone, is_anonymous) VALUES (${id}, ${callName}, ${phone}, ${isAnonymous}) RETURNING id, display_name, phone, is_anonymous, created_at ` return row } /** * Insert a mitra row. If `isOnline` is true, also creates the mitra_online_status row * so pairing.findAvailableMitras includes it. */ export const createMitra = async ({ id = randomUUID(), callName = `TestMitra-${id.slice(0, 6)}`, phone = null, isActive = true, isOnline = false, } = {}) => { const sql = db() // mitras.phone is NOT NULL UNIQUE — synthesize a unique phone if not given. const finalPhone = phone || `+62800${Math.floor(Math.random() * 1e10).toString().padStart(10, '0')}` const [row] = await sql` INSERT INTO mitras (id, display_name, phone, is_active) VALUES (${id}, ${callName}, ${finalPhone}, ${isActive}) RETURNING id, display_name, phone, is_active, created_at ` if (isOnline) { const now = new Date() await sql` INSERT INTO mitra_online_status (mitra_id, is_online, last_online_at, last_heartbeat_at, updated_at) VALUES (${id}, true, ${now}, ${now}, ${now}) ON CONFLICT (mitra_id) DO UPDATE SET is_online = true, last_online_at = ${now}, last_heartbeat_at = ${now}, updated_at = ${now} ` // Mirror to Valkey so findAvailableMitras (Valkey-driven) sees this mitra. // resetDb already FLUSHDBs Valkey, so seeding here per-mitra keeps tests // hermetic without depending on production's startup seed. const v = getTestValkey() await v.multi() .sadd('mitras:online', id) .set(`mitra:heartbeat:${id}`, now.toISOString()) .set(`mitra:capacity:${id}`, 0) .exec() } if (!isActive) { const v = getTestValkey() await v.sadd('mitras:deactivated', id) } return row } /** * Reset app_config rows to their canonical defaults. Tests that mutate config call * this in afterEach (or rely on the global beforeEach in resetAll). */ export const seedDefaultConfig = () => resetAppConfig() /** * Insert (or fetch) a control-center user with full `config` permissions. Used by * /internal/config/* route tests that need a JWT subject that survives the * `attachCcUser` + `requirePermission('config', …)` preHandler chain. * * Idempotent: re-runs return the same row by email. We do NOT truncate cc_user / roles * between tests (db.js documents the rationale), so subsequent test files inherit * whatever this seeded. */ export const createCcUser = async ({ email = `cc-test-${randomUUID().slice(0, 8)}@halobestie.test`, displayName = 'CC Test User', permissions = { mitra: ['create', 'read', 'update', 'delete'], control_center_users: ['create', 'read', 'update', 'delete'], config: ['read', 'update'], roles: ['create', 'read', 'update', 'delete'], }, } = {}) => { const sql = db() // One role per test invocation, named after a slice of the email so re-runs don't // collide with the seeded `super_admin` role from seed.js. const roleName = `cc-test-role-${email.slice(0, 16)}` const [role] = await sql` INSERT INTO roles (name, permissions) VALUES (${roleName}, ${sql.json(permissions)}) ON CONFLICT (name) DO UPDATE SET permissions = EXCLUDED.permissions RETURNING id ` const [user] = await sql` INSERT INTO control_center_users (email, display_name, role_id, password_hash) VALUES (${email}, ${displayName}, ${role.id}, 'unused-for-jwt-tests') ON CONFLICT (email) DO UPDATE SET role_id = EXCLUDED.role_id RETURNING id, email, display_name, role_id, created_at ` return { ...user, role: { id: role.id, permissions } } } /** * Convenience: full reset between tests. Truncates Phase 3.7 tables, restores * default config rows. */ export { resetDb, resetDbHard, resetAppConfig } from './db.js'