import assert from "node:assert/strict"; import { writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; import { verifyManagedEnvironment } from "./full-local-environment.mjs"; // Public synthetic fixtures already used by the local browser suite. They are // never read from an operator account, environment file or production service. export const localAccounts = [ ["CEO", "ceo", "Ceo"], ["ADMIN", "admin", "Admin"], ["MANAGER", "manager", "Manager"], ["EMPLOYEE", "employee", "Employee"], ["FINANCE", "finance", "Finance"], ["FINANCE_MGR", "finance_mgr", "FinanceMgr"], ["HR", "hr", "Hr"], ["ADMINISTRATION", "administration", "Administration"], ].map(([role, identifier, prefix]) => ({ role, identifier: `${identifier}@hyinda.com`, password: `${prefix}Password1234` })); export function validateAccountPreparation({ origin, initialPassword, databaseName }) { const url = new URL(origin); assert.equal(url.protocol, "http:", "synthetic_origin_required"); assert.equal(url.hostname, "127.0.0.1", "synthetic_loopback_required"); assert.ok(url.port && url.pathname === "/" && !url.search && !url.hash && !url.username && !url.password, "synthetic_origin_invalid"); assert.match(databaseName ?? "", /^hyb_oa_webgate_[a-zA-Z0-9_]+$/, "synthetic_database_required"); assert.ok(initialPassword?.length >= 24 && localAccounts.every(a => a.password !== initialPassword), "distinct_initial_password_required"); } export async function prepareAccounts({ origin, initialPassword, databaseName, fetchImpl = fetch }) { validateAccountPreparation({ origin, initialPassword, databaseName }); const checks = []; for (const account of localAccounts) { const request = async (route, payload, token) => { const response = await fetchImpl(`${origin}/api/auth/${route}`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(15000), headers: { "content-type": "application/json", "x-device-fingerprint": `qa-initialize-${account.role}`, ...(token ? { authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(payload), }); // Do not expose raw auth response/payload in failures or receipts. const body = await response.json(); return { status: response.status, data: body.data }; }; const login = await request("login", { identifier: account.identifier, password: initialPassword }); assert.equal(login.status, 200, `initial_login_${account.role}`); assert.equal(login.data?.mustChangePassword, true, `initial_change_required_${account.role}`); const changed = await request("change-password", { oldPassword: initialPassword, newPassword: account.password }, login.data.accessToken); assert.equal(changed.status, 200, `change_password_${account.role}`); assert.equal(changed.data?.mustChangePassword, false, `changed_password_${account.role}`); const old = await request("login", { identifier: account.identifier, password: initialPassword }); assert.equal(old.status, 401, `old_password_rejected_${account.role}`); const fresh = await request("login", { identifier: account.identifier, password: account.password }); assert.equal(fresh.status, 200, `fresh_login_${account.role}`); assert.equal(fresh.data?.mustChangePassword, false, `fresh_password_ready_${account.role}`); checks.push({ role: account.role, initialChangeRequired: true, changedThroughApi: true, oldPasswordRejected: true, newLoginReady: true }); } return { status: "PASS", scope: "run-owned synthetic accounts only", accounts: checks }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { try { verifyManagedEnvironment(process.env.QA_ENVIRONMENT_CONTEXT); validateAccountPreparation({ origin: process.env.E2E_BASE_URL, initialPassword: process.env.QA_SYNTHETIC_INITIAL_PASSWORD, databaseName: process.env.SEED_DEV_DATABASE_CONFIRM }); const require = createRequire(new URL("../../client/package.json", import.meta.url)); const { chromium } = require("@playwright/test"); const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage(); await page.goto(`${process.env.E2E_BASE_URL}/login`); await page.getByTestId("login-identifier").fill(localAccounts[1].identifier); await page.getByTestId("login-password").fill(process.env.QA_SYNTHETIC_INITIAL_PASSWORD); await page.getByTestId("login-submit").click(); await page.waitForURL(url => url.pathname === "/change-password"); assert.equal(await page.getByTestId("change-password-old").isVisible(), true); } finally { await browser.close(); } const result = await prepareAccounts({ origin: process.env.E2E_BASE_URL, initialPassword: process.env.QA_SYNTHETIC_INITIAL_PASSWORD, databaseName: process.env.SEED_DEV_DATABASE_CONFIRM }); result.initialBrowserChangePasswordGate = true; writeFileSync(process.argv[2], `${JSON.stringify(result, null, 2)}\n`, { flag: "wx", mode: 0o600 }); console.log(`Synthetic account preparation: PASS (${result.accounts.length} roles)`); } catch { console.error("Synthetic account preparation failed; business browser tests were not started"); process.exitCode = 1; } }