247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import { NextRequest } from 'next/server'
|
|
import { GET } from '@/app/api/materials/download/route'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { getServerSession } from 'next-auth'
|
|
import {
|
|
canUserDownloadMaterial,
|
|
canUserDownloadSource,
|
|
isSuperAdmin,
|
|
} from '@/lib/department-permission'
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
material: {
|
|
findFirst: jest.fn(),
|
|
},
|
|
user: {
|
|
findUnique: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('next-auth', () => ({
|
|
getServerSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
authOptions: {},
|
|
}))
|
|
|
|
jest.mock('@/lib/department-permission', () => {
|
|
const actual = jest.requireActual('@/lib/department-permission')
|
|
return {
|
|
...actual,
|
|
canUserDownloadMaterial: jest.fn(),
|
|
canUserDownloadSource: jest.fn(),
|
|
isSuperAdmin: jest.fn(),
|
|
}
|
|
})
|
|
|
|
describe('materials download route video wait period', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'admin-1',
|
|
role: 'ADMIN',
|
|
department: '管理部',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ isSuperAdmin: true })
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
|
;(canUserDownloadMaterial as jest.Mock).mockResolvedValue(true)
|
|
;(canUserDownloadSource as jest.Mock).mockResolvedValue(true)
|
|
})
|
|
|
|
function mockMaterial(materialType: string, createdAt: Date, zipFilePath = '') {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
|
id: 'material-1',
|
|
zipFilePath,
|
|
zipFileName: 'video.mp4',
|
|
productModel: 'SPU001',
|
|
categoryName: '店铺',
|
|
sceneName: '场景',
|
|
productCategoryName: '品类',
|
|
materialType,
|
|
downloadCount: 0,
|
|
createdAt,
|
|
})
|
|
}
|
|
|
|
test.each(['share=1', 'shareId=anything'])(
|
|
'匿名请求不能通过 %s 绕过下载登录校验',
|
|
async (shareParam) => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue(null)
|
|
|
|
const response = await GET(
|
|
new NextRequest(`http://localhost/api/materials/download?id=material-1&${shareParam}`),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(401)
|
|
expect(body.code).toBe('UNAUTHORIZED')
|
|
expect(prisma.material.findFirst).not.toHaveBeenCalled()
|
|
},
|
|
)
|
|
|
|
test('无下载权限用户不能通过 share 参数绕过个人权限', async () => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '测试部门',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: false })
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(body.code).toBe('FORBIDDEN')
|
|
expect(prisma.material.findFirst).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('已软删除素材不能通过旧 ID 直接下载', async () => {
|
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(null)
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-deleted'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(404)
|
|
expect(body.code).toBe('MATERIAL_NOT_FOUND')
|
|
expect(prisma.material.findFirst).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: {
|
|
id: 'material-deleted',
|
|
deletedAt: null,
|
|
},
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('任务原图源文件不能通过 share 参数绕过源文件权限', async () => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '测试部门',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: true })
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
;(canUserDownloadMaterial as jest.Mock).mockResolvedValue(true)
|
|
;(canUserDownloadSource as jest.Mock).mockResolvedValue(false)
|
|
mockMaterial('任务-3D原图', new Date('2026-01-01T00:00:00.000Z'), 'uploads/source.zip')
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(body.code).toBe('NO_PERMISSION')
|
|
expect(canUserDownloadSource).toHaveBeenCalledWith(
|
|
'测试部门',
|
|
'USER',
|
|
null,
|
|
'任务-3D原图',
|
|
'user-1',
|
|
)
|
|
})
|
|
|
|
test('部门下载权限不能通过 share 参数绕过', async () => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '测试部门',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: true })
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
;(canUserDownloadMaterial as jest.Mock).mockResolvedValue(false)
|
|
mockMaterial('3D场景', new Date('2026-01-01T00:00:00.000Z'), 'uploads/source.zip')
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(body.code).toBe('NO_PERMISSION')
|
|
expect(canUserDownloadSource).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('非安装视频未满 15 天时应禁止下载,分享链接也不能绕过', async () => {
|
|
mockMaterial('淘系-主图视频', new Date())
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1&validate=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(body.code).toBe('VIDEO_WAIT_PERIOD')
|
|
expect(body.details).toContain('满 15 天')
|
|
})
|
|
|
|
test('安装视频不受 15 天限制', async () => {
|
|
mockMaterial('安装视频', new Date())
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1&validate=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(404)
|
|
expect(body.code).toBe('NO_DOWNLOADABLE_FILES')
|
|
})
|
|
|
|
test('直播切片上传当天不受 15 天限制,仍需通过正常下载校验', async () => {
|
|
;(getServerSession as jest.Mock).mockResolvedValue({
|
|
user: {
|
|
id: 'user-1',
|
|
role: 'USER',
|
|
department: '测试部门',
|
|
managedDepartment: null,
|
|
},
|
|
})
|
|
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: true })
|
|
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
|
mockMaterial('直播切片', new Date())
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&validate=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(404)
|
|
expect(body.code).toBe('NO_DOWNLOADABLE_FILES')
|
|
expect(canUserDownloadMaterial).toHaveBeenCalled()
|
|
})
|
|
|
|
test('非安装视频满 15 天后不再被等待期拦截', async () => {
|
|
const uploadedAt = new Date(Date.now() - 16 * 24 * 60 * 60 * 1000)
|
|
mockMaterial('抖音-千川素材', uploadedAt)
|
|
|
|
const response = await GET(
|
|
new NextRequest('http://localhost/api/materials/download?id=material-1&share=1&validate=1'),
|
|
)
|
|
const body = await response.json()
|
|
|
|
expect(response.status).toBe(404)
|
|
expect(body.code).toBe('NO_DOWNLOADABLE_FILES')
|
|
})
|
|
})
|