gongxiangp2/lib/__tests__/product-assets-route.test.ts
2026-05-07 16:37:22 +08:00

304 lines
9.4 KiB
TypeScript

/** @jest-environment node */
import { GET } from '@/app/api/external/product-assets/route'
import { getMaterialById, getMaterialByTaskNo, TASK_STATUS, updateTaskStatus } from '@/lib/product-package'
import { logApiCall, validateApiKey } from '@/lib/external-api'
jest.mock('@/lib/external-api', () => {
const { NextResponse } = require('next/server')
return {
getApiKeyFromRequest: jest.fn((request: { headers: Headers }) => request.headers.get('X-API-Key')),
getClientIp: jest.fn(() => '127.0.0.1'),
logApiCall: jest.fn().mockResolvedValue(undefined),
successResponse: jest.fn((data: unknown, message?: string) =>
NextResponse.json({
success: true,
...(message ? { message } : {}),
data,
}),
),
errorResponse: jest.fn((error: { code: string; message: string; status: number }) =>
NextResponse.json(
{
success: false,
error: {
code: error.code,
message: error.message,
},
},
{ status: error.status },
),
),
validateApiKey: jest.fn(),
API_ERROR: {
INVALID_API_KEY: { code: 'INVALID_API_KEY', message: 'API Key 无效或缺失', status: 401 },
TASK_NOT_FOUND: { code: 'TASK_NOT_FOUND', message: '任务号不存在', status: 404 },
MATERIAL_NOT_FOUND: { code: 'MATERIAL_NOT_FOUND', message: '素材不存在', status: 404 },
MATERIAL_DELETED: { code: 'MATERIAL_DELETED', message: '素材已被删除', status: 410 },
INVALID_TASK_NO: { code: 'INVALID_TASK_NO', message: '任务号格式错误', status: 400 },
INVALID_MATERIAL_ID: { code: 'INVALID_MATERIAL_ID', message: '素材ID格式错误', status: 400 },
INVALID_QUERY: { code: 'INVALID_QUERY', message: '请提供 taskNo 或 materialId', status: 400 },
SERVER_ERROR: { code: 'SERVER_ERROR', message: '服务器内部错误', status: 500 },
},
}
})
jest.mock('@/lib/product-package', () => ({
validateTaskNo: jest.fn((taskNo?: string | null) => Boolean(taskNo && taskNo.startsWith('TASK'))),
getMaterialByTaskNo: jest.fn(),
getMaterialById: jest.fn(),
updateTaskStatus: jest.fn().mockResolvedValue(undefined),
softDeleteByTaskNo: jest.fn(),
TASK_STATUS: {
PENDING: 'pending',
COMPLETED: 'completed',
FETCHED: 'fetched',
},
ASSET_TYPE: {
MAIN: 'main',
SKU: 'sku',
DETAIL: 'detail',
PSD: 'psd',
},
}))
jest.mock('@/lib/download-token', () => ({
generateDownloadUrl: jest.fn((_baseUrl: string, { materialId }: { materialId: string }) => `https://download.example/${materialId}`),
}))
jest.mock('@/lib/public-app-url', () => ({
getPublicAppOrigin: jest.fn(() => 'https://sc.jiansun.vip'),
}))
jest.mock('@/lib/url-rewrite', () => ({
rewriteStorageUrlForClient: jest.fn((url?: string | null) => (url ? `rewritten:${url}` : null)),
}))
describe('external product-assets route', () => {
beforeEach(() => {
jest.clearAllMocks()
;(validateApiKey as jest.Mock).mockResolvedValue({
id: 'api-key-1',
name: 'auto-listing',
isActive: true,
})
})
function createRequest(url: string) {
return {
nextUrl: new URL(url),
headers: new Headers({
'X-API-Key': 'test-key',
'user-agent': 'jest',
}),
} as any
}
test('materialId 查询应返回任务详情正式资源,并为所有资源组补齐 sortOrder', async () => {
;(getMaterialById as jest.Mock).mockResolvedValue({
id: '35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
taskNo: null,
productModel: 'SPU-001',
categoryName: '旗舰店',
sceneName: '客厅',
materialType: '任务-详情',
taskStatus: TASK_STATUS.COMPLETED,
isFullSet: true,
deletedAt: null,
createdAt: new Date('2026-03-25T09:00:00.000Z'),
updatedAt: new Date('2026-03-25T09:10:00.000Z'),
assets: [
{
id: 'asset-main',
assetType: 'main',
fileUrl: 'https://s3.example/main.zip',
fileName: '主图.zip',
fileSize: BigInt(10),
sortOrder: 0,
},
{
id: 'asset-sku',
assetType: 'sku',
fileUrl: 'https://s3.example/sku.zip',
fileName: 'SKU.zip',
fileSize: BigInt(20),
sortOrder: 1,
},
{
id: 'asset-detail',
assetType: 'detail',
fileUrl: 'https://s3.example/detail.zip',
fileName: '详情.zip',
fileSize: BigInt(30),
sortOrder: 2,
},
{
id: 'asset-psd',
assetType: 'psd',
fileUrl: 'https://s3.example/source.psd',
fileName: '源文件.psd',
fileSize: BigInt(40),
sortOrder: 3,
},
],
})
const response = await GET(
createRequest(
'https://example.com/api/external/product-assets?materialId=35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
),
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data).toEqual({
materialId: '35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
taskNo: null,
productModel: 'SPU-001',
categoryName: '旗舰店',
shopName: '旗舰店',
sceneName: '客厅',
isFullSet: true,
status: TASK_STATUS.FETCHED,
createdAt: '2026-03-25T09:00:00.000Z',
updatedAt: '2026-03-25T09:10:00.000Z',
assets: {
main: [
{
id: 'asset-main',
url: 'rewritten:https://s3.example/main.zip',
fileName: '主图.zip',
fileSize: 10,
sortOrder: 0,
},
],
sku: [
{
id: 'asset-sku',
url: 'rewritten:https://s3.example/sku.zip',
fileName: 'SKU.zip',
fileSize: 20,
sortOrder: 1,
},
],
detail: [
{
id: 'asset-detail',
url: 'rewritten:https://s3.example/detail.zip',
fileName: '详情.zip',
fileSize: 30,
sortOrder: 2,
},
],
psd: [
{
id: 'asset-psd',
url: 'rewritten:https://s3.example/source.psd',
fileName: '源文件.psd',
fileSize: 40,
sortOrder: 3,
},
],
},
downloadUrl: 'https://download.example/35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
downloadUrls: {
all: 'https://download.example/35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
},
})
expect(updateTaskStatus).toHaveBeenCalledWith(
'35d8644b-1e09-4ce8-9bb0-c529b39ba52e',
TASK_STATUS.FETCHED,
expect.objectContaining({
taskFetchedAt: expect.any(Date),
}),
)
expect(getMaterialByTaskNo).not.toHaveBeenCalled()
expect(logApiCall).toHaveBeenCalled()
})
test('未提供 taskNo 和 materialId 时应立即失败', async () => {
const response = await GET(createRequest('https://example.com/api/external/product-assets'))
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({
success: false,
error: {
code: 'INVALID_QUERY',
message: '请提供 taskNo 或 materialId',
},
})
})
test('任务-其他应继续走老接口地址,但返回轻量 files 结构', async () => {
;(getMaterialByTaskNo as jest.Mock).mockResolvedValue({
id: 'material-task-other-1',
taskNo: 'TASK202603310001',
productModel: 'TASK202603310001',
categoryName: '通用',
sceneName: '通用',
materialType: '任务-其他',
taskStatus: TASK_STATUS.COMPLETED,
isFullSet: false,
deletedAt: null,
previewUrl: 'https://s3.example/task-other-cover.webp',
zipFileName: '任务其他源文件.zip',
zipFileSize: BigInt(52428800),
createdAt: new Date('2026-03-31T08:00:00.000Z'),
updatedAt: new Date('2026-03-31T08:05:00.000Z'),
assets: [
{
id: 'asset-preview',
assetType: 'preview',
fileUrl: 'https://s3.example/task-other-cover.webp',
fileName: 'cover.webp',
fileSize: BigInt(1024),
sortOrder: 0,
},
{
id: 'asset-zip',
assetType: 'zip',
fileUrl: 'uploads/materials/task-other.zip',
fileName: '任务其他源文件.zip',
fileSize: BigInt(52428800),
sortOrder: 0,
},
],
})
const response = await GET(
createRequest('https://example.com/api/external/product-assets?taskNo=TASK202603310001'),
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data).toEqual({
materialId: 'material-task-other-1',
materialType: '任务-其他',
taskNo: 'TASK202603310001',
productModel: 'TASK202603310001',
categoryName: '通用',
shopName: '通用',
sceneName: '通用',
isFullSet: false,
status: TASK_STATUS.FETCHED,
createdAt: '2026-03-31T08:00:00.000Z',
updatedAt: '2026-03-31T08:05:00.000Z',
files: [
{
id: 'material-task-other-1',
url: 'rewritten:https://s3.example/task-other-cover.webp',
downloadUrl: 'https://download.example/material-task-other-1',
fileName: '任务其他源文件.zip',
fileSize: 52428800,
},
],
downloadUrls: {
all: 'https://download.example/material-task-other-1',
},
})
})
})