205 lines
6.1 KiB
TypeScript
205 lines
6.1 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import { GET } from '@/app/api/external/dingtalk-material-match/route'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { logApiCall, validateApiKey } from '@/lib/external-api'
|
||
|
||
jest.mock('@/lib/external-api', () => {
|
||
const { NextResponse } = require('next/server')
|
||
return {
|
||
getApiKeyFromRequest: jest.fn(
|
||
(request: { headers: Headers; nextUrl: URL }, options?: { allowQuery?: boolean }) =>
|
||
request.headers.get('X-API-Key') ||
|
||
(options?.allowQuery ? request.nextUrl.searchParams.get('apiKey') : null),
|
||
),
|
||
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,
|
||
}),
|
||
),
|
||
validateApiKey: jest.fn(),
|
||
}
|
||
})
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
material: {
|
||
findFirst: jest.fn(),
|
||
},
|
||
},
|
||
}))
|
||
|
||
jest.mock('@/lib/public-app-url', () => ({
|
||
getPublicAppOrigin: jest.fn(() => 'https://sc.jiansun.vip'),
|
||
}))
|
||
|
||
jest.mock('@/lib/url-rewrite', () => ({
|
||
rewriteStorageUrlForClient: jest.fn((url?: string | null) => {
|
||
if (!url) {
|
||
return null
|
||
}
|
||
|
||
return /^https?:\/\//i.test(url) ? url : `/${String(url).replace(/^\/+/, '')}`
|
||
}),
|
||
}))
|
||
|
||
describe('external dingtalk-material-match route', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
;(prisma.material.findFirst as jest.Mock).mockReset()
|
||
;(logApiCall as jest.Mock).mockReset().mockResolvedValue(undefined)
|
||
;(validateApiKey as jest.Mock).mockReset().mockResolvedValue({
|
||
id: 'api-key-1',
|
||
name: 'test-key',
|
||
isActive: true,
|
||
})
|
||
})
|
||
|
||
function createRequest(url: string, apiKey = 'test-key') {
|
||
return {
|
||
nextUrl: new URL(url),
|
||
headers: new Headers({
|
||
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
||
'user-agent': 'jest',
|
||
}),
|
||
} as any
|
||
}
|
||
|
||
test('缺少 SPU 时应返回 400', async () => {
|
||
const response = await GET(createRequest('https://example.com/api/external/dingtalk-material-match'))
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(400)
|
||
expect(body).toEqual({
|
||
success: false,
|
||
error: {
|
||
code: 'MISSING_SPU',
|
||
message: 'SPU 为必填项',
|
||
},
|
||
})
|
||
})
|
||
|
||
test('命中素材时应返回任意地址与匹配摘要', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValueOnce({
|
||
id: 'material-1',
|
||
productModel: 'SPU001',
|
||
materialType: '任务-详情',
|
||
categoryName: '顾家家居',
|
||
sceneName: '客厅',
|
||
productCategoryName: '沙发',
|
||
previewUrl: 'uploads/previews/spu001.jpg',
|
||
videoPreview540Url: null,
|
||
videoCoverUrl: null,
|
||
zipFilePath: 'uploads/zip/spu001.zip',
|
||
updatedAt: new Date('2026-04-30T08:00:00.000Z'),
|
||
})
|
||
|
||
const response = await GET(
|
||
createRequest(
|
||
'https://example.com/api/external/dingtalk-material-match?requirementType=任务-详情&brand=顾家家居&sceneCategory=客厅&productCategory=沙发&spu=spu001',
|
||
),
|
||
)
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(body.success).toBe(true)
|
||
expect(body.data).toEqual({
|
||
matched: true,
|
||
message: '查询成功',
|
||
spu: 'SPU001',
|
||
materialId: 'material-1',
|
||
requirementType: '任务-详情',
|
||
brand: '顾家家居',
|
||
sceneCategory: '客厅',
|
||
productCategory: '沙发',
|
||
address: 'https://sc.jiansun.vip/uploads/previews/spu001.jpg',
|
||
updatedAt: '2026-04-30T08:00:00.000Z',
|
||
})
|
||
expect(prisma.material.findFirst).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
where: expect.objectContaining({
|
||
productModel: 'SPU001',
|
||
materialType: '任务-详情',
|
||
categoryName: '顾家家居',
|
||
sceneName: '客厅',
|
||
productCategoryName: '沙发',
|
||
}),
|
||
}),
|
||
)
|
||
})
|
||
|
||
test('只传 SPU 时也应查询,不强制要求其它筛选项', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValueOnce({
|
||
id: 'material-spu-only',
|
||
productModel: 'SPU002',
|
||
materialType: '抖音-千川素材',
|
||
categoryName: '顾家家居',
|
||
sceneName: '客厅',
|
||
productCategoryName: '沙发',
|
||
previewUrl: 'https://s3.example/preview.webp',
|
||
videoPreview540Url: null,
|
||
videoCoverUrl: null,
|
||
zipFilePath: 'uploads/zip/spu002.zip',
|
||
updatedAt: new Date('2026-05-07T08:00:00.000Z'),
|
||
})
|
||
|
||
const response = await GET(
|
||
createRequest('https://example.com/api/external/dingtalk-material-match?spu=spu002'),
|
||
)
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(body.success).toBe(true)
|
||
expect(body.data.matched).toBe(true)
|
||
expect(prisma.material.findFirst).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
where: {
|
||
deletedAt: null,
|
||
productModel: 'SPU002',
|
||
taskDetailPrivate: false,
|
||
},
|
||
}),
|
||
)
|
||
})
|
||
|
||
test('未命中时应返回该SPU下暂无素材', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValueOnce(null)
|
||
|
||
const response = await GET(
|
||
createRequest('https://example.com/api/external/dingtalk-material-match?spu=spu001&brand=顾家家居'),
|
||
)
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(body.success).toBe(true)
|
||
expect(body.data).toEqual({
|
||
matched: false,
|
||
message: '该SPU下暂无素材',
|
||
spu: 'SPU001',
|
||
requirementType: null,
|
||
brand: '顾家家居',
|
||
sceneCategory: null,
|
||
productCategory: null,
|
||
address: null,
|
||
})
|
||
})
|
||
|
||
test('允许钉钉用固定 URL 参数传 apiKey,避免每次手填请求头', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValueOnce(null)
|
||
|
||
const response = await GET(
|
||
createRequest(
|
||
'https://example.com/api/external/dingtalk-material-match?apiKey=test-key&spu=spu001',
|
||
'',
|
||
),
|
||
)
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(validateApiKey).toHaveBeenCalledWith('test-key')
|
||
})
|
||
})
|