后端补充高风险链路测试
This commit is contained in:
@@ -169,3 +169,105 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('redeemClaimTaskWithInventoryFallbackWithDeps waits for inventory when used code has no replacement', async () => {
|
||||||
|
const events = []
|
||||||
|
const consumed = []
|
||||||
|
const reserveCalls = []
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
task: {
|
||||||
|
id: 57,
|
||||||
|
browser_session_id: 'txbs-used',
|
||||||
|
},
|
||||||
|
orderItem: {
|
||||||
|
sku_code: 'df-cdk-used',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const initialInventoryItem = {
|
||||||
|
id: 7,
|
||||||
|
display_value: 'USED1234CODE5678',
|
||||||
|
credential_type: 'tencent_code',
|
||||||
|
inventory_group_code: 'B组',
|
||||||
|
}
|
||||||
|
|
||||||
|
let thrown = null
|
||||||
|
try {
|
||||||
|
await redeemClaimTaskWithInventoryFallbackWithDeps(
|
||||||
|
context,
|
||||||
|
initialInventoryItem,
|
||||||
|
{
|
||||||
|
redeemTencentBrowserSession: async () => ({
|
||||||
|
redeem: {
|
||||||
|
final: {
|
||||||
|
redeem: {
|
||||||
|
popup: {
|
||||||
|
text: '兑换结果',
|
||||||
|
detail: '兑换码已使用。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
markInventoryItemConsumed: async (inventoryItemId, reason, updatedAt) => {
|
||||||
|
consumed.push({ inventoryItemId, reason, updatedAt })
|
||||||
|
return { id: inventoryItemId, status: 'consumed', invalid_reason: reason, updated_at: updatedAt }
|
||||||
|
},
|
||||||
|
reserveInventoryForTask: async (payload) => {
|
||||||
|
reserveCalls.push(payload)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
createTaskEvent: async (taskId, eventType, payload, createdAt) => {
|
||||||
|
events.push({ taskId, eventType, payload, createdAt })
|
||||||
|
},
|
||||||
|
nowIso: () => '2026-04-14T12:05:00.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
thrown = error
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(thrown)
|
||||||
|
assert.equal(thrown.errorCode, 'claim_inventory_replacement_exhausted')
|
||||||
|
assert.equal(thrown.redeemTaskState.taskStatus, 'waiting_inventory')
|
||||||
|
assert.equal(thrown.redeemTaskState.inventoryStatus, 'pending')
|
||||||
|
assert.equal(thrown.redeemTaskState.deliveryStatus, 'pending')
|
||||||
|
assert.equal(thrown.redeemTaskState.attempts.length, 1)
|
||||||
|
assert.deepEqual(consumed, [
|
||||||
|
{
|
||||||
|
inventoryItemId: 7,
|
||||||
|
reason: '兑换码已使用。',
|
||||||
|
updatedAt: '2026-04-14T12:05:00.000Z',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert.deepEqual(reserveCalls, [
|
||||||
|
{
|
||||||
|
skuCode: 'df-cdk-used',
|
||||||
|
taskId: 57,
|
||||||
|
credentialType: 'tencent_code',
|
||||||
|
roleKey: 'primary_code',
|
||||||
|
inventoryGroupCodes: ['B组'],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert.deepEqual(
|
||||||
|
events.map((event) => ({
|
||||||
|
taskId: event.taskId,
|
||||||
|
eventType: event.eventType,
|
||||||
|
payload: event.payload,
|
||||||
|
createdAt: event.createdAt,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
taskId: 57,
|
||||||
|
eventType: 'claim_redeem_code_used',
|
||||||
|
payload: {
|
||||||
|
inventoryItemId: 7,
|
||||||
|
codeMasked: 'USED****5678',
|
||||||
|
resultCode: '',
|
||||||
|
resultMessage: '兑换码已使用。',
|
||||||
|
},
|
||||||
|
createdAt: '2026-04-14T12:05:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import crypto from 'node:crypto'
|
|||||||
|
|
||||||
import { runtimeConfig } from '../../config/runtime.js'
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
import {
|
import {
|
||||||
|
buildWebhookEventInput,
|
||||||
parseAgisoTradeRequest,
|
parseAgisoTradeRequest,
|
||||||
resolveAgisoTradeIgnoreReason,
|
resolveAgisoTradeIgnoreReason,
|
||||||
shouldEnrichAgisoXianyuTrade,
|
shouldEnrichAgisoXianyuTrade,
|
||||||
@@ -147,3 +148,35 @@ test('verifyAgisoSignatureWithSecret supports documented and legacy signatures',
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('buildWebhookEventInput preserves raw request payload for later replay', () => {
|
||||||
|
const input = buildWebhookEventInput(
|
||||||
|
{
|
||||||
|
headers: { 'x-request-id': 'req-1' },
|
||||||
|
query: { timestamp: '1713097840', sign: 'bad-sign' },
|
||||||
|
body: { json: '{"biz_order_id":"4502259793206016214"}' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: 'agiso',
|
||||||
|
platform: 'xianyu',
|
||||||
|
shopId: 'shop-9',
|
||||||
|
shopName: '咸鱼店铺',
|
||||||
|
eventType: 'payment_success',
|
||||||
|
eventKey: 'agiso:xianyu:4502259793206016214:payment_success',
|
||||||
|
signatureValid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(input.provider, 'agiso')
|
||||||
|
assert.equal(input.platform, 'xianyu')
|
||||||
|
assert.equal(input.eventType, 'payment_success')
|
||||||
|
assert.equal(input.eventKey, 'agiso:xianyu:4502259793206016214:payment_success')
|
||||||
|
assert.equal(input.signatureValid, false)
|
||||||
|
assert.equal(input.processed, false)
|
||||||
|
assert.equal(input.processError, '')
|
||||||
|
assert.equal(input.relatedOrderId, null)
|
||||||
|
assert.deepEqual(JSON.parse(input.headersJson), { 'x-request-id': 'req-1' })
|
||||||
|
assert.deepEqual(JSON.parse(input.queryJson), { timestamp: '1713097840', sign: 'bad-sign' })
|
||||||
|
assert.deepEqual(JSON.parse(input.bodyJson), { json: '{"biz_order_id":"4502259793206016214"}' })
|
||||||
|
assert.match(input.createdAt, /^\d{4}-\d{2}-\d{2}T/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ test('hasAgisoAutoDeliverySucceeded detects successful context on any task', ()
|
|||||||
]), false)
|
]), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('hasAgisoAutoDeliverySucceeded handles string and invalid task context payloads', () => {
|
||||||
|
assert.equal(hasAgisoAutoDeliverySucceeded([
|
||||||
|
{ context_json: '{bad-json' },
|
||||||
|
{ context_json: JSON.stringify({ agisoAutoDelivery: { status: 'success' } }) },
|
||||||
|
]), true)
|
||||||
|
|
||||||
|
assert.equal(hasAgisoAutoDeliverySucceeded([
|
||||||
|
{ context_json: '{bad-json' },
|
||||||
|
{ context_json: JSON.stringify({ agisoAutoDelivery: { status: 'skipped' } }) },
|
||||||
|
]), false)
|
||||||
|
})
|
||||||
|
|
||||||
test('isAgisoAutoDeliverySuccess accepts empty successful payloads and explicit success codes', () => {
|
test('isAgisoAutoDeliverySuccess accepts empty successful payloads and explicit success codes', () => {
|
||||||
assert.equal(isAgisoAutoDeliverySuccess(200, {}), true)
|
assert.equal(isAgisoAutoDeliverySuccess(200, {}), true)
|
||||||
assert.equal(isAgisoAutoDeliverySuccess(200, { IsSuccess: true }), true)
|
assert.equal(isAgisoAutoDeliverySuccess(200, { IsSuccess: true }), true)
|
||||||
|
|||||||
@@ -247,13 +247,22 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test`
|
- `npm test`
|
||||||
|
23. 高风险链路补测试:
|
||||||
|
- webhook 事件入库 payload 可稳定保留原始 headers/query/body,便于后续 replay
|
||||||
|
- 库存换码在“兑换码已使用且无替换库存”时进入等待库存状态,并记录已使用事件
|
||||||
|
- Agiso 自动发货成功标记可从字符串 context 中识别,且会忽略无效 JSON
|
||||||
|
24. Docker 内验证通过:
|
||||||
|
- 相关 3 个测试文件共 14 个用例通过
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm test` 共 125 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
第一批继续推进时,建议按这个顺序:
|
第一批继续推进时,建议按这个顺序:
|
||||||
|
|
||||||
1. 为 webhook、库存换码、自动发货补测试
|
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
||||||
2. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
2. 逐步移除服务层 `@ts-nocheck`,优先处理自动发货、claim session、webhook service
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user