351 lines
10 KiB
TypeScript
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()
|
|
})
|
|
})
|