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

399 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { ensureSystemSettingsOptionalColumns } = require('@/lib/system-settings-columns')
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,
enablePublicShare: 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.enablePublicShare).toBeUndefined()
expect(data.enabledBuiltinTemplates).toBeUndefined()
expect(data.homeDeleteButtonMode).toBeUndefined()
expect(ensureSystemSettingsOptionalColumns).not.toHaveBeenCalled()
expect(prisma.systemSettings.create).not.toHaveBeenCalled()
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
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.enablePublicShare).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('GET 没有系统设置记录时只返回内存默认值,不创建数据库记录', async () => {
getServerSession.mockResolvedValue(null)
prisma.systemSettings.findFirst.mockResolvedValue(null)
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toMatchObject({
headerBgColor: '#1f2937',
siteName: 'FLNXI素材库',
headerLogoUrl: '',
loginLogoUrl: '',
})
expect(ensureSystemSettingsOptionalColumns).not.toHaveBeenCalled()
expect(prisma.systemSettings.create).not.toHaveBeenCalled()
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
it('GET 只在响应中整理旧 Logo,不写回数据库', async () => {
getServerSession.mockResolvedValue(null)
prisma.systemSettings.findFirst.mockResolvedValue({
...mockSettings,
headerLogoUrl: '/logo/logo1.png',
loginLogoUrl: '',
})
const response = await GET()
const data = await response.json()
expect(response.status).toBe(200)
expect(data.headerLogoUrl).toBe('/logo/header.png')
expect(data.loginLogoUrl).toBe('/logo/login.png')
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
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,
enablePublicShare: false,
}),
}))
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toContain('最高权限')
expect(ensureSystemSettingsOptionalColumns).toHaveBeenCalled()
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,
enablePublicShare: 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,
}),
})
expect(prisma.$executeRawUnsafe).toHaveBeenCalledWith(
`UPDATE "system_settings" SET "enablePublicShare" = $1 WHERE "id" = $2`,
false,
'settings-1',
)
})
})
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' },
})
})
})