426 lines
15 KiB
TypeScript
426 lines
15 KiB
TypeScript
import { NextRequest } from 'next/server'
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
authOptions: {},
|
|
}))
|
|
|
|
jest.mock('next-auth', () => ({
|
|
getServerSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
$executeRawUnsafe: jest.fn(),
|
|
$queryRawUnsafe: jest.fn(),
|
|
$transaction: jest.fn(),
|
|
material: {
|
|
updateMany: jest.fn(),
|
|
},
|
|
user: {
|
|
findUnique: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
jest.mock('@/lib/s3', () => ({
|
|
deleteFromS3: jest.fn(),
|
|
isS3Url: jest.fn(() => false),
|
|
}))
|
|
|
|
jest.mock('@/lib/system-integrity', () => ({
|
|
checkSystemIntegrity: jest.fn(),
|
|
repairSystemIntegrity: jest.fn(),
|
|
ensureBuiltinTemplates: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@/lib/license', () => ({
|
|
canUseSystemSettings: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('fs/promises', () => ({
|
|
access: jest.fn(),
|
|
unlink: jest.fn(),
|
|
}))
|
|
|
|
const { getServerSession } = require('next-auth')
|
|
const { prisma } = require('@/lib/prisma')
|
|
const { repairSystemIntegrity } = require('@/lib/system-integrity')
|
|
const { canUseSystemSettings } = require('@/lib/license')
|
|
|
|
import { POST as cleanupDatabase } from '../cleanup-database/route'
|
|
import { POST as migrateMaterialFields } from '../migrate-material-fields/route'
|
|
import { POST as resetMaterialRankings } from '../reset-material-rankings/route'
|
|
import { POST as transferMaterialOwner } from '../materials/transfer-owner/route'
|
|
import { POST as repairSystemIntegrityRoute } from '../../system/integrity/route'
|
|
|
|
describe('admin destructive actions require super admin', () => {
|
|
beforeEach(() => {
|
|
jest.resetAllMocks()
|
|
canUseSystemSettings.mockResolvedValue(true)
|
|
})
|
|
|
|
it('普通管理员不能清理数据库', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: false },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await cleanupDatabase(
|
|
new NextRequest('http://localhost/api/admin/cleanup-database', { method: 'POST' })
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
|
where: { id: 'admin-1' },
|
|
select: { isSuperAdmin: true, isActive: true },
|
|
})
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号可以清理数据库', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
prisma.$executeRawUnsafe
|
|
.mockResolvedValueOnce(1)
|
|
.mockResolvedValueOnce(2)
|
|
.mockResolvedValueOnce(3)
|
|
prisma.$queryRawUnsafe
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{ table_name: 'invitations', count: 0 },
|
|
{ table_name: 'danmakus', count: 0 },
|
|
{ table_name: 'materials', count: 0 },
|
|
{ table_name: 'users', count: 1 },
|
|
])
|
|
|
|
const response = await cleanupDatabase(
|
|
new NextRequest('http://localhost/api/admin/cleanup-database', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '清理全部素材' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(prisma.$executeRawUnsafe).toHaveBeenCalled()
|
|
})
|
|
|
|
it('免费版即使是最高权限账号也不能清理数据库', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
canUseSystemSettings.mockResolvedValue(false)
|
|
|
|
const response = await cleanupDatabase(
|
|
new NextRequest('http://localhost/api/admin/cleanup-database', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '清理全部素材' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('当前许可证未包含系统设置功能')
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号清理数据库时也必须输入确认短语', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
|
|
const response = await cleanupDatabase(
|
|
new NextRequest('http://localhost/api/admin/cleanup-database', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '删除' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(400)
|
|
expect(data.error).toContain('确认短语')
|
|
expect(data.requiredConfirmPhrase).toBe('清理全部素材')
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通管理员不能重置排行榜数据', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: false },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await resetMaterialRankings(
|
|
new NextRequest('http://localhost/api/admin/reset-material-rankings', { method: 'POST' })
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(prisma.$transaction).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('即使旧会话带最高权限标记,只要数据库已撤销也不能重置排行榜数据', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await resetMaterialRankings(
|
|
new NextRequest('http://localhost/api/admin/reset-material-rankings', { method: 'POST' })
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(prisma.$transaction).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号可以重置排行榜数据', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
const tx = {
|
|
$executeRawUnsafe: jest
|
|
.fn()
|
|
.mockResolvedValueOnce(1)
|
|
.mockResolvedValueOnce(2)
|
|
.mockResolvedValueOnce(3),
|
|
}
|
|
prisma.$transaction.mockImplementation((callback: any) => callback(tx))
|
|
|
|
const response = await resetMaterialRankings(
|
|
new NextRequest('http://localhost/api/admin/reset-material-rankings', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '重置排行榜数据' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(tx.$executeRawUnsafe).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('最高权限账号重置排行榜数据时也必须输入确认短语', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
|
|
const response = await resetMaterialRankings(
|
|
new NextRequest('http://localhost/api/admin/reset-material-rankings', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '重置' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(400)
|
|
expect(data.error).toContain('确认短语')
|
|
expect(data.requiredConfirmPhrase).toBe('重置排行榜数据')
|
|
expect(prisma.$transaction).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('免费版即使是最高权限账号也不能重置排行榜数据', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
canUseSystemSettings.mockResolvedValue(false)
|
|
|
|
const response = await resetMaterialRankings(
|
|
new NextRequest('http://localhost/api/admin/reset-material-rankings', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '重置排行榜数据' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('当前许可证未包含系统设置功能')
|
|
expect(prisma.$transaction).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通管理员不能批量移交素材归属', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: false },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await transferMaterialOwner(
|
|
new NextRequest('http://localhost/api/admin/materials/transfer-owner', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ materialIds: ['material-1'], targetUserId: 'user-2' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(prisma.material.updateMany).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号可以批量移交素材归属', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique
|
|
.mockResolvedValueOnce({ isSuperAdmin: true })
|
|
.mockResolvedValueOnce({
|
|
id: 'user-2',
|
|
username: 'target-user',
|
|
realName: '目标用户',
|
|
email: 'target@example.com',
|
|
})
|
|
prisma.material.updateMany.mockResolvedValue({ count: 1 })
|
|
|
|
const response = await transferMaterialOwner(
|
|
new NextRequest('http://localhost/api/admin/materials/transfer-owner', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ materialIds: ['material-1'], targetUserId: 'user-2' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(prisma.material.updateMany).toHaveBeenCalledWith({
|
|
where: { deletedAt: null, id: { in: ['material-1'] } },
|
|
data: expect.objectContaining({
|
|
ownerType: 'USER',
|
|
ownerId: 'user-2',
|
|
userId: 'user-2',
|
|
authorName: '目标用户',
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('普通管理员不能执行素材字段迁移', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: false },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await migrateMaterialFields(
|
|
new NextRequest('http://localhost/api/admin/migrate-material-fields', { method: 'POST' })
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号可以执行素材字段迁移', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
prisma.$executeRawUnsafe.mockResolvedValue(1)
|
|
prisma.$queryRawUnsafe.mockResolvedValue(1)
|
|
|
|
const response = await migrateMaterialFields(
|
|
new NextRequest('http://localhost/api/admin/migrate-material-fields', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '执行字段迁移' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(prisma.$executeRawUnsafe).toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号执行素材字段迁移时也必须输入确认短语', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
|
|
const response = await migrateMaterialFields(
|
|
new NextRequest('http://localhost/api/admin/migrate-material-fields', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ confirmPhrase: '迁移' }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(400)
|
|
expect(data.error).toContain('确认短语')
|
|
expect(data.requiredConfirmPhrase).toBe('执行字段迁移')
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('免费版即使是最高权限账号也不能执行素材字段迁移', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
canUseSystemSettings.mockResolvedValue(false)
|
|
|
|
const response = await migrateMaterialFields(
|
|
new NextRequest('http://localhost/api/admin/migrate-material-fields', { method: 'POST' })
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('当前许可证未包含系统设置功能')
|
|
expect(prisma.$executeRawUnsafe).not.toHaveBeenCalled()
|
|
expect(prisma.$queryRawUnsafe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('普通管理员不能执行系统完整性修复', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'admin-1', role: 'ADMIN', isSuperAdmin: false },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
|
|
|
|
const response = await repairSystemIntegrityRoute(
|
|
new NextRequest('http://localhost/api/system/integrity', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ fixOrphanedMaterials: true }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(403)
|
|
expect(data.error).toContain('最高权限')
|
|
expect(repairSystemIntegrity).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('最高权限账号可以执行系统完整性修复', async () => {
|
|
getServerSession.mockResolvedValue({
|
|
user: { id: 'super-1', role: 'ADMIN', isSuperAdmin: true },
|
|
})
|
|
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
|
|
repairSystemIntegrity.mockResolvedValue({ success: true, repaired: 1, errors: [] })
|
|
|
|
const response = await repairSystemIntegrityRoute(
|
|
new NextRequest('http://localhost/api/system/integrity', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ fixOrphanedMaterials: true }),
|
|
})
|
|
)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.mode).toBe('full')
|
|
expect(repairSystemIntegrity).toHaveBeenCalledWith({ fixOrphanedMaterials: true })
|
|
})
|
|
})
|