From 84bb6edf90c1b24a963518a869f7b018527c32cd Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Thu, 3 Dec 2020 15:55:48 +0500 Subject: [PATCH] Add password compliance workflow (#40) --- src/logistration/LoginFailure.jsx | 70 +++++++++++++------ src/logistration/LoginPage.jsx | 14 ++-- src/logistration/_style.scss | 3 +- src/logistration/data/constants.js | 2 + src/logistration/data/sagas.js | 4 +- src/logistration/data/selectors.js | 5 ++ src/logistration/messages.jsx | 7 +- src/logistration/tests/LoginFailure.test.jsx | 41 +++++++++++ src/logistration/tests/LoginPage.test.jsx | 4 +- .../__snapshots__/LoginFailure.test.jsx.snap | 60 ++++++++++++++++ 10 files changed, 181 insertions(+), 29 deletions(-) create mode 100644 src/logistration/data/constants.js create mode 100644 src/logistration/tests/LoginFailure.test.jsx create mode 100644 src/logistration/tests/__snapshots__/LoginFailure.test.jsx.snap diff --git a/src/logistration/LoginFailure.jsx b/src/logistration/LoginFailure.jsx index f1d4d393..03be8952 100644 --- a/src/logistration/LoginFailure.jsx +++ b/src/logistration/LoginFailure.jsx @@ -1,8 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Alert, Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import { NON_COMPLIANT_PASSWORD_EXCEPTION } from './data/constants'; +import messages from './messages'; const processLink = (link) => { let matches; @@ -13,6 +16,9 @@ const processLink = (link) => { }; const LoginFailureMessage = (props) => { + const errorMessage = props.errors; + const { errorCode, intl } = props; + const Link = (args) => ( <> {args.beforeLink} @@ -23,26 +29,47 @@ const LoginFailureMessage = (props) => { ); - const errorMessage = props.errors; - let errorList = errorMessage.trim().split('\n'); - errorList = errorList.map((error) => { - let matches; - if (error.includes('a href')) { - matches = processLink(error); - const [beforeLink, link, linkText, afterLink] = matches; - return ( -
  • - + {intl.formatMessage(messages['logistration.non.compliant.password.title'])}, + lineBreak:
    , + }} />
  • ); - } - return
  • {error}
  • ; - }); + break; + default: + // TODO: use errorCode instead of processing errorMessages on frontend + errorList = errorMessage.trim().split('\n'); + errorList = errorList.map((error) => { + let matches; + if (error.includes('a href')) { + matches = processLink(error); + const [beforeLink, link, linkText, afterLink] = matches; + return ( +
  • + +
  • + ); + } + return
  • {error}
  • ; + }); + } return ( @@ -62,9 +89,12 @@ const LoginFailureMessage = (props) => { LoginFailureMessage.defaultProps = { errors: '', + errorCode: null, }; LoginFailureMessage.propTypes = { errors: PropTypes.string, + errorCode: PropTypes.string, + intl: intlShape.isRequired, }; -export default LoginFailureMessage; +export default injectIntl(LoginFailureMessage); diff --git a/src/logistration/LoginPage.jsx b/src/logistration/LoginPage.jsx index 234dd93a..522b074c 100644 --- a/src/logistration/LoginPage.jsx +++ b/src/logistration/LoginPage.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import ConfirmationAlert from './ConfirmationAlert'; import { getThirdPartyAuthContext, loginRequest } from './data/actions'; -import { loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors'; +import { loginErrorSelector, loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors'; import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration'; import LoginHelpLinks from './LoginHelpLinks'; import LoginFailureMessage from './LoginFailure'; @@ -140,7 +140,9 @@ class LoginPage extends React.Component { platformName={thirdPartyAuthContext.platformName} /> )} - {this.props.loginError ? : null} + {this.props.loginError + ? + : null} {this.props.forgotPassword.status === 'complete' ? : null}

    @@ -250,7 +252,10 @@ LoginPage.propTypes = { }), getThirdPartyAuthContext: PropTypes.func.isRequired, intl: intlShape.isRequired, - loginError: PropTypes.string, + loginError: PropTypes.shape({ + value: PropTypes.string, + errorCode: PropTypes.string, + }), loginRequest: PropTypes.func.isRequired, loginResult: PropTypes.shape({ redirectUrl: PropTypes.string, @@ -270,10 +275,11 @@ const mapStateToProps = state => { const forgotPassword = forgotPasswordResultSelector(state); const loginResult = loginRequestSelector(state); const thirdPartyAuthContext = thirdPartyAuthContextSelector(state); + const loginError = loginErrorSelector(state); return { - loginError: state.logistration.loginError, submitState: state.logistration.submitState, forgotPassword, + loginError, loginResult, thirdPartyAuthContext, }; diff --git a/src/logistration/_style.scss b/src/logistration/_style.scss index e0ce7463..dee24c65 100644 --- a/src/logistration/_style.scss +++ b/src/logistration/_style.scss @@ -2,6 +2,7 @@ // #COLORS // ---------------------------- $link-blue: #23419f; +$mfe-font-color: #23419f; $font-blue: #126f9a; $white: #FFFFFF; @@ -16,7 +17,7 @@ $apple-black: #000000; $apple-focus-black: $apple-black; .font-color { - color: $font-blue; + color: $mfe-font-color; } .login-container { diff --git a/src/logistration/data/constants.js b/src/logistration/data/constants.js new file mode 100644 index 00000000..2d49df51 --- /dev/null +++ b/src/logistration/data/constants.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const NON_COMPLIANT_PASSWORD_EXCEPTION = 'NonCompliantPasswordException'; diff --git a/src/logistration/data/sagas.js b/src/logistration/data/sagas.js index fedd9b7f..f57713f7 100644 --- a/src/logistration/data/sagas.js +++ b/src/logistration/data/sagas.js @@ -1,5 +1,7 @@ import { call, put, takeEvery } from 'redux-saga/effects'; +import { camelCaseObject } from '@edx/frontend-platform'; + // Actions import { REGISTER_NEW_USER, @@ -51,7 +53,7 @@ export function* handleLoginRequest(action) { } catch (e) { const statusCodes = [400]; if (e.response && statusCodes.includes(e.response.status)) { - yield put(loginRequestFailure(e.response.data.value)); + yield put(loginRequestFailure(camelCaseObject(e.response.data))); } } } diff --git a/src/logistration/data/selectors.js b/src/logistration/data/selectors.js index 4dc7d718..13763bc1 100644 --- a/src/logistration/data/selectors.js +++ b/src/logistration/data/selectors.js @@ -9,6 +9,11 @@ export const loginRequestSelector = createSelector( logistration => logistration.loginResult, ); +export const loginErrorSelector = createSelector( + logistrationSelector, + logistration => logistration.loginError, +); + export const registrationRequestSelector = createSelector( logistrationSelector, logistration => logistration.registrationResult, diff --git a/src/logistration/messages.jsx b/src/logistration/messages.jsx index 3d442448..8b18c99a 100644 --- a/src/logistration/messages.jsx +++ b/src/logistration/messages.jsx @@ -100,7 +100,12 @@ const messages = defineMessages({ 'logistration.login.institution.login.sign.in.with': { id: 'logistration.login.institution.login.sign.in.with', defaultMessage: 'or sign in with', - description: 'gives hint about other sign options ', + description: 'gives hint about other sign options', + }, + 'logistration.non.compliant.password.title': { + id: 'logistration.non.compliant.password.title', + defaultMessage: 'We recently changed our password requirements', + description: 'A title that appears in bold before error message for non-compliant password', }, }); diff --git a/src/logistration/tests/LoginFailure.test.jsx b/src/logistration/tests/LoginFailure.test.jsx new file mode 100644 index 00000000..2135318a --- /dev/null +++ b/src/logistration/tests/LoginFailure.test.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import LoginFailureMessage from '../LoginFailure'; +import { NON_COMPLIANT_PASSWORD_EXCEPTION } from '../data/constants'; + +describe('LoginFailureMessage', () => { + let props = {}; + + beforeEach(() => { + props = { + errors: 'Some error occurred logging you in.', + }; + }); + + it('should match non compliant password error message snapshot', () => { + props = { + ...props, + errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION, + }; + + const tree = renderer.create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should match direct render of error message snapshot', () => { + const tree = renderer.create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/logistration/tests/LoginPage.test.jsx b/src/logistration/tests/LoginPage.test.jsx index 1a1c9031..44374540 100644 --- a/src/logistration/tests/LoginPage.test.jsx +++ b/src/logistration/tests/LoginPage.test.jsx @@ -109,7 +109,7 @@ describe('LoginPage', () => { ...initialState, logistration: { ...initialState.logistration, - loginError: 'Email or password is incorrect.', + loginError: { value: 'Email or password is incorrect.' }, }, }); @@ -122,7 +122,7 @@ describe('LoginPage', () => { ...initialState, logistration: { ...initialState.logistration, - loginError: 'To be on the safe side, you can reset your password here before you try again.\n', + loginError: { value: 'To be on the safe side, you can reset your password here before you try again.\n' }, }, }); diff --git a/src/logistration/tests/__snapshots__/LoginFailure.test.jsx.snap b/src/logistration/tests/__snapshots__/LoginFailure.test.jsx.snap new file mode 100644 index 00000000..ef04a704 --- /dev/null +++ b/src/logistration/tests/__snapshots__/LoginFailure.test.jsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoginFailureMessage should match direct render of error message snapshot 1`] = ` +

    +
    +

    + + We couldn't sign you in. + +

    +
      +
    • + Some error occurred logging you in. +
    • +
    +
    +
    +`; + +exports[`LoginFailureMessage should match non compliant password error message snapshot 1`] = ` +
    +
    +

    + + We couldn't sign you in. + +

    +
      +
    • + + + We recently changed our password requirements + + +
      + Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe. +
      +
    • +
    +
    +
    +`;