汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
83 行
5.2 KiB
JavaScript
83 行
5.2 KiB
JavaScript
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;
|
|
}
|
|
}
|