test(order): compare import templates across independent test customers
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
Reference in New Issue
Block a user