gongxiangp2/app/admin/settings/page.test.tsx
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

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()
})
})