diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx index 6bc2ce5a89..8d699de6a4 100644 --- a/src/register/RegistrationPage.test.jsx +++ b/src/register/RegistrationPage.test.jsx @@ -11,19 +11,16 @@ import { mockNavigate, BrowserRouter as Router } from 'react-router-dom'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; -import RegistrationFailureMessage from './components/RegistrationFailure'; import { backupRegistrationFormBegin, clearRegistrationBackendError, registerNewUser, setUserPipelineDataLoaded, } from './data/actions'; -import { - FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED, -} from './data/constants'; +import { INTERNAL_SERVER_ERROR } from './data/constants'; import RegistrationPage from './RegistrationPage'; import { - AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, + AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, } from '../data/constants'; jest.mock('@edx/frontend-platform/analytics', () => ({ @@ -36,7 +33,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ })); const IntlRegistrationPage = injectIntl(RegistrationPage); -const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage); const mockStore = configureStore(); jest.mock('react-router-dom', () => { @@ -95,7 +91,6 @@ describe('RegistrationPage', () => { currentProvider: null, finishAuthUrl: null, providers: [], - secondaryProviders: [], pipelineUserDetails: null, countryCode: null, }; @@ -153,11 +148,7 @@ describe('RegistrationPage', () => { }; const ssoProvider = { - id: 'oa2-apple-id', name: 'Apple', - iconClass: 'apple', - iconImage: 'https://openedx.devstack.lms/logo.png', - loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard', }; describe('Test Registration Page', () => { @@ -173,10 +164,6 @@ describe('RegistrationPage', () => { country: 'Select your country or region of residence', }; - const secondaryProviders = { - id: 'saml-test', name: 'Test University', loginUrl: '/dummy-auth', registerUrl: '/dummy_auth', - }; - // ******** test registration form submission ******** it('should submit form for valid input', () => { @@ -341,81 +328,6 @@ describe('RegistrationPage', () => { expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('email')); }); - // ******** test alert messages ******** - - it('should match third party auth alert', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - currentProvider: 'Apple', - }, - }, - }); - - const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before ' - + 'you start learning with '}${ getConfig().SITE_NAME }.`; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage); - }); - - it('should match internal server error message', () => { - const expectedMessage = 'We couldn\'t create your account.An error has occurred. Try refreshing the page, or check your internet connection.'; - props = { - errorCode: INTERNAL_SERVER_ERROR, - failureCount: 0, - }; - - const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('div.alert-heading').length).toEqual(1); - expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); - }); - - it('should match registration api rate limit error message', () => { - const expectedMessage = 'We couldn\'t create your account.Too many failed registration attempts. Try again later.'; - props = { - errorCode: FORBIDDEN_REQUEST, - failureCount: 0, - }; - - const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('div.alert-heading').length).toEqual(1); - expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); - }); - - it('should match tpa session expired error message', () => { - const expectedMessage = 'We couldn\'t create your account.Registration using Google has timed out.'; - props = { - context: { - provider: 'Google', - }, - errorCode: TPA_SESSION_EXPIRED, - failureCount: 0, - }; - - const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('div.alert-heading').length).toEqual(1); - expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); - }); - - it('should match tpa authentication failed error message', () => { - const expectedMessageSubstring = 'We are sorry, you are not authorized to access'; - props = { - context: { - provider: 'Google', - }, - errorCode: TPA_AUTHENTICATION_FAILURE, - failureCount: 0, - }; - - const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('div.alert-heading').length).toEqual(1); - expect(registrationPage.find('div.alert').first().text()).toContain(expectedMessageSubstring); - }); - // ******** test form buttons and fields ******** it('should match default button state', () => { @@ -451,58 +363,6 @@ describe('RegistrationPage', () => { }); }); - it('should show single sign on provider button', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [ssoProvider], - }, - }, - }); - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1); - }); - - it('should display institution register button', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: 'true', - }); - - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], - }, - }, - }); - delete window.location; - window.location = { href: getConfig().BASE_URL }; - - const root = mount(routerWrapper(reduxWrapper())); - expect(root.text().includes('Institution/campus credentials')).toBe(true); - - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); - }); - - it('should display InstitutionLogistration if insitutionLogin prop is true', () => { - props = { - ...props, - institutionLogin: true, - }; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find('.institutions__heading').text()).toEqual('Register with institution/campus credentials'); - }); - it('should show button label based on cta query params value', () => { const buttonLabel = 'Register'; delete window.location; @@ -560,57 +420,6 @@ describe('RegistrationPage', () => { expect(window.location.href).toBe(dashboardURL); }); - it('should redirect to social auth provider url on SSO button click', () => { - const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard'; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - registerUrl, - }], - }, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL }; - - const loginPage = mount(routerWrapper(reduxWrapper())); - - loginPage.find('button#oa2-apple-id').simulate('click'); - expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl); - }); - - it('should redirect to finishAuthUrl upon successful registration via SSO', () => { - const authCompleteUrl = '/auth/complete/google-oauth2/'; - store = mockStore({ - ...initialState, - register: { - ...initialState.register, - registrationResult: { - success: true, - }, - }, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - finishAuthUrl: authCompleteUrl, - }, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL }; - - renderer.create(routerWrapper(reduxWrapper())); - expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); - }); - it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => { mergeConfig({ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true, @@ -672,112 +481,6 @@ describe('RegistrationPage', () => { expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING); }); - // ******** test hinted third party auth ******** - - it('should render tpa button for tpa_hint id matching one of the primary providers', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [ssoProvider], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name); - expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true); - }); - - it('should display skeleton if tpa_hint is true and thirdPartyAuthContext is pending', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthApiStatus: PENDING_STATE, - }, - }); - - delete window.location; - window.location = { - href: getConfig().BASE_URL.concat(LOGIN_PAGE), - search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`, - }; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find('.react-loading-skeleton').exists()).toBeTruthy(); - }); - - it('should render icon if icon classes are missing in providers', () => { - ssoProvider.iconClass = null; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [ssoProvider], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; - ssoProvider.iconImage = null; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find(`button#${ssoProvider.id}`).find('div').find('span').hasClass('pgn__icon')).toEqual(true); - }); - - it('should render tpa button for tpa_hint id matching one of the secondary providers', () => { - secondaryProviders.skipHintedLogin = true; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - secondaryProviders: [secondaryProviders], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; - - mount(routerWrapper(reduxWrapper())); - expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl); - }); - - it('should render regular tpa button for invalid tpa_hint value', () => { - const expectedMessage = `${ssoProvider.name}`; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - providers: [ssoProvider], - }, - thirdPartyAuthApiStatus: COMPLETE_STATE, - }, - }); - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(expectedMessage); - }); - // ******** miscellaneous tests ******** it('should backup the registration form state when shouldBackupState is true', () => { @@ -1068,163 +771,5 @@ describe('RegistrationPage', () => { totalRegistrationTime: 0, })); }); - - it('should display errorMessage if third party authentication fails', () => { - jest.spyOn(global.Date, 'now').mockImplementation(() => 0); - getLocale.mockImplementation(() => ('en-us')); - - store = mockStore({ - ...initialState, - register: { - ...initialState.register, - backendCountryCode: 'PK', - userPipelineDataLoaded: false, - }, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthApiStatus: COMPLETE_STATE, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - currentProvider: null, - pipelineUserDetails: {}, - errorMessage: 'An error occurred', - }, - }, - }); - - store.dispatch = jest.fn(store.dispatch); - - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find('div.alert-heading').length).toEqual(1); - expect(registrationPage.find('div.alert').first().text()).toContain('An error occurred'); - }); - }); - - describe('Test Configurable Fields', () => { - mergeConfig({ - ENABLE_DYNAMIC_REGISTRATION_FIELDS: true, - SHOW_CONFIGURABLE_EDX_FIELDS: true, - }); - - it('should render fields returned by backend', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - fieldDescriptions: { - profession: { name: 'profession', type: 'text', label: 'Profession' }, - terms_of_service: { - name: FIELDS.TERMS_OF_SERVICE, - error_message: 'You must agree to the Terms and Service agreement of our site', - }, - }, - }, - }); - const registrationPage = mount(routerWrapper(reduxWrapper())); - expect(registrationPage.find('#profession').exists()).toBeTruthy(); - expect(registrationPage.find('#tos').exists()).toBeTruthy(); - }); - - it('should submit form with fields returned by backend in payload', () => { - getLocale.mockImplementation(() => ('en-us')); - jest.spyOn(global.Date, 'now').mockImplementation(() => 0); - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - fieldDescriptions: { - profession: { name: 'profession', type: 'text', label: 'Profession' }, - }, - extendedProfile: ['profession'], - }, - }); - - const payload = { - name: 'John Doe', - username: 'john_doe', - email: 'john.doe@example.com', - password: 'password1', - country: 'Pakistan', - honor_code: true, - profession: 'Engineer', - totalRegistrationTime: 0, - }; - - store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(routerWrapper(reduxWrapper())); - - populateRequiredFields(registrationPage, payload); - registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } }); - registrationPage.find('button.btn-brand').simulate('click'); - expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); - }); - - it('should show error messages for required fields on empty form submission', () => { - const professionError = 'Enter your profession'; - const countryError = 'Select your country or region of residence'; - const confirmEmailError = 'Enter your email'; - - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - fieldDescriptions: { - profession: { - name: 'profession', type: 'text', label: 'Profession', error_message: professionError, - }, - confirm_email: { - name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError, - }, - country: { name: 'country' }, - }, - }, - }); - - const registrationPage = mount(routerWrapper(reduxWrapper())); - registrationPage.find('button.btn-brand').simulate('click'); - - expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); - expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError); - expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError); - }); - - it('should show error if email and confirm email fields do not match', () => { - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - fieldDescriptions: { - confirm_email: { - name: 'confirm_email', type: 'text', label: 'Confirm Email', - }, - }, - }, - }); - const registrationPage = mount(routerWrapper(reduxWrapper())); - registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } }); - registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test2@gmail.com', name: 'confirm_email' } }); - expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.'); - }); - - it('should run validations for configurable focused field on form submission', () => { - const professionError = 'Enter your profession'; - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - fieldDescriptions: { - profession: { - name: 'profession', type: 'text', label: 'Profession', error_message: professionError, - }, - }, - }, - }); - - const registrationPage = mount(routerWrapper(reduxWrapper())); - registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } }); - registrationPage.find('button.btn-brand').simulate('click'); - - expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); - }); }); }); diff --git a/src/register/components/ConfigurableRegistrationForm.test.jsx b/src/register/components/ConfigurableRegistrationForm.test.jsx index 282a25eb5b..eb6028f12b 100644 --- a/src/register/components/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/ConfigurableRegistrationForm.test.jsx @@ -10,7 +10,9 @@ import { BrowserRouter as Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import ConfigurableRegistrationForm from './ConfigurableRegistrationForm'; +import { registerNewUser } from '../data/actions'; import { FIELDS } from '../data/constants'; +import RegistrationPage from '../RegistrationPage'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), @@ -22,6 +24,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ })); const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm); +const IntlRegistrationPage = injectIntl(RegistrationPage); const mockStore = configureStore(); jest.mock('react-router-dom', () => { @@ -113,6 +116,8 @@ describe('ConfigurableRegistrationForm', () => { setFormFields: jest.fn(), registrationEmbedded: false, autoSubmitRegistrationForm: false, + handleInstitutionLogin: jest.fn(), + institutionLogin: false, }; window.location = { search: '' }; getLocale.mockImplementationOnce(() => ('en-us')); @@ -122,6 +127,19 @@ describe('ConfigurableRegistrationForm', () => { jest.clearAllMocks(); }); + const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => { + registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } }); + registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } }); + registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } }); + + registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } }); + registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } }); + + if (!isThirdPartyAuth) { + registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } }); + } + }; + describe('Test Configurable Fields', () => { mergeConfig({ ENABLE_DYNAMIC_REGISTRATION_FIELDS: true, @@ -182,5 +200,128 @@ describe('ConfigurableRegistrationForm', () => { [FIELDS.TERMS_OF_SERVICE]: true, }); }); + + it('should render fields returned by backend', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + profession: { name: 'profession', type: 'text', label: 'Profession' }, + terms_of_service: { + name: FIELDS.TERMS_OF_SERVICE, + error_message: 'You must agree to the Terms and Service agreement of our site', + }, + }, + }, + }); + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find('#profession').exists()).toBeTruthy(); + expect(registrationPage.find('#tos').exists()).toBeTruthy(); + }); + + it('should submit form with fields returned by backend in payload', () => { + mergeConfig({ + SHOW_CONFIGURABLE_EDX_FIELDS: true, + }); + getLocale.mockImplementation(() => ('en-us')); + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + profession: { name: 'profession', type: 'text', label: 'Profession' }, + }, + extendedProfile: ['profession'], + }, + }); + + const payload = { + name: 'John Doe', + username: 'john_doe', + email: 'john.doe@example.com', + password: 'password1', + country: 'Pakistan', + honor_code: true, + profession: 'Engineer', + totalRegistrationTime: 0, + }; + + store.dispatch = jest.fn(store.dispatch); + const registrationPage = mount(routerWrapper(reduxWrapper())); + + populateRequiredFields(registrationPage, payload); + registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } }); + registrationPage.find('button.btn-brand').simulate('click'); + expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); + }); + + it('should show error messages for required fields on empty form submission', () => { + const professionError = 'Enter your profession'; + const countryError = 'Select your country or region of residence'; + const confirmEmailError = 'Enter your email'; + + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + profession: { + name: 'profession', type: 'text', label: 'Profession', error_message: professionError, + }, + confirm_email: { + name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError, + }, + country: { name: 'country' }, + }, + }, + }); + + const registrationPage = mount(routerWrapper(reduxWrapper())); + registrationPage.find('button.btn-brand').simulate('click'); + + expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); + expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError); + expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError); + }); + it('should show error if email and confirm email fields do not match', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + confirm_email: { + name: 'confirm_email', type: 'text', label: 'Confirm Email', + }, + }, + }, + }); + const registrationPage = mount(routerWrapper(reduxWrapper())); + registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } }); + registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test2@gmail.com', name: 'confirm_email' } }); + expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.'); + }); + + it('should run validations for configurable focused field on form submission', () => { + const professionError = 'Enter your profession'; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + profession: { + name: 'profession', type: 'text', label: 'Profession', error_message: professionError, + }, + }, + }, + }); + + const registrationPage = mount(routerWrapper(reduxWrapper())); + registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } }); + registrationPage.find('button.btn-brand').simulate('click'); + + expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); + }); }); }); diff --git a/src/register/components/RegistrationFailure.test.jsx b/src/register/components/RegistrationFailure.test.jsx new file mode 100644 index 0000000000..08bd39a3df --- /dev/null +++ b/src/register/components/RegistrationFailure.test.jsx @@ -0,0 +1,207 @@ +import React from 'react'; +import { Provider } from 'react-redux'; + +import { mergeConfig } from '@edx/frontend-platform'; +import { + configure, getLocale, injectIntl, IntlProvider, +} from '@edx/frontend-platform/i18n'; +import { mount } from 'enzyme'; +import { BrowserRouter as Router } from 'react-router-dom'; +import configureStore from 'redux-mock-store'; + +import RegistrationFailureMessage from './RegistrationFailure'; +import { + FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED, +} from '../data/constants'; +import RegistrationPage from '../RegistrationPage'; + +jest.mock('@edx/frontend-platform/analytics', () => ({ + sendPageEvent: jest.fn(), + sendTrackEvent: jest.fn(), +})); +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + getLocale: jest.fn(), +})); + +const IntlRegistrationPage = injectIntl(RegistrationPage); +const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage); +const mockStore = configureStore(); + +jest.mock('react-router-dom', () => { + const mockNavigation = jest.fn(); + + // eslint-disable-next-line react/prop-types + const Navigate = ({ to }) => { + mockNavigation(to); + return
; + }; + + return { + ...jest.requireActual('react-router-dom'), + Navigate, + }; +}); + +describe('RegistrationFailure', () => { + mergeConfig({ + PRIVACY_POLICY: 'https://privacy-policy.com', + TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com', + USER_RETENTION_COOKIE_NAME: 'authn-returning-user', + }); + + let props = {}; + let store = {}; + const registrationFormData = { + configurableFormFields: { + marketingEmailsOptIn: true, + }, + formFields: { + name: '', email: '', username: '', password: '', + }, + emailSuggestion: { + suggestion: '', type: '', + }, + errors: { + name: '', email: '', username: '', password: '', + }, + }; + + const reduxWrapper = children => ( + + {children} + + ); + + const routerWrapper = children => ( + + {children} + + ); + + const thirdPartyAuthContext = { + currentProvider: null, + finishAuthUrl: null, + providers: [], + secondaryProviders: [], + pipelineUserDetails: null, + countryCode: null, + }; + + const initialState = { + register: { + registrationResult: { success: false, redirectUrl: '' }, + registrationError: {}, + registrationFormData, + usernameSuggestions: [], + }, + commonComponents: { + thirdPartyAuthApiStatus: null, + thirdPartyAuthContext, + fieldDescriptions: {}, + optionalFields: { + fields: {}, + extended_profile: [], + }, + }, + }; + + beforeEach(() => { + store = mockStore(initialState); + configure({ + loggingService: { logError: jest.fn() }, + config: { + ENVIRONMENT: 'production', + LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', + }, + messages: { 'es-419': {}, de: {}, 'en-us': {} }, + }); + props = { + handleInstitutionLogin: jest.fn(), + institutionLogin: false, + }; + window.location = { search: '' }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Test Registration Failure', () => { + mergeConfig({ + SHOW_CONFIGURABLE_EDX_FIELDS: true, + }); + getLocale.mockImplementation(() => ('en-us')); + + it('should match internal server error message', () => { + const expectedMessage = 'We couldn\'t create your account.An error has occurred. Try refreshing the page, or check your internet connection.'; + props = { + errorCode: INTERNAL_SERVER_ERROR, + failureCount: 0, + }; + + const registrationPage = mount(reduxWrapper()); + expect(registrationPage.find('div.alert-heading').length).toEqual(1); + expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); + }); + + it('should match registration api rate limit error message', () => { + const expectedMessage = 'We couldn\'t create your account.Too many failed registration attempts. Try again later.'; + props = { + errorCode: FORBIDDEN_REQUEST, + failureCount: 0, + }; + + const registrationPage = mount(reduxWrapper()); + expect(registrationPage.find('div.alert-heading').length).toEqual(1); + expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); + }); + + it('should match tpa session expired error message', () => { + const expectedMessage = 'We couldn\'t create your account.Registration using Google has timed out.'; + props = { + context: { + provider: 'Google', + }, + errorCode: TPA_SESSION_EXPIRED, + failureCount: 0, + }; + + const registrationPage = mount(reduxWrapper()); + expect(registrationPage.find('div.alert-heading').length).toEqual(1); + expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage); + }); + + it('should match tpa authentication failed error message', () => { + const expectedMessageSubstring = 'We are sorry, you are not authorized to access'; + props = { + context: { + provider: 'Google', + }, + errorCode: TPA_AUTHENTICATION_FAILURE, + failureCount: 0, + }; + + const registrationPage = mount(reduxWrapper()); + expect(registrationPage.find('div.alert-heading').length).toEqual(1); + expect(registrationPage.find('div.alert').first().text()).toContain(expectedMessageSubstring); + }); + + it('should display error message based on the error code returned by API', () => { + store = mockStore({ + ...initialState, + register: { + ...initialState.register, + registrationError: { + errorCode: INTERNAL_SERVER_ERROR, + }, + }, + }); + + const registrationPage = mount(routerWrapper(reduxWrapper())).find('RegistrationPage'); + expect(registrationPage.find('div#validation-errors').first().text()).toContain( + 'An error has occurred. Try refreshing the page, or check your internet connection.', + ); + }); + }); +}); diff --git a/src/register/components/ThirdPartyAuth.test.jsx b/src/register/components/ThirdPartyAuth.test.jsx new file mode 100644 index 0000000000..276eba54a0 --- /dev/null +++ b/src/register/components/ThirdPartyAuth.test.jsx @@ -0,0 +1,395 @@ +import React from 'react'; +import { Provider } from 'react-redux'; + +import { getConfig, mergeConfig } from '@edx/frontend-platform'; +import { + configure, getLocale, injectIntl, IntlProvider, +} from '@edx/frontend-platform/i18n'; +import { mount } from 'enzyme'; +import { BrowserRouter as Router } from 'react-router-dom'; +import renderer from 'react-test-renderer'; +import configureStore from 'redux-mock-store'; + +import { + COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, +} from '../../data/constants'; +import RegistrationPage from '../RegistrationPage'; + +jest.mock('@edx/frontend-platform/analytics', () => ({ + sendPageEvent: jest.fn(), + sendTrackEvent: jest.fn(), +})); +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + getLocale: jest.fn(), +})); + +const IntlRegistrationPage = injectIntl(RegistrationPage); +const mockStore = configureStore(); + +jest.mock('react-router-dom', () => { + const mockNavigation = jest.fn(); + + // eslint-disable-next-line react/prop-types + const Navigate = ({ to }) => { + mockNavigation(to); + return
; + }; + + return { + ...jest.requireActual('react-router-dom'), + Navigate, + mockNavigate: mockNavigation, + }; +}); + +describe('ThirdPartyAuth', () => { + mergeConfig({ + PRIVACY_POLICY: 'https://privacy-policy.com', + TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com', + USER_RETENTION_COOKIE_NAME: 'authn-returning-user', + }); + + let props = {}; + let store = {}; + const registrationFormData = { + configurableFormFields: { + marketingEmailsOptIn: true, + }, + formFields: { + name: '', email: '', username: '', password: '', + }, + emailSuggestion: { + suggestion: '', type: '', + }, + errors: { + name: '', email: '', username: '', password: '', + }, + }; + + const reduxWrapper = children => ( + + {children} + + ); + + const routerWrapper = children => ( + + {children} + + ); + + const thirdPartyAuthContext = { + currentProvider: null, + finishAuthUrl: null, + providers: [], + secondaryProviders: [], + pipelineUserDetails: null, + countryCode: null, + }; + + const initialState = { + register: { + registrationResult: { success: false, redirectUrl: '' }, + registrationError: {}, + registrationFormData, + usernameSuggestions: [], + }, + commonComponents: { + thirdPartyAuthApiStatus: null, + thirdPartyAuthContext, + fieldDescriptions: {}, + optionalFields: { + fields: {}, + extended_profile: [], + }, + }, + }; + + beforeEach(() => { + store = mockStore(initialState); + configure({ + loggingService: { logError: jest.fn() }, + config: { + ENVIRONMENT: 'production', + LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', + }, + messages: { 'es-419': {}, de: {}, 'en-us': {} }, + }); + props = { + handleInstitutionLogin: jest.fn(), + institutionLogin: false, + }; + window.location = { search: '' }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const ssoProvider = { + id: 'oa2-apple-id', + name: 'Apple', + iconClass: 'apple', + iconImage: 'https://openedx.devstack.lms/logo.png', + loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard', + }; + + describe('Test Third Party Auth', () => { + mergeConfig({ + SHOW_CONFIGURABLE_EDX_FIELDS: true, + }); + getLocale.mockImplementation(() => ('en-us')); + + const secondaryProviders = { + id: 'saml-test', name: 'Test University', loginUrl: '/dummy-auth', registerUrl: '/dummy_auth', + }; + + it('should render tpa button for tpa_hint id matching one of the primary providers', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + thirdPartyAuthApiStatus: COMPLETE_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name); + expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true); + }); + + it('should display skeleton if tpa_hint is true and thirdPartyAuthContext is pending', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthApiStatus: PENDING_STATE, + }, + }); + + delete window.location; + window.location = { + href: getConfig().BASE_URL.concat(LOGIN_PAGE), + search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`, + }; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find('.react-loading-skeleton').exists()).toBeTruthy(); + }); + + it('should render icon if icon classes are missing in providers', () => { + ssoProvider.iconClass = null; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + thirdPartyAuthApiStatus: COMPLETE_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; + ssoProvider.iconImage = null; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find(`button#${ssoProvider.id}`).find('div').find('span').hasClass('pgn__icon')).toEqual(true); + }); + + it('should render tpa button for tpa_hint id matching one of the secondary providers', () => { + secondaryProviders.skipHintedLogin = true; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + secondaryProviders: [secondaryProviders], + }, + thirdPartyAuthApiStatus: COMPLETE_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; + + mount(routerWrapper(reduxWrapper())); + expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl); + }); + + it('should render regular tpa button for invalid tpa_hint value', () => { + const expectedMessage = `${ssoProvider.name}`; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + thirdPartyAuthApiStatus: COMPLETE_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(expectedMessage); + }); + + it('should show single sign on provider button', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + }, + }); + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1); + }); + + it('should show single sign on provider button', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + }, + }); + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1); + }); + + it('should display InstitutionLogistration if insitutionLogin prop is true', () => { + props = { + ...props, + institutionLogin: true, + }; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find('.institutions__heading').text()).toEqual('Register with institution/campus credentials'); + }); + + it('should redirect to social auth provider url on SSO button click', () => { + const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard'; + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [{ + ...ssoProvider, + registerUrl, + }], + }, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL }; + + const loginPage = mount(routerWrapper(reduxWrapper())); + + loginPage.find('button#oa2-apple-id').simulate('click'); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl); + }); + + it('should redirect to finishAuthUrl upon successful registration via SSO', () => { + const authCompleteUrl = '/auth/complete/google-oauth2/'; + store = mockStore({ + ...initialState, + register: { + ...initialState.register, + registrationResult: { + success: true, + }, + }, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + finishAuthUrl: authCompleteUrl, + }, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL }; + + renderer.create(routerWrapper(reduxWrapper())); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); + }); + + // ******** test alert messages ******** + + it('should match third party auth alert', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + currentProvider: 'Apple', + }, + }, + }); + + const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before ' + + 'you start learning with '}${ getConfig().SITE_NAME }.`; + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage); + }); + it('should display errorMessage if third party authentication fails', () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + getLocale.mockImplementation(() => ('en-us')); + + store = mockStore({ + ...initialState, + register: { + ...initialState.register, + backendCountryCode: 'PK', + userPipelineDataLoaded: false, + }, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthApiStatus: COMPLETE_STATE, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + currentProvider: null, + pipelineUserDetails: {}, + errorMessage: 'An error occurred', + }, + }, + }); + + store.dispatch = jest.fn(store.dispatch); + + const registrationPage = mount(routerWrapper(reduxWrapper())); + expect(registrationPage.find('div.alert-heading').length).toEqual(1); + expect(registrationPage.find('div.alert').first().text()).toContain('An error occurred'); + }); + }); +});