2026-05-25 15:13:49 +08:00

348 lines
11 KiB
TypeScript

import { NextRequest } from 'next/server'
import { GET, PUT } from '../route'
import { POST as uploadLogo } from '../upload-logo/route'
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
systemSettings: {
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
$queryRaw: jest.fn(),
$executeRawUnsafe: jest.fn(),
},
}))
jest.mock('@/lib/cache', () => ({
cache: {
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
},
}))
jest.mock('@/lib/system-settings-columns', () => ({
ensureSystemSettingsOptionalColumns: jest.fn(),
}))
jest.mock('@/lib/config', () => ({
defaultLogos: {
header: '/logo/header.png',
login: '/logo/login.png',
},
featureFlags: {
aiEditEnabled: false,
tunnelEnabled: false,
},
}))
jest.mock('@/lib/license', () => ({
canUseSystemSettings: jest.fn(),
}))
jest.mock('@/lib/s3', () => ({
uploadToS3: jest.fn(),
}))
const { getServerSession } = require('next-auth')
const { prisma } = require('@/lib/prisma')
const { cache } = require('@/lib/cache')
const { canUseSystemSettings } = require('@/lib/license')
const { uploadToS3 } = require('@/lib/s3')
const mockSettings = {
id: 'settings-1',
headerBgColor: '#1f2937',
siteName: 'FLNXI素材库',
premiumRatio: 10,
updatedAt: new Date('2026-05-24T00:00:00.000Z'),
createdAt: new Date('2026-05-23T00:00:00.000Z'),
headerLogoUrl: '/logo/header.png',
loginLogoUrl: '/logo/login.png',
s3AccessKey: 'access-key',
s3Bucket: 'private-bucket',
s3Endpoint: 'http://minio:9000',
s3PublicUrl: 'http://minio.local/materials',
s3Region: 'us-east-1',
s3SecretKey: 'secret-key',
enableRegistration: true,
enableUserActivation: false,
enableAiImageEdit: true,
enableTunnelService: true,
}
describe('GET /api/system-settings', () => {
const originalEnv = process.env
beforeEach(() => {
jest.resetAllMocks()
process.env = { ...originalEnv }
cache.get.mockReturnValue(null)
prisma.systemSettings.findFirst.mockResolvedValue({ ...mockSettings })
prisma.$queryRaw.mockResolvedValue([
{
enabledBuiltinTemplates: '["3dmax-material"]',
homeDeleteButtonMode: 'ADMIN_ONLY',
premiumRatio: 10,
},
])
})
afterAll(() => {
process.env = originalEnv
})
it('未登录时只返回公开展示字段,不返回对象存储和功能开关', async () => {
getServerSession.mockResolvedValue(null)
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toMatchObject({
headerBgColor: '#1f2937',
siteName: 'FLNXI素材库',
headerLogoUrl: '/logo/header.png',
loginLogoUrl: '/logo/login.png',
})
expect(data.s3AccessKey).toBeUndefined()
expect(data.s3SecretKey).toBeUndefined()
expect(data.s3Bucket).toBeUndefined()
expect(data.s3Endpoint).toBeUndefined()
expect(data.enableRegistration).toBeUndefined()
expect(data.enableUserActivation).toBeUndefined()
expect(data.enableAiImageEdit).toBeUndefined()
expect(data.enableTunnelService).toBeUndefined()
expect(data.enabledBuiltinTemplates).toBeUndefined()
expect(data.homeDeleteButtonMode).toBeUndefined()
})
it('普通管理员登录时返回后台设置但不返回对象存储密钥', async () => {
getServerSession.mockResolvedValue({
user: { id: 'admin-id', role: 'ADMIN', isSuperAdmin: false },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data.s3AccessKey).toBeUndefined()
expect(data.s3SecretKey).toBeUndefined()
expect(data.s3Bucket).toBe('private-bucket')
expect(data.enableRegistration).toBe(true)
expect(data.enableAiImageEdit).toBe(true)
expect(data.enabledBuiltinTemplates).toBe('["3dmax-material"]')
expect(data.homeDeleteButtonMode).toBe('ADMIN_ONLY')
})
it('最高权限账号登录时才返回对象存储密钥', async () => {
getServerSession.mockResolvedValue({
user: { id: 'super-id', role: 'ADMIN', isSuperAdmin: true },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data.s3AccessKey).toBe('access-key')
expect(data.s3SecretKey).toBe('secret-key')
expect(data.s3Bucket).toBe('private-bucket')
})
it('GET 不会用环境变量补写对象存储密钥', async () => {
getServerSession.mockResolvedValue(null)
prisma.systemSettings.findFirst.mockResolvedValue({
...mockSettings,
s3AccessKey: null,
s3SecretKey: null,
s3Bucket: null,
s3Endpoint: null,
s3PublicUrl: null,
})
process.env.S3_ACCESS_KEY_ID = 'env-access-key'
process.env.S3_SECRET_ACCESS_KEY = 'env-secret-key'
process.env.S3_BUCKET = 'env-bucket'
process.env.S3_ENDPOINT = 'http://env-minio:9000'
await GET()
expect(prisma.systemSettings.update).not.toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
s3AccessKey: expect.any(String),
}),
}),
)
expect(prisma.systemSettings.update).not.toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
s3SecretKey: expect.any(String),
}),
}),
)
})
it('数据库异常时,未登录读取会返回公共兜底设置而不是 500', async () => {
getServerSession.mockResolvedValue(null)
prisma.systemSettings.findFirst.mockRejectedValue(new Error('database offline'))
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toMatchObject({
headerBgColor: '#1f2937',
siteName: 'FLNXI素材库',
headerLogoUrl: '',
loginLogoUrl: '',
})
})
})
describe('PUT /api/system-settings 权限边界', () => {
beforeEach(() => {
jest.resetAllMocks()
cache.get.mockReturnValue(null)
canUseSystemSettings.mockResolvedValue(true)
prisma.systemSettings.findFirst.mockResolvedValue({ ...mockSettings })
prisma.systemSettings.update.mockResolvedValue({
...mockSettings,
siteName: '新系统名',
})
})
it('普通管理员不能修改任何系统设置', async () => {
getServerSession.mockResolvedValue({
user: { id: 'admin-id', role: 'ADMIN', isSuperAdmin: false },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
const response = await PUT(new NextRequest('http://localhost/api/system-settings', {
method: 'PUT',
body: JSON.stringify({
siteName: '普通管理员不应能改',
s3SecretKey: 'new-secret',
enableRegistration: false,
}),
}))
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toContain('最高权限')
expect(canUseSystemSettings).not.toHaveBeenCalled()
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
it('旧会话仍带最高权限标记但数据库已撤销时不能修改系统设置', async () => {
getServerSession.mockResolvedValue({
user: { id: 'admin-id', role: 'ADMIN', isSuperAdmin: true },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
const response = await PUT(new NextRequest('http://localhost/api/system-settings', {
method: 'PUT',
body: JSON.stringify({ siteName: '不应写入' }),
}))
expect(response.status).toBe(403)
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
it('最高权限账号可以修改系统设置', async () => {
getServerSession.mockResolvedValue({
user: { id: 'super-id', role: 'ADMIN', isSuperAdmin: true },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
const response = await PUT(new NextRequest('http://localhost/api/system-settings', {
method: 'PUT',
body: JSON.stringify({
siteName: '新系统名',
s3SecretKey: 'new-secret',
enableRegistration: false,
}),
}))
const data = await response.json()
expect(response.status).toBe(200)
expect(data.siteName).toBe('新系统名')
expect(prisma.systemSettings.update).toHaveBeenCalledWith({
where: { id: 'settings-1' },
data: expect.objectContaining({
siteName: '新系统名',
s3SecretKey: 'new-secret',
enableRegistration: false,
}),
})
})
})
describe('POST /api/system-settings/upload-logo 权限边界', () => {
beforeEach(() => {
jest.resetAllMocks()
uploadToS3.mockResolvedValue('https://cdn.example.com/header-logo.png')
prisma.systemSettings.findFirst.mockResolvedValue({ ...mockSettings })
prisma.systemSettings.update.mockResolvedValue({
...mockSettings,
headerLogoUrl: 'https://cdn.example.com/header-logo.png',
})
})
function createLogoRequest() {
const formData = new FormData()
formData.set('type', 'header')
formData.set('logo', new File([Buffer.from('logo')], 'logo.png', { type: 'image/png' }))
return new NextRequest('http://localhost/api/system-settings/upload-logo', {
method: 'POST',
body: formData,
})
}
it('普通管理员不能上传系统 LOGO', async () => {
getServerSession.mockResolvedValue({
user: { id: 'admin-id', role: 'ADMIN', isSuperAdmin: false },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: false })
const response = await uploadLogo(createLogoRequest())
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toContain('最高权限')
expect(uploadToS3).not.toHaveBeenCalled()
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
it('最高权限账号可以上传系统 LOGO', async () => {
getServerSession.mockResolvedValue({
user: { id: 'super-id', role: 'ADMIN', isSuperAdmin: true },
})
prisma.user.findUnique.mockResolvedValue({ isSuperAdmin: true })
const response = await uploadLogo(createLogoRequest())
const data = await response.json()
expect(response.status).toBe(200)
expect(data.logoUrl).toBe('https://cdn.example.com/header-logo.png')
expect(uploadToS3).toHaveBeenCalled()
expect(prisma.systemSettings.update).toHaveBeenCalledWith({
where: { id: 'settings-1' },
data: { headerLogoUrl: 'https://cdn.example.com/header-logo.png' },
})
})
})