2026-07-31 09:41:40 +08:00

211 lines
6.9 KiB
TypeScript

/** @jest-environment node */
import { GET, PUT } from '@/app/api/system-settings/route'
import { getServerSession } from 'next-auth'
import { prisma } from '@/lib/prisma'
import { isSystemAdmin } from '@/lib/department-permission'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
systemSettings: {
findFirst: jest.fn(),
count: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
},
}))
jest.mock('@/lib/department-permission', () => ({
isSystemAdmin: jest.fn((role: string | null | undefined) =>
role === 'ADMIN' || role === 'SYSTEM_ADMIN',
),
}))
jest.mock('@/lib/url-rewrite', () => ({
proxyHttpUrl: jest.fn((url: string) => url),
}))
const storedSettings = {
id: 'settings-1',
headerBgColor: '#123456',
siteName: '测试素材库',
headerLogoUrl: 'https://static.example.com/header.png',
loginLogoUrl: 'https://static.example.com/login.png',
s3Endpoint: 'https://storage.internal.example.com',
s3AccessKey: 'test-access-key',
s3SecretKey: 'test-secret-key',
s3Bucket: 'materials',
s3Region: 'us-east-1',
s3PublicUrl: 'https://cdn.example.com/materials',
maintenanceEnabled: true,
maintenanceMessage: '计划维护',
maintenanceStartTime: new Date('2026-07-30T10:00:00.000Z'),
maintenanceEndTime: new Date('2026-07-30T11:00:00.000Z'),
}
function createPutRequest(body: unknown) {
return {
json: jest.fn().mockResolvedValue(body),
} as any
}
describe('/api/system-settings 安全响应', () => {
beforeEach(() => {
jest.clearAllMocks()
;(isSystemAdmin as jest.Mock).mockImplementation(
(role: string | null | undefined) =>
role === 'ADMIN' || role === 'SYSTEM_ADMIN',
)
;(prisma.systemSettings.count as jest.Mock).mockResolvedValue(1)
})
test('匿名 GET 只返回公开白名单且禁止缓存', async () => {
;(getServerSession as jest.Mock).mockResolvedValue(null)
;(prisma.systemSettings.findFirst as jest.Mock).mockResolvedValue(storedSettings)
const response = await GET()
const body = await response.json()
expect(response.status).toBe(200)
expect(response.headers.get('cache-control')).toContain('no-store')
expect(body).toEqual({
headerBgColor: '#123456',
siteName: '测试素材库',
headerLogoUrl: 'https://static.example.com/header.png',
loginLogoUrl: 'https://static.example.com/login.png',
maintenanceEnabled: true,
maintenanceMessage: '计划维护',
maintenanceStartTime: '2026-07-30T10:00:00.000Z',
maintenanceEndTime: '2026-07-30T11:00:00.000Z',
})
expect(body).not.toHaveProperty('id')
expect(body).not.toHaveProperty('s3Endpoint')
expect(body).not.toHaveProperty('s3AccessKey')
expect(body).not.toHaveProperty('s3SecretKey')
expect(body).not.toHaveProperty('s3CredentialsConfigured')
expect(prisma.systemSettings.create).not.toHaveBeenCalled()
expect(prisma.systemSettings.count).not.toHaveBeenCalled()
})
test('系统管理员 GET 仅获得非敏感配置与 configured 状态', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'admin-1', role: 'SYSTEM_ADMIN' },
})
;(prisma.systemSettings.findFirst as jest.Mock).mockResolvedValue(storedSettings)
const response = await GET()
const body = await response.json()
expect(response.status).toBe(200)
expect(body).toEqual(expect.objectContaining({
s3Endpoint: 'https://storage.internal.example.com',
s3Bucket: 'materials',
s3Region: 'us-east-1',
s3PublicUrl: 'https://cdn.example.com/materials',
s3CredentialsConfigured: true,
}))
expect(body).not.toHaveProperty('s3AccessKey')
expect(body).not.toHaveProperty('s3SecretKey')
expect(prisma.systemSettings.count).toHaveBeenCalledWith({
where: {
id: storedSettings.id,
s3AccessKey: { not: '' },
s3SecretKey: { not: '' },
},
})
})
test('普通用户不能更新系统设置', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-1', role: 'USER' },
})
const response = await PUT(createPutRequest({ siteName: '无权修改' }))
const body = await response.json()
expect(response.status).toBe(403)
expect(body).toEqual({ error: '无权限' })
expect(response.headers.get('cache-control')).toContain('no-store')
expect(prisma.systemSettings.findFirst).not.toHaveBeenCalled()
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
test('凭据必须成对覆盖写入', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'admin-1', role: 'ADMIN' },
})
const response = await PUT(createPutRequest({
s3AccessKey: 'new-access-key',
s3SecretKey: '',
}))
const body = await response.json()
expect(response.status).toBe(400)
expect(body).toEqual({ error: 'Access Key 和 Secret Key 必须同时填写' })
expect(prisma.systemSettings.update).not.toHaveBeenCalled()
})
test('管理员可覆盖写入新凭据,但响应不返回任何凭据原文', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'admin-1', role: 'ADMIN' },
})
;(prisma.systemSettings.findFirst as jest.Mock).mockResolvedValue({
id: storedSettings.id,
})
;(prisma.systemSettings.update as jest.Mock).mockResolvedValue({
...storedSettings,
s3AccessKey: 'new-access-key',
s3SecretKey: 'new-secret-key',
})
const response = await PUT(createPutRequest({
siteName: '更新后的素材库',
s3AccessKey: ' new-access-key ',
s3SecretKey: ' new-secret-key ',
}))
const body = await response.json()
expect(response.status).toBe(200)
expect(prisma.systemSettings.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: storedSettings.id },
data: expect.objectContaining({
siteName: '更新后的素材库',
s3AccessKey: 'new-access-key',
s3SecretKey: 'new-secret-key',
}),
}),
)
expect(body.s3CredentialsConfigured).toBe(true)
expect(body).not.toHaveProperty('s3AccessKey')
expect(body).not.toHaveProperty('s3SecretKey')
expect(JSON.stringify(body)).not.toContain('new-secret-key')
expect(prisma.systemSettings.count).not.toHaveBeenCalled()
})
test('超级管理员标记可以更新设置', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'super-1', role: 'USER', isSuperAdmin: true },
})
;(prisma.systemSettings.findFirst as jest.Mock).mockResolvedValue({
id: storedSettings.id,
})
;(prisma.systemSettings.update as jest.Mock).mockResolvedValue(storedSettings)
const response = await PUT(createPutRequest({ siteName: '超级管理员修改' }))
expect(response.status).toBe(200)
expect(prisma.systemSettings.update).toHaveBeenCalled()
})
})