348 lines
11 KiB
TypeScript
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' },
|
|
})
|
|
})
|
|
})
|