/** @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, '详情模板', '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 () => { 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') }) })