Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { useSession } from 'next-auth/react'
|
|
import SystemSettings from '@/app/admin/settings/page'
|
|
|
|
const showToastMock = jest.fn()
|
|
|
|
jest.mock('next-auth/react', () => ({
|
|
useSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: () => ({
|
|
push: jest.fn(),
|
|
refresh: jest.fn(),
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/components/Header', () => ({
|
|
__esModule: true,
|
|
default: () => <div data-testid="header" />,
|
|
}))
|
|
|
|
jest.mock('@/hooks/useToast', () => ({
|
|
useToast: () => ({
|
|
toast: null,
|
|
showToast: showToastMock,
|
|
}),
|
|
}))
|
|
|
|
jest.mock('@/components/ToastBubble', () => ({
|
|
__esModule: true,
|
|
default: () => null,
|
|
}))
|
|
|
|
describe('系统设置页凭据脱敏', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(useSession as jest.Mock).mockReturnValue({
|
|
data: {
|
|
user: { id: 'admin-1', role: 'ADMIN' },
|
|
},
|
|
status: 'authenticated',
|
|
})
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
headerBgColor: '#1f2937',
|
|
siteName: '由壹点3D素材',
|
|
headerLogoUrl: 'https://s3.jiansun.vip/gongxiangpan/brand/header.png',
|
|
headerLogoPreviewUrl: '/api/proxy-media?url=header-preview',
|
|
loginLogoUrl: 'https://s3.jiansun.vip/gongxiangpan/brand/login.png',
|
|
loginLogoPreviewUrl: '/api/proxy-media?url=login-preview',
|
|
s3Endpoint: 'https://storage.example.com',
|
|
s3Bucket: 'materials',
|
|
s3Region: 'us-east-1',
|
|
s3PublicUrl: 'https://cdn.example.com/materials',
|
|
s3CredentialsConfigured: true,
|
|
// 即使旧服务误返回这些字段,页面也不得回填或展示。
|
|
s3AccessKey: 'should-never-render-access-key',
|
|
s3SecretKey: 'should-never-render-secret-key',
|
|
maintenanceEnabled: false,
|
|
maintenanceMessage: null,
|
|
maintenanceStartTime: null,
|
|
maintenanceEndTime: null,
|
|
}),
|
|
}) as jest.Mock
|
|
})
|
|
|
|
test('只显示 configured 状态,凭据输入框始终为空', async () => {
|
|
render(<SystemSettings />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('已配置')).toBeInTheDocument()
|
|
})
|
|
|
|
expect(screen.getByLabelText('新 Access Key')).toHaveValue('')
|
|
expect(screen.getByLabelText('新 Secret Key')).toHaveValue('')
|
|
expect(screen.queryByDisplayValue('should-never-render-access-key')).not.toBeInTheDocument()
|
|
expect(screen.queryByDisplayValue('should-never-render-secret-key')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('should-never-render-access-key')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('should-never-render-secret-key')).not.toBeInTheDocument()
|
|
})
|
|
|
|
test('未填写新凭据时,保存请求不会覆盖现有凭据', async () => {
|
|
render(<SystemSettings />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('已配置')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
const [, requestInit] = (global.fetch as jest.Mock).mock.calls[1]
|
|
const payload = JSON.parse(requestInit.body)
|
|
expect(payload).not.toHaveProperty('s3AccessKey')
|
|
expect(payload).not.toHaveProperty('s3SecretKey')
|
|
expect(payload.headerLogoUrl).toBe('https://s3.jiansun.vip/gongxiangpan/brand/header.png')
|
|
expect(payload.loginLogoUrl).toBe('https://s3.jiansun.vip/gongxiangpan/brand/login.png')
|
|
expect(JSON.stringify(payload)).not.toContain('/api/proxy-media')
|
|
})
|
|
|
|
test('恢复默认使用镜像内置品牌图,不再写入旧存储域名', async () => {
|
|
render(<SystemSettings />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('已配置')).toBeInTheDocument()
|
|
})
|
|
|
|
const resetButtons = screen.getAllByRole('button', { name: '恢复默认' })
|
|
fireEvent.click(resetButtons[0])
|
|
fireEvent.click(resetButtons[1])
|
|
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
const [, requestInit] = (global.fetch as jest.Mock).mock.calls[1]
|
|
const payload = JSON.parse(requestInit.body)
|
|
expect(payload.headerLogoUrl).toBe('/brand/header-logo.png')
|
|
expect(payload.loginLogoUrl).toBe('/brand/login-logo.png')
|
|
expect(JSON.stringify(payload)).not.toContain('s3.1080p.hk')
|
|
})
|
|
|
|
test('文件选择器和前端校验只接受服务端支持的三种 Logo 格式', async () => {
|
|
const { container } = render(<SystemSettings />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('已配置')).toBeInTheDocument()
|
|
})
|
|
|
|
const inputs = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="file"]'))
|
|
expect(inputs).toHaveLength(2)
|
|
expect(inputs.every((input) => input.accept === 'image/png,image/jpeg,image/webp')).toBe(true)
|
|
|
|
fireEvent.change(inputs[0], {
|
|
target: { files: [new File(['<svg />'], 'logo.svg', { type: 'image/svg+xml' })] },
|
|
})
|
|
|
|
expect(showToastMock).toHaveBeenCalledWith(
|
|
'LOGO仅支持 PNG、JPEG 或 WebP 格式',
|
|
{ variant: 'error' },
|
|
)
|
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test.each([
|
|
{ user: { id: 'system-admin-1', role: 'SYSTEM_ADMIN' }, label: 'SYSTEM_ADMIN' },
|
|
{ user: { id: 'super-admin-1', role: 'USER', isSuperAdmin: true }, label: '超级管理员' },
|
|
])('$label 权限账号可以正常看到设置页面', async ({ user }) => {
|
|
;(useSession as jest.Mock).mockReturnValue({
|
|
data: { user },
|
|
status: 'authenticated',
|
|
})
|
|
|
|
render(<SystemSettings />)
|
|
|
|
expect(await screen.findByRole('heading', { name: '系统设置' })).toBeInTheDocument()
|
|
})
|
|
})
|