@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,58 @@
1
+ import { camelCaseObject, logError, logInfo } from '@openedx/frontend-base';
2
+ import { call, put, takeEvery } from 'redux-saga/effects';
3
+
4
+ import {
5
+ fetchRealtimeValidationsBegin,
6
+ fetchRealtimeValidationsFailure,
7
+ fetchRealtimeValidationsSuccess,
8
+ REGISTER_FORM_VALIDATIONS,
9
+ REGISTER_NEW_USER,
10
+ registerNewUserBegin,
11
+ registerNewUserFailure,
12
+ registerNewUserSuccess,
13
+ } from './actions';
14
+ import { INTERNAL_SERVER_ERROR } from './constants';
15
+ import { getFieldsValidations, registerRequest } from './service';
16
+
17
+ export function* handleNewUserRegistration(action) {
18
+ try {
19
+ yield put(registerNewUserBegin());
20
+
21
+ const { authenticatedUser, redirectUrl, success } = yield call(registerRequest, action.payload.registrationInfo);
22
+
23
+ yield put(registerNewUserSuccess(
24
+ camelCaseObject(authenticatedUser),
25
+ redirectUrl,
26
+ success,
27
+ ));
28
+ } catch (e) {
29
+ const statusCodes = [400, 403, 409];
30
+ if (e.response && statusCodes.includes(e.response.status)) {
31
+ yield put(registerNewUserFailure(camelCaseObject(e.response.data)));
32
+ logInfo(e);
33
+ } else {
34
+ yield put(registerNewUserFailure({ errorCode: INTERNAL_SERVER_ERROR }));
35
+ logError(e);
36
+ }
37
+ }
38
+ }
39
+
40
+ export function* fetchRealtimeValidations(action) {
41
+ try {
42
+ yield put(fetchRealtimeValidationsBegin());
43
+ const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
44
+
45
+ yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
46
+ } catch (e) {
47
+ if (e.response && e.response.status === 403) {
48
+ yield put(fetchRealtimeValidationsFailure());
49
+ logInfo(e);
50
+ } else {
51
+ logError(e);
52
+ }
53
+ }
54
+ }
55
+ export default function* saga() {
56
+ yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
57
+ yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);
58
+ }
@@ -0,0 +1,34 @@
1
+ import { createSelector } from 'reselect';
2
+
3
+ /**
4
+ * Selector for backend validations which processes the api output and generates a
5
+ * key value dict for field errors.
6
+ * @returns {{username: string}|{name: string}|*|{}|null}
7
+ */
8
+ const getRegistrationError = state => state.register.registrationError;
9
+ const getValidations = state => state.register.validations;
10
+
11
+ const getBackendValidations = createSelector(
12
+ [getRegistrationError, getValidations],
13
+ (registrationError, validations) => {
14
+ if (validations) {
15
+ return validations.validationDecisions;
16
+ }
17
+
18
+ if (Object.keys(registrationError).length > 0) {
19
+ const fields = Object.keys(registrationError).filter(
20
+ (fieldName) => !(fieldName in ['errorCode', 'usernameSuggestions']),
21
+ );
22
+
23
+ const validationDecisions = {};
24
+ fields.forEach(field => {
25
+ validationDecisions[field] = registrationError[field][0].userMessage || '';
26
+ });
27
+ return validationDecisions;
28
+ }
29
+
30
+ return null;
31
+ }
32
+ );
33
+
34
+ export default getBackendValidations;
@@ -0,0 +1,45 @@
1
+ import { getAuthenticatedHttpClient, getSiteConfig, getHttpClient } from '@openedx/frontend-base';
2
+ import * as QueryString from 'query-string';
3
+
4
+ export async function registerRequest(registrationInformation) {
5
+ const requestConfig = {
6
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
7
+ isPublic: true,
8
+ };
9
+
10
+ const { data } = await getAuthenticatedHttpClient()
11
+ .post(
12
+ `${getSiteConfig().lmsBaseUrl}/api/user/v2/account/registration/`,
13
+ QueryString.stringify(registrationInformation),
14
+ requestConfig,
15
+ )
16
+ .catch((e) => {
17
+ throw (e);
18
+ });
19
+
20
+ return {
21
+ redirectUrl: data.redirect_url || `${getSiteConfig().lmsBaseUrl}/dashboard`,
22
+ success: data.success || false,
23
+ authenticatedUser: data.authenticated_user,
24
+ };
25
+ }
26
+
27
+ export async function getFieldsValidations(formPayload) {
28
+ const requestConfig = {
29
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
30
+ };
31
+
32
+ const { data } = await getHttpClient()
33
+ .post(
34
+ `${getSiteConfig().lmsBaseUrl}/api/user/v1/validation/registration`,
35
+ QueryString.stringify(formPayload),
36
+ requestConfig,
37
+ )
38
+ .catch((e) => {
39
+ throw (e);
40
+ });
41
+
42
+ return {
43
+ fieldValidations: data,
44
+ };
45
+ }
@@ -0,0 +1,277 @@
1
+ import { getSiteConfig } from '@openedx/frontend-base';
2
+
3
+ import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, PENDING_STATE } from '../../../data/constants';
4
+ import {
5
+ BACKUP_REGISTRATION_DATA,
6
+ REGISTER_CLEAR_USERNAME_SUGGESTIONS,
7
+ REGISTER_FORM_VALIDATIONS,
8
+ REGISTER_NEW_USER,
9
+ REGISTER_SET_COUNTRY_CODE,
10
+ REGISTER_SET_EMAIL_SUGGESTIONS,
11
+ REGISTER_SET_USER_PIPELINE_DATA_LOADED,
12
+ REGISTRATION_CLEAR_BACKEND_ERROR,
13
+ } from '../actions';
14
+ import reducer from '../reducers';
15
+
16
+ describe('Registration Reducer Tests', () => {
17
+ const defaultState = {
18
+ backendCountryCode: '',
19
+ registrationError: {},
20
+ registrationResult: {},
21
+ registrationFormData: {
22
+ configurableFormFields: {
23
+ marketingEmailsOptIn: true,
24
+ },
25
+ formFields: {
26
+ name: '', email: '', username: '', password: '',
27
+ },
28
+ emailSuggestion: {
29
+ suggestion: '', type: '',
30
+ },
31
+ errors: {
32
+ name: '', email: '', username: '', password: '',
33
+ },
34
+ },
35
+ validations: null,
36
+ submitState: DEFAULT_STATE,
37
+ userPipelineDataLoaded: false,
38
+ usernameSuggestions: [],
39
+ validationApiRateLimited: false,
40
+ shouldBackupState: false,
41
+ };
42
+
43
+ it('should return the initial state', () => {
44
+ expect(reducer(undefined, {})).toEqual(defaultState);
45
+ });
46
+
47
+ it('should set username suggestions returned by the backend validations', () => {
48
+ const validations = {
49
+ usernameSuggestions: ['test12'],
50
+ validationDecisions: {
51
+ name: '',
52
+ },
53
+ };
54
+ const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = validations;
55
+ const action = {
56
+ type: REGISTER_FORM_VALIDATIONS.SUCCESS,
57
+ payload: { validations },
58
+ };
59
+
60
+ expect(reducer(defaultState, action)).toEqual(
61
+ {
62
+ ...defaultState,
63
+ usernameSuggestions,
64
+ validations: validationWithoutUsernameSuggestions,
65
+ },
66
+ );
67
+ });
68
+
69
+ it('should set email suggestions', () => {
70
+ const emailSuggestion = {
71
+ type: 'test type',
72
+ suggestion: 'test suggestion',
73
+ };
74
+ const action = {
75
+ type: REGISTER_SET_EMAIL_SUGGESTIONS,
76
+ payload: { emailSuggestion },
77
+ };
78
+
79
+ expect(reducer(defaultState, action)).toEqual(
80
+ {
81
+ ...defaultState,
82
+ registrationFormData: {
83
+ ...defaultState.registrationFormData,
84
+ emailSuggestion: {
85
+ type: 'test type', suggestion: 'test suggestion',
86
+ },
87
+ },
88
+ }
89
+ );
90
+ });
91
+
92
+ it('should set redirect url dashboard on registration success action', () => {
93
+ const payload = {
94
+ redirectUrl: `${getSiteConfig().baseUrl}${DEFAULT_REDIRECT_URL}`,
95
+ success: true,
96
+ };
97
+ const action = {
98
+ type: REGISTER_NEW_USER.SUCCESS,
99
+ payload,
100
+ };
101
+
102
+ expect(reducer(defaultState, action)).toEqual(
103
+ {
104
+ ...defaultState,
105
+ registrationResult: payload,
106
+ },
107
+ );
108
+ });
109
+
110
+ it('should set the registration call and set the registration error object empty', () => {
111
+ const action = {
112
+ type: REGISTER_NEW_USER.BEGIN,
113
+ };
114
+
115
+ expect(reducer({
116
+ ...defaultState,
117
+ registrationError: {
118
+ email: 'This email already exist.',
119
+ },
120
+ }, action)).toEqual(
121
+ {
122
+ ...defaultState,
123
+ submitState: PENDING_STATE,
124
+ registrationError: {},
125
+ },
126
+ );
127
+ });
128
+
129
+ it('should show username suggestions returned by registration error', () => {
130
+ const payload = { usernameSuggestions: ['test12'] };
131
+ const action = {
132
+ type: REGISTER_NEW_USER.FAILURE,
133
+ payload,
134
+ };
135
+
136
+ expect(reducer(defaultState, action)).toEqual(
137
+ {
138
+ ...defaultState,
139
+ registrationError: payload,
140
+ usernameSuggestions: payload.usernameSuggestions,
141
+ },
142
+ );
143
+ });
144
+ it('should set the register user when SSO pipeline data is loaded', () => {
145
+ const payload = { value: true };
146
+ const action = {
147
+ type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
148
+ payload,
149
+ };
150
+
151
+ expect(reducer(defaultState, action)).toEqual(
152
+ {
153
+ ...defaultState,
154
+ userPipelineDataLoaded: true,
155
+ },
156
+ );
157
+ });
158
+
159
+ it('should set country code on blur', () => {
160
+ const action = {
161
+ type: REGISTER_SET_COUNTRY_CODE,
162
+ payload: { countryCode: 'PK' },
163
+ };
164
+
165
+ expect(reducer({
166
+ ...defaultState,
167
+ registrationFormData: {
168
+ ...defaultState.registrationFormData,
169
+ configurableFormFields: {
170
+ ...defaultState.registrationFormData.configurableFormFields,
171
+ country: {
172
+ name: 'Pakistan',
173
+ code: 'PK',
174
+ },
175
+ },
176
+ },
177
+ }, action)).toEqual(
178
+ {
179
+ ...defaultState,
180
+ registrationFormData: {
181
+ ...defaultState.registrationFormData,
182
+ configurableFormFields: {
183
+ ...defaultState.registrationFormData.configurableFormFields,
184
+ country: {
185
+ name: 'Pakistan',
186
+ code: 'PK',
187
+ },
188
+ },
189
+ },
190
+ },
191
+ );
192
+ });
193
+ it(' registration api failure when api rate limit hits', () => {
194
+ const action = {
195
+ type: REGISTER_FORM_VALIDATIONS.FAILURE,
196
+ };
197
+
198
+ expect(reducer(defaultState, action)).toEqual(
199
+ {
200
+ ...defaultState,
201
+ validationApiRateLimited: true,
202
+ validations: null,
203
+ },
204
+ );
205
+ });
206
+ it('should clear username suggestions', () => {
207
+ const state = {
208
+ ...defaultState,
209
+ usernameSuggestions: ['test_1'],
210
+ };
211
+ const action = {
212
+ type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
213
+ };
214
+
215
+ expect(reducer(state, action)).toEqual({ ...defaultState });
216
+ });
217
+
218
+ it('should take back data during form reset', () => {
219
+ const state = {
220
+ ...defaultState,
221
+ shouldBackupState: true,
222
+ };
223
+ const action = {
224
+ type: BACKUP_REGISTRATION_DATA.BASE,
225
+ };
226
+
227
+ expect(reducer(state, action)).toEqual({
228
+ ...defaultState,
229
+ shouldBackupState: true,
230
+ });
231
+ });
232
+
233
+ it('should not reset username suggestions and fields data during form reset', () => {
234
+ const state = {
235
+ ...defaultState,
236
+ usernameSuggestions: ['test1', 'test2'],
237
+ };
238
+ const action = {
239
+ type: BACKUP_REGISTRATION_DATA.BEGIN,
240
+ payload: { ...state.registrationFormData },
241
+ };
242
+
243
+ expect(reducer(state, action)).toEqual(state);
244
+ });
245
+
246
+ it('should reset email error field data on focus of email field', () => {
247
+ const state = {
248
+ ...defaultState,
249
+ registrationError: { email: `This email is already associated with an existing or previous ${getSiteConfig().siteName} account` },
250
+ };
251
+ const action = {
252
+ type: REGISTRATION_CLEAR_BACKEND_ERROR,
253
+ payload: 'email',
254
+ };
255
+
256
+ expect(reducer(state, action)).toEqual({
257
+ ...state,
258
+ registrationError: {},
259
+ });
260
+ });
261
+
262
+ it('should set country code', () => {
263
+ const countryCode = 'PK';
264
+
265
+ const action = {
266
+ type: REGISTER_SET_COUNTRY_CODE,
267
+ payload: { countryCode },
268
+ };
269
+
270
+ expect(reducer(defaultState, action)).toEqual(
271
+ {
272
+ ...defaultState,
273
+ backendCountryCode: countryCode,
274
+ },
275
+ );
276
+ });
277
+ });
@@ -0,0 +1,239 @@
1
+ import { camelCaseObject } from '@openedx/frontend-base';
2
+ import { runSaga } from 'redux-saga';
3
+
4
+ import { initializeMockServices } from '../../../setupTest';
5
+ import * as actions from '../actions';
6
+ import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from '../constants';
7
+ import {
8
+ fetchRealtimeValidations,
9
+ handleNewUserRegistration,
10
+ } from '../sagas';
11
+ import * as api from '../service';
12
+
13
+ const { loggingService } = initializeMockServices();
14
+
15
+ describe('fetchRealtimeValidations', () => {
16
+ const params = {
17
+ payload: {
18
+ registrationFormData: {
19
+ email: 'test@test.com',
20
+ username: '',
21
+ password: 'test-password',
22
+ name: 'test-name',
23
+ honor_code: true,
24
+ country: 'test-country',
25
+ },
26
+ },
27
+ };
28
+
29
+ beforeEach(() => {
30
+ loggingService.logInfo.mockReset();
31
+ });
32
+
33
+ const data = {
34
+ validationDecisions: {
35
+ username: 'Username must be between 2 and 30 characters long.',
36
+ },
37
+ };
38
+
39
+ it('should call service and dispatch success action', async () => {
40
+ const getFieldsValidations = jest.spyOn(api, 'getFieldsValidations')
41
+ .mockImplementation(() => Promise.resolve({ fieldValidations: data }));
42
+
43
+ const dispatched = [];
44
+ await runSaga(
45
+ { dispatch: (action) => dispatched.push(action) },
46
+ fetchRealtimeValidations,
47
+ params,
48
+ );
49
+
50
+ expect(getFieldsValidations).toHaveBeenCalledTimes(1);
51
+ expect(dispatched).toEqual([
52
+ actions.fetchRealtimeValidationsBegin(),
53
+ actions.fetchRealtimeValidationsSuccess(data),
54
+ ]);
55
+ getFieldsValidations.mockClear();
56
+ });
57
+
58
+ it('should call service and dispatch error action', async () => {
59
+ const validationRatelimitResponse = {
60
+ response: {
61
+ status: 403,
62
+ data: {
63
+ detail: 'You do not have permission to perform this action.',
64
+ },
65
+ },
66
+ };
67
+ const getFieldsValidations = jest.spyOn(api, 'getFieldsValidations')
68
+ .mockImplementation(() => Promise.reject(validationRatelimitResponse));
69
+
70
+ const dispatched = [];
71
+ await runSaga(
72
+ { dispatch: (action) => dispatched.push(action) },
73
+ fetchRealtimeValidations,
74
+ params,
75
+ );
76
+
77
+ expect(getFieldsValidations).toHaveBeenCalledTimes(1);
78
+ expect(loggingService.logInfo).toHaveBeenCalled();
79
+ expect(dispatched).toEqual([
80
+ actions.fetchRealtimeValidationsBegin(),
81
+ actions.fetchRealtimeValidationsFailure(
82
+ validationRatelimitResponse.response.data,
83
+ validationRatelimitResponse.response.status,
84
+ ),
85
+ ]);
86
+ getFieldsValidations.mockClear();
87
+ });
88
+
89
+ it('should call logError on 500 server error', async () => {
90
+ const validationRatelimitResponse = {
91
+ response: {
92
+ status: 500,
93
+ data: {},
94
+ },
95
+ };
96
+ const getFieldsValidations = jest.spyOn(api, 'getFieldsValidations')
97
+ .mockImplementation(() => Promise.reject(validationRatelimitResponse));
98
+
99
+ const dispatched = [];
100
+ await runSaga(
101
+ { dispatch: (action) => dispatched.push(action) },
102
+ fetchRealtimeValidations,
103
+ params,
104
+ );
105
+
106
+ expect(getFieldsValidations).toHaveBeenCalledTimes(1);
107
+ expect(loggingService.logError).toHaveBeenCalled();
108
+ getFieldsValidations.mockClear();
109
+ });
110
+ });
111
+
112
+ describe('handleNewUserRegistration', () => {
113
+ const params = {
114
+ payload: {
115
+ registrationFormData: {
116
+ email: 'test@test.com',
117
+ username: 'test-username',
118
+ password: 'test-password',
119
+ name: 'test-name',
120
+ honor_code: true,
121
+ country: 'test-country',
122
+ },
123
+ },
124
+ };
125
+
126
+ beforeEach(() => {
127
+ loggingService.logError.mockReset();
128
+ loggingService.logInfo.mockReset();
129
+ });
130
+
131
+ it('should call service and dispatch success action', async () => {
132
+ const authenticatedUser = { username: 'test', user_id: 123 };
133
+ const data = {
134
+ redirectUrl: '/dashboard',
135
+ success: true,
136
+ authenticatedUser,
137
+ };
138
+ const registerRequest = jest.spyOn(api, 'registerRequest')
139
+ .mockImplementation(() => Promise.resolve(data));
140
+
141
+ const dispatched = [];
142
+ await runSaga(
143
+ { dispatch: (action) => dispatched.push(action) },
144
+ handleNewUserRegistration,
145
+ params,
146
+ );
147
+
148
+ expect(registerRequest).toHaveBeenCalledTimes(1);
149
+ expect(dispatched).toEqual([
150
+ actions.registerNewUserBegin(),
151
+ actions.registerNewUserSuccess(camelCaseObject(authenticatedUser), data.redirectUrl, data.success),
152
+ ]);
153
+ registerRequest.mockClear();
154
+ });
155
+
156
+ it('should handle 500 error code', async () => {
157
+ const registerErrorResponse = {
158
+ response: {
159
+ status: 500,
160
+ data: {
161
+ errorCode: INTERNAL_SERVER_ERROR,
162
+ },
163
+ },
164
+ };
165
+
166
+ const registerRequest = jest.spyOn(api, 'registerRequest').mockImplementation(() => Promise.reject(registerErrorResponse));
167
+
168
+ const dispatched = [];
169
+ await runSaga(
170
+ { dispatch: (action) => dispatched.push(action) },
171
+ handleNewUserRegistration,
172
+ params,
173
+ );
174
+
175
+ expect(loggingService.logError).toHaveBeenCalled();
176
+ expect(dispatched).toEqual([
177
+ actions.registerNewUserBegin(),
178
+ actions.registerNewUserFailure(camelCaseObject(registerErrorResponse.response.data)),
179
+ ]);
180
+ registerRequest.mockClear();
181
+ });
182
+
183
+ it('should call service and dispatch error action', async () => {
184
+ const registerErrorResponse = {
185
+ response: {
186
+ status: 400,
187
+ data: {
188
+ error: 'something went wrong',
189
+ },
190
+ },
191
+ };
192
+ const registerRequest = jest.spyOn(api, 'registerRequest')
193
+ .mockImplementation(() => Promise.reject(registerErrorResponse));
194
+
195
+ const dispatched = [];
196
+ await runSaga(
197
+ { dispatch: (action) => dispatched.push(action) },
198
+ handleNewUserRegistration,
199
+ params,
200
+ );
201
+
202
+ expect(registerRequest).toHaveBeenCalledTimes(1);
203
+ expect(loggingService.logInfo).toHaveBeenCalled();
204
+ expect(dispatched).toEqual([
205
+ actions.registerNewUserBegin(),
206
+ actions.registerNewUserFailure(registerErrorResponse.response.data),
207
+ ]);
208
+ registerRequest.mockClear();
209
+ });
210
+
211
+ it('should handle rate limit error code', async () => {
212
+ const registerErrorResponse = {
213
+ response: {
214
+ status: 403,
215
+ data: {
216
+ errorCode: FORBIDDEN_REQUEST,
217
+ },
218
+ },
219
+ };
220
+
221
+ const registerRequest = jest.spyOn(api, 'registerRequest')
222
+ .mockImplementation(() => Promise.reject(registerErrorResponse));
223
+
224
+ const dispatched = [];
225
+ await runSaga(
226
+ { dispatch: (action) => dispatched.push(action) },
227
+ handleNewUserRegistration,
228
+ params,
229
+ );
230
+
231
+ expect(registerRequest).toHaveBeenCalledTimes(1);
232
+ expect(loggingService.logInfo).toHaveBeenCalled();
233
+ expect(dispatched).toEqual([
234
+ actions.registerNewUserBegin(),
235
+ actions.registerNewUserFailure(registerErrorResponse.response.data),
236
+ ]);
237
+ registerRequest.mockClear();
238
+ });
239
+ });