418 lines
13 KiB
TypeScript
418 lines
13 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 {
|
||
canUserViewMaterial,
|
||
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(),
|
||
findFirst: jest.fn(),
|
||
},
|
||
materialFavorite: {
|
||
findMany: jest.fn(),
|
||
findFirst: 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(),
|
||
canUserViewMaterial: jest.fn(),
|
||
}))
|
||
|
||
jest.mock('@/lib/user-permission-inherit', () => ({ canUserAccessPrivateTaskDetail: jest.fn().mockResolvedValue(false) }))
|
||
|
||
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)
|
||
;(canUserViewMaterial as jest.Mock).mockResolvedValue(true)
|
||
;(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,
|
||
},
|
||
},
|
||
])
|
||
;(prisma.materialLike.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.materialFavorite.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.materialLike.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.materialFavorite.findFirst as jest.Mock).mockResolvedValue(null)
|
||
})
|
||
|
||
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({
|
||
where: expect.objectContaining({
|
||
isIncompleteShell: false,
|
||
}),
|
||
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(['买家秀图片'])
|
||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.material.count as jest.Mock).mockResolvedValue(0)
|
||
|
||
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).toHaveBeenCalledWith(expect.objectContaining({
|
||
where: expect.objectContaining({ AND: [{ OR: [
|
||
{ taskDetailPrivate: false, materialType: { in: ['买家秀图片'] } },
|
||
{ materialType: '任务-详情', taskDetailPrivate: true, taskDetailRequesterUserId: 'user-1' },
|
||
] }] }),
|
||
}))
|
||
})
|
||
|
||
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,
|
||
},
|
||
},
|
||
])
|
||
;(prisma.materialAsset.findMany as jest.Mock).mockResolvedValue([
|
||
{
|
||
materialId: 'task-page-1',
|
||
assetType: 'preview',
|
||
fileUrl: 'https://example.com/task-page-preview.webp',
|
||
sortOrder: 0,
|
||
},
|
||
])
|
||
|
||
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.not.objectContaining({
|
||
assets: expect.anything(),
|
||
}),
|
||
take: 20,
|
||
}),
|
||
)
|
||
expect(prisma.materialAsset.findMany).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
where: expect.objectContaining({
|
||
materialId: { in: ['task-page-1'] },
|
||
assetType: { in: ['main', 'sku', 'main-preview', 'sku-preview', 'preview'] },
|
||
}),
|
||
}),
|
||
)
|
||
expect(body.materials[0].previewUrl).toBe('https://example.com/task-page-preview.webp')
|
||
})
|
||
|
||
test('创意资产库 deferTotal 时不应先做全库 count,并用多取 1 条判断是否还有下一页', async () => {
|
||
const pageItems = Array.from({ length: 21 }, (_, index) => ({
|
||
id: `material-${index + 1}`,
|
||
userId: 'user-1',
|
||
previewUrl: `https://example.com/preview-${index + 1}.jpg`,
|
||
productModel: `SPU-${index + 1}`,
|
||
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: false,
|
||
taskDetailSourceType: null,
|
||
user: {
|
||
username: 'zhangsan',
|
||
realName: '张三',
|
||
avatar: null,
|
||
},
|
||
}))
|
||
;(prisma.material.findMany as jest.Mock).mockResolvedValue(pageItems)
|
||
|
||
const request = new NextRequest('http://localhost/api/materials?page=1&pageSize=20&sortBy=createdAt&deferTotal=1')
|
||
const response = await GET(request)
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(prisma.material.count).not.toHaveBeenCalled()
|
||
expect(prisma.material.findMany).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
skip: 0,
|
||
take: 21,
|
||
}),
|
||
)
|
||
expect(body.materials).toHaveLength(20)
|
||
expect(body.hasMore).toBe(true)
|
||
expect(body.totalCount).toBeUndefined()
|
||
expect(body.totalPages).toBeUndefined()
|
||
})
|
||
|
||
test('uploadId 状态查询仍应直接返回 uploading 空壳,不套用普通列表隐藏条件', async () => {
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'uploading-shell-1',
|
||
userId: 'user-1',
|
||
previewUrl: '',
|
||
productModel: 'SPU-UPLOADING',
|
||
reportTitle: null,
|
||
categoryName: '通用',
|
||
sceneName: '通用',
|
||
platformName: '',
|
||
productCategoryName: '',
|
||
materialType: '任务-页面',
|
||
isAiGenerated: false,
|
||
taskDetailSourceType: null,
|
||
taskDetailRequesterUserId: null,
|
||
taskDetailRequesterName: null,
|
||
renderer: '',
|
||
authorName: '张三',
|
||
zipFileName: '',
|
||
zipFileSize: BigInt(0),
|
||
downloadCount: 0,
|
||
likeCount: 0,
|
||
createdAt: new Date('2026-08-12T00:00:00.000Z'),
|
||
videoSourceUrl: null,
|
||
displayImages: [],
|
||
videoPreview540Url: null,
|
||
videoPreviewClipUrl: null,
|
||
videoCoverUrl: null,
|
||
videoDuration: null,
|
||
videoProcessingStatus: null,
|
||
user: {
|
||
username: 'zhangsan',
|
||
realName: '张三',
|
||
avatar: null,
|
||
},
|
||
assets: [],
|
||
})
|
||
|
||
const response = await GET(
|
||
new NextRequest('http://localhost/api/materials?uploadId=uploading-shell-1'),
|
||
)
|
||
const body = await response.json()
|
||
|
||
expect(response.status).toBe(200)
|
||
expect(body.material.id).toBe('uploading-shell-1')
|
||
expect(prisma.material.findFirst).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
where: { uploadId: 'uploading-shell-1', deletedAt: null },
|
||
}),
|
||
)
|
||
expect(prisma.material.count).not.toHaveBeenCalled()
|
||
expect(prisma.material.findMany).not.toHaveBeenCalled()
|
||
})
|
||
})
|