gongxiangp2/lib/__tests__/material-assets-authorization.test.ts

177 lines
5.8 KiB
TypeScript

/** @jest-environment node */
import { getServerSession } from 'next-auth'
import { prisma } from '@/lib/prisma'
import { addAsset, getAssetsByMaterialId } from '@/lib/product-package'
import { deleteFileIfUnreferenced } from '@/lib/file-cleanup'
import {
DELETE,
GET,
POST,
} from '@/app/api/materials/[id]/assets/route'
jest.mock('next-auth', () => ({ getServerSession: jest.fn() }))
jest.mock('@/lib/auth', () => ({ authOptions: {} }))
jest.mock('@/lib/department-permission', () => ({
canUserViewMaterial: jest.fn().mockResolvedValue(false),
isSuperAdmin: jest.fn().mockResolvedValue(false),
isSystemAdmin: jest.fn(() => false),
}))
jest.mock('@/lib/product-package', () => ({
getAssetsByMaterialId: jest.fn(),
addAsset: jest.fn(),
}))
jest.mock('@/lib/file-cleanup', () => ({ deleteFileIfUnreferenced: jest.fn() }))
jest.mock('@/lib/material-log', () => ({ createMaterialLog: jest.fn() }))
jest.mock('@/lib/material-cleanup', () => ({ normalizeDisplayImages: jest.fn(() => []) }))
jest.mock('@/lib/prisma', () => ({
prisma: {
material: {
findUnique: jest.fn(),
update: jest.fn(),
},
materialAsset: {
findUnique: jest.fn(),
findFirst: jest.fn(),
},
$transaction: jest.fn(),
},
}))
describe('material assets route authorization', () => {
beforeEach(() => {
jest.clearAllMocks()
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-2', role: 'USER', department: '设计部' },
})
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '任务-详情',
previewUrl: '',
zipFilePath: '',
zipFileName: '',
zipFileSize: BigInt(0),
displayImages: [],
})
;(deleteFileIfUnreferenced as jest.Mock).mockResolvedValue({
success: true,
skipped: true,
reason: 'physical_delete_disabled',
})
})
test('无查看权限的用户不得读取资产列表', async () => {
const response = await GET({} as any, { params: { id: 'material-1' } })
expect(response.status).toBe(403)
expect(getAssetsByMaterialId).not.toHaveBeenCalled()
})
test('非所有者不得添加资产', async () => {
const response = await POST({
json: jest.fn().mockResolvedValue({ assetType: 'preview', fileUrl: 'uploads/a.webp' }),
} as any, { params: { id: 'material-1' } })
expect(response.status).toBe(403)
expect(addAsset).not.toHaveBeenCalled()
})
test('非所有者不得删除资产', async () => {
const response = await DELETE({
url: 'http://localhost/api/materials/material-1/assets?assetId=asset-1',
} as any, { params: { id: 'material-1' } })
expect(response.status).toBe(403)
expect(prisma.materialAsset.findUnique).not.toHaveBeenCalled()
})
test('素材所有者也不能通过客户端 fileUrl 直接注册资产', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-1', role: 'USER', department: '设计部' },
})
const response = await POST({
json: jest.fn().mockResolvedValue({
assetType: 'preview',
fileUrl: 'D:\\app\\sentinel.txt',
}),
} as any, { params: { id: 'material-1' } })
const body = await response.json()
expect(response.status).toBe(403)
expect(body.error).toContain('受控上传流程')
expect(addAsset).not.toHaveBeenCalled()
expect(prisma.material.findUnique).not.toHaveBeenCalled()
})
test('删除历史资产记录时不得根据不可信 fileUrl 执行物理删除', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-1', role: 'USER', department: '设计部' },
})
// 该接口只服务非打包类素材;任务-详情走单图编辑接口
;(prisma.material.findUnique as jest.Mock).mockResolvedValue({
id: 'material-1',
userId: 'user-1',
materialType: '视频',
previewUrl: '',
zipFilePath: '',
zipFileName: '',
zipFileSize: BigInt(0),
displayImages: [],
})
;(prisma.materialAsset.findUnique as jest.Mock).mockResolvedValue({
id: 'asset-1',
materialId: 'material-1',
assetType: 'preview',
fileUrl: 'D:\\app\\sentinel.txt',
fileName: 'sentinel.txt',
})
;(prisma.materialAsset.findFirst as jest.Mock).mockResolvedValue(null)
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback: any) =>
callback({
materialAsset: {
delete: jest.fn().mockResolvedValue({ id: 'asset-1' }),
},
material: {
update: jest.fn().mockResolvedValue({ id: 'material-1' }),
},
}),
)
const response = await DELETE({
url: 'http://localhost/api/materials/material-1/assets?assetId=asset-1',
} as any, { params: { id: 'material-1' } })
const body = await response.json()
expect(response.status).toBe(200)
expect(deleteFileIfUnreferenced).toHaveBeenCalledWith(
'D:\\app\\sentinel.txt',
{
excludeAssetIds: ['asset-1'],
allowPhysicalDelete: false,
},
)
expect(body.details).toEqual({
fileDeleted: false,
fileSkipped: true,
fileCleanupError: null,
})
})
test('任务-详情素材不得通过该接口直删资产(会导致 ZIP 与预览不一致)', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-1', role: 'USER', department: '设计部' },
})
const response = await DELETE({
url: 'http://localhost/api/materials/material-1/assets?assetId=asset-1',
} as any, { params: { id: 'material-1' } })
const body = await response.json()
expect(response.status).toBe(409)
expect(body.error).toContain('单图编辑接口')
expect(prisma.materialAsset.findUnique).not.toHaveBeenCalled()
})
})