Files
user-system/frontend/admin/src/pages/auth/RegisterPage/RegisterPage.test.tsx

383 lines
13 KiB
TypeScript

import { MemoryRouter } from 'react-router-dom'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { message } from 'antd'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AuthCapabilities, RegisterResponse } from '@/types'
import { RegisterPage } from './RegisterPage'
const TEXT = {
usernamePlaceholder: '用户名',
nicknamePlaceholder: '昵称(选填)',
emailPlaceholder: '邮箱地址(选填)',
phonePlaceholder: '手机号(选填)',
passwordPlaceholder: '密码',
confirmPasswordPlaceholder: '确认密码',
createAccount: '创建账号',
returnLogin: '返回登录',
forgotPassword: '忘记密码?',
sendCode: '获取验证码',
resendActivation: '重新发送激活邮件',
adminBootstrapTitle: '当前仍需管理员初始化',
adminBootstrapAction: '初始化管理员',
phoneCodeValidation: '请输入 6 位短信验证码',
activeSummary: '账号已创建,现在可以返回登录页使用新账号登录。',
inactiveSummaryNoEmail: '账号已创建,请按页面提示完成激活后再登录。',
smsDisabledDescription: '支持用户名和邮箱注册;当前环境未启用短信能力,因此手机号注册暂不可用。',
}
const getAuthCapabilitiesMock = vi.fn<() => Promise<AuthCapabilities>>()
const registerMock = vi.fn<(payload: unknown) => Promise<RegisterResponse>>()
const sendSmsCodeMock = vi.fn<(payload: unknown) => Promise<void>>()
const defaultCapabilities: AuthCapabilities = {
password: true,
email_activation: false,
email_code: false,
sms_code: false,
password_reset: false,
admin_bootstrap_required: false,
oauth_providers: [],
}
const activeRegisterResponse: RegisterResponse = {
id: 2,
username: 'new-user',
email: 'new-user@example.com',
phone: '',
nickname: 'New User',
avatar: '',
status: 1,
}
vi.mock('@/services/auth', () => ({
getAuthCapabilities: () => getAuthCapabilitiesMock(),
register: (payload: unknown) => registerMock(payload),
sendSmsCode: (payload: unknown) => sendSmsCodeMock(payload),
}))
function renderRegisterPage() {
return render(
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }} initialEntries={['/register']}>
<RegisterPage />
</MemoryRouter>,
)
}
function fillBaseRegistrationFields(values?: {
username?: string
nickname?: string
email?: string
password?: string
confirmPassword?: string
}) {
fireEvent.change(screen.getByPlaceholderText(TEXT.usernamePlaceholder), {
target: { value: values?.username ?? 'new-user' },
})
if (values?.nickname !== undefined || screen.queryByPlaceholderText(TEXT.nicknamePlaceholder)) {
fireEvent.change(screen.getByPlaceholderText(TEXT.nicknamePlaceholder), {
target: { value: values?.nickname ?? 'New User' },
})
}
if (values?.email !== undefined || screen.queryByPlaceholderText(TEXT.emailPlaceholder)) {
fireEvent.change(screen.getByPlaceholderText(TEXT.emailPlaceholder), {
target: { value: values?.email ?? 'new-user@example.com' },
})
}
fireEvent.change(screen.getByPlaceholderText(TEXT.passwordPlaceholder), {
target: { value: values?.password ?? 'SecurePass123!' },
})
fireEvent.change(screen.getByPlaceholderText(TEXT.confirmPasswordPlaceholder), {
target: { value: values?.confirmPassword ?? values?.password ?? 'SecurePass123!' },
})
}
describe('RegisterPage', () => {
beforeEach(() => {
getAuthCapabilitiesMock.mockReset()
registerMock.mockReset()
sendSmsCodeMock.mockReset()
getAuthCapabilitiesMock.mockResolvedValue(defaultCapabilities)
registerMock.mockResolvedValue(activeRegisterResponse)
sendSmsCodeMock.mockResolvedValue(undefined)
vi.spyOn(message, 'success').mockImplementation(() => undefined as never)
vi.spyOn(message, 'error').mockImplementation(() => undefined as never)
})
afterEach(() => {
vi.restoreAllMocks()
})
it('renders username registration fields and hides phone registration when sms is disabled', async () => {
renderRegisterPage()
await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))
expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()
expect(screen.getByPlaceholderText(TEXT.nicknamePlaceholder)).toBeInTheDocument()
expect(screen.getByPlaceholderText(TEXT.emailPlaceholder)).toBeInTheDocument()
expect(screen.queryByPlaceholderText(TEXT.phonePlaceholder)).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /返回登录/ })).toBeInTheDocument()
})
it('falls back to default capabilities when loading capabilities fails', async () => {
getAuthCapabilitiesMock.mockRejectedValue(new Error('capabilities unavailable'))
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument())
expect(screen.getByText(TEXT.smsDisabledDescription)).toBeInTheDocument()
expect(screen.queryByPlaceholderText(TEXT.phonePlaceholder)).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: TEXT.forgotPassword })).not.toBeInTheDocument()
})
it('shows the forgot-password entry when the capability is enabled', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
password_reset: true,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByRole('link', { name: TEXT.forgotPassword })).toBeInTheDocument())
})
it('sends register sms codes with the normalized purpose payload and starts the countdown', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
sms_code: true,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument())
fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), {
target: { value: '13812345678' },
})
fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode }))
await waitFor(() =>
expect(sendSmsCodeMock).toHaveBeenCalledWith({
phone: '13812345678',
purpose: 'register',
}),
)
expect(message.success).toHaveBeenCalledTimes(1)
expect(screen.getByRole('button', { name: '60s' })).toBeInTheDocument()
})
it('ignores form validation errors when the phone format is invalid', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
sms_code: true,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument())
fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), {
target: { value: '123' },
})
fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode }))
await waitFor(() => expect(sendSmsCodeMock).not.toHaveBeenCalled())
expect(message.error).not.toHaveBeenCalled()
expect(screen.queryByRole('button', { name: '60s' })).not.toBeInTheDocument()
})
it('surfaces sms code send failures from the backend', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
sms_code: true,
})
sendSmsCodeMock.mockRejectedValue(new Error('sms send failed'))
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument())
fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), {
target: { value: '13812345678' },
})
fireEvent.click(screen.getByRole('button', { name: TEXT.sendCode }))
await waitFor(() => expect(message.error).toHaveBeenCalledWith('sms send failed'))
expect(screen.queryByRole('button', { name: '60s' })).not.toBeInTheDocument()
})
it('blocks sms-backed registration when the phone code is missing', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
sms_code: true,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields()
fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), {
target: { value: '13812345678' },
})
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
await waitFor(() => expect(registerMock).not.toHaveBeenCalled())
expect(message.error).not.toHaveBeenCalled()
expect(screen.getByRole('button', { name: TEXT.createAccount })).toBeInTheDocument()
})
it('submits the self-service registration payload and shows the success state', async () => {
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields()
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
await waitFor(() =>
expect(registerMock).toHaveBeenCalledWith({
username: 'new-user',
password: 'SecurePass123!',
nickname: 'New User',
email: 'new-user@example.com',
phone: undefined,
phone_code: undefined,
}),
)
expect(await screen.findByRole('button', { name: TEXT.returnLogin })).toBeInTheDocument()
expect(screen.getByText(TEXT.activeSummary)).toBeInTheDocument()
})
it('submits sms-backed registration payload with trimmed username, nickname, and phone code', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
sms_code: true,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.phonePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields({
username: ' new-user ',
nickname: ' New User ',
email: 'new-user@example.com',
})
fireEvent.change(screen.getByPlaceholderText(TEXT.phonePlaceholder), {
target: { value: '13812345678' },
})
fireEvent.change(screen.getAllByRole('textbox')[4], {
target: { value: ' 654321 ' },
})
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
await waitFor(() =>
expect(registerMock).toHaveBeenCalledWith({
username: 'new-user',
password: 'SecurePass123!',
nickname: 'New User',
email: 'new-user@example.com',
phone: '13812345678',
phone_code: '654321',
}),
)
})
it('surfaces self-service registration failures from the backend', async () => {
registerMock.mockRejectedValue(new Error('register failed'))
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields()
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
await waitFor(() => expect(message.error).toHaveBeenCalledWith('register failed'))
expect(screen.getByRole('button', { name: TEXT.createAccount })).toBeInTheDocument()
})
it('shows a resend-activation entry for newly created inactive email accounts', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
email_activation: true,
})
registerMock.mockResolvedValue({
id: 3,
username: 'inactive-user',
email: 'inactive-user@example.com',
phone: '',
nickname: 'Inactive User',
avatar: '',
status: 0,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields({
username: 'inactive-user',
nickname: '',
email: 'inactive-user@example.com',
})
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
const resendLink = await screen.findByRole('link', { name: TEXT.resendActivation })
expect(resendLink).toHaveAttribute('href', '/activate-account?email=inactive-user%40example.com')
})
it('shows the generic activation summary when the new inactive account has no email address', async () => {
registerMock.mockResolvedValue({
id: 4,
username: 'inactive-without-email',
email: '',
phone: '',
nickname: '',
avatar: '',
status: 0,
})
renderRegisterPage()
await waitFor(() => expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument())
fillBaseRegistrationFields({
username: 'inactive-without-email',
nickname: '',
email: '',
})
fireEvent.click(screen.getByRole('button', { name: TEXT.createAccount }))
expect(await screen.findByText(TEXT.inactiveSummaryNoEmail)).toBeInTheDocument()
expect(screen.queryByRole('link', { name: TEXT.resendActivation })).not.toBeInTheDocument()
})
it('shows an admin bootstrap entry when the system still has no active admin', async () => {
getAuthCapabilitiesMock.mockResolvedValue({
...defaultCapabilities,
admin_bootstrap_required: true,
})
renderRegisterPage()
expect(await screen.findByText(TEXT.adminBootstrapTitle)).toBeInTheDocument()
expect(screen.getByRole('button', { name: TEXT.adminBootstrapAction }).closest('a')).toHaveAttribute('href', '/bootstrap-admin')
})
})