import { prisma } from '@/lib/prisma' import { SPECIAL_INIT_MATERIAL_TYPES, buildMaterialSearchOrConditions, buildMaterialWhereFromSearchParams, createMaterialQueryContext, isProvablyIncompleteSpecialInitShell, } from '@/lib/material-query' import { getUserViewableMaterialTypes, isSuperAdmin, isSystemAdmin, } from '@/lib/department-permission' jest.mock('@/lib/prisma', () => ({ prisma: { materialSku: { findMany: jest.fn(), }, user: { findMany: jest.fn(), }, }, })) jest.mock('@/lib/department-permission', () => ({ getUserViewableMaterialTypes: jest.fn(), isSuperAdmin: jest.fn(), isSystemAdmin: jest.fn(), })) describe('material search conditions', () => { beforeEach(() => { jest.clearAllMocks() ;(prisma.materialSku.findMany as jest.Mock).mockResolvedValue([]) ;(prisma.user.findMany as jest.Mock).mockResolvedValue([]) ;(getUserViewableMaterialTypes as jest.Mock).mockResolvedValue(['任务-详情']) ;(isSuperAdmin as jest.Mock).mockResolvedValue(false) ;(isSystemAdmin as unknown as jest.Mock).mockReturnValue(false) }) test('完整素材 UUID 应使用精确匹配', async () => { const materialId = '954554fb-50b4-45c4-9234-9e94b6948953' const conditions = await buildMaterialSearchOrConditions(materialId) expect(conditions).toEqual(expect.arrayContaining([{ id: materialId }])) }) test('非 UUID 的历史素材 ID 也应使用精确匹配', async () => { const materialId = 'legacy-material-1' const conditions = await buildMaterialSearchOrConditions(materialId) expect(conditions).toEqual(expect.arrayContaining([{ id: materialId }])) }) test('同一请求的多组筛选条件复用权限和搜索上下文', async () => { const searchParams = new URLSearchParams('search=GBGJ-BG0070&category=顾家') const user = { id: 'user-1', role: 'USER', department: '品牌创意部', } const context = await createMaterialQueryContext(searchParams, user) const first = await buildMaterialWhereFromSearchParams(searchParams, user, { context }) const second = await buildMaterialWhereFromSearchParams(searchParams, user, { context, exclude: 'category', }) expect(first.empty).toBe(false) expect(first.where.categoryName).toBe('顾家') expect(second.where.categoryName).toBeUndefined() expect(getUserViewableMaterialTypes).toHaveBeenCalledTimes(1) expect(isSuperAdmin).toHaveBeenCalledTimes(1) expect(prisma.materialSku.findMany).toHaveBeenCalledTimes(1) expect(prisma.user.findMany).toHaveBeenCalledTimes(1) }) test('复用上下文后仍拒绝普通用户查看回收站素材', async () => { await expect( createMaterialQueryContext( new URLSearchParams('showDeleted=true'), { id: 'user-1', role: 'USER', department: '品牌创意部' }, ), ).rejects.toMatchObject({ code: 'FORBIDDEN_DELETED_MATERIALS', status: 403, }) }) test('复用上下文后仍拦截用户无权查看的素材类型', async () => { const searchParams = new URLSearchParams('materialType=3D场景') const user = { id: 'user-1', role: 'USER', department: '品牌创意部', } const context = await createMaterialQueryContext(searchParams, user) const result = await buildMaterialWhereFromSearchParams(searchParams, user, { context }) expect(result.empty).toBe(true) }) test('超级管理员不读取部门素材权限', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) const context = await createMaterialQueryContext( new URLSearchParams(), { id: 'super-admin', role: 'ADMIN' }, ) expect(context.isSuperAdmin).toBe(true) expect(getUserViewableMaterialTypes).not.toHaveBeenCalled() }) test('正常列表应统一隐藏专用 init 产生的无文件未完成空壳', async () => { ;(getUserViewableMaterialTypes as jest.Mock).mockResolvedValue(['任务-页面']) const result = await buildMaterialWhereFromSearchParams( new URLSearchParams('materialType=任务-页面'), { id: 'user-1', role: 'USER', department: '品牌创意部' }, ) expect(result.empty).toBe(false) expect(result.where.isIncompleteShell).toBe(false) expect(result.where.NOT).toBeUndefined() }) test.each(SPECIAL_INIT_MATERIAL_TYPES)( '%s 的 uploading 无文件记录应判定为不可见空壳', (materialType) => { expect(isProvablyIncompleteSpecialInitShell({ materialType, taskStatus: 'uploading', previewUrl: '', zipFilePath: '', zipFileName: '', displayImages: [], assets: [], })).toBe(true) }, ) test.each(SPECIAL_INIT_MATERIAL_TYPES)( '%s 的 completed 记录即使字段异常为空也不应被空壳规则隐藏', (materialType) => { expect(isProvablyIncompleteSpecialInitShell({ materialType, taskStatus: 'completed', previewUrl: '', zipFilePath: '', zipFileName: '', displayImages: [], assets: [], })).toBe(false) }, ) test.each([ ['previewUrl', { previewUrl: '/uploads/preview.jpg' }], ['zipFilePath', { zipFilePath: '/uploads/source.zip' }], ['zipFileName', { zipFileName: 'https://storage.example/source.zip' }], ['displayImages', { displayImages: ['/uploads/display.jpg'] }], ['assets', { assets: [{ id: 'asset-1' }] }], ])('有真实 %s 证据的 uploading 记录应继续可见', (_label, evidence) => { expect(isProvablyIncompleteSpecialInitShell({ materialType: '任务-页面', taskStatus: 'uploading', previewUrl: '', zipFilePath: '', zipFileName: '', displayImages: [], assets: [], ...evidence, })).toBe(false) }) test('product-package init 的 null 状态只有具备专属初始化标记时才隐藏', () => { expect(isProvablyIncompleteSpecialInitShell({ materialType: '任务-详情', taskStatus: null, previewUrl: '', zipFilePath: '', zipFileName: '', displayImages: [], assets: [], taskDetailSourceType: 'AI', taskDetailRequesterUserId: 'requester-1', })).toBe(true) }) test('历史任务详情的有效 null 状态没有新 init 双标记时不应误隐藏', () => { expect(isProvablyIncompleteSpecialInitShell({ materialType: '任务-详情', taskStatus: null, previewUrl: '', zipFilePath: '', zipFileName: '', displayImages: [], assets: [], taskDetailSourceType: null, taskDetailRequesterUserId: null, })).toBe(false) }) test('month=2026 按整年筛选', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) const result = await buildMaterialWhereFromSearchParams( new URLSearchParams('month=2026'), { id: 'super-admin', role: 'ADMIN' }, ) expect(result.where.createdAt).toEqual({ gte: new Date(2026, 0, 1), lt: new Date(2027, 0, 1), }) }) test('month=2026-08 仍按单月筛选', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) const result = await buildMaterialWhereFromSearchParams( new URLSearchParams('month=2026-08'), { id: 'super-admin', role: 'ADMIN' }, ) expect(result.where.createdAt).toEqual({ gte: new Date(2026, 7, 1), lt: new Date(2026, 8, 1), }) }) test('回收站查询不应套用任务-页面空记录隐藏条件', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) const result = await buildMaterialWhereFromSearchParams( new URLSearchParams('showDeleted=true'), { id: 'super-admin', role: 'ADMIN' }, ) expect(result.where.deletedAt).toEqual({ not: null }) expect(result.where.NOT).toBeUndefined() }) })