flnxi-Wu/app/api/admin/__tests__/destructive-actions-superadmin.test.ts
2026-06-01 11:40:02 +08:00

314 lines
11 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('fs/promises', () => ({
access: jest.fn(),
unlink: jest.fn(),
}))
const { getServerSession } = require('next-auth')
const { prisma } = require('@/lib/prisma')
const { repairSystemIntegrity } = require('@/lib/system-integrity')
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()
})
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 })
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' })
)
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: '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' })
)
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: '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 })
})
})