test(order): compare import templates across independent test customers

This commit is contained in:
2026-10-03 15:57:36 +08:00
parent 18935f1379
commit 8f356b1337
4 changed files with 65 additions and 1 deletions
+2
View File
@@ -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=<load_workspace_dependencies返回的Python路径>`;只使用显式 TEST 验收配置。内部 `client-import-template-inspect.py` 下载模板并在内存用 bundled openpyxl 读取,不写 Excel;与完整启用仓库分页、各国内仓渠道并集、当前客户取货/私人地址逐项对照,同时检查命名范围、隐藏选项和 50 行下拉。只登录/退出可用 POST,不导入或创建业务数据,不保存原始模板和客户地址;报告独占 `reports/client-template-<run-id>/`,失败保留。对应 `.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-<run-id>/`,请求前持久记录脱敏意图,失败保留且结束退出自己的会话;不保存凭据或原始客户明细。`client-workbench-test-env.test.mjs` 用故障响应验证拒绝截断、重复、错页和客户泄漏,不能替代真实 TEST 结果。
@@ -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")
+32 -1
View File
@@ -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=<bundled workspace 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 })
@@ -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'))
})