From 8f356b1337dbc24f29ce6f792c42fd978c5d4640 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 3 Oct 2026 15:57:36 +0800 Subject: [PATCH] test(order): compare import templates across independent test customers --- scripts/README.md | 2 ++ scripts/client-import-template-inspect.py | 3 ++ scripts/client-import-template-test-env.mjs | 33 ++++++++++++++++++- .../client-import-template-test-env.test.mjs | 28 ++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 scripts/client-import-template-test-env.test.mjs diff --git a/scripts/README.md b/scripts/README.md index 42898ab08..243ab654d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,5 +1,7 @@ # 后端脚本管理与全量 HTTP 验收索引 +模板检查器可另传显式 `--customer-profile=<同一授权目录下test-ui-*.env>`,仅替换该次独立会话的测试客户凭据,Gateway 与 TEST 环境保持由 `test-acceptance.env` 决定;拒绝生产、数据库文件和目录外路径。报告只保存权威客户 ID 的 SHA256,以 `compareCustomerTemplates` 对照两个不同客户的匹配结果;读取地址/模板不串客户与“提交外客户地址被拒绝”分开记录。 + `client-import-template-test-env.mjs` 为批量导入模板只读 TEST 检查,默认 `--plan` 零网络。执行参数沿用 `--execute --environment=TEST --run-id=<新批次>`,另传 `--python=`;只使用显式 TEST 验收配置。内部 `client-import-template-inspect.py` 下载模板并在内存用 bundled openpyxl 读取,不写 Excel;与完整启用仓库分页、各国内仓渠道并集、当前客户取货/私人地址逐项对照,同时检查命名范围、隐藏选项和 50 行下拉。只登录/退出可用 POST,不导入或创建业务数据,不保存原始模板和客户地址;报告独占 `reports/client-template-/`,失败保留。对应 `.test.py` 在内存构造最小 OOXML 检查缺项、额外项、错范围和重复名称;真实第二客户负向隔离及 Excel 桌面操作另验。 `client-workbench-test-env.mjs` 是客户订单工作台只读 TEST 分页验收入口,默认 `--plan` 零网络。Firstleg/Order 新工作台接口部署后,以显式 `node --env-file=<用户指定目录的test-acceptance.env> scripts/client-workbench-test-env.mjs --execute --environment=TEST --run-id=<新批次>` 执行;独立客户登录及退出是唯一 POST,不创建订单、不写 SQL、不调用会补建预入库记录的旧草稿列表。要求稳定样本超过 200 条,遍历完整工作台及独立统一订单集合,核对完整正式单集合、费用/草稿待办、报关队列分页数量、21页切片、渠道筛选和伪造客户参数。报关业务资格、第二客户负向权限、页面及写后刷新仍需另验。报告独占 `reports/client-workbench-/`,请求前持久记录脱敏意图,失败保留且结束退出自己的会话;不保存凭据或原始客户明细。`client-workbench-test-env.test.mjs` 用故障响应验证拒绝截断、重复、错页和客户泄漏,不能替代真实 TEST 结果。 diff --git a/scripts/client-import-template-inspect.py b/scripts/client-import-template-inspect.py index bc86a6079..0280bb4ed 100644 --- a/scripts/client-import-template-inspect.py +++ b/scripts/client-import-template-inspect.py @@ -135,6 +135,9 @@ def main(config): auth = request("POST", "/auth/login", {"loginId": config["username"], "password": config["password"]}) token, refresh = auth.get("accessToken", ""), auth.get("refreshToken", "") require(token and refresh, "Incomplete login") + profile = request("GET", "/merchant/v1/iam/tenant/profile") + require(profile.get("id") is not None, "Missing authenticated customer identity") + report["customerIdentitySha256"] = hashlib.sha256(str(profile["id"]).encode()).hexdigest() warehouses, total, page_no = [], None, 1 while True: page = request("GET", f"/merchant/v1/warehouses?status=ACTIVE&page={page_no}&size=200") diff --git a/scripts/client-import-template-test-env.mjs b/scripts/client-import-template-test-env.mjs index 250747351..ab2ef9620 100644 --- a/scripts/client-import-template-test-env.mjs +++ b/scripts/client-import-template-test-env.mjs @@ -1,4 +1,6 @@ import { spawn } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { parseEnv } from 'node:util' import { resolve, dirname } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { executionOptions } from './client-workbench-test-env.mjs' @@ -7,18 +9,47 @@ export async function execute(args = process.argv.slice(2), execArgs = process.e if (!args.length || args.length === 1 && args[0] === '--plan') return { plan: true, businessWrites: false, checks: ['full warehouse catalog', 'channel union', 'own customer addresses', 'named dropdown ranges', '50 input rows'], required: 'Explicit TEST profile, new run-id, and --python=' } - if (args.length !== 4 || !args[3].startsWith('--python=')) throw new Error('Explicit bundled Python required') + if (![4, 5].includes(args.length) || !args[3].startsWith('--python=')) throw new Error('Explicit bundled Python required') const options = executionOptions(args.slice(0, 3), execArgs, env) const python = resolve(args[3].slice(9)) if (!python.replaceAll('\\', '/').endsWith('/codex-primary-runtime/dependencies/python/python.exe')) throw new Error('Use bundled workspace Python') const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const config = { environment: 'TEST', base: options.base, username: env.CHONGCHONG_MERCHANT_USERNAME, password: env.CHONGCHONG_MERCHANT_PASSWORD, runId: options.runId } + if (args[4]) { + const profile = customerProfilePath(args[4], execArgs[0].slice(11)) + const credentials = parseEnv(await readFile(profile, 'utf8')) + if (!credentials.MERCHANT_TEST_USERNAME || !credentials.MERCHANT_TEST_PASSWORD) throw new Error('Missing TEST customer credentials') + config.username = credentials.MERCHANT_TEST_USERNAME + config.password = credentials.MERCHANT_TEST_PASSWORD + } const child = spawn(python, [resolve(root, 'scripts/client-import-template-inspect.py')], { cwd: root, windowsHide: true, stdio: ['pipe', 'inherit', 'inherit'] }) child.stdin.end(JSON.stringify(config)) const code = await new Promise((accept, reject) => { child.on('error', reject); child.on('close', accept) }) return { success: code === 0, report: resolve(root, 'reports', `client-template-${options.runId}`, 'report.json') } } +export function customerProfilePath(argument, environmentFile) { + if (!argument.startsWith('--customer-profile=')) throw new Error('Unknown argument') + const profile = resolve(argument.slice(19)) + if (dirname(profile) !== dirname(resolve(environmentFile)) || !/^test-ui-[A-Za-z0-9_-]+\.env$/.test(profile.split(/[\\/]/).at(-1))) + throw new Error('Customer profile must be an explicit TEST UI file beside the authorized TEST configuration') + return profile +} +export function compareCustomerTemplates(primary, control) { + const checks = { + bothRunsPassed: primary.success === true && control.success === true, + bothSessionsClosed: primary.sessionClosed === true && control.sessionClosed === true, + differentCustomers: typeof primary.customerIdentitySha256 === 'string' && typeof control.customerIdentitySha256 === 'string' + && primary.customerIdentitySha256 !== control.customerIdentitySha256, + primaryHasPrivateAddress: primary.template?.checks?.['私人目的仓地址']?.actualCount > 0, + controlHasNoPrivateAddress: control.template?.checks?.['私人目的仓地址']?.actualCount === 0, + privateOptionsMatchOwnDirectory: [primary, control].every(r => r.template?.checks?.['私人目的仓地址']?.passed === true), + pickupOptionsMatchOwnDirectory: [primary, control].every(r => r.template?.checks?.['取货地址']?.passed === true), + differentPickupAddressSamples: primary.template?.checks?.['取货地址']?.actualSha256 !== control.template?.checks?.['取货地址']?.actualSha256, + } + return { success: Object.values(checks).every(Boolean), checks, + limitation: 'Read-time customer isolation only; importing another customer address remains a separate negative test' } +} if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { execute().then(result => { console.log(JSON.stringify(result)); if (result.success === false) process.exitCode = 1 }) .catch(() => { console.error('Template audit did not start. Check explicit TEST profile, runtime and new run-id.'); process.exitCode = 1 }) diff --git a/scripts/client-import-template-test-env.test.mjs b/scripts/client-import-template-test-env.test.mjs new file mode 100644 index 000000000..0d5de2e5d --- /dev/null +++ b/scripts/client-import-template-test-env.test.mjs @@ -0,0 +1,28 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { resolve } from 'node:path' +import { customerProfilePath, compareCustomerTemplates, execute } from './client-import-template-test-env.mjs' + +const directory = 'C:/Users/BY-OY/.codex/env/chongchong-global' +test('template plan performs no login and requires no runtime', async () => { + assert.equal((await execute(['--plan'], [], {})).plan, true) +}) +test('cross-customer read evidence rejects same identity, leaked options and incomplete cleanup', () => { + const report = (id, privateCount) => ({ success: true, sessionClosed: true, customerIdentitySha256: id, + template: { checks: { '私人目的仓地址': { actualCount: privateCount, passed: true }, '取货地址': { actualSha256: id, passed: true } } } }) + const primary = report('customer-A', 1), control = report('customer-B', 0) + assert.equal(compareCustomerTemplates(primary, control).success, true) + assert.equal(compareCustomerTemplates(primary, primary).success, false) + assert.equal(compareCustomerTemplates(primary, report('customer-B', 1)).success, false) + assert.equal(compareCustomerTemplates(primary, { ...control, sessionClosed: false }).success, false) + const failed = report('customer-B', 0); failed.template.checks['私人目的仓地址'].passed = false + assert.equal(compareCustomerTemplates(primary, failed).success, false) +}) +test('explicit existing TEST UI customer profile cannot select production or escape the authorized directory', () => { + const argument = `--customer-profile=${directory}/test-ui-fixture.env` + assert.equal(customerProfilePath(argument, directory + '/test-acceptance.env'), resolve(directory, 'test-ui-fixture.env')) + for (const value of ['prod-acceptance.env', '../test-ui-fixture.env', 'test-database.env', 'test-ui-fixture.env/another.env']) { + assert.throws(() => customerProfilePath(`--customer-profile=${directory}/${value}`, directory + '/test-acceptance.env')) + } + assert.throws(() => customerProfilePath('--unknown=file', directory + '/test-acceptance.env')) +})