Ratelimit handling for validate and password reset endpoints.
* This patch implements ratelimit handling code for validate token and password reset end points. * Adds graceful error messages for such case VAN-312
This commit is contained in:
@@ -3,18 +3,38 @@ import { Alert } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from './messages';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
|
||||
const APIFailureMessage = (props) => {
|
||||
const { intl, header } = props;
|
||||
const { intl, header, errorCode } = props;
|
||||
let errorMessage = null;
|
||||
let id = null;
|
||||
|
||||
switch (errorCode) {
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
id = INTERNAL_SERVER_ERROR;
|
||||
errorMessage = intl.formatMessage(messages['internal.server.error.message']);
|
||||
break;
|
||||
case API_RATELIMIT_ERROR:
|
||||
id = API_RATELIMIT_ERROR;
|
||||
errorMessage = intl.formatMessage(messages['server.ratelimit.error.message']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column mw-500">
|
||||
<Alert id="internal-server-error" variant="danger">
|
||||
<Alert id={id} variant="danger">
|
||||
<Alert.Heading>
|
||||
{header}
|
||||
</Alert.Heading>
|
||||
{intl.formatMessage(messages['internal.server.error.message'])}
|
||||
<ul>
|
||||
<li key={errorMessage}>
|
||||
{errorMessage}
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,6 +44,7 @@ const APIFailureMessage = (props) => {
|
||||
APIFailureMessage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
errorCode: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(APIFailureMessage);
|
||||
|
||||
@@ -28,6 +28,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'server.ratelimit.error.message': {
|
||||
id: 'server.ratelimit.error.message',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
// enterprise sso strings
|
||||
'enterprisetpa.title.heading': {
|
||||
id: 'enterprisetpa.title.heading',
|
||||
|
||||
@@ -12,6 +12,7 @@ export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft
|
||||
|
||||
// Error Codes
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
||||
|
||||
// States
|
||||
export const DEFAULT_STATE = 'default';
|
||||
|
||||
@@ -46,7 +46,7 @@ const ForgotPasswordPage = (props) => {
|
||||
);
|
||||
}
|
||||
if (status === INTERNAL_SERVER_ERROR) {
|
||||
return <APIFailureMessage header={header} />;
|
||||
return <APIFailureMessage header={header} errorCode={INTERNAL_SERVER_ERROR} />;
|
||||
}
|
||||
return status === 'forbidden' ? <RequestInProgressAlert /> : null;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
APIFailureMessage,
|
||||
} from '../common-components';
|
||||
import Spinner from './Spinner';
|
||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
@@ -34,7 +34,9 @@ const ResetPasswordPage = (props) => {
|
||||
const [bannerErrorMessage, setbannerErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (props.status === 'failure' && props.errors !== INTERNAL_SERVER_ERROR) {
|
||||
if (props.status === 'failure'
|
||||
&& props.errors !== INTERNAL_SERVER_ERROR
|
||||
&& props.errors !== API_RATELIMIT_ERROR) {
|
||||
setbannerErrorMessage(props.errors);
|
||||
setvalidationMessage(props.errors);
|
||||
setPasswordValidValue(false);
|
||||
@@ -105,7 +107,11 @@ const ResetPasswordPage = (props) => {
|
||||
}
|
||||
} else if (props.status === 'invalid' && props.errors === INTERNAL_SERVER_ERROR) {
|
||||
return (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.token.validation.sever.error'])} />
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.token.validation.sever.error'])} errorCode={INTERNAL_SERVER_ERROR} />
|
||||
);
|
||||
} else if (props.status === 'invalid' && props.errors === API_RATELIMIT_ERROR) {
|
||||
return (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.server.ratelimit.error'])} errorCode={API_RATELIMIT_ERROR} />
|
||||
);
|
||||
} else if (props.status === 'invalid') {
|
||||
return <InvalidTokenMessage />;
|
||||
@@ -116,7 +122,10 @@ const ResetPasswordPage = (props) => {
|
||||
<>
|
||||
|
||||
{props.status === 'failure' && props.errors === INTERNAL_SERVER_ERROR ? (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.request.server.error'])} />
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.request.server.error'])} errorCode={INTERNAL_SERVER_ERROR} />
|
||||
) : null}
|
||||
{props.status === 'failure' && props.errors === API_RATELIMIT_ERROR ? (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.server.ratelimit.error'])} errorCode={API_RATELIMIT_ERROR} />
|
||||
) : null}
|
||||
<div id="main" className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column mw-500">
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from './actions';
|
||||
|
||||
import { validateToken, resetPassword } from './service';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
import { INTERNAL_SERVER_ERROR, API_RATELIMIT_ERROR } from '../../data/constants';
|
||||
|
||||
// Services
|
||||
export function* handleValidateToken(action) {
|
||||
@@ -28,7 +28,13 @@ export function* handleValidateToken(action) {
|
||||
yield put(validateTokenFailure(isValid));
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(validateTokenFailure(INTERNAL_SERVER_ERROR));
|
||||
const statusCodes = [429];
|
||||
if (err.response && statusCodes.includes(err.response.status)) {
|
||||
yield put(validateTokenFailure(API_RATELIMIT_ERROR));
|
||||
} else {
|
||||
yield put(validateTokenFailure(INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +52,12 @@ export function* handleResetPassword(action) {
|
||||
yield put(resetPasswordFailure(resetErrors));
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(resetPasswordFailure(INTERNAL_SERVER_ERROR));
|
||||
const statusCodes = [429];
|
||||
if (err.response && statusCodes.includes(err.response.status)) {
|
||||
yield put(resetPasswordFailure(API_RATELIMIT_ERROR));
|
||||
} else {
|
||||
yield put(resetPasswordFailure(INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { handleResetPassword, handleValidateToken } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../../data/constants';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../../../data/constants';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
@@ -51,9 +51,17 @@ describe('handleResetPassword', () => {
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
|
||||
it('should call service and dispatch error action', async () => {
|
||||
it('should call service and dispatch internal server error action', async () => {
|
||||
const errorResponse = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
const resetPassword = jest.spyOn(api, 'resetPassword')
|
||||
.mockImplementation(() => Promise.reject());
|
||||
.mockImplementation(() => Promise.reject(errorResponse));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
@@ -66,6 +74,30 @@ describe('handleResetPassword', () => {
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(INTERNAL_SERVER_ERROR)]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
|
||||
it('should call service and dispatch ratelimit error', async () => {
|
||||
const errorResponse = {
|
||||
response: {
|
||||
status: 429,
|
||||
data: {
|
||||
errorCode: API_RATELIMIT_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
const resetPassword = jest.spyOn(api, 'resetPassword')
|
||||
.mockImplementation(() => Promise.reject(errorResponse));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
{ dispatch: (action) => dispatched.push(action) },
|
||||
handleResetPassword,
|
||||
params,
|
||||
);
|
||||
|
||||
expect(resetPassword).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(API_RATELIMIT_ERROR)]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleValidateToken', () => {
|
||||
@@ -80,9 +112,17 @@ describe('handleValidateToken', () => {
|
||||
loggingService.logError.mockReset();
|
||||
});
|
||||
|
||||
it('check server error on api failure', async () => {
|
||||
it('check internal server error on api failure', async () => {
|
||||
const errorResponse = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
const validateToken = jest.spyOn(api, 'validateToken')
|
||||
.mockImplementation(() => Promise.reject());
|
||||
.mockImplementation(() => Promise.reject(errorResponse));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
@@ -93,5 +133,30 @@ describe('handleValidateToken', () => {
|
||||
|
||||
expect(validateToken).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), validateTokenFailure(INTERNAL_SERVER_ERROR)]);
|
||||
validateToken.mockClear();
|
||||
});
|
||||
|
||||
it('should call service and dispatch ratelimit error', async () => {
|
||||
const errorResponse = {
|
||||
response: {
|
||||
status: 429,
|
||||
data: {
|
||||
errorCode: API_RATELIMIT_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
const validateToken = jest.spyOn(api, 'validateToken')
|
||||
.mockImplementation(() => Promise.reject(errorResponse));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
{ dispatch: (action) => dispatched.push(action) },
|
||||
handleValidateToken,
|
||||
params,
|
||||
);
|
||||
|
||||
expect(validateToken).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), validateTokenFailure(API_RATELIMIT_ERROR)]);
|
||||
validateToken.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,6 +71,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Token validation failure',
|
||||
description: 'Failed to validate reset password token error message.',
|
||||
},
|
||||
'reset.server.ratelimit.error': {
|
||||
id: 'reset.server.ratelimit.error',
|
||||
defaultMessage: 'Too many requests.',
|
||||
description: 'Too many request at server end point',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { resetPassword } from '../data/actions';
|
||||
import { APIFailureMessage } from '../../common-components';
|
||||
|
||||
import ResetPasswordPage from '../ResetPasswordPage';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
@@ -268,4 +268,24 @@ describe('ResetPasswordPage', () => {
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('check failure banner rendered on validate token api ratelimit', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'invalid',
|
||||
errors: API_RATELIMIT_ERROR,
|
||||
};
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('check failure banner rendered on reset password api ratelimit', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'failure',
|
||||
errors: API_RATELIMIT_ERROR,
|
||||
};
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user