From c80cf4fa8175077de697d240ae8d19759e399c93 Mon Sep 17 00:00:00 2001 From: adeelehsan Date: Fri, 6 Nov 2020 21:18:20 +0500 Subject: [PATCH] Added saml integration for login and registration --- src/logistration/InstitutionLogistration.jsx | 103 ++++++++++++++++++ src/logistration/LoginHelpLinks.jsx | 2 +- src/logistration/LoginHelpLinks.messages.jsx | 32 ------ src/logistration/LoginPage.jsx | 35 +++++- src/logistration/RegistrationPage.jsx | 65 ++++++++++- src/logistration/data/service.js | 1 - src/logistration/messages.jsx | 77 +++++++++++++ src/logistration/tests/LoginPage.test.jsx | 53 +++++++-- .../tests/RegistrationPage.test.jsx | 43 +++++++- .../__snapshots__/LoginPage.test.jsx.snap | 85 ++++++++++----- .../RegistrationPage.test.jsx.snap | 4 +- 11 files changed, 420 insertions(+), 80 deletions(-) create mode 100644 src/logistration/InstitutionLogistration.jsx delete mode 100644 src/logistration/LoginHelpLinks.messages.jsx create mode 100644 src/logistration/messages.jsx diff --git a/src/logistration/InstitutionLogistration.jsx b/src/logistration/InstitutionLogistration.jsx new file mode 100644 index 00000000..455e9abc --- /dev/null +++ b/src/logistration/InstitutionLogistration.jsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { getConfig } from '@edx/frontend-platform'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import messages from './messages'; + +export const RenderInstitutionButton = props => { + const { onSubmitHandler, secondaryProviders, buttonTitle } = props; + if (secondaryProviders !== undefined && secondaryProviders.length > 0) { + return ( + + ); + } + return <>; +}; + + +const InstitutionLogistration = props => { + const lmsBaseUrl = getConfig().LMS_BASE_URL; + const { + intl, + onSubmitHandler, + secondaryProviders, + headingTitle, + buttonTitle, + } = props; + + return ( + <> +
+
+

+ {headingTitle} +

+
+

+ {intl.formatMessage(messages['logistration.institution.login.page.sub.heading'])} +

+
+ +
+
+
+

or

+
+ +
+
+ + ); +}; + +const LogistrationDefaultProps = { + secondaryProviders: [], + buttonTitle: '', +}; +const LogistrationProps = { + onSubmitHandler: PropTypes.func.isRequired, + secondaryProviders: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequried, + loginUrl: PropTypes.string.isRequired, + })), + buttonTitle: PropTypes.string, +}; + +RenderInstitutionButton.propTypes = { + ...LogistrationProps, +}; +RenderInstitutionButton.defaultProps = { + ...LogistrationDefaultProps, +}; + +InstitutionLogistration.propTypes = { + ...LogistrationProps, + intl: intlShape.isRequired, + headingTitle: PropTypes.string, +}; +InstitutionLogistration.defaultProps = { + ...LogistrationDefaultProps, + headingTitle: '', +}; + +export default injectIntl(InstitutionLogistration); diff --git a/src/logistration/LoginHelpLinks.jsx b/src/logistration/LoginHelpLinks.jsx index 6ed4a61b..12d18e61 100644 --- a/src/logistration/LoginHelpLinks.jsx +++ b/src/logistration/LoginHelpLinks.jsx @@ -5,8 +5,8 @@ import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import SwitchContent from './SwitchContent'; -import messages from './LoginHelpLinks.messages'; import { REGISTER_PAGE, RESET_PAGE } from '../data/constants'; +import messages from './messages'; const LoginHelpLinks = (props) => { const { intl, page } = props; diff --git a/src/logistration/LoginHelpLinks.messages.jsx b/src/logistration/LoginHelpLinks.messages.jsx deleted file mode 100644 index 8d245101..00000000 --- a/src/logistration/LoginHelpLinks.messages.jsx +++ /dev/null @@ -1,32 +0,0 @@ - -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'logistration.need.help.signing.in.collapsible.menu': { - id: 'logistration.need.help.signing.in.collapsible.menu', - defaultMessage: 'Need help signing in?', - description: 'A button for collapsible need help signing in menu on login page', - }, - 'logistration.need.other.help.signing.in.collapsible.menu': { - id: 'logistration.need.other.help.signing.in.collapsible.menu', - defaultMessage: 'Need other help signing in?', - description: 'A button for collapsible need other help signing in menu on forgot password page', - }, - 'logistration.register.link': { - id: 'logistration.register.link', - defaultMessage: 'Create an account', - description: 'Register page link', - }, - 'logistration.forgot.password.link': { - id: 'logistration.forgot.password.link', - defaultMessage: 'Forgot my password', - description: 'Forgot password link', - }, - 'logistration.other.sign.in.issues': { - id: 'logistration.other.sign.in.issues', - defaultMessage: 'Other sign-in issues', - description: 'A link that redirects to sign-in issues help', - }, -}); - -export default messages; diff --git a/src/logistration/LoginPage.jsx b/src/logistration/LoginPage.jsx index 2ca65c50..2da45495 100644 --- a/src/logistration/LoginPage.jsx +++ b/src/logistration/LoginPage.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { Button, Input, ValidationFormGroup } from '@edx/paragon'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { forgotPasswordResultSelector } from '../forgot-password'; import ConfirmationAlert from './ConfirmationAlert'; @@ -14,7 +15,8 @@ import LoginFailureMessage from './LoginFailure'; import RedirectLogistration from './RedirectLogistration'; import SocialAuthProviders from './SocialAuthProviders'; import ThirdPartyAuthAlert from './ThirdPartyAuthAlert'; - +import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration'; +import messages from './messages'; class LoginPage extends React.Component { constructor(props, context) { @@ -30,6 +32,7 @@ class LoginPage extends React.Component { emailValid: false, passwordValid: false, formValid: false, + institutionLogin: false, }; } @@ -41,6 +44,10 @@ class LoginPage extends React.Component { this.props.getThirdPartyAuthContext(payload); } + handleInstitutionLogin = () => { + this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin })); + } + handleSubmit = (e) => { e.preventDefault(); const params = (new URL(document.location)).searchParams; @@ -104,8 +111,18 @@ class LoginPage extends React.Component { } render() { + const { intl } = this.props; const { currentProvider, finishAuthUrl, providers } = this.props.thirdPartyAuthContext; - + if (this.state.institutionLogin) { + return ( + + ); + } return ( <> Create an Account.

+

{intl.formatMessage(messages['logistration.login.institution.login.sign.in'])}

+ +
+

{intl.formatMessage(messages['logistration.login.institution.login.sign.in.with'])}

+
-

Sign In

-
{ + this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin })); + } + handleSubmit = (e) => { e.preventDefault(); const params = (new URL(document.location)).searchParams; @@ -142,6 +163,16 @@ class RegistrationPage extends React.Component { } render() { + if (this.state.institutionLogin) { + return ( + + ); + } return ( <> Create an account using - + + or create a new one here
@@ -267,11 +303,14 @@ RegistrationPage.defaultProps = { registrationResult: null, registerNewUser: null, registrationError: null, + thirdPartyAuthContext: {}, }; RegistrationPage.propTypes = { + intl: intlShape.isRequired, registerNewUser: PropTypes.func, + getThirdPartyAuthContext: PropTypes.func.isRequired, registrationResult: PropTypes.shape({ redirectUrl: PropTypes.string, success: PropTypes.bool, @@ -280,12 +319,27 @@ RegistrationPage.propTypes = { email: PropTypes.array, username: PropTypes.array, }), + thirdPartyAuthContext: PropTypes.shape({ + currentProvider: PropTypes.string, + providers: PropTypes.array, + secondaryProviders: PropTypes.array, + finishAuthUrl: PropTypes.string, + pipelineUserDetails: PropTypes.shape({ + email: PropTypes.string, + fullname: PropTypes.string, + firstName: PropTypes.string, + lastName: PropTypes.string, + username: PropTypes.string, + }), + }), }; const mapStateToProps = state => { const registrationResult = registrationRequestSelector(state); + const thirdPartyAuthContext = thirdPartyAuthContextSelector(state); return { registrationResult, + thirdPartyAuthContext, registrationError: state.logistration.registrationError, }; }; @@ -293,6 +347,7 @@ const mapStateToProps = state => { export default connect( mapStateToProps, { + getThirdPartyAuthContext, registerNewUser, }, -)(RegistrationPage); +)(injectIntl(RegistrationPage)); diff --git a/src/logistration/data/service.js b/src/logistration/data/service.js index 715fd740..ec30170e 100644 --- a/src/logistration/data/service.js +++ b/src/logistration/data/service.js @@ -62,7 +62,6 @@ export async function getThirdPartyAuthContext(urlParams) { .catch((e) => { throw (e); }); - return { thirdPartyAuthContext: camelCaseObject(data), }; diff --git a/src/logistration/messages.jsx b/src/logistration/messages.jsx new file mode 100644 index 00000000..7e5905eb --- /dev/null +++ b/src/logistration/messages.jsx @@ -0,0 +1,77 @@ + +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'logistration.need.help.signing.in.collapsible.menu': { + id: 'logistration.need.help.signing.in.collapsible.menu', + defaultMessage: 'Need help signing in?', + description: 'A button for collapsible need help signing in menu on login page', + }, + 'logistration.need.other.help.signing.in.collapsible.menu': { + id: 'logistration.need.other.help.signing.in.collapsible.menu', + defaultMessage: 'Need other help signing in?', + description: 'A button for collapsible need other help signing in menu on forgot password page', + }, + 'logistration.register.link': { + id: 'logistration.register.link', + defaultMessage: 'Create an account', + description: 'Register page link', + }, + 'logistration.forgot.password.link': { + id: 'logistration.forgot.password.link', + defaultMessage: 'Forgot password?', + description: 'Forgot password link', + }, + 'logistration.other.sign.in.issues': { + id: 'logistration.other.sign.in.issues', + defaultMessage: 'Other sign-in issues', + description: 'A link that redirects to sign-in issues help', + }, + 'logistration.login.institution.login.button': { + id: 'logistration.login.institution.login.button', + defaultMessage: 'Use my university info', + description: 'shows institutions list', + }, + 'logistration.login.institution.login.page.title': { + id: 'logistration.login.institution.login.page.title', + defaultMessage: 'Sign in with Institution/Campus Credentials', + description: 'Heading of institution page', + }, + 'logistration.institution.login.page.sub.heading': { + id: 'logistration.institution.login.page.sub.heading', + defaultMessage: 'Choose your institution from the list below:', + description: 'Heading of the institutions list', + }, + 'logistration.login.institution.login.page.back.button': { + id: 'logistration.login.institution.login.page.back.button', + defaultMessage: 'Back', + description: 'return to login page', + }, + 'logistration.register.institution.login.button': { + id: 'logistration.register.institution.login.button', + defaultMessage: 'Use my institution/campus credentials', + description: 'shows institutions list', + }, + 'logistration.register.institution.login.page.title': { + id: 'logistration.register.institution.login.page.title', + defaultMessage: 'Register with Institution/Campus Credentials', + description: 'Heading of institution page', + }, + 'logistration.register.institution.login.page.back.button': { + id: 'logistration.register.institution.login.page.back.button', + defaultMessage: 'Create an Account', + description: 'return to login page', + }, + 'logistration.login.institution.login.sign.in': { + id: 'logistration.login.institution.login.sign.in', + defaultMessage: 'Sign In', + description: 'Sign In text', + }, + '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 ', + }, +}); + +export default messages; diff --git a/src/logistration/tests/LoginPage.test.jsx b/src/logistration/tests/LoginPage.test.jsx index bacca972..30b26626 100644 --- a/src/logistration/tests/LoginPage.test.jsx +++ b/src/logistration/tests/LoginPage.test.jsx @@ -6,8 +6,8 @@ import configureStore from 'redux-mock-store'; import { getConfig } from '@edx/frontend-platform'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; - import LoginPage from '../LoginPage'; +import { RenderInstitutionButton } from '../InstitutionLogistration'; const IntlLoginPage = injectIntl(LoginPage); const mockStore = configureStore(); @@ -22,6 +22,7 @@ describe('LoginPage', () => { currentProvider: null, finishAuthUrl: null, providers: [], + secondaryProviders: [], }, }, }; @@ -29,6 +30,13 @@ describe('LoginPage', () => { let props = {}; let store = {}; + const secondaryProviders = { + id: 'saml-test', + name: 'Test University', + loginUrl: '/dummy-auth', + registerUrl: '/dummy_auth', + }; + const appleProvider = { id: 'oa2-apple-id', name: 'Apple', @@ -169,13 +177,6 @@ describe('LoginPage', () => { expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); }); - it('should call the componentDidMount lifecycle method', () => { - const spy = jest.spyOn(LoginPage.WrappedComponent.prototype, 'componentDidMount'); - - mount(reduxWrapper()); - expect(spy).toHaveBeenCalled(); - }); - it('should redirect to social auth provider url', () => { const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard'; store = mockStore({ @@ -219,4 +220,40 @@ describe('LoginPage', () => { const loginPage = mount(reduxWrapper()); expect(loginPage.find('#tpa-alert').find('span').text()).toEqual(expectedMessage); }); + + it('should display institution login button', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + const root = mount(reduxWrapper()); + expect(root.text().includes('Use my university info')).toBe(true); + }); + + it('should not display institution login button', () => { + const root = mount(reduxWrapper()); + expect(root.text().includes('Use my university info')).toBe(false); + }); + + it('should display institution login page', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + const loginPage = mount(reduxWrapper()); + loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); + expect(loginPage.text().includes('Test University')).toBe(true); + }); }); diff --git a/src/logistration/tests/RegistrationPage.test.jsx b/src/logistration/tests/RegistrationPage.test.jsx index f7ec2ca3..222dc1ec 100644 --- a/src/logistration/tests/RegistrationPage.test.jsx +++ b/src/logistration/tests/RegistrationPage.test.jsx @@ -2,9 +2,11 @@ import React from 'react'; import { Provider } from 'react-redux'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; +import { mount } from 'enzyme'; import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n'; import RegistrationPage from '../RegistrationPage'; +import { RenderInstitutionButton } from '../InstitutionLogistration'; const IntlRegistrationPage = injectIntl(RegistrationPage); const mockStore = configureStore(); @@ -14,12 +16,20 @@ describe('./RegistrationPage.js', () => { const initialState = { logistration: { registrationResult: { success: false, redirectUrl: '' }, + thirdPartyAuthContext: { secondaryProviders: [] }, }, }; let props = {}; let store = {}; + const secondaryProviders = { + id: 'saml-test', + name: 'Test University', + loginUrl: '/dummy-auth', + registerUrl: '/dummy_auth', + }; + const reduxWrapper = children => ( {children} @@ -59,7 +69,7 @@ describe('./RegistrationPage.js', () => { store = mockStore({ ...store, logistration: { - ...store.logistration, + ...initialState.logistration, registrationResult: { success: true, redirectUrl: dasboardUrl, @@ -72,6 +82,37 @@ describe('./RegistrationPage.js', () => { expect(window.location.href).toBe(dasboardUrl); }); + it('should display institution register button', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + const root = mount(reduxWrapper()); + expect(root.text().includes('Use my institution/campus credentials')).toBe(true); + }); + + it('should not display institution register button', () => { + store = mockStore({ + ...initialState, + logistration: { + ...initialState.logistration, + thirdPartyAuthContext: { + ...initialState.logistration.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + }, + }); + const root = mount(reduxWrapper()); + root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true }); + expect(root.text().includes('Test University')).toBe(true); + }); + it('should show error message on 409', () => { const windowSpy = jest.spyOn(global, 'window', 'get'); windowSpy.mockImplementation(() => ({ diff --git a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap index 456251ca..e2d70143 100644 --- a/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap +++ b/src/logistration/tests/__snapshots__/LoginPage.test.jsx.snap @@ -25,17 +25,24 @@ exports[`LoginPage should match TPA provider snapshot 1`] = `

+

+ Sign In +

+
+

+ or sign in with +

+
-

- Sign In -

@@ -223,17 +230,24 @@ exports[`LoginPage should match default section snapshot 1`] = `

+

+ Sign In +

+
+

+ or sign in with +

+
-

- Sign In -

@@ -387,17 +401,24 @@ exports[`LoginPage should match forget password alert message snapshot 1`] = `

+

+ Sign In +

+
+

+ or sign in with +

+
-

- Sign In -

@@ -574,17 +595,24 @@ exports[`LoginPage should show error message on 400 1`] = `

+

+ Sign In +

+
+

+ or sign in with +

+
-

- Sign In -

@@ -769,17 +797,24 @@ exports[`LoginPage should show error message on 400 on receiving link 1`] = `

+

+ Sign In +

+
+

+ or sign in with +

+
-

- Sign In -

diff --git a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap index 96815537..a6e563d4 100644 --- a/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap +++ b/src/logistration/tests/__snapshots__/RegistrationPage.test.jsx.snap @@ -90,7 +90,7 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = ` Google