/** @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', }, }), ) }) 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') }) })