Public Access
feat(registration): show the wait time after too many attempts
This commit is contained in:
1 parent
2cacc88844
commit
8dcd03f219
4 files changed
+63
-3
No files matched your search
+2
-1
@@ -32,7 +32,8 @@
|
|||||||
"ERROR_INVALID_INVITE_CODE": "Der Einladungscode ist ungültig oder wurde bereits verwendet.",
|
"ERROR_INVALID_INVITE_CODE": "Der Einladungscode ist ungültig oder wurde bereits verwendet.",
|
||||||
"ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft.",
|
"ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft.",
|
||||||
"DISPLAY_NAME": "Anzeigename (optional)",
|
"DISPLAY_NAME": "Anzeigename (optional)",
|
||||||
"ERROR_DISPLAY_NAME_IN_USE": "Dieser Anzeigename ist schon vergeben."
|
"ERROR_DISPLAY_NAME_IN_USE": "Dieser Anzeigename ist schon vergeben.",
|
||||||
|
"ERROR_TOO_MANY_ATTEMPTS": "Zu viele fehlgeschlagene Versuche. Bitte in {{minutes}} Minuten erneut versuchen."
|
||||||
},
|
},
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"TITLE": "Einstellungen",
|
"TITLE": "Einstellungen",
|
||||||
|
|||||||
+2
-1
@@ -32,7 +32,8 @@
|
|||||||
"ERROR_INVALID_INVITE_CODE": "The invite code is invalid or has already been used.",
|
"ERROR_INVALID_INVITE_CODE": "The invite code is invalid or has already been used.",
|
||||||
"ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account.",
|
"ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account.",
|
||||||
"DISPLAY_NAME": "Display name (optional)",
|
"DISPLAY_NAME": "Display name (optional)",
|
||||||
"ERROR_DISPLAY_NAME_IN_USE": "This display name is already taken."
|
"ERROR_DISPLAY_NAME_IN_USE": "This display name is already taken.",
|
||||||
|
"ERROR_TOO_MANY_ATTEMPTS": "Too many failed attempts. Please try again in {{minutes}} minutes."
|
||||||
},
|
},
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"TITLE": "Settings",
|
"TITLE": "Settings",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const BACKEND_ERROR_KEY = {
|
|||||||
INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE',
|
INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE',
|
||||||
STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE',
|
STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE',
|
||||||
DISPLAY_NAME_ALREADY_IN_USE: 'REGISTRATION.ERROR_DISPLAY_NAME_IN_USE',
|
DISPLAY_NAME_ALREADY_IN_USE: 'REGISTRATION.ERROR_DISPLAY_NAME_IN_USE',
|
||||||
|
TOO_MANY_ATTEMPTS: 'REGISTRATION.ERROR_TOO_MANY_ATTEMPTS',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RegistrationPage() {
|
export default function RegistrationPage() {
|
||||||
@@ -65,8 +66,11 @@ export default function RegistrationPage() {
|
|||||||
|
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
const mappedKey = BACKEND_ERROR_KEY[data.code];
|
const mappedKey = BACKEND_ERROR_KEY[data.code];
|
||||||
|
const options = response.status === 429
|
||||||
|
? {minutes: Math.ceil(Number(response.headers.get('Retry-After') ?? 900) / 60)}
|
||||||
|
: undefined;
|
||||||
setFeedback({
|
setFeedback({
|
||||||
message: mappedKey ? t(mappedKey) : data.message || t('REGISTRATION.ERROR_GENERIC'),
|
message: mappedKey ? t(mappedKey, options) : data.message || t('REGISTRATION.ERROR_GENERIC'),
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||||
|
import {cleanup, fireEvent, render, screen} from '@testing-library/react';
|
||||||
|
import {MemoryRouter} from 'react-router-dom';
|
||||||
|
import RegistrationPage from './RegistrationPage.jsx';
|
||||||
|
|
||||||
|
// Own mock: the shared one drops the {{minutes}} value this test checks.
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key, options) => (options?.minutes === undefined ? key : `${key}:${options.minutes}`),
|
||||||
|
i18n: {language: 'de'},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function errorResponse(status, code, headers = {}) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status,
|
||||||
|
headers: new Headers(headers),
|
||||||
|
json: async () => ({code}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitValidForm() {
|
||||||
|
render(<MemoryRouter><RegistrationPage/></MemoryRouter>);
|
||||||
|
fireEvent.change(screen.getByLabelText('REGISTRATION.EMAIL'), {target: {value: 'a@b.de'}});
|
||||||
|
fireEvent.change(screen.getByLabelText('REGISTRATION.PASSWORD'), {target: {value: 'secret123'}});
|
||||||
|
fireEvent.change(screen.getByLabelText('REGISTRATION.PASSWORD_CONFIRMATION'), {target: {value: 'secret123'}});
|
||||||
|
fireEvent.change(screen.getByLabelText('REGISTRATION.INVITE_CODE'), {target: {value: 'code'}});
|
||||||
|
fireEvent.change(screen.getByLabelText('REGISTRATION.STEAM_ID'), {target: {value: '76561198000000001'}});
|
||||||
|
fireEvent.click(screen.getByRole('button', {name: 'REGISTRATION.SUBMIT'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RegistrationPage', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the wait time when the IP is blocked', async () => {
|
||||||
|
globalThis.fetch = vi.fn(async () => errorResponse(429, 'TOO_MANY_ATTEMPTS', {'Retry-After': '900'}));
|
||||||
|
|
||||||
|
submitValidForm();
|
||||||
|
|
||||||
|
expect(await screen.findByText('REGISTRATION.ERROR_TOO_MANY_ATTEMPTS:15')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps a known backend error to its message', async () => {
|
||||||
|
globalThis.fetch = vi.fn(async () => errorResponse(400, 'INVALID_INVITATION_CODE'));
|
||||||
|
|
||||||
|
submitValidForm();
|
||||||
|
|
||||||
|
expect(await screen.findByText('REGISTRATION.ERROR_INVALID_INVITE_CODE')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user