2026-05-28 15:11:36 +08:00

351 lines
10 KiB
TypeScript

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()
})
})