457 lines
16 KiB
TypeScript
457 lines
16 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import {
|
||
AUTO_LISTING_DELIVERY_STATUS,
|
||
AUTO_LISTING_EVENT_TYPE,
|
||
createExternalNotificationTarget,
|
||
listExternalNotificationDeliveries,
|
||
listExternalNotificationTargets,
|
||
queueTaskDetailBarcodeImagesUpdatedNotification,
|
||
queueTaskDetailFullSetNotification,
|
||
processExternalNotificationDelivery,
|
||
TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE,
|
||
retryExternalNotificationDelivery,
|
||
updateExternalNotificationTarget,
|
||
} from '@/lib/external-auto-listing'
|
||
import { prisma } from '@/lib/prisma'
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
$executeRawUnsafe: jest.fn(),
|
||
$queryRawUnsafe: jest.fn(),
|
||
$transaction: jest.fn(),
|
||
material: {
|
||
findFirst: jest.fn(),
|
||
},
|
||
materialAssetBarcode: {
|
||
findMany: jest.fn(),
|
||
},
|
||
},
|
||
}))
|
||
|
||
jest.mock('@/lib/public-app-url', () => ({
|
||
getPublicAppOrigin: jest.fn(() => 'https://sc.jiansun.vip'),
|
||
}))
|
||
|
||
describe('external auto listing', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
jest.useFakeTimers()
|
||
;(prisma.$executeRawUnsafe as jest.Mock).mockResolvedValue(undefined)
|
||
;(prisma.$queryRawUnsafe as jest.Mock).mockResolvedValue([])
|
||
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) =>
|
||
callback({
|
||
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
||
$queryRawUnsafe: jest.fn().mockResolvedValue([]),
|
||
}),
|
||
)
|
||
;(prisma.materialAssetBarcode.findMany as jest.Mock).mockResolvedValue([])
|
||
})
|
||
|
||
afterEach(() => {
|
||
jest.useRealTimers()
|
||
})
|
||
|
||
test('应能创建并更新自动上架通知目标', async () => {
|
||
;(prisma.$queryRawUnsafe as jest.Mock)
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'bearer',
|
||
authConfig: { token: 'secret-token' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
},
|
||
])
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'bearer',
|
||
authConfig: { token: 'secret-token' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
},
|
||
])
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台-新版',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: false,
|
||
authType: 'header',
|
||
authConfig: { headerName: 'X-Signature', headerValue: 'abc123' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:30:00.000Z'),
|
||
},
|
||
])
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台-新版',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: false,
|
||
authType: 'header',
|
||
authConfig: { headerName: 'X-Signature', headerValue: 'abc123' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:30:00.000Z'),
|
||
},
|
||
])
|
||
|
||
const created = await createExternalNotificationTarget({
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
authType: 'bearer',
|
||
authConfig: { token: 'secret-token' },
|
||
})
|
||
|
||
expect(created).toEqual({
|
||
id: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'bearer',
|
||
authConfig: { token: 'secret-token' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: '2026-03-25T10:00:00.000Z',
|
||
updatedAt: '2026-03-25T10:00:00.000Z',
|
||
})
|
||
|
||
const listed = await listExternalNotificationTargets()
|
||
expect(listed).toHaveLength(1)
|
||
|
||
const updated = await updateExternalNotificationTarget('target-1', {
|
||
name: '自动上架平台-新版',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: false,
|
||
authType: 'header',
|
||
authConfig: {
|
||
headerName: 'X-Signature',
|
||
headerValue: 'abc123',
|
||
},
|
||
})
|
||
|
||
expect(updated).toEqual({
|
||
id: 'target-1',
|
||
name: '自动上架平台-新版',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: false,
|
||
authType: 'header',
|
||
authConfig: { headerName: 'X-Signature', headerValue: 'abc123' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE, TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: '2026-03-25T10:00:00.000Z',
|
||
updatedAt: '2026-03-25T10:30:00.000Z',
|
||
})
|
||
})
|
||
|
||
test('投递成功后应标记为 succeeded', async () => {
|
||
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) =>
|
||
callback({
|
||
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
||
$queryRawUnsafe: jest.fn().mockResolvedValue([
|
||
{
|
||
id: 'delivery-1',
|
||
status: 'pending',
|
||
payload: { eventId: 'evt-1' },
|
||
attemptCount: 0,
|
||
maxAttempts: 5,
|
||
updatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
targetId: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'bearer',
|
||
authConfig: { token: 'secret-token' },
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
targetUpdatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
},
|
||
]),
|
||
}),
|
||
)
|
||
global.fetch = jest.fn(async () => new Response('ok', { status: 200 })) as any
|
||
|
||
const result = await processExternalNotificationDelivery('delivery-1')
|
||
|
||
expect(result).toEqual({
|
||
success: true,
|
||
status: AUTO_LISTING_DELIVERY_STATUS.SUCCEEDED,
|
||
})
|
||
expect(prisma.$executeRawUnsafe).toHaveBeenCalledWith(
|
||
expect.stringContaining(`UPDATE "${'external_notification_deliveries'}"`),
|
||
'delivery-1',
|
||
AUTO_LISTING_DELIVERY_STATUS.SUCCEEDED,
|
||
200,
|
||
'ok',
|
||
)
|
||
})
|
||
|
||
test('创建通知任务时应把预览图和拉取地址写入 payload', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'material-1',
|
||
taskNo: null,
|
||
productModel: 'XG025503',
|
||
categoryName: '旗舰店',
|
||
sceneName: '客厅',
|
||
materialType: '任务-详情',
|
||
previewUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/cover.webp',
|
||
isFullSet: true,
|
||
createdAt: new Date('2026-03-25T08:00:00.000Z'),
|
||
assets: [
|
||
{
|
||
id: 'asset-main-1',
|
||
assetType: 'main-preview',
|
||
fileUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/main/1.webp',
|
||
fileName: 'main-1.webp',
|
||
sortOrder: 0,
|
||
},
|
||
{
|
||
id: 'asset-sku-1',
|
||
assetType: 'sku-preview',
|
||
fileUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/sku/1.webp',
|
||
fileName: 'sku-1.webp',
|
||
sortOrder: 1,
|
||
},
|
||
{
|
||
id: 'asset-detail-1',
|
||
assetType: 'detail-preview',
|
||
fileUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/detail/1.webp',
|
||
fileName: 'detail-1.webp',
|
||
sortOrder: 2,
|
||
},
|
||
],
|
||
})
|
||
;(prisma.$queryRawUnsafe as jest.Mock)
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'none',
|
||
authConfig: {},
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
},
|
||
])
|
||
.mockResolvedValueOnce([{ id: 'delivery-1' }])
|
||
|
||
const createdIds = await queueTaskDetailFullSetNotification('material-1')
|
||
const insertCall = (prisma.$queryRawUnsafe as jest.Mock).mock.calls[1]
|
||
const payload = JSON.parse(insertCall[9])
|
||
|
||
expect(createdIds).toEqual(['delivery-1'])
|
||
expect(payload.pullUrl).toBe(
|
||
'https://sc.jiansun.vip/api/external/product-assets?materialId=material-1',
|
||
)
|
||
expect(payload.coverPreviewUrl).toBe(
|
||
'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/cover.webp',
|
||
)
|
||
expect(payload.previewAssets).toEqual({
|
||
main: [
|
||
{
|
||
id: 'asset-main-1',
|
||
url: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/main/1.webp',
|
||
fileName: 'main-1.webp',
|
||
sortOrder: 0,
|
||
},
|
||
],
|
||
sku: [
|
||
{
|
||
id: 'asset-sku-1',
|
||
url: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/sku/1.webp',
|
||
fileName: 'sku-1.webp',
|
||
sortOrder: 1,
|
||
},
|
||
],
|
||
detail: [
|
||
{
|
||
id: 'asset-detail-1',
|
||
url: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/detail/1.webp',
|
||
fileName: 'detail-1.webp',
|
||
sortOrder: 2,
|
||
},
|
||
],
|
||
})
|
||
})
|
||
|
||
test('投递失败后应进入 retrying,并支持手工补发查询', async () => {
|
||
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) =>
|
||
callback({
|
||
$executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
|
||
$queryRawUnsafe: jest.fn().mockResolvedValue([
|
||
{
|
||
id: 'delivery-2',
|
||
status: 'pending',
|
||
payload: { eventId: 'evt-2' },
|
||
attemptCount: 0,
|
||
maxAttempts: 5,
|
||
updatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
targetId: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'none',
|
||
authConfig: {},
|
||
eventTypes: [AUTO_LISTING_EVENT_TYPE],
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
targetUpdatedAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
},
|
||
]),
|
||
}),
|
||
)
|
||
;(prisma.$queryRawUnsafe as jest.Mock)
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'delivery-2',
|
||
targetId: 'target-1',
|
||
targetName: '自动上架平台',
|
||
eventType: AUTO_LISTING_EVENT_TYPE,
|
||
materialId: 'material-1',
|
||
materialType: '任务-详情',
|
||
taskNo: null,
|
||
status: AUTO_LISTING_DELIVERY_STATUS.RETRYING,
|
||
attemptCount: 1,
|
||
maxAttempts: 5,
|
||
nextRetryAt: new Date('2026-03-25T10:01:00.000Z'),
|
||
lastStatusCode: 500,
|
||
lastResponseBody: 'server error',
|
||
lastError: 'HTTP 500',
|
||
createdAt: new Date('2026-03-25T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-03-25T10:00:01.000Z'),
|
||
completedAt: null,
|
||
},
|
||
])
|
||
global.fetch = jest.fn(async () => new Response('server error', { status: 500 })) as any
|
||
|
||
const result = await processExternalNotificationDelivery('delivery-2')
|
||
const deliveries = await listExternalNotificationDeliveries()
|
||
await retryExternalNotificationDelivery('delivery-2')
|
||
|
||
expect(result).toEqual({
|
||
success: false,
|
||
status: AUTO_LISTING_DELIVERY_STATUS.RETRYING,
|
||
})
|
||
expect(deliveries).toHaveLength(1)
|
||
expect(deliveries[0].status).toBe(AUTO_LISTING_DELIVERY_STATUS.RETRYING)
|
||
expect(prisma.$executeRawUnsafe).toHaveBeenCalledWith(
|
||
expect.stringContaining(`UPDATE "${'external_notification_deliveries'}"`),
|
||
'delivery-2',
|
||
AUTO_LISTING_DELIVERY_STATUS.PENDING,
|
||
)
|
||
})
|
||
|
||
test('条码图片更新通知应展开成一条码一记录并附带 lookupUrl', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'material-1',
|
||
taskNo: 'TASK202604220001',
|
||
productModel: 'XG025503',
|
||
categoryName: '旗舰店',
|
||
sceneName: '客厅',
|
||
materialType: '任务-详情',
|
||
authorName: '旧作者',
|
||
createdAt: new Date('2026-04-22T09:00:00.000Z'),
|
||
user: {
|
||
realName: '张三',
|
||
username: 'zhangsan',
|
||
},
|
||
assets: [
|
||
{
|
||
id: 'asset-sku-1',
|
||
fileUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/sku/1.webp',
|
||
fileName: 'sku-1.webp',
|
||
sortOrder: 0,
|
||
skuCode: 'SKU-001',
|
||
createdAt: new Date('2026-04-22T09:00:00.000Z'),
|
||
updatedAt: new Date('2026-04-22T09:05:00.000Z'),
|
||
barcodes: [{ barcode: '690000000001' }, { barcode: '690000000002' }],
|
||
},
|
||
],
|
||
})
|
||
;(prisma.materialAssetBarcode.findMany as jest.Mock).mockResolvedValue([
|
||
{
|
||
barcode: '690000000001',
|
||
assetId: 'asset-sku-1',
|
||
asset: {
|
||
createdAt: new Date('2026-04-22T09:00:00.000Z'),
|
||
sortOrder: 0,
|
||
},
|
||
},
|
||
{
|
||
barcode: '690000000002',
|
||
assetId: 'asset-sku-1',
|
||
asset: {
|
||
createdAt: new Date('2026-04-22T09:00:00.000Z'),
|
||
sortOrder: 0,
|
||
},
|
||
},
|
||
])
|
||
;(prisma.$queryRawUnsafe as jest.Mock)
|
||
.mockResolvedValueOnce([
|
||
{
|
||
id: 'target-1',
|
||
name: '自动上架平台',
|
||
callbackUrl: 'https://auto.example.com/callback',
|
||
isActive: true,
|
||
authType: 'none',
|
||
authConfig: {},
|
||
eventTypes: [TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE],
|
||
createdAt: new Date('2026-04-22T10:00:00.000Z'),
|
||
updatedAt: new Date('2026-04-22T10:00:00.000Z'),
|
||
},
|
||
])
|
||
.mockResolvedValueOnce([{ id: 'delivery-barcode-1' }])
|
||
|
||
const createdIds = await queueTaskDetailBarcodeImagesUpdatedNotification('material-1', ['asset-sku-1'])
|
||
const insertCall = (prisma.$queryRawUnsafe as jest.Mock).mock.calls[1]
|
||
const payload = JSON.parse(insertCall[9])
|
||
|
||
expect(createdIds).toEqual(['delivery-barcode-1'])
|
||
expect(insertCall[3]).toBe(TASK_DETAIL_BARCODE_IMAGES_UPDATED_EVENT_TYPE)
|
||
expect(typeof insertCall[7]).toBe('string')
|
||
expect(insertCall[7]).toBe(payload.eventId)
|
||
expect(payload.items).toEqual([
|
||
{
|
||
materialId: 'material-1',
|
||
assetId: 'asset-sku-1',
|
||
skuCode: 'SKU-001',
|
||
barcode: '690000000001',
|
||
previewUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/sku/1.webp',
|
||
fileName: 'sku-1.webp',
|
||
sortOrder: 0,
|
||
uploadedAt: '2026-04-22T09:00:00.000Z',
|
||
updatedAt: '2026-04-22T09:05:00.000Z',
|
||
uploaderName: '张三',
|
||
isLatest: true,
|
||
lookupUrl:
|
||
'https://sc.jiansun.vip/api/external/product-barcode-images?barcode=690000000001',
|
||
},
|
||
{
|
||
materialId: 'material-1',
|
||
assetId: 'asset-sku-1',
|
||
skuCode: 'SKU-001',
|
||
barcode: '690000000002',
|
||
previewUrl: 'https://s3.jiansun.vip/gongxiangpan/public-display/task-detail/material-1/sku/1.webp',
|
||
fileName: 'sku-1.webp',
|
||
sortOrder: 0,
|
||
uploadedAt: '2026-04-22T09:00:00.000Z',
|
||
updatedAt: '2026-04-22T09:05:00.000Z',
|
||
uploaderName: '张三',
|
||
isLatest: true,
|
||
lookupUrl:
|
||
'https://sc.jiansun.vip/api/external/product-barcode-images?barcode=690000000002',
|
||
},
|
||
])
|
||
})
|
||
})
|