227 lines
6.9 KiB
TypeScript
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,
|
|
}),
|
|
)
|
|
})
|
|
})
|