249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
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()
|
|
})
|
|
})
|