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', () => {
|
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');
|
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 LoginShell from './auth/LoginShell.js';
|
||||||
import OAuthSlot from './auth/OAuthSlot.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 }) {
|
export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@@ -87,14 +132,14 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
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();
|
const result = await res.json();
|
||||||
commitSession('user', result.token, undefined, localStorage);
|
commitSession('user', result.token, undefined, localStorage);
|
||||||
onLogin?.();
|
onLogin?.();
|
||||||
navigate('/user/dashboard');
|
navigate('/user/dashboard');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || '注册失败');
|
setError(resolveRegisterErrorMessage(err, '注册失败'));
|
||||||
void loadCaptcha();
|
void loadCaptcha();
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user