gongxiangp2/lib/__tests__/materials-route-ai-generated.test.ts

418 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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