177 lines
5.8 KiB
TypeScript
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()
|
|
})
|
|
})
|