gongxiangp2/lib/__tests__/product-barcode-images-route.test.ts

227 lines
6.9 KiB
TypeScript

/** @jest-environment node */
import { GET } from '@/app/api/external/product-barcode-images/route'
import { logApiCall, validateApiKey } from '@/lib/external-api'
import { prisma } from '@/lib/prisma'
jest.mock('@/lib/prisma', () => ({
prisma: {
materialAssetBarcode: {
findMany: jest.fn(),
},
},
}))
jest.mock('@/lib/url-rewrite', () => ({
rewriteStorageUrlForClient: jest.fn((url?: string | null) => (url ? `rewritten:${url}` : null)),
}))
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 },
INVALID_BARCODE: { code: 'INVALID_BARCODE', message: '产品条码不能为空', status: 400 },
BARCODE_NOT_FOUND: { code: 'BARCODE_NOT_FOUND', message: '未找到对应的产品条码图片', status: 404 },
SERVER_ERROR: { code: 'SERVER_ERROR', message: '服务器内部错误', status: 500 },
},
}
})
describe('external product-barcode-images route', () => {
beforeEach(() => {
jest.clearAllMocks()
;(validateApiKey as jest.Mock).mockResolvedValue({
id: 'api-key-1',
name: 'product-platform',
isActive: true,
})
;(prisma.materialAssetBarcode.findMany as jest.Mock).mockResolvedValue([])
})
function createRequest(url: string) {
return {
nextUrl: new URL(url),
headers: new Headers({
'X-API-Key': 'test-key',
'user-agent': 'jest',
}),
} as any
}
test('API Key 无效时应返回 401', async () => {
;(validateApiKey as jest.Mock).mockResolvedValue(null)
const response = await GET(
createRequest('https://example.com/api/external/product-barcode-images?barcode=690000000001'),
)
const body = await response.json()
expect(response.status).toBe(401)
expect(body.error.code).toBe('INVALID_API_KEY')
})
test('barcode 为空时应返回 400', async () => {
const response = await GET(
createRequest('https://example.com/api/external/product-barcode-images?barcode='),
)
const body = await response.json()
expect(response.status).toBe(400)
expect(body.error.code).toBe('INVALID_BARCODE')
expect(logApiCall).toHaveBeenCalledWith(
expect.objectContaining({
queryType: 'barcode',
statusCode: 400,
}),
)
})
test('找不到条码时应返回 404', async () => {
const response = await GET(
createRequest('https://example.com/api/external/product-barcode-images?barcode=690000000001'),
)
const body = await response.json()
expect(response.status).toBe(404)
expect(body.error.code).toBe('BARCODE_NOT_FOUND')
})
test('命中多条历史记录时应按最新优先返回并标记 isLatest', async () => {
;(prisma.materialAssetBarcode.findMany as jest.Mock).mockResolvedValue([
{
assetId: 'asset-old',
barcode: '690000000001',
asset: {
id: 'asset-old',
fileUrl: 'https://s3.example/old.webp',
fileName: 'old.webp',
sortOrder: 1,
skuCode: 'SKU-OLD',
createdAt: new Date('2026-04-21T09:00:00.000Z'),
updatedAt: new Date('2026-04-21T09:05:00.000Z'),
barcodes: [{ barcode: '690000000001' }],
material: {
id: 'material-old',
taskNo: 'TASK202604210001',
productModel: 'SPU-OLD',
categoryName: '老店铺',
sceneName: '卧室',
authorName: '旧作者',
user: {
realName: '李四',
username: 'lisi',
},
},
},
},
{
assetId: 'asset-new',
barcode: '690000000001',
asset: {
id: 'asset-new',
fileUrl: 'https://s3.example/new.webp',
fileName: 'new.webp',
sortOrder: 0,
skuCode: 'SKU-NEW',
createdAt: new Date('2026-04-22T09:00:00.000Z'),
updatedAt: new Date('2026-04-22T09:05:00.000Z'),
barcodes: [{ barcode: '690000000001' }, { barcode: '690000000002' }],
material: {
id: 'material-new',
taskNo: 'TASK202604220001',
productModel: 'SPU-NEW',
categoryName: '新店铺',
sceneName: '客厅',
authorName: '新作者',
user: {
realName: '张三',
username: 'zhangsan',
},
},
},
},
])
const response = await GET(
createRequest('https://example.com/api/external/product-barcode-images?barcode=690000000001'),
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data).toEqual({
barcode: '690000000001',
total: 2,
items: [
{
materialId: 'material-new',
assetId: 'asset-new',
taskNo: 'TASK202604220001',
productModel: 'SPU-NEW',
categoryName: '新店铺',
shopName: '新店铺',
sceneName: '客厅',
skuCode: 'SKU-NEW',
barcodes: ['690000000001', '690000000002'],
previewUrl: 'rewritten:https://s3.example/new.webp',
fileName: 'new.webp',
sortOrder: 0,
uploadedAt: '2026-04-22T09:00:00.000Z',
updatedAt: '2026-04-22T09:05:00.000Z',
uploaderName: '张三',
isLatest: true,
},
{
materialId: 'material-old',
assetId: 'asset-old',
taskNo: 'TASK202604210001',
productModel: 'SPU-OLD',
categoryName: '老店铺',
shopName: '老店铺',
sceneName: '卧室',
skuCode: 'SKU-OLD',
barcodes: ['690000000001'],
previewUrl: 'rewritten:https://s3.example/old.webp',
fileName: 'old.webp',
sortOrder: 1,
uploadedAt: '2026-04-21T09:00:00.000Z',
updatedAt: '2026-04-21T09:05:00.000Z',
uploaderName: '李四',
isLatest: false,
},
],
})
expect(logApiCall).toHaveBeenCalledWith(
expect.objectContaining({
queryType: 'barcode',
queryValue: '690000000001',
statusCode: 200,
}),
)
})
})