import { NextRequest } from 'next/server' import { GET, PATCH, POST } from '../route' jest.mock('@/lib/auth', () => ({ authOptions: {}, })) jest.mock('next-auth', () => ({ getServerSession: jest.fn(), })) jest.mock('@/lib/prisma', () => ({ prisma: { material: { findUnique: jest.fn(), update: jest.fn(), }, user: { findUnique: jest.fn(), }, department: { findMany: jest.fn(), }, }, })) jest.mock('@/lib/admin-permissions', () => ({ isCurrentSuperAdmin: jest.fn(), })) jest.mock('@/lib/video-transcode-service', () => ({ enqueueVideoTranscodeJob: jest.fn(), recoverStaleVideoTranscodeJobs: jest.fn(), })) const { getServerSession } = require('next-auth') const { prisma } = require('@/lib/prisma') const { isCurrentSuperAdmin } = require('@/lib/admin-permissions') const { enqueueVideoTranscodeJob, recoverStaleVideoTranscodeJobs } = require('@/lib/video-transcode-service') describe('/api/materials/transcode 权限边界', () => { beforeEach(() => { jest.resetAllMocks() }) it('未登录时拒绝查询转码状态', async () => { getServerSession.mockResolvedValue(null) const response = await GET( new NextRequest('http://localhost/api/materials/transcode?materialId=material-1') ) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('请先登录') expect(prisma.material.findUnique).not.toHaveBeenCalled() }) it('普通用户不能查询别人素材的转码状态', async () => { getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } }) prisma.user.findUnique.mockResolvedValue({ id: 'user-1', role: 'USER', isSuperAdmin: false, department: '设计部', managedDepartment: null, }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-2', createdByUserId: 'user-2', ownerType: 'TEAM', ownerId: 'team-1', user: { id: 'user-2', department: '运营部', }, videoProcessingStatus: 'pending', videoPreview540Url: null, videoPreviewClipUrl: null, videoCoverUrl: null, videoDuration: null, }) const response = await GET( new NextRequest('http://localhost/api/materials/transcode?materialId=material-1') ) const data = await response.json() expect(response.status).toBe(403) expect(data.error).toBe('无权查看该素材的转码状态') }) it('当前负责人可以查询自己的转码状态', async () => { getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } }) prisma.user.findUnique.mockResolvedValue({ id: 'user-1', role: 'USER', isSuperAdmin: false, department: '设计部', managedDepartment: null, }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-1', createdByUserId: 'user-1', ownerType: 'USER', ownerId: 'user-1', user: { id: 'user-1', department: '设计部', }, videoProcessingStatus: 'done', videoPreview540Url: 'preview.mp4', videoPreviewClipUrl: null, videoCoverUrl: 'cover.jpg', videoDuration: 60, }) const response = await GET( new NextRequest('http://localhost/api/materials/transcode?materialId=material-1') ) const data = await response.json() expect(response.status).toBe(200) expect(data.status).toBe('done') expect(data.hasPreview540).toBe(true) }) it('未登录时拒绝触发转码任务', async () => { getServerSession.mockResolvedValue(null) const response = await POST( new NextRequest('http://localhost/api/materials/transcode', { method: 'POST', body: JSON.stringify({ materialId: 'material-1' }), }) ) const data = await response.json() expect(response.status).toBe(401) expect(data.error).toBe('请先登录') expect(prisma.material.update).not.toHaveBeenCalled() expect(enqueueVideoTranscodeJob).not.toHaveBeenCalled() }) it('普通用户不能触发别人素材的转码任务', async () => { getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } }) prisma.user.findUnique.mockResolvedValue({ id: 'user-1', role: 'USER', isSuperAdmin: false, department: '设计部', managedDepartment: null, }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-2', createdByUserId: 'user-2', ownerType: 'TEAM', ownerId: 'team-1', user: { id: 'user-2', department: '运营部', }, materialKind: 'VIDEO', videoSourceUrl: 'source.mp4', videoCoverUrl: null, videoProcessingStatus: 'pending', materialType: '视频', }) const response = await POST( new NextRequest('http://localhost/api/materials/transcode', { method: 'POST', body: JSON.stringify({ materialId: 'material-1' }), }) ) const data = await response.json() expect(response.status).toBe(403) expect(data.error).toBe('无权触发该素材的转码任务') expect(prisma.material.update).not.toHaveBeenCalled() expect(enqueueVideoTranscodeJob).not.toHaveBeenCalled() }) it('历史上传者不再自动拥有转码管理权', async () => { getServerSession.mockResolvedValue({ user: { id: 'user-1', role: 'USER' } }) prisma.user.findUnique.mockResolvedValue({ id: 'user-1', role: 'USER', isSuperAdmin: false, department: '设计部', managedDepartment: null, }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-2', createdByUserId: 'user-1', ownerType: 'TEAM', ownerId: 'team-1', user: { id: 'user-2', department: '运营部', }, materialKind: 'VIDEO', videoSourceUrl: 'source.mp4', videoCoverUrl: null, videoProcessingStatus: 'pending', materialType: '视频', }) const response = await POST( new NextRequest('http://localhost/api/materials/transcode', { method: 'POST', body: JSON.stringify({ materialId: 'material-1' }), }) ) const data = await response.json() expect(response.status).toBe(403) expect(data.error).toBe('无权触发该素材的转码任务') expect(enqueueVideoTranscodeJob).not.toHaveBeenCalled() }) it('部门管理员可以触发管理范围内素材的转码任务', async () => { getServerSession.mockResolvedValue({ user: { id: 'admin-1', role: 'ADMIN' } }) prisma.user.findUnique.mockResolvedValue({ id: 'admin-1', role: 'ADMIN', isSuperAdmin: false, department: '设计部', managedDepartment: '设计部', }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-2', createdByUserId: 'user-2', ownerType: 'TEAM', ownerId: 'team-1', user: { id: 'user-2', department: '设计部-建模组', }, materialKind: 'VIDEO', videoSourceUrl: 'source.mp4', videoCoverUrl: null, videoProcessingStatus: 'pending', materialType: '视频', }) enqueueVideoTranscodeJob.mockResolvedValue(undefined) const response = await POST( new NextRequest('http://localhost/api/materials/transcode', { method: 'POST', body: JSON.stringify({ materialId: 'material-1' }), }) ) const data = await response.json() expect(response.status).toBe(200) expect(data.status).toBe('pending') expect(enqueueVideoTranscodeJob).toHaveBeenCalledWith({ materialId: 'material-1', videoSourceUrl: 'source.mp4', needsCoverImage: true, forceRetry: false, }) }) it('普通管理员不能跨部门触发转码任务', async () => { getServerSession.mockResolvedValue({ user: { id: 'admin-1', role: 'ADMIN' } }) prisma.user.findUnique.mockResolvedValue({ id: 'admin-1', role: 'ADMIN', isSuperAdmin: false, department: '设计部', managedDepartment: '设计部', }) prisma.material.findUnique.mockResolvedValue({ id: 'material-1', userId: 'user-2', createdByUserId: 'user-2', ownerType: 'TEAM', ownerId: 'team-1', user: { id: 'user-2', department: '运营部', }, materialKind: 'VIDEO', videoSourceUrl: 'source.mp4', videoCoverUrl: null, videoProcessingStatus: 'pending', materialType: '视频', }) prisma.department.findMany.mockResolvedValue([ { name: '设计部' }, { name: '运营部' }, ]) const response = await POST( new NextRequest('http://localhost/api/materials/transcode', { method: 'POST', body: JSON.stringify({ materialId: 'material-1' }), }) ) const data = await response.json() expect(response.status).toBe(403) expect(data.error).toBe('无权触发该素材的转码任务') expect(enqueueVideoTranscodeJob).not.toHaveBeenCalled() }) it('普通管理员不能触发卡住转码任务恢复', async () => { getServerSession.mockResolvedValue({ user: { id: 'admin-1', role: 'ADMIN' } }) isCurrentSuperAdmin.mockResolvedValue(false) const response = await PATCH( new NextRequest('http://localhost/api/materials/transcode', { method: 'PATCH', body: JSON.stringify({ action: 'recover' }), }) ) const data = await response.json() expect(response.status).toBe(403) expect(data.error).toBe('无权限') expect(isCurrentSuperAdmin).toHaveBeenCalledWith({ user: { id: 'admin-1', role: 'ADMIN' } }) expect(recoverStaleVideoTranscodeJobs).not.toHaveBeenCalled() }) it('最高权限账号可以触发卡住转码任务恢复', async () => { getServerSession.mockResolvedValue({ user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true } }) isCurrentSuperAdmin.mockResolvedValue(true) recoverStaleVideoTranscodeJobs.mockResolvedValue({ recovered: 2 }) const response = await PATCH( new NextRequest('http://localhost/api/materials/transcode', { method: 'PATCH', body: JSON.stringify({ action: 'recover' }), }) ) const data = await response.json() expect(response.status).toBe(200) expect(data.recovered).toBe(2) expect(isCurrentSuperAdmin).toHaveBeenCalledWith({ user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true } }) expect(recoverStaleVideoTranscodeJobs).toHaveBeenCalled() }) })