289 lines
8.5 KiB
TypeScript
289 lines
8.5 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import { NextRequest } from 'next/server'
|
|
import { getServerSession } from 'next-auth'
|
|
|
|
import { GET } from '@/app/api/materials/route'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { getUserViewableMaterialTypes, isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
|
|
|
jest.mock('next-auth', () => ({
|
|
getServerSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
authOptions: {},
|
|
}))
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
count: jest.fn(),
|
|
findFirst: jest.fn(),
|
|
findMany: jest.fn(),
|
|
},
|
|
materialLike: {
|
|
findMany: jest.fn(),
|
|
},
|
|
materialFavorite: {
|
|
findMany: jest.fn(),
|
|
},
|
|
materialAsset: {
|
|
findMany: jest.fn(),
|
|
},
|
|
materialSku: {
|
|
findMany: jest.fn(),
|
|
},
|
|
user: {
|
|
findMany: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/department-permission', () => ({
|
|
isSystemAdmin: jest.fn(),
|
|
getUserViewableMaterialTypes: jest.fn(),
|
|
isSuperAdmin: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/s3', () => ({
|
|
deleteFromS3: jest.fn(),
|
|
isS3Url: jest.fn(() => false),
|
|
}))
|
|
|
|
jest.mock('@/lib/file-cleanup', () => ({
|
|
deleteFiles: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-cleanup', () => ({
|
|
collectMaterialFiles: jest.fn(),
|
|
deleteMaterialRelatedRecords: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-log', () => ({
|
|
createMaterialLog: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-download-log', () => ({
|
|
ensureMaterialDownloadLogsTable: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/url-rewrite', () => ({
|
|
rewriteMaterialStorageUrlsForClient: jest.fn((value) => value),
|
|
rewriteMediaUrlForClient: jest.fn((value) => value),
|
|
}))
|
|
|
|
jest.mock('@/lib/network-detection', () => ({
|
|
detectIsIntranet: jest.fn(() => false),
|
|
}))
|
|
|
|
jest.mock('@/lib/product-package', () => ({
|
|
getMaterialCoverUrl: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/material-type-mapper', () => ({
|
|
isProductPackageType: jest.fn((materialType: string) => materialType === '任务-详情'),
|
|
}))
|
|
|
|
describe('materials route ai generated', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '设计部',
|
|
managedDepartment: null,
|
|
isSuperAdmin: false,
|
|
},
|
|
})
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
;(isSystemAdmin as jest.Mock).mockReturnValue(false)
|
|
;(getUserViewableMaterialTypes as jest.Mock).mockResolvedValue(['任务-详情'])
|
|
;(prisma.material.count as jest.Mock).mockResolvedValue(1)
|
|
;(prisma.material.findMany as jest.Mock).mockResolvedValue([
|
|
{
|
|
id: 'material-1',
|
|
userId: 'user-1',
|
|
previewUrl: 'https://example.com/preview.jpg',
|
|
productModel: 'SPU-001',
|
|
categoryName: '旗舰店',
|
|
sceneName: '客厅',
|
|
platformName: '天猫',
|
|
productCategoryName: '沙发',
|
|
materialType: '任务-详情',
|
|
renderer: '',
|
|
authorName: '张三',
|
|
zipFileName: 'sample.zip',
|
|
zipFileSize: BigInt(1024),
|
|
downloadCount: 0,
|
|
likeCount: 0,
|
|
createdAt: new Date('2026-03-25T08:00:00.000Z'),
|
|
videoSourceUrl: null,
|
|
displayImages: [],
|
|
videoPreview540Url: null,
|
|
videoPreviewClipUrl: null,
|
|
videoCoverUrl: null,
|
|
videoDuration: null,
|
|
videoProcessingStatus: null,
|
|
transcodeLastError: null,
|
|
transcodeAttempts: 0,
|
|
isAiGenerated: true,
|
|
taskDetailSourceType: 'AI',
|
|
user: {
|
|
username: 'zhangsan',
|
|
realName: '张三',
|
|
avatar: null,
|
|
},
|
|
assets: [],
|
|
},
|
|
])
|
|
;(prisma.materialLike.findMany as jest.Mock).mockResolvedValue([])
|
|
;(prisma.materialFavorite.findMany as jest.Mock).mockResolvedValue([])
|
|
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([])
|
|
})
|
|
|
|
test('列表接口应返回 isAiGenerated 标记,供首页展示 AI生成 标签', async () => {
|
|
const request = new NextRequest('http://localhost/api/materials?page=1&pageSize=20')
|
|
|
|
const response = await GET(request)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.material.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
select: expect.objectContaining({
|
|
isAiGenerated: true,
|
|
taskDetailSourceType: true,
|
|
taskDetailRequesterUserId: true,
|
|
taskDetailRequesterName: true,
|
|
}),
|
|
}),
|
|
)
|
|
expect(body.materials).toHaveLength(1)
|
|
expect(body.materials[0]).toEqual(
|
|
expect.objectContaining({
|
|
id: 'material-1',
|
|
isAiGenerated: true,
|
|
taskDetailSourceType: 'AI',
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('未登录访问列表接口应返回 401', async () => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue(null)
|
|
|
|
const request = new NextRequest('http://localhost/api/materials?page=1&pageSize=20')
|
|
|
|
const response = await GET(request)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(401)
|
|
expect(body.code).toBe('UNAUTHORIZED')
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('普通用户不能通过 deleted=true 查看回收站素材', async () => {
|
|
const request = new NextRequest('http://localhost/api/materials?deleted=true&page=1&pageSize=20')
|
|
|
|
const response = await GET(request)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(body.code).toBe('FORBIDDEN_DELETED_MATERIALS')
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('share 参数不能绕过普通素材列表权限过滤', async () => {
|
|
;(getUserViewableMaterialTypes as jest.Mock).mockResolvedValue(['买家秀图片'])
|
|
|
|
const request = new NextRequest('http://localhost/api/materials?share=1&materialType=任务-详情&page=1&pageSize=20')
|
|
|
|
const response = await GET(request)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(body.materials).toEqual([])
|
|
expect(body.totalCount).toBe(0)
|
|
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('任务-页面缺少顶层 previewUrl 时应使用 preview 资产作为首页封面', async () => {
|
|
;(getUserViewableMaterialTypes as jest.Mock).mockResolvedValue(['任务-页面'])
|
|
;(prisma.material.findMany as jest.Mock).mockResolvedValue([
|
|
{
|
|
id: 'task-page-1',
|
|
userId: 'user-1',
|
|
previewUrl: '',
|
|
productModel: 'SJ0371',
|
|
categoryName: '索菲亚',
|
|
sceneName: '书房',
|
|
platformName: '',
|
|
productCategoryName: '',
|
|
materialType: '任务-页面',
|
|
renderer: '',
|
|
authorName: '张三',
|
|
zipFileName: 'task-page.zip',
|
|
zipFileSize: BigInt(1024),
|
|
downloadCount: 0,
|
|
likeCount: 0,
|
|
createdAt: new Date('2026-06-26T08:00:00.000Z'),
|
|
videoSourceUrl: null,
|
|
displayImages: [],
|
|
videoPreview540Url: null,
|
|
videoPreviewClipUrl: null,
|
|
videoCoverUrl: null,
|
|
videoDuration: null,
|
|
videoProcessingStatus: null,
|
|
transcodeLastError: null,
|
|
transcodeAttempts: 0,
|
|
isAiGenerated: false,
|
|
taskDetailSourceType: null,
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
user: {
|
|
username: 'zhangsan',
|
|
realName: '张三',
|
|
avatar: null,
|
|
},
|
|
assets: [
|
|
{
|
|
id: 'preview-1',
|
|
assetType: 'preview',
|
|
fileUrl: 'https://example.com/task-page-preview.webp',
|
|
fileName: 'preview-0.webp',
|
|
fileSize: null,
|
|
sortOrder: 0,
|
|
createdAt: new Date('2026-06-26T08:00:00.000Z'),
|
|
updatedAt: new Date('2026-06-26T08:00:00.000Z'),
|
|
skuCode: null,
|
|
barcodes: [],
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials?materialType=任务-页面&page=1&pageSize=20'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(prisma.material.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
select: expect.objectContaining({
|
|
assets: expect.objectContaining({
|
|
where: {
|
|
assetType: {
|
|
in: ['main-preview', 'sku-preview', 'detail-preview', 'preview'],
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
}),
|
|
)
|
|
expect(body.materials[0].previewUrl).toBe('https://example.com/task-page-preview.webp')
|
|
})
|
|
})
|