@openedx/frontend-app-authn 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +661 -0
  2. package/README.rst +204 -0
  3. package/package.json +79 -0
  4. package/src/Main.tsx +23 -0
  5. package/src/__mocks__/file.js +1 -0
  6. package/src/__mocks__/svg.js +1 -0
  7. package/src/app.scss +6 -0
  8. package/src/app.ts +43 -0
  9. package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
  10. package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
  11. package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
  12. package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
  13. package/src/base-container/components/default-layout/index.jsx +3 -0
  14. package/src/base-container/components/default-layout/messages.js +16 -0
  15. package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
  16. package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
  17. package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
  18. package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
  19. package/src/base-container/components/image-layout/index.jsx +4 -0
  20. package/src/base-container/components/image-layout/index.scss +37 -0
  21. package/src/base-container/components/image-layout/messages.js +16 -0
  22. package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
  23. package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
  24. package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
  25. package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
  26. package/src/base-container/components/welcome-page-layout/messages.js +21 -0
  27. package/src/base-container/index.jsx +70 -0
  28. package/src/base-container/tests/BaseContainer.test.jsx +44 -0
  29. package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
  30. package/src/common-components/EnterpriseSSO.jsx +110 -0
  31. package/src/common-components/FormGroup.jsx +106 -0
  32. package/src/common-components/InstitutionLogistration.jsx +102 -0
  33. package/src/common-components/NotFoundPage.jsx +15 -0
  34. package/src/common-components/PasswordField.jsx +175 -0
  35. package/src/common-components/RedirectLogistration.jsx +94 -0
  36. package/src/common-components/RegisterFaIcons.jsx +8 -0
  37. package/src/common-components/SocialAuthProviders.jsx +75 -0
  38. package/src/common-components/ThirdPartyAuth.jsx +123 -0
  39. package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
  40. package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
  41. package/src/common-components/data/actions.js +27 -0
  42. package/src/common-components/data/reducers.js +63 -0
  43. package/src/common-components/data/sagas.js +32 -0
  44. package/src/common-components/data/selectors.js +28 -0
  45. package/src/common-components/data/service.js +23 -0
  46. package/src/common-components/data/tests/reducer.test.js +82 -0
  47. package/src/common-components/data/tests/sagas.test.js +71 -0
  48. package/src/common-components/index.jsx +14 -0
  49. package/src/common-components/messages.jsx +127 -0
  50. package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
  51. package/src/common-components/tests/FormField.test.jsx +289 -0
  52. package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
  53. package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
  54. package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
  55. package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
  56. package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
  57. package/src/constants.ts +1 -0
  58. package/src/data/configureStore.js +33 -0
  59. package/src/data/constants.js +38 -0
  60. package/src/data/countries.ts +59 -0
  61. package/src/data/oneTrust.js +3 -0
  62. package/src/data/reducers.js +36 -0
  63. package/src/data/sagas.js +19 -0
  64. package/src/data/tests/cookies.test.js +42 -0
  65. package/src/data/tests/dataUtils.test.js +19 -0
  66. package/src/data/tests/reduxUtils.test.js +14 -0
  67. package/src/data/utils/cookies.js +12 -0
  68. package/src/data/utils/dataUtils.js +81 -0
  69. package/src/data/utils/index.js +11 -0
  70. package/src/data/utils/reduxUtils.js +34 -0
  71. package/src/data/utils/useMobileResponsive.js +24 -0
  72. package/src/field-renderer/FieldRenderer.jsx +156 -0
  73. package/src/field-renderer/index.jsx +2 -0
  74. package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
  75. package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
  76. package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
  77. package/src/forgot-password/data/actions.js +32 -0
  78. package/src/forgot-password/data/reducers.js +58 -0
  79. package/src/forgot-password/data/sagas.js +35 -0
  80. package/src/forgot-password/data/selectors.js +10 -0
  81. package/src/forgot-password/data/service.js +21 -0
  82. package/src/forgot-password/data/tests/reducers.test.js +34 -0
  83. package/src/forgot-password/data/tests/sagas.test.js +67 -0
  84. package/src/forgot-password/index.js +5 -0
  85. package/src/forgot-password/messages.js +128 -0
  86. package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
  87. package/src/i18n/index.js +25 -0
  88. package/src/index.ts +3 -0
  89. package/src/login/AccountActivationMessage.jsx +78 -0
  90. package/src/login/ChangePasswordPrompt.jsx +91 -0
  91. package/src/login/LoginFailure.jsx +231 -0
  92. package/src/login/LoginPage.jsx +368 -0
  93. package/src/login/data/actions.js +39 -0
  94. package/src/login/data/constants.js +20 -0
  95. package/src/login/data/reducers.js +76 -0
  96. package/src/login/data/sagas.js +45 -0
  97. package/src/login/data/service.js +24 -0
  98. package/src/login/data/tests/reducers.test.js +155 -0
  99. package/src/login/data/tests/sagas.test.js +110 -0
  100. package/src/login/index.js +5 -0
  101. package/src/login/messages.jsx +211 -0
  102. package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
  103. package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
  104. package/src/login/tests/LoginFailure.test.jsx +323 -0
  105. package/src/login/tests/LoginPage.test.jsx +839 -0
  106. package/src/logistration/Logistration.jsx +189 -0
  107. package/src/logistration/Logistration.test.jsx +275 -0
  108. package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
  109. package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
  110. package/src/progressive-profiling/data/actions.js +22 -0
  111. package/src/progressive-profiling/data/reducers.js +38 -0
  112. package/src/progressive-profiling/data/sagas.js +24 -0
  113. package/src/progressive-profiling/data/selectors.js +14 -0
  114. package/src/progressive-profiling/data/service.js +17 -0
  115. package/src/progressive-profiling/index.js +5 -0
  116. package/src/progressive-profiling/messages.jsx +57 -0
  117. package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
  118. package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
  119. package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
  120. package/src/register/RegistrationFields/CountryField/validator.js +32 -0
  121. package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
  122. package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
  123. package/src/register/RegistrationFields/EmailField/constants.js +131 -0
  124. package/src/register/RegistrationFields/EmailField/validator.js +126 -0
  125. package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
  126. package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
  127. package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
  128. package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
  129. package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
  130. package/src/register/RegistrationFields/NameField/validator.js +22 -0
  131. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
  132. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
  133. package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
  134. package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
  135. package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
  136. package/src/register/RegistrationFields/index.js +6 -0
  137. package/src/register/RegistrationPage.jsx +428 -0
  138. package/src/register/RegistrationPage.test.jsx +854 -0
  139. package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
  140. package/src/register/components/RegistrationFailure.jsx +80 -0
  141. package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
  142. package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
  143. package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
  144. package/src/register/data/actions.js +85 -0
  145. package/src/register/data/constants.js +13 -0
  146. package/src/register/data/reducers.js +140 -0
  147. package/src/register/data/sagas.js +58 -0
  148. package/src/register/data/selectors.js +34 -0
  149. package/src/register/data/service.js +45 -0
  150. package/src/register/data/tests/reducers.test.js +277 -0
  151. package/src/register/data/tests/sagas.test.js +239 -0
  152. package/src/register/data/tests/utils.test.js +98 -0
  153. package/src/register/data/utils.js +153 -0
  154. package/src/register/index.js +4 -0
  155. package/src/register/messages.jsx +206 -0
  156. package/src/reset-password/ResetPasswordFailure.jsx +55 -0
  157. package/src/reset-password/ResetPasswordPage.jsx +224 -0
  158. package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
  159. package/src/reset-password/data/actions.js +50 -0
  160. package/src/reset-password/data/constants.js +16 -0
  161. package/src/reset-password/data/reducers.js +44 -0
  162. package/src/reset-password/data/sagas.js +67 -0
  163. package/src/reset-password/data/selectors.js +10 -0
  164. package/src/reset-password/data/service.js +61 -0
  165. package/src/reset-password/data/tests/sagas.test.js +185 -0
  166. package/src/reset-password/index.js +5 -0
  167. package/src/reset-password/messages.js +88 -0
  168. package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
  169. package/src/routes.jsx +79 -0
  170. package/src/sass/_base_component.scss +202 -0
  171. package/src/sass/_forgot_password.scss +3 -0
  172. package/src/sass/_login_page.scss +3 -0
  173. package/src/sass/_progressive_profiling_page.scss +30 -0
  174. package/src/sass/_registration.scss +123 -0
  175. package/src/sass/_reset_password.scss +3 -0
  176. package/src/sass/_style.scss +465 -0
  177. package/src/setupTest.js +68 -0
@@ -0,0 +1,439 @@
1
+ import { Provider } from 'react-redux';
2
+
3
+ import {
4
+ configureI18n, getSiteConfig, getLocale, injectIntl, IntlProvider, mergeAppConfig
5
+ } from '@openedx/frontend-base';
6
+ import { fireEvent, render } from '@testing-library/react';
7
+ import { BrowserRouter as Router } from 'react-router-dom';
8
+ import configureStore from 'redux-mock-store';
9
+
10
+ import {
11
+ COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
12
+ } from '../../../data/constants';
13
+ import { appId } from '../../../constants';
14
+ import RegistrationPage from '../../RegistrationPage';
15
+
16
+ jest.mock('@openedx/frontend-base', () => ({
17
+ ...jest.requireActual('@openedx/frontend-base'),
18
+ sendPageEvent: jest.fn(),
19
+ sendTrackEvent: jest.fn(),
20
+ getLocale: jest.fn(),
21
+ }));
22
+
23
+ const IntlRegistrationPage = injectIntl(RegistrationPage);
24
+ const mockStore = configureStore();
25
+
26
+ jest.mock('react-router-dom', () => {
27
+ const mockNavigation = jest.fn();
28
+
29
+ // eslint-disable-next-line react/prop-types
30
+ const Navigate = ({ to }) => {
31
+ mockNavigation(to);
32
+ return <div />;
33
+ };
34
+
35
+ return {
36
+ ...jest.requireActual('react-router-dom'),
37
+ Navigate,
38
+ mockNavigate: mockNavigation,
39
+ };
40
+ });
41
+
42
+ describe('ThirdPartyAuth', () => {
43
+ mergeAppConfig(appId, {
44
+ PRIVACY_POLICY: 'https://privacy-policy.com',
45
+ TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
46
+ USER_RETENTION_COOKIE_NAME: 'authn-returning-user',
47
+ });
48
+
49
+ let props = {};
50
+ let store = {};
51
+ const registrationFormData = {
52
+ configurableFormFields: {
53
+ marketingEmailsOptIn: true,
54
+ },
55
+ formFields: {
56
+ name: '', email: '', username: '', password: '',
57
+ },
58
+ emailSuggestion: {
59
+ suggestion: '', type: '',
60
+ },
61
+ errors: {
62
+ name: '', email: '', username: '', password: '',
63
+ },
64
+ };
65
+
66
+ const reduxWrapper = children => (
67
+ <IntlProvider locale="en">
68
+ <Provider store={store}>{children}</Provider>
69
+ </IntlProvider>
70
+ );
71
+
72
+ const routerWrapper = children => (
73
+ <Router>
74
+ {children}
75
+ </Router>
76
+ );
77
+
78
+ const thirdPartyAuthContext = {
79
+ currentProvider: null,
80
+ finishAuthUrl: null,
81
+ providers: [],
82
+ secondaryProviders: [],
83
+ pipelineUserDetails: null,
84
+ countryCode: null,
85
+ };
86
+
87
+ const initialState = {
88
+ register: {
89
+ registrationResult: { success: false, redirectUrl: '' },
90
+ registrationError: {},
91
+ registrationFormData,
92
+ usernameSuggestions: [],
93
+ },
94
+ commonComponents: {
95
+ thirdPartyAuthApiStatus: null,
96
+ thirdPartyAuthContext,
97
+ fieldDescriptions: {},
98
+ optionalFields: {
99
+ fields: {},
100
+ extended_profile: [],
101
+ },
102
+ },
103
+ };
104
+
105
+ beforeEach(() => {
106
+ store = mockStore(initialState);
107
+ configureI18n({
108
+ messages: { 'es-419': {}, de: {}, 'en-us': {} },
109
+ });
110
+ props = {
111
+ handleInstitutionLogin: jest.fn(),
112
+ institutionLogin: false,
113
+ };
114
+ window.location = { search: '' };
115
+ });
116
+
117
+ afterEach(() => {
118
+ jest.clearAllMocks();
119
+ });
120
+
121
+ const ssoProvider = {
122
+ id: 'oa2-apple-id',
123
+ name: 'Apple',
124
+ iconClass: 'apple',
125
+ iconImage: 'https://openedx.devstack.lms/logo.png',
126
+ loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
127
+ };
128
+
129
+ describe('Test Third Party Auth', () => {
130
+ getLocale.mockImplementation(() => ('en-us'));
131
+
132
+ const secondaryProviders = {
133
+ id: 'saml-test', name: 'Test University', loginUrl: '/dummy-auth', registerUrl: '/dummy_auth',
134
+ };
135
+
136
+ it('should not display password field when current provider is present', () => {
137
+ store = mockStore({
138
+ ...initialState,
139
+ commonComponents: {
140
+ ...initialState.commonComponents,
141
+ thirdPartyAuthContext: {
142
+ ...initialState.commonComponents.thirdPartyAuthContext,
143
+ currentProvider: ssoProvider.name,
144
+ },
145
+ },
146
+ });
147
+
148
+ const { queryByLabelText } = render(
149
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
150
+ );
151
+
152
+ const passwordField = queryByLabelText('Password');
153
+
154
+ expect(passwordField).toBeNull();
155
+ });
156
+
157
+ it('should render tpa button for tpa_hint id matching one of the primary providers', () => {
158
+ store = mockStore({
159
+ ...initialState,
160
+ commonComponents: {
161
+ ...initialState.commonComponents,
162
+ thirdPartyAuthContext: {
163
+ ...initialState.commonComponents.thirdPartyAuthContext,
164
+ providers: [ssoProvider],
165
+ },
166
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
167
+ },
168
+ });
169
+
170
+ delete window.location;
171
+ window.location = { href: getSiteConfig().baseUrl.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
172
+
173
+ const { container } = render(
174
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
175
+ );
176
+ const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
177
+
178
+ expect(tpaButton).toBeTruthy();
179
+ expect(tpaButton.textContent).toEqual(ssoProvider.name);
180
+ expect(tpaButton.classList.contains('btn-tpa')).toBe(true);
181
+ expect(tpaButton.classList.contains(`btn-${ssoProvider.id}`)).toBe(true);
182
+ });
183
+
184
+ it('should display skeleton if tpa_hint is true and thirdPartyAuthContext is pending', () => {
185
+ store = mockStore({
186
+ ...initialState,
187
+ commonComponents: {
188
+ ...initialState.commonComponents,
189
+ thirdPartyAuthApiStatus: PENDING_STATE,
190
+ },
191
+ });
192
+
193
+ delete window.location;
194
+ window.location = {
195
+ href: getSiteConfig().baseUrl.concat(LOGIN_PAGE),
196
+ search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
197
+ };
198
+
199
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
200
+ const skeletonElement = container.querySelector('.react-loading-skeleton');
201
+
202
+ expect(skeletonElement).toBeTruthy();
203
+ });
204
+
205
+ it('should render icon if icon classes are missing in providers', () => {
206
+ ssoProvider.iconClass = null;
207
+ store = mockStore({
208
+ ...initialState,
209
+ commonComponents: {
210
+ ...initialState.commonComponents,
211
+ thirdPartyAuthContext: {
212
+ ...initialState.commonComponents.thirdPartyAuthContext,
213
+ providers: [ssoProvider],
214
+ },
215
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
216
+ },
217
+ });
218
+
219
+ delete window.location;
220
+ window.location = { href: getSiteConfig().baseUrl.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
221
+ ssoProvider.iconImage = null;
222
+
223
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
224
+ const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
225
+
226
+ expect(iconElement).toBeTruthy();
227
+ });
228
+
229
+ it('should render tpa button for tpa_hint id matching one of the secondary providers', () => {
230
+ secondaryProviders.skipHintedLogin = true;
231
+ store = mockStore({
232
+ ...initialState,
233
+ commonComponents: {
234
+ ...initialState.commonComponents,
235
+ thirdPartyAuthContext: {
236
+ ...initialState.commonComponents.thirdPartyAuthContext,
237
+ secondaryProviders: [secondaryProviders],
238
+ },
239
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
240
+ },
241
+ });
242
+
243
+ delete window.location;
244
+ window.location = { href: getSiteConfig().baseUrl.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
245
+
246
+ render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
247
+ expect(window.location.href).toEqual(getSiteConfig().lmsBaseUrl + secondaryProviders.registerUrl);
248
+ });
249
+
250
+ it('should render regular tpa button for invalid tpa_hint value', () => {
251
+ const expectedMessage = `${ssoProvider.name}`;
252
+ store = mockStore({
253
+ ...initialState,
254
+ commonComponents: {
255
+ ...initialState.commonComponents,
256
+ thirdPartyAuthContext: {
257
+ ...initialState.commonComponents.thirdPartyAuthContext,
258
+ providers: [ssoProvider],
259
+ },
260
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
261
+ },
262
+ });
263
+
264
+ delete window.location;
265
+ window.location = { href: getSiteConfig().baseUrl.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
266
+
267
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
268
+ const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
269
+
270
+ expect(providerButton.textContent).toEqual(expectedMessage);
271
+ });
272
+
273
+ it('should show single sign on provider button', () => {
274
+ store = mockStore({
275
+ ...initialState,
276
+ commonComponents: {
277
+ ...initialState.commonComponents,
278
+ thirdPartyAuthContext: {
279
+ ...initialState.commonComponents.thirdPartyAuthContext,
280
+ providers: [ssoProvider],
281
+ },
282
+ },
283
+ });
284
+
285
+ const { container } = render(
286
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
287
+ );
288
+
289
+ const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
290
+
291
+ expect(buttonsWithId.length).toEqual(1);
292
+ });
293
+
294
+ it('should show single sign on provider button', () => {
295
+ store = mockStore({
296
+ ...initialState,
297
+ commonComponents: {
298
+ ...initialState.commonComponents,
299
+ thirdPartyAuthContext: {
300
+ ...initialState.commonComponents.thirdPartyAuthContext,
301
+ providers: [ssoProvider],
302
+ },
303
+ },
304
+ });
305
+
306
+ const { container } = render(
307
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
308
+ );
309
+
310
+ const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
311
+
312
+ expect(buttonsWithId.length).toEqual(1);
313
+ });
314
+
315
+ it('should display InstitutionLogistration if insitutionLogin prop is true', () => {
316
+ props = {
317
+ ...props,
318
+ institutionLogin: true,
319
+ };
320
+
321
+ const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
322
+ const headingElement = getByText('Register with institution/campus credentials');
323
+ expect(headingElement).toBeTruthy();
324
+ });
325
+
326
+ it('should redirect to social auth provider url on SSO button click', () => {
327
+ const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
328
+ store = mockStore({
329
+ ...initialState,
330
+ commonComponents: {
331
+ ...initialState.commonComponents,
332
+ thirdPartyAuthContext: {
333
+ ...initialState.commonComponents.thirdPartyAuthContext,
334
+ providers: [{
335
+ ...ssoProvider,
336
+ registerUrl,
337
+ }],
338
+ },
339
+ },
340
+ });
341
+
342
+ delete window.location;
343
+ window.location = { href: getSiteConfig().baseUrl };
344
+
345
+ const { container } = render(
346
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
347
+ );
348
+
349
+ const ssoButton = container.querySelector('button#oa2-apple-id');
350
+ fireEvent.click(ssoButton);
351
+
352
+ expect(window.location.href).toBe(getSiteConfig().lmsBaseUrl + registerUrl);
353
+ });
354
+
355
+ it('should redirect to finishAuthUrl upon successful registration via SSO', () => {
356
+ const authCompleteUrl = '/auth/complete/google-oauth2/';
357
+ store = mockStore({
358
+ ...initialState,
359
+ register: {
360
+ ...initialState.register,
361
+ registrationResult: {
362
+ success: true,
363
+ },
364
+ },
365
+ commonComponents: {
366
+ ...initialState.commonComponents,
367
+ thirdPartyAuthContext: {
368
+ ...initialState.commonComponents.thirdPartyAuthContext,
369
+ finishAuthUrl: authCompleteUrl,
370
+ },
371
+ },
372
+ });
373
+
374
+ delete window.location;
375
+ window.location = { href: getSiteConfig().baseUrl };
376
+
377
+ render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
378
+ expect(window.location.href).toBe(getSiteConfig().lmsBaseUrl + authCompleteUrl);
379
+ });
380
+
381
+ // ******** test alert messages ********
382
+
383
+ it('should match third party auth alert', () => {
384
+ store = mockStore({
385
+ ...initialState,
386
+ commonComponents: {
387
+ ...initialState.commonComponents,
388
+ thirdPartyAuthContext: {
389
+ ...initialState.commonComponents.thirdPartyAuthContext,
390
+ currentProvider: 'Apple',
391
+ },
392
+ },
393
+ });
394
+
395
+ const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
396
+ + 'you start learning with '}${getSiteConfig().siteName}.`;
397
+
398
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
399
+ const tpaAlert = container.querySelector('#tpa-alert p');
400
+ expect(tpaAlert.textContent).toEqual(expectedMessage);
401
+ });
402
+
403
+ it('should display errorMessage if third party authentication fails', () => {
404
+ jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
405
+ getLocale.mockImplementation(() => ('en-us'));
406
+
407
+ store = mockStore({
408
+ ...initialState,
409
+ register: {
410
+ ...initialState.register,
411
+ backendCountryCode: 'PK',
412
+ userPipelineDataLoaded: false,
413
+ },
414
+ commonComponents: {
415
+ ...initialState.commonComponents,
416
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
417
+ thirdPartyAuthContext: {
418
+ ...initialState.commonComponents.thirdPartyAuthContext,
419
+ currentProvider: null,
420
+ pipelineUserDetails: {},
421
+ errorMessage: 'An error occurred',
422
+ },
423
+ },
424
+ });
425
+
426
+ store.dispatch = jest.fn(store.dispatch);
427
+
428
+ const { container } = render(
429
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
430
+ );
431
+
432
+ const alertHeading = container.querySelector('div.alert-heading');
433
+ expect(alertHeading).toBeTruthy();
434
+
435
+ const alert = container.querySelector('div.alert');
436
+ expect(alert.textContent).toContain('An error occurred');
437
+ });
438
+ });
439
+ });
@@ -0,0 +1,85 @@
1
+ import { AsyncActionType } from '../../data/utils';
2
+
3
+ export const BACKUP_REGISTRATION_DATA = new AsyncActionType('REGISTRATION', 'BACKUP_REGISTRATION_DATA');
4
+ export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
5
+ export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
6
+ export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_SUGGESTIONS';
7
+ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERROR';
8
+ export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
9
+ export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
10
+ export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
11
+
12
+ // Backup registration form
13
+ export const backupRegistrationForm = () => ({
14
+ type: BACKUP_REGISTRATION_DATA.BASE,
15
+ });
16
+
17
+ export const backupRegistrationFormBegin = (data) => ({
18
+ type: BACKUP_REGISTRATION_DATA.BEGIN,
19
+ payload: { ...data },
20
+ });
21
+
22
+ // Validate fields from the backend
23
+ export const fetchRealtimeValidations = (formPayload) => ({
24
+ type: REGISTER_FORM_VALIDATIONS.BASE,
25
+ payload: { formPayload },
26
+ });
27
+
28
+ export const fetchRealtimeValidationsBegin = () => ({
29
+ type: REGISTER_FORM_VALIDATIONS.BEGIN,
30
+ });
31
+
32
+ export const fetchRealtimeValidationsSuccess = (validations) => ({
33
+ type: REGISTER_FORM_VALIDATIONS.SUCCESS,
34
+ payload: { validations },
35
+ });
36
+
37
+ export const fetchRealtimeValidationsFailure = () => ({
38
+ type: REGISTER_FORM_VALIDATIONS.FAILURE,
39
+ });
40
+
41
+ // Set email field frontend validations
42
+ export const setEmailSuggestionInStore = (emailSuggestion) => ({
43
+ type: REGISTER_SET_EMAIL_SUGGESTIONS,
44
+ payload: { emailSuggestion },
45
+ });
46
+
47
+ // Register
48
+ export const registerNewUser = registrationInfo => ({
49
+ type: REGISTER_NEW_USER.BASE,
50
+ payload: { registrationInfo },
51
+ });
52
+
53
+ export const registerNewUserBegin = () => ({
54
+ type: REGISTER_NEW_USER.BEGIN,
55
+ });
56
+
57
+ export const registerNewUserSuccess = (authenticatedUser, redirectUrl, success) => ({
58
+ type: REGISTER_NEW_USER.SUCCESS,
59
+ payload: { authenticatedUser, redirectUrl, success },
60
+
61
+ });
62
+
63
+ export const registerNewUserFailure = (error) => ({
64
+ type: REGISTER_NEW_USER.FAILURE,
65
+ payload: { ...error },
66
+ });
67
+
68
+ export const clearUsernameSuggestions = () => ({
69
+ type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
70
+ });
71
+
72
+ export const clearRegistrationBackendError = (fieldName) => ({
73
+ type: REGISTRATION_CLEAR_BACKEND_ERROR,
74
+ payload: fieldName,
75
+ });
76
+
77
+ export const setCountryFromThirdPartyAuthContext = (countryCode) => ({
78
+ type: REGISTER_SET_COUNTRY_CODE,
79
+ payload: { countryCode },
80
+ });
81
+
82
+ export const setUserPipelineDataLoaded = (value) => ({
83
+ type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
84
+ payload: { value },
85
+ });
@@ -0,0 +1,13 @@
1
+ // Registration Fields
2
+ export const FIELDS = {
3
+ COUNTRY: 'country',
4
+ HONOR_CODE: 'honor_code',
5
+ TERMS_OF_SERVICE: 'terms_of_service',
6
+ };
7
+
8
+ // Registration Error Codes
9
+ export const FORBIDDEN_REQUEST = 'forbidden-request';
10
+ export const FORM_SUBMISSION_ERROR = 'form-submission-error';
11
+ export const INTERNAL_SERVER_ERROR = 'internal-server-error';
12
+ export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
13
+ export const TPA_SESSION_EXPIRED = 'tpa-session-expired';
@@ -0,0 +1,140 @@
1
+ import {
2
+ BACKUP_REGISTRATION_DATA,
3
+ REGISTER_CLEAR_USERNAME_SUGGESTIONS,
4
+ REGISTER_FORM_VALIDATIONS,
5
+ REGISTER_NEW_USER,
6
+ REGISTER_SET_COUNTRY_CODE,
7
+ REGISTER_SET_EMAIL_SUGGESTIONS,
8
+ REGISTER_SET_USER_PIPELINE_DATA_LOADED,
9
+ REGISTRATION_CLEAR_BACKEND_ERROR,
10
+ } from './actions';
11
+ import {
12
+ DEFAULT_STATE,
13
+ PENDING_STATE,
14
+ } from '../../data/constants';
15
+
16
+ export const storeName = 'register';
17
+
18
+ export const defaultState = {
19
+ backendCountryCode: '',
20
+ registrationError: {},
21
+ registrationResult: {},
22
+ registrationFormData: {
23
+ configurableFormFields: {
24
+ marketingEmailsOptIn: true,
25
+ },
26
+ formFields: {
27
+ name: '', email: '', username: '', password: '',
28
+ },
29
+ emailSuggestion: {
30
+ suggestion: '', type: '',
31
+ },
32
+ errors: {
33
+ name: '', email: '', username: '', password: '',
34
+ },
35
+ },
36
+ validations: null,
37
+ submitState: DEFAULT_STATE,
38
+ userPipelineDataLoaded: false,
39
+ usernameSuggestions: [],
40
+ validationApiRateLimited: false,
41
+ shouldBackupState: false,
42
+ };
43
+
44
+ const reducer = (state = defaultState, action = {}) => {
45
+ switch (action.type) {
46
+ case BACKUP_REGISTRATION_DATA.BASE:
47
+ return {
48
+ ...state,
49
+ shouldBackupState: true,
50
+ };
51
+ case BACKUP_REGISTRATION_DATA.BEGIN:
52
+ return {
53
+ ...state,
54
+ usernameSuggestions: state.usernameSuggestions,
55
+ registrationFormData: { ...action.payload },
56
+ userPipelineDataLoaded: state.userPipelineDataLoaded,
57
+ };
58
+ case REGISTER_NEW_USER.BEGIN:
59
+ return {
60
+ ...state,
61
+ submitState: PENDING_STATE,
62
+ registrationError: {},
63
+ };
64
+ case REGISTER_NEW_USER.SUCCESS: {
65
+ return {
66
+ ...state,
67
+ registrationResult: action.payload,
68
+ };
69
+ }
70
+ case REGISTER_NEW_USER.FAILURE: {
71
+ const { usernameSuggestions } = action.payload;
72
+ return {
73
+ ...state,
74
+ registrationError: { ...action.payload },
75
+ submitState: DEFAULT_STATE,
76
+ validations: null,
77
+ usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
78
+ };
79
+ }
80
+ case REGISTRATION_CLEAR_BACKEND_ERROR: {
81
+ const registrationErrorTemp = state.registrationError;
82
+ delete registrationErrorTemp[action.payload];
83
+ return {
84
+ ...state,
85
+ registrationError: { ...registrationErrorTemp },
86
+ };
87
+ }
88
+ case REGISTER_FORM_VALIDATIONS.SUCCESS: {
89
+ const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
90
+ return {
91
+ ...state,
92
+ validations: validationWithoutUsernameSuggestions,
93
+ usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
94
+ };
95
+ }
96
+ case REGISTER_FORM_VALIDATIONS.FAILURE:
97
+ return {
98
+ ...state,
99
+ validationApiRateLimited: true,
100
+ validations: null,
101
+ };
102
+ case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
103
+ return {
104
+ ...state,
105
+ usernameSuggestions: [],
106
+ };
107
+ case REGISTER_SET_COUNTRY_CODE: {
108
+ const { countryCode } = action.payload;
109
+ if (!state.registrationFormData.configurableFormFields.country) {
110
+ return {
111
+ ...state,
112
+ backendCountryCode: countryCode,
113
+ };
114
+ }
115
+ return state;
116
+ }
117
+ case REGISTER_SET_USER_PIPELINE_DATA_LOADED: {
118
+ const { value } = action.payload;
119
+ return {
120
+ ...state,
121
+ userPipelineDataLoaded: value,
122
+ };
123
+ }
124
+ case REGISTER_SET_EMAIL_SUGGESTIONS:
125
+ return {
126
+ ...state,
127
+ registrationFormData: {
128
+ ...state.registrationFormData,
129
+ emailSuggestion: action.payload.emailSuggestion,
130
+ },
131
+ };
132
+ default:
133
+ return {
134
+ ...state,
135
+ shouldBackupState: false,
136
+ };
137
+ }
138
+ };
139
+
140
+ export default reducer;