fix: render registration errors as text
This commit is contained in:
@@ -470,6 +470,135 @@ describe('Login surface', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('renders structured user registration errors as readable text', async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
id: 'captcha-test',
|
||||
image: 'data:image/png;base64,test',
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({
|
||||
error: {
|
||||
message: '图像验证码错误,请重新输入',
|
||||
type: 'Bad Request',
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
id: 'captcha-next',
|
||||
image: 'data:image/png;base64,next',
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
let root!: ReactTestRenderer;
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<UserRegister />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.root.findByProps({ id: 'reg-username-input' }).props.onChange({ target: { value: 'demo' } });
|
||||
root.root.findByProps({ id: 'reg-email-input' }).props.onChange({ target: { value: 'demo@example.com' } });
|
||||
root.root.findByProps({ id: 'reg-password-input' }).props.onChange({ target: { value: 'secret1' } });
|
||||
root.root.findByProps({ id: 'reg-confirm-password-input' }).props.onChange({ target: { value: 'secret1' } });
|
||||
root.root.findByProps({ id: 'reg-captcha-input' }).props.onChange({ target: { value: 'wrong' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await root.root.findByType('form').props.onSubmit({ preventDefault: vi.fn() });
|
||||
});
|
||||
|
||||
const pageText = collectText(root.root);
|
||||
expect(pageText).toContain('图像验证码错误,请重新输入');
|
||||
expect(pageText).not.toContain('[object Object]');
|
||||
expect(pageText).not.toContain('Object');
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/users/register', expect.objectContaining({
|
||||
method: 'POST',
|
||||
}));
|
||||
} finally {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('does not render object-shaped registration exceptions as object text', async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
id: 'captcha-test',
|
||||
image: 'data:image/png;base64,test',
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockRejectedValueOnce({
|
||||
error: {
|
||||
message: '注册请求过于频繁,请稍后再试。',
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
id: 'captcha-next',
|
||||
image: 'data:image/png;base64,next',
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
let root!: ReactTestRenderer;
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<UserRegister />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.root.findByProps({ id: 'reg-username-input' }).props.onChange({ target: { value: 'demo' } });
|
||||
root.root.findByProps({ id: 'reg-email-input' }).props.onChange({ target: { value: 'demo@example.com' } });
|
||||
root.root.findByProps({ id: 'reg-password-input' }).props.onChange({ target: { value: 'secret1' } });
|
||||
root.root.findByProps({ id: 'reg-confirm-password-input' }).props.onChange({ target: { value: 'secret1' } });
|
||||
root.root.findByProps({ id: 'reg-captcha-input' }).props.onChange({ target: { value: 'wrong' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await root.root.findByType('form').props.onSubmit({ preventDefault: vi.fn() });
|
||||
});
|
||||
|
||||
const pageText = collectText(root.root);
|
||||
expect(pageText).toContain('注册请求过于频繁,请稍后再试。');
|
||||
expect(pageText).not.toContain('[object Object]');
|
||||
} finally {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the local WeChat OAuth mark unclipped in CSS', () => {
|
||||
const css = readFileSync(resolve(process.cwd(), 'src/web/index.css'), 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
|
||||
@@ -4,6 +4,51 @@ import { commitSession } from '../authSession.js';
|
||||
import LoginShell from './auth/LoginShell.js';
|
||||
import OAuthSlot from './auth/OAuthSlot.js';
|
||||
|
||||
function isObjectObjectText(value: string): boolean {
|
||||
return value.trim() === '[object Object]' || value.trim() === 'Object';
|
||||
}
|
||||
|
||||
function readableObjectSummary(value: Record<string, unknown>): string {
|
||||
try {
|
||||
const text = JSON.stringify(value);
|
||||
return text && !isObjectObjectText(text) ? text.slice(0, 160) : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRegisterErrorCandidate(value: unknown): string {
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim();
|
||||
return text && !isObjectObjectText(text) ? text : '';
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const key of ['message', 'error', 'detail', 'reason', 'msg']) {
|
||||
const nested = resolveRegisterErrorCandidate(record[key]);
|
||||
if (nested) return nested;
|
||||
}
|
||||
return readableObjectSummary(record);
|
||||
}
|
||||
|
||||
function resolveRegisterErrorMessage(payload: any, fallback: string): string {
|
||||
const candidates = [
|
||||
payload?.message,
|
||||
payload?.error?.message,
|
||||
payload?.error,
|
||||
payload?.detail?.message,
|
||||
payload?.detail,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const message = resolveRegisterErrorCandidate(candidate);
|
||||
if (message) return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -87,14 +132,14 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data?.error || `注册失败 (${res.status})`);
|
||||
throw new Error(resolveRegisterErrorMessage(data, `注册失败 (${res.status})`));
|
||||
}
|
||||
const result = await res.json();
|
||||
commitSession('user', result.token, undefined, localStorage);
|
||||
onLogin?.();
|
||||
navigate('/user/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '注册失败');
|
||||
setError(resolveRegisterErrorMessage(err, '注册失败'));
|
||||
void loadCaptcha();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
Reference in New Issue
Block a user