304 lines
9.4 KiB
TypeScript
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',
|
|
},
|
|
})
|
|
})
|
|
})
|