Files
baiyue-firstleg/scripts/admin-test-env-modules/return-boundary-scenarios-contract.test.mjs
T

101 lines
6.6 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { rejected, classifyRace, stableState, verifyNoMutation } from './return-boundary-scenarios.mjs'
import { run as createMerchant } from './merchant-qualification-fixture.mjs'
const response = (code = '200', http = 200) => ({ http, body: { code }, requestId: 'request', traceId: 'trace' })
test('拒绝断言只接受明确业务错误,不把网络故障、越权、成功当成参数拒绝', () => {
for (const code of ['A0400', 'A0422']) assert.equal(rejected(response(code), [400, 422]), true)
for (const code of ['200', 'A0403', 'A0409', 'S0500', 'S0503']) assert.equal(rejected(response(code), [400, 422]), false)
for (const http of [0, 401, 403, 500, 502, 503]) assert.equal(rejected(response('error', http), [400, 422]), false)
assert.equal(rejected({ ...response('A0409'), traceId: '' }, [409]), false)
})
test('并发判定区分双成功、双失败、服务故障与唯一成功', () => {
assert.deepEqual(classifyRace([response(), response('A0409')]), { successes: 1, conflicts: 1, unexpected: 0 })
assert.deepEqual(classifyRace([response(), response()]), { successes: 2, conflicts: 0, unexpected: 0 })
assert.deepEqual(classifyRace([response('A0409'), response('A0409')]), { successes: 0, conflicts: 2, unexpected: 0 })
assert.deepEqual(classifyRace([response(), response('S0500', 500)]), { successes: 1, conflicts: 0, unexpected: 1 })
})
test('库存快照保留订单、批次、SKU、来源与状态变化,仅忽略排序及展示时间', () => {
const baseline = [{ returnNo: 'R1', receiptBatchNo: 'B1', quantity: 2, status: 'AVAILABLE', skuTypes: 1, items: [{ skuCode: 'A', quantity: 2 }], updatedAt: 'old' }]
assert.deepEqual(stableState(baseline), stableState([{ ...baseline[0], updatedAt: 'new' }]))
for (const change of [{ returnNo: 'R2' }, { receiptBatchNo: 'B2' }, { quantity: 1 }, { status: 'MERGED' }, { skuTypes: 0 }, { items: [] }]) {
assert.notDeepEqual(stableState(baseline), stableState([{ ...baseline[0], ...change }]))
}
})
test('每个失败请求都立即前后回查,即使接口错误类型不符也检查落账', async () => {
for (const code of ['A0409', '200', 'S0500']) {
let quantity = 2
const events = [], checks = []
await verifyNoMutation({ name: '拒绝操作', snapshot: async () => { events.push('snapshot'); return { quantity } },
action: async () => { events.push('action'); quantity -= 1; return response(code) },
expected: result => rejected(result, [409]), check: (name, result, passed) => checks.push(passed) })
assert.deepEqual(events, ['snapshot', 'action', 'snapshot'])
assert.deepEqual(checks, [code === 'A0409', false])
}
})
test('缺少快照不得执行危险动作或伪造未落账通过', async () => {
let called = false
await assert.rejects(verifyNoMutation({ snapshot: async () => { throw new Error('snapshot unavailable') },
action: async () => { called = true }, check: () => {}, expected: () => true }), /snapshot unavailable/)
assert.equal(called, false)
})
test('扩展由退货主模块调用且不进入换标出库,保留各并发响应诊断及清理', async () => {
const [source, main] = await Promise.all(['return-boundary-scenarios.mjs', 'phase4-overseas-return-receipt.mjs']
.map(file => readFile(new URL(file, import.meta.url), 'utf8')))
assert.match(main, /await runReturnBoundaries\(context, \{ merchantToken, warehouse, picker, access \}\)/)
for (const name of ['跨客户隔离', '跨仓隔离', '并发签收', '并发合箱', '多单全量合箱与来源追溯', '多候选与输入边界']) assert.ok(source.includes(`scenario('${name}'`))
assert.match(source, /recordRace\('并发签收'/)
assert.match(source, /recordRace\('并发合箱'/)
assert.match(source, /recordRace\('并发部分合箱'/)
assert.match(source, /concurrencyRounds: 3/)
assert.match(source, /publishEnvironment: false/)
assert.match(source, /finally[\s\S]*disablePicker/)
assert.match(source, /executed: false, passed: false/)
assert.doesNotMatch(source, /\bfetch\s*\(|process\.env|relabel-applications|outbound-orders|outbound-tasks|\b(?:INSERT|UPDATE|SELECT)\s/i)
})
test('第二客户准备运行真实fixture编排但不替换主客户内存与环境凭据', async () => {
const names = ['MERCHANT_TEST_USERNAME', 'MERCHANT_TEST_PASSWORD']
const saved = names.map(name => process.env[name])
try {
process.env.MERCHANT_TEST_USERNAME = 'primary-user'
process.env.MERCHANT_TEST_PASSWORD = 'primary-placeholder'
const primary = { context: { merchantQualificationFixture: { tenantId: 'A' } }, createdResources: {} }
const secondary = Object.create(primary)
Object.assign(secondary, { marker: 'OFFLINE-20260915-B', options: {}, context: { ...primary.context }, createdResources: {},
findAvailablePhone: async () => '00000000000',
assert: (module, name, result, condition) => assert.ok(condition, name),
apiSuccess: (module, name, result) => { assert.equal(result.body.code, '200', name); return result.body.data },
requestWithBody: async () => ({ body: { code: '200', data: {} } }),
request: async (method, path) => {
let data
if (path.endsWith('/iam/tenants')) data = { id: 'B', administratorUsername: 'secondary-user', temporaryPassword: 'secondary-placeholder' }
else if (path.endsWith('/submit-review')) data = { status: 'PENDING' }
else if (path.endsWith('/iam/reviews')) data = [{ id: 'review-B', tenantId: 'B' }]
else if (path.endsWith('/dimension-initialization')) data = { status: 'READY' }
else if (path.endsWith('/approve')) data = {}
else if (path.endsWith('/qualifications')) data = ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'].map(qualificationType => ({ qualificationType, status: 'APPROVED' }))
else throw new Error(`Unexpected fixture request: ${method} ${path}`)
return { http: 200, body: { code: '200', data } }
} })
await createMerchant(secondary, { publishEnvironment: false })
assert.equal(primary.context.merchantQualificationFixture.tenantId, 'A')
assert.equal(secondary.context.merchantQualificationFixture.tenantId, 'B')
assert.equal(process.env.MERCHANT_TEST_USERNAME, 'primary-user')
assert.equal(process.env.MERCHANT_TEST_PASSWORD, 'primary-placeholder')
assert.ok(!JSON.stringify(secondary.createdResources).includes('secondary-placeholder'))
await createMerchant(secondary)
assert.equal(process.env.MERCHANT_TEST_USERNAME, 'secondary-user')
} finally {
names.forEach((name, index) => { if (saved[index] === undefined) delete process.env[name]; else process.env[name] = saved[index] })
}
})