2026-04-02 11:20:20 +08:00
|
|
|
import { MemoryRouter } from 'react-router-dom'
|
|
|
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
|
|
|
import userEvent from '@testing-library/user-event'
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
|
|
|
|
|
import { AuthContext, type AuthContextValue } from '@/app/providers/auth-context'
|
|
|
|
|
import type { AuthCapabilities, TokenBundle } from '@/types'
|
|
|
|
|
import { BootstrapAdminPage } from './BootstrapAdminPage'
|
|
|
|
|
|
|
|
|
|
const getAuthCapabilitiesMock = vi.fn<() => Promise<AuthCapabilities>>()
|
2026-04-23 07:14:12 +08:00
|
|
|
const bootstrapAdminMock = vi.fn<(payload: unknown, bootstrapSecret: string) => Promise<TokenBundle>>()
|
2026-04-02 11:20:20 +08:00
|
|
|
const onLoginSuccessMock = vi.fn<(tokenBundle: TokenBundle) => Promise<void>>()
|
|
|
|
|
|
|
|
|
|
vi.mock('@/services/auth', () => ({
|
|
|
|
|
getAuthCapabilities: () => getAuthCapabilitiesMock(),
|
2026-04-23 07:14:12 +08:00
|
|
|
bootstrapAdmin: (payload: unknown, bootstrapSecret: string) => bootstrapAdminMock(payload, bootstrapSecret),
|
2026-04-02 11:20:20 +08:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const authContextValue: AuthContextValue = {
|
|
|
|
|
user: null,
|
|
|
|
|
roles: [],
|
|
|
|
|
isAdmin: false,
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
onLoginSuccess: (tokenBundle) => onLoginSuccessMock(tokenBundle),
|
|
|
|
|
logout: vi.fn(async () => {}),
|
|
|
|
|
refreshUser: vi.fn(async () => {}),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderBootstrapAdminPage() {
|
|
|
|
|
return render(
|
|
|
|
|
<MemoryRouter initialEntries={['/bootstrap-admin']}>
|
|
|
|
|
<AuthContext.Provider value={authContextValue}>
|
|
|
|
|
<BootstrapAdminPage />
|
|
|
|
|
</AuthContext.Provider>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('BootstrapAdminPage', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
getAuthCapabilitiesMock.mockReset()
|
|
|
|
|
bootstrapAdminMock.mockReset()
|
|
|
|
|
onLoginSuccessMock.mockReset()
|
|
|
|
|
|
|
|
|
|
getAuthCapabilitiesMock.mockResolvedValue({
|
|
|
|
|
password: true,
|
|
|
|
|
email_activation: false,
|
|
|
|
|
email_code: false,
|
|
|
|
|
sms_code: false,
|
|
|
|
|
password_reset: false,
|
|
|
|
|
admin_bootstrap_required: true,
|
|
|
|
|
oauth_providers: [],
|
|
|
|
|
})
|
|
|
|
|
bootstrapAdminMock.mockResolvedValue({
|
|
|
|
|
access_token: 'access-token',
|
|
|
|
|
refresh_token: 'refresh-token',
|
|
|
|
|
expires_in: 7200,
|
|
|
|
|
user: {
|
|
|
|
|
id: 1,
|
|
|
|
|
username: 'bootstrap_admin',
|
|
|
|
|
email: 'bootstrap_admin@example.com',
|
|
|
|
|
phone: '',
|
|
|
|
|
nickname: 'Bootstrap Admin',
|
|
|
|
|
avatar: '',
|
|
|
|
|
status: 1,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
onLoginSuccessMock.mockResolvedValue(undefined)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders the first-admin bootstrap form when the system has no active admin', async () => {
|
|
|
|
|
renderBootstrapAdminPage()
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('heading', { name: '初始化首个管理员账号' })).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByPlaceholderText('管理员用户名')).toBeInTheDocument()
|
2026-04-23 07:14:12 +08:00
|
|
|
expect(screen.getByPlaceholderText('引导密钥')).toBeInTheDocument()
|
2026-04-02 11:20:20 +08:00
|
|
|
expect(screen.getByPlaceholderText('管理员密码')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole('button', { name: '完成初始化并进入系统' })).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('submits the bootstrap request and hands the created session to the auth provider', async () => {
|
|
|
|
|
const user = userEvent.setup()
|
|
|
|
|
renderBootstrapAdminPage()
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByPlaceholderText('管理员用户名')).toBeInTheDocument())
|
|
|
|
|
|
|
|
|
|
await user.type(screen.getByPlaceholderText('管理员用户名'), 'bootstrap_admin')
|
|
|
|
|
await user.type(screen.getByPlaceholderText('管理员昵称(选填)'), 'Bootstrap Admin')
|
|
|
|
|
await user.type(screen.getByPlaceholderText('管理员邮箱(选填)'), 'bootstrap_admin@example.com')
|
2026-04-23 07:14:12 +08:00
|
|
|
await user.type(screen.getByPlaceholderText('引导密钥'), 'bootstrap-secret')
|
2026-04-02 11:20:20 +08:00
|
|
|
await user.type(screen.getByPlaceholderText('管理员密码'), 'Bootstrap123!@#')
|
|
|
|
|
await user.type(screen.getByPlaceholderText('确认管理员密码'), 'Bootstrap123!@#')
|
|
|
|
|
await user.click(screen.getByRole('button', { name: '完成初始化并进入系统' }))
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
2026-04-23 07:14:12 +08:00
|
|
|
expect(bootstrapAdminMock).toHaveBeenCalledWith(
|
|
|
|
|
{
|
|
|
|
|
username: 'bootstrap_admin',
|
|
|
|
|
nickname: 'Bootstrap Admin',
|
|
|
|
|
email: 'bootstrap_admin@example.com',
|
|
|
|
|
password: 'Bootstrap123!@#',
|
|
|
|
|
},
|
|
|
|
|
'bootstrap-secret',
|
|
|
|
|
),
|
2026-04-02 11:20:20 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(onLoginSuccessMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
|
access_token: 'access-token',
|
|
|
|
|
refresh_token: 'refresh-token',
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows an informational state when admin bootstrap is already closed', async () => {
|
|
|
|
|
getAuthCapabilitiesMock.mockResolvedValue({
|
|
|
|
|
password: true,
|
|
|
|
|
email_activation: false,
|
|
|
|
|
email_code: false,
|
|
|
|
|
sms_code: false,
|
|
|
|
|
password_reset: false,
|
|
|
|
|
admin_bootstrap_required: false,
|
|
|
|
|
oauth_providers: [],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
renderBootstrapAdminPage()
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText('管理员已完成初始化')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole('link', { name: '返回登录' })).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
})
|