fix: close inbound hold bypass paths

This commit is contained in:
2026-09-07 12:10:11 +08:00
parent 6b479d1a68
commit 41582fe17b
28 changed files with 899 additions and 11 deletions
@@ -21,6 +21,17 @@ test('入库扣货模块覆盖隔离、幂等、冲突、候选门禁和统一
assert.match(source, /解除后入库单恢复智能装柜候选/)
assert.match(source, /统一订单列表同步展示扣货字段/)
assert.match(source, /统一订单列表同步恢复扣货字段/)
assert.match(source, /扣货缺少幂等键返回A0400/)
assert.match(source, /扣货空白原因返回A0422/)
assert.match(source, /扣货超长原因返回A0422/)
assert.match(source, /扣货负版本返回A0422/)
assert.match(source, /扣货无效凭证ID返回A0422/)
assert.match(source, /错误客户不能执行扣货/)
assert.match(source, /已扣货时使用新键再次扣货返回A0409/)
assert.match(source, /未扣货时使用新键再次解除返回A0409/)
assert.match(source, /上传本轮扣货关联凭证/)
assert.match(source, /尚未进入不可变历史的凭证禁止下载/)
assert.match(source, /进入不可变历史后的扣货凭证可下载且内容一致/)
assert.match(source, /summary\?\.holdFlag/)
assert.match(source, /summary\?\.currentHoldReason/)
assert.doesNotMatch(source, /\.holdCargo\b|\.holdReason\b|\bholdCargo\s*:|\bholdReason\s*:/)
@@ -39,6 +39,13 @@ function write(context, path, body, key) {
return context.request('POST', path, body, context.token, { 'Idempotency-Key': key })
}
function sameSnapshot(actual, expected) {
return actual?.holdFlag === expected?.holdFlag
&& actual?.currentHoldReason === expected?.currentHoldReason
&& Number(actual?.version) === Number(expected?.version)
&& historyFacts(actual) === historyFacts(expected)
}
export async function run(context) {
const fixture = context.createdResources.orders || {}
const tenantId = id(fixture.tenantId)
@@ -95,21 +102,87 @@ export async function run(context) {
`/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold?tenantId=${encodeURIComponent(foreignTenantId)}`)
expectCode(context, '错误客户不能读取扣货快照', foreignResponse, 'A0404')
const invalidCommands = [
['扣货缺少幂等键返回A0400', context.request('POST', holdPath,
{ expectedVersion: initialVersion, reason: '缺少幂等键' }, context.token), 'A0400'],
['扣货空白原因返回A0422', write(context, holdPath,
{ expectedVersion: initialVersion, reason: ' ' }, `${context.marker}-hold-blank`), 'A0422'],
['扣货超长原因返回A0422', write(context, holdPath,
{ expectedVersion: initialVersion, reason: '测'.repeat(1001) }, `${context.marker}-hold-long-reason`), 'A0422'],
['扣货负版本返回A0422', write(context, holdPath,
{ expectedVersion: -1, reason: '负版本' }, `${context.marker}-hold-negative-version`), 'A0422'],
['扣货无效凭证ID返回A0422', write(context, holdPath,
{ expectedVersion: initialVersion, reason: '无效凭证', evidenceFileId: 0 }, `${context.marker}-hold-bad-evidence`), 'A0422'],
['错误客户不能执行扣货', write(context,
`/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold?tenantId=${encodeURIComponent(foreignTenantId)}`,
{ expectedVersion: initialVersion, reason: '跨客户扣货' }, `${context.marker}-hold-foreign`), 'A0404'],
]
for (const [label, pending, expectedCode] of invalidCommands) {
expectCode(context, label, await pending, expectedCode)
}
const afterInvalidResponse = await context.request('GET', holdPath)
const afterInvalid = context.apiSuccess(MODULE, '回查非法命令后的初始快照', afterInvalidResponse,
evidence(afterInvalidResponse), OPTIONS)
context.assert(MODULE, '非法命令不改变版本、状态或历史', afterInvalidResponse,
sameSnapshot(afterInvalid, initialHold),
`version=${afterInvalid?.version}, history=${afterInvalid?.history?.length ?? 'missing'}`, OPTIONS)
const evidenceBytes = new TextEncoder().encode(`%PDF-1.4\n${context.marker}\n%%EOF`)
const evidencePath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold/evidence?tenantId=${encodeURIComponent(tenantId)}`
const evidenceResponse = await context.requestMultipart('POST', evidencePath, {
content: evidenceBytes, type: 'application/pdf', name: `${context.marker}-hold.pdf`,
})
const evidenceFile = context.apiSuccess(MODULE, '上传本轮扣货关联凭证', evidenceResponse,
evidence(evidenceResponse), OPTIONS)
const evidenceFileId = id(evidenceFile?.fileId)
context.assert(MODULE, '扣货凭证返回安全文件元数据和字符串长整型ID', evidenceResponse,
Boolean(evidenceFileId) && typeof evidenceFile?.fileId === 'string'
&& evidenceFile?.fileName === `${context.marker}-hold.pdf`
&& evidenceFile?.contentType === 'application/pdf'
&& Number(evidenceFile?.sizeBytes) === evidenceBytes.length,
`fileId=${evidenceFileId || 'missing'}, fileName=${evidenceFile?.fileName || 'missing'}, contentType=${evidenceFile?.contentType || 'missing'}`,
OPTIONS)
if (!evidenceFileId) throw new Error('hold evidence upload missing fileId')
const evidenceDownloadPath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold/evidence/${encodeURIComponent(evidenceFileId)}/download?tenantId=${encodeURIComponent(tenantId)}`
const unreferencedDownload = await context.requestBinary('GET', evidenceDownloadPath)
expectCode(context, '尚未进入不可变历史的凭证禁止下载', unreferencedDownload, 'A0404')
const reason = `${context.marker} 财务复核扣货`.slice(0, 1000)
const holdKey = `${context.marker}-inbound-hold`.slice(0, 128)
const holdBody = { expectedVersion: initialVersion, reason }
const holdBody = { expectedVersion: initialVersion, reason, evidenceFileId }
const holdResponse = await write(context, holdPath, holdBody, holdKey)
const held = context.apiSuccess(MODULE, '手工扣货入库单', holdResponse, evidence(holdResponse), OPTIONS)
context.assert(MODULE, '扣货写入当前快照和不可变历史', holdResponse,
held?.holdFlag === true && held?.currentHoldReason === reason && held?.canHold === false
&& held?.canRelease === true && Number(held?.version) === initialVersion + 1
&& held?.history?.[0]?.action === 'HOLD' && held?.history?.[0]?.reason === reason,
&& held?.history?.[0]?.action === 'HOLD' && held?.history?.[0]?.reason === reason
&& id(held?.history?.[0]?.evidenceFileId) === evidenceFileId,
`holdFlag=${held?.holdFlag}, version=${held?.version}, history=${held?.history?.length ?? 'missing'}`,
OPTIONS)
const heldVersion = Number(held?.version)
const heldHistorySize = held?.history?.length
const heldHistoryFacts = historyFacts(held)
const evidenceDownload = await context.requestBinary('GET', evidenceDownloadPath)
context.assert(MODULE, '进入不可变历史后的扣货凭证可下载且内容一致', evidenceDownload,
evidenceDownload?.http === 200 && evidenceDownload?.body == null
&& evidenceDownload?.contentType?.includes('application/pdf')
&& Buffer.from(evidenceDownload?.bytes || []).equals(Buffer.from(evidenceBytes)),
`HTTP=${evidenceDownload?.http ?? 'missing'}, contentType=${evidenceDownload?.contentType || 'missing'}, bytes=${evidenceDownload?.bytes?.length ?? 'missing'}`,
OPTIONS)
const duplicateStateResponse = await write(context, holdPath,
{ expectedVersion: heldVersion, reason: `${context.marker} 重复状态` },
`${context.marker}-inbound-hold-again`.slice(0, 128))
expectCode(context, '已扣货时使用新键再次扣货返回A0409', duplicateStateResponse, 'A0409')
const heldAfterDuplicateResponse = await context.request('GET', holdPath)
const heldAfterDuplicate = context.apiSuccess(MODULE, '回查重复扣货后的当前快照', heldAfterDuplicateResponse,
evidence(heldAfterDuplicateResponse), OPTIONS)
context.assert(MODULE, '重复扣货不改变版本、原因或历史', heldAfterDuplicateResponse,
sameSnapshot(heldAfterDuplicate, held),
`version=${heldAfterDuplicate?.version}, history=${heldAfterDuplicate?.history?.length ?? 'missing'}`, OPTIONS)
const replayResponse = await write(context, holdPath, holdBody, holdKey)
const replayed = context.apiSuccess(MODULE, '扣货请求同键同载荷重放', replayResponse,
evidence(replayResponse), OPTIONS)
@@ -180,6 +253,18 @@ export async function run(context) {
&& historyFactsFrom(historyRows(released).slice(1)) === heldHistoryFacts,
`before=${heldHistorySize}, after=${releasedHistorySize}`, OPTIONS)
const duplicateReleaseResponse = await write(context, releasePath,
{ expectedVersion: releasedVersion, reason: `${context.marker} 重复解除` },
`${context.marker}-inbound-release-again`.slice(0, 128))
expectCode(context, '未扣货时使用新键再次解除返回A0409', duplicateReleaseResponse, 'A0409')
const releasedAfterDuplicateResponse = await context.request('GET', holdPath)
const releasedAfterDuplicate = context.apiSuccess(MODULE, '回查重复解除后的当前快照',
releasedAfterDuplicateResponse, evidence(releasedAfterDuplicateResponse), OPTIONS)
context.assert(MODULE, '重复解除不改变版本、状态或历史', releasedAfterDuplicateResponse,
sameSnapshot(releasedAfterDuplicate, released),
`version=${releasedAfterDuplicate?.version}, history=${releasedAfterDuplicate?.history?.length ?? 'missing'}`,
OPTIONS)
const releaseReplayResponse = await write(context, releasePath, releaseBody, releaseKey)
const releaseReplay = context.apiSuccess(MODULE, '解除请求同键同载荷重放', releaseReplayResponse,
evidence(releaseReplayResponse), OPTIONS)
@@ -55,6 +55,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
'firstleg-service', 'event-bridge-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '装柜/船节点 Outbox 按 eventId 投递'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/FeePaymentVoucherFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '校验付款凭证元数据'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/InboundHoldFileClient.java',
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '扣货凭证上传无幂等键,元数据与下载可重复'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClient.java',
'firstleg-service', 'finance-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '费用 Outbox 按事件幂等键投递'),
feign('firstleg/src/main/java/com/baiyue/firstleg/client/IamCustomerSnapshotClient.java',
@@ -98,11 +98,11 @@ async function discoverFeignEdges() {
return clients.sort((left, right) => left.path.localeCompare(right.path))
}
test('all 54 Feign clients have reviewed context, access and idempotency metadata', async () => {
test('all 55 Feign clients have reviewed context, access and idempotency metadata', async () => {
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN')
.map(({ path, source, target }) => ({ path, source, target }))
.sort((left, right) => left.path.localeCompare(right.path))
assert.equal(reviewed.length, 54)
assert.equal(reviewed.length, 55)
assert.deepEqual(await discoverFeignEdges(), reviewed)
})