@openedx/frontend-base 1.0.0-alpha.0 → 1.0.0-alpha.2

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 (164) hide show
  1. package/config/types.js +0 -2
  2. package/config/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  3. package/package.json +8 -1
  4. package/runtime/__mocks__/file.js +1 -0
  5. package/runtime/__mocks__/svg.js +1 -0
  6. package/runtime/__mocks__/universal-cookie.js +6 -0
  7. package/runtime/analytics/interface.test.js +242 -0
  8. package/runtime/auth/AxiosJwtAuthService.test.jsx +1076 -0
  9. package/runtime/auth/interceptors/createRetryInterceptor.test.js +23 -0
  10. package/runtime/config/getExternalLinkUrl.test.js +76 -0
  11. package/runtime/i18n/lib.test.js +230 -0
  12. package/runtime/initialize.async.function.config.test.js +43 -0
  13. package/runtime/initialize.const.config.test.js +41 -0
  14. package/runtime/initialize.function.config.test.js +41 -0
  15. package/runtime/initialize.test.js +356 -0
  16. package/runtime/logging/NewRelicLoggingService.test.js +214 -0
  17. package/runtime/react/AuthenticatedPageRoute.test.jsx +135 -0
  18. package/runtime/react/ErrorBoundary.test.jsx +83 -0
  19. package/runtime/react/SiteProvider.test.jsx +66 -0
  20. package/runtime/react/hooks.test.jsx +104 -0
  21. package/runtime/routing/utils.test.ts +7 -0
  22. package/runtime/scripts/GoogleAnalyticsLoader.test.ts +77 -0
  23. package/runtime/site.config.test.tsx +33 -0
  24. package/runtime/slots/Slot.test.tsx +40 -0
  25. package/runtime/slots/layout/DefaultSlotLayout.test.tsx +31 -0
  26. package/runtime/slots/layout/hooks.test.tsx +178 -0
  27. package/runtime/slots/layout/utils.test.ts +67 -0
  28. package/runtime/slots/types.ts +1 -0
  29. package/runtime/slots/utils.test.ts +64 -0
  30. package/runtime/slots/utils.ts +28 -9
  31. package/runtime/testing/initializeMockApp.test.ts +66 -0
  32. package/runtime/utils.test.js +116 -0
  33. package/shell/Logo.test.tsx +32 -0
  34. package/shell/__mocks__/file.js +1 -0
  35. package/shell/__mocks__/svg.js +1 -0
  36. package/shell/__mocks__/universal-cookie.js +6 -0
  37. package/shell/app.ts +14 -0
  38. package/shell/dev/devHome/app.ts +2 -2
  39. package/shell/dev/slotShowcase/app.tsx +9 -9
  40. package/shell/header/app.tsx +3 -3
  41. package/shell/router/createRouter.test.tsx +50 -0
  42. package/shell/router/getAppRoutes.test.tsx +59 -0
  43. package/shell/site.config.dev.tsx +3 -3
  44. package/shell/site.config.test.tsx +16 -0
  45. package/tools/dist/cli/intl-imports.test.js +146 -0
  46. package/tools/dist/cli/openedx.js +1 -15
  47. package/tools/dist/cli/utils/printUsage.js +0 -9
  48. package/tools/dist/types.js +0 -2
  49. package/tools/dist/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  50. package/types.ts +1 -1
  51. package/eslint.config.js +0 -18
  52. package/frontend-base.d.ts +0 -8
  53. package/jest.config.js +0 -7
  54. package/openedx-frontend-base.tgz +0 -0
  55. package/test-site/app.d.ts +0 -15
  56. package/test-site/dist/176.436443549ebb858db483.js +0 -2
  57. package/test-site/dist/176.436443549ebb858db483.js.map +0 -1
  58. package/test-site/dist/362.536eff787d2380fe246c.js +0 -2
  59. package/test-site/dist/362.536eff787d2380fe246c.js.map +0 -1
  60. package/test-site/dist/653.486966b108d224551296.js +0 -2
  61. package/test-site/dist/653.486966b108d224551296.js.map +0 -1
  62. package/test-site/dist/74e025d3fe9a7b7f8503054e2563b353.jpg +0 -0
  63. package/test-site/dist/806.323cf6496ad0a7fe73a7.js +0 -3
  64. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.LICENSE.txt +0 -106
  65. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.map +0 -1
  66. package/test-site/dist/95ec738c0b7faac5b5c9126794446bbd.svg +0 -4
  67. package/test-site/dist/app.612058b36c74787759ac.css +0 -61
  68. package/test-site/dist/app.612058b36c74787759ac.css.map +0 -1
  69. package/test-site/dist/app.612058b36c74787759ac.js +0 -2
  70. package/test-site/dist/app.612058b36c74787759ac.js.map +0 -1
  71. package/test-site/dist/cb28cdb1468c915e27e5cec9af64f22f.svg +0 -1
  72. package/test-site/dist/index.html +0 -1
  73. package/test-site/dist/report.html +0 -39
  74. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js +0 -2
  75. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js.map +0 -1
  76. package/test-site/eslint.config.js +0 -12
  77. package/test-site/package-lock.json +0 -19226
  78. package/test-site/package.json +0 -29
  79. package/test-site/public/index.html +0 -10
  80. package/test-site/site.config.build.tsx +0 -27
  81. package/test-site/site.config.dev.tsx +0 -27
  82. package/test-site/src/authenticated-page/AuthenticatedPage.tsx +0 -18
  83. package/test-site/src/authenticated-page/i18n/index.ts +0 -27
  84. package/test-site/src/authenticated-page/index.tsx +0 -28
  85. package/test-site/src/example-page/ExamplePage.tsx +0 -79
  86. package/test-site/src/example-page/Image.tsx +0 -11
  87. package/test-site/src/example-page/ParagonPreview.jsx +0 -66
  88. package/test-site/src/example-page/apple.jpg +0 -0
  89. package/test-site/src/example-page/apple.svg +0 -1
  90. package/test-site/src/example-page/index.ts +0 -16
  91. package/test-site/src/i18n/README.md +0 -3
  92. package/test-site/src/i18n/messages/frontend-app-sample/ar.json +0 -4
  93. package/test-site/src/i18n/messages/frontend-app-sample/eo.json +0 -1
  94. package/test-site/src/i18n/messages/frontend-app-sample/es_419.json +0 -4
  95. package/test-site/src/i18n/messages/frontend-component-emptylangs/ar.json +0 -1
  96. package/test-site/src/i18n/messages/frontend-component-singlelang/ar.json +0 -3
  97. package/test-site/src/iframe-widget/IframeWidget.tsx +0 -14
  98. package/test-site/src/iframe-widget/index.ts +0 -16
  99. package/test-site/src/index.tsx +0 -3
  100. package/test-site/src/messages.js +0 -11
  101. package/test-site/src/site.scss +0 -11
  102. package/test-site/tsconfig.json +0 -14
  103. package/tools/babel/babel.config.js +0 -27
  104. package/tools/babel.config.js +0 -3
  105. package/tools/cli/README.md +0 -29
  106. package/tools/cli/commands/pack.ts +0 -9
  107. package/tools/cli/commands/release.ts +0 -27
  108. package/tools/cli/commands/serve.ts +0 -43
  109. package/tools/cli/intl-imports.ts +0 -274
  110. package/tools/cli/openedx.ts +0 -101
  111. package/tools/cli/transifex-utils.ts +0 -75
  112. package/tools/cli/utils/ensureConfigFilenameOption.ts +0 -40
  113. package/tools/cli/utils/formatter.ts +0 -10
  114. package/tools/cli/utils/getResolvedConfigPath.ts +0 -23
  115. package/tools/cli/utils/prettyPrintTitle.ts +0 -15
  116. package/tools/cli/utils/printUsage.ts +0 -53
  117. package/tools/config-helpers/createConfig.ts +0 -8
  118. package/tools/config-helpers/createLintConfig.ts +0 -14
  119. package/tools/config-helpers/getBaseConfig.ts +0 -11
  120. package/tools/defaultConfigPaths.ts +0 -30
  121. package/tools/dist/cli/commands/pack.js +0 -14
  122. package/tools/dist/cli/commands/release.js +0 -28
  123. package/tools/eslint/base.eslint.config.js +0 -124
  124. package/tools/eslint/modules.d.ts +0 -5
  125. package/tools/eslint.config.js +0 -15
  126. package/tools/index.ts +0 -3
  127. package/tools/jest/jest.config.js +0 -30
  128. package/tools/jest.config.js +0 -19
  129. package/tools/tsconfig.json +0 -24
  130. package/tools/types.ts +0 -21
  131. package/tools/typescript/tsconfig.json +0 -32
  132. package/tools/webpack/common-config/README.md +0 -15
  133. package/tools/webpack/common-config/all/getCodeRules.ts +0 -51
  134. package/tools/webpack/common-config/all/getFileLoaderRules.ts +0 -23
  135. package/tools/webpack/common-config/all/getIgnoreWarnings.ts +0 -13
  136. package/tools/webpack/common-config/all/getImageMinimizer.ts +0 -26
  137. package/tools/webpack/common-config/all/getStylesheetRule.ts +0 -111
  138. package/tools/webpack/common-config/dev/getDevServer.ts +0 -35
  139. package/tools/webpack/common-config/index.ts +0 -6
  140. package/tools/webpack/common-config/site/getHtmlWebpackPlugin.ts +0 -11
  141. package/tools/webpack/modules.d.ts +0 -6
  142. package/tools/webpack/plugins/html-webpack-new-relic-plugin/HtmlWebpackNewRelicPlugin.ts +0 -102
  143. package/tools/webpack/plugins/html-webpack-new-relic-plugin/LICENSE +0 -21
  144. package/tools/webpack/plugins/html-webpack-new-relic-plugin/README.md +0 -7
  145. package/tools/webpack/plugins/html-webpack-new-relic-plugin/index.js +0 -3
  146. package/tools/webpack/plugins/html-webpack-new-relic-plugin/test/fixtures/entry.js +0 -1
  147. package/tools/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.ts +0 -134
  148. package/tools/webpack/plugins/paragon-webpack-plugin/index.ts +0 -3
  149. package/tools/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.ts +0 -71
  150. package/tools/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.ts +0 -72
  151. package/tools/webpack/plugins/paragon-webpack-plugin/utils/index.ts +0 -6
  152. package/tools/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.ts +0 -131
  153. package/tools/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.ts +0 -144
  154. package/tools/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.ts +0 -106
  155. package/tools/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.ts +0 -54
  156. package/tools/webpack/types.ts +0 -69
  157. package/tools/webpack/utils/getLocalAliases.ts +0 -65
  158. package/tools/webpack/utils/getPublicPath.ts +0 -3
  159. package/tools/webpack/utils/getResolvedSiteConfigPath.ts +0 -28
  160. package/tools/webpack/utils/paragonUtils.ts +0 -152
  161. package/tools/webpack/webpack.config.build.ts +0 -93
  162. package/tools/webpack/webpack.config.dev.shell.ts +0 -122
  163. package/tools/webpack/webpack.config.dev.ts +0 -90
  164. package/tsconfig.json +0 -23
@@ -0,0 +1,356 @@
1
+ import {
2
+ SITE_ANALYTICS_INITIALIZED,
3
+ SITE_AUTH_INITIALIZED,
4
+ SITE_CONFIG_INITIALIZED,
5
+ SITE_I18N_INITIALIZED,
6
+ SITE_INIT_ERROR,
7
+ SITE_LOGGING_INITIALIZED,
8
+ SITE_PUBSUB_INITIALIZED,
9
+ SITE_READY,
10
+ } from './constants';
11
+ import { initialize } from './initialize';
12
+
13
+ import { configureAnalytics, SegmentAnalyticsService } from './analytics';
14
+ import {
15
+ AxiosJwtAuthService,
16
+ configureAuth,
17
+ ensureAuthenticatedUser,
18
+ fetchAuthenticatedUser,
19
+ getAuthenticatedHttpClient,
20
+ getAuthenticatedUser,
21
+ hydrateAuthenticatedUser,
22
+ } from './auth';
23
+ import configureCache from './auth/LocalForageCache';
24
+ import { getSiteConfig, mergeSiteConfig } from './config';
25
+ import { configureI18n } from './i18n';
26
+ import {
27
+ configureLogging,
28
+ getLoggingService,
29
+ logError,
30
+ NewRelicLoggingService,
31
+ } from './logging';
32
+ import { clearAllSubscriptions, subscribe } from './subscriptions';
33
+
34
+ jest.mock('./logging');
35
+ jest.mock('./auth');
36
+ jest.mock('./analytics');
37
+ jest.mock('./i18n');
38
+ jest.mock('./auth/LocalForageCache');
39
+
40
+ let config = null;
41
+ const newConfig = {
42
+ common: {
43
+ siteName: 'Test Case',
44
+ accessTokenCookieName: 'edx-jwt-cookie-header-payload',
45
+ csrfTokenApiPath: '/csrf/api/v1/token',
46
+ languagePreferenceCookieName: 'openedx-language-preference',
47
+ lmsBaseUrl: 'http://test.example.com:18000',
48
+ loginUrl: 'http://test.example.com:18000/login',
49
+ logoutUrl: 'http://test.example.com:18000/logout',
50
+ refreshAccessTokenApiPath: '/login_refresh',
51
+ segmentKey: '',
52
+ userInfoCookieName: 'edx-user-info',
53
+ ignoredErrorRegex: '',
54
+ },
55
+ auth: {
56
+ INFO_EMAIL: 'openedx@example.com',
57
+ ACTIVATION_EMAIL_SUPPORT_LINK: 'http//support.test.com',
58
+ },
59
+ learning: {
60
+ LEGACY_THEME_NAME: 'example',
61
+ DISCUSSIONS_MFE_BASE_URL: 'http://test.example.com:2002',
62
+ },
63
+ };
64
+
65
+ describe('initialize', () => {
66
+ beforeEach(() => {
67
+ config = getSiteConfig();
68
+ fetchAuthenticatedUser.mockReset();
69
+ ensureAuthenticatedUser.mockReset();
70
+ hydrateAuthenticatedUser.mockReset();
71
+ logError.mockReset();
72
+ clearAllSubscriptions();
73
+ });
74
+
75
+ it('should call default handlers in the absence of overrides', async () => {
76
+ const expectedEvents = [
77
+ SITE_PUBSUB_INITIALIZED,
78
+ SITE_CONFIG_INITIALIZED,
79
+ SITE_LOGGING_INITIALIZED,
80
+ SITE_AUTH_INITIALIZED,
81
+ SITE_ANALYTICS_INITIALIZED,
82
+ SITE_I18N_INITIALIZED,
83
+ SITE_READY,
84
+ ];
85
+
86
+ function checkDispatchedDone(eventName) {
87
+ const index = expectedEvents.indexOf(eventName);
88
+ if (index > -1) {
89
+ expectedEvents.splice(index, 1);
90
+ } else {
91
+ throw new Error(`Unexpected event dispatched! ${eventName}`);
92
+ }
93
+ }
94
+
95
+ subscribe(SITE_PUBSUB_INITIALIZED, checkDispatchedDone);
96
+ subscribe(SITE_CONFIG_INITIALIZED, checkDispatchedDone);
97
+ subscribe(SITE_LOGGING_INITIALIZED, checkDispatchedDone);
98
+ subscribe(SITE_AUTH_INITIALIZED, checkDispatchedDone);
99
+ subscribe(SITE_ANALYTICS_INITIALIZED, checkDispatchedDone);
100
+ subscribe(SITE_I18N_INITIALIZED, checkDispatchedDone);
101
+ subscribe(SITE_READY, checkDispatchedDone);
102
+
103
+ const messages = { i_am: 'a message' };
104
+ await initialize({ messages });
105
+
106
+ expect(configureLogging).toHaveBeenCalledWith(NewRelicLoggingService, { config });
107
+ expect(configureAuth).toHaveBeenCalledWith(AxiosJwtAuthService, {
108
+ loggingService: getLoggingService(),
109
+ config,
110
+ middleware: [],
111
+ });
112
+ expect(configureAnalytics).toHaveBeenCalledWith(SegmentAnalyticsService, {
113
+ config,
114
+ loggingService: getLoggingService(),
115
+ httpClient: getAuthenticatedHttpClient(),
116
+ });
117
+ expect(configureI18n).toHaveBeenCalledWith({
118
+ messages,
119
+ });
120
+ expect(fetchAuthenticatedUser).toHaveBeenCalled();
121
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
122
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
123
+ expect(logError).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it('should call ensureAuthenticatedUser', async () => {
127
+ const messages = { i_am: 'a message' };
128
+ await initialize({ messages, requireAuthenticatedUser: true });
129
+
130
+ expect(fetchAuthenticatedUser).not.toHaveBeenCalled();
131
+ expect(ensureAuthenticatedUser).toHaveBeenCalled();
132
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
133
+ expect(logError).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('should call hydrateAuthenticatedUser if option is set and authenticated', async () => {
137
+ getAuthenticatedUser.mockReturnValue({ userId: 'abc123', username: 'Barry' });
138
+
139
+ const messages = { i_am: 'a message' };
140
+ await initialize({ messages, hydrateAuthenticatedUser: true });
141
+
142
+ expect(fetchAuthenticatedUser).toHaveBeenCalled();
143
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
144
+ expect(hydrateAuthenticatedUser).toHaveBeenCalled();
145
+ expect(logError).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('should not call hydrateAuthenticatedUser if option is set but anonymous', async () => {
149
+ getAuthenticatedUser.mockReturnValue(null);
150
+
151
+ const messages = { i_am: 'a message' };
152
+ await initialize({ messages, hydrateAuthenticatedUser: true });
153
+
154
+ expect(fetchAuthenticatedUser).toHaveBeenCalled();
155
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
156
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
157
+ expect(logError).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it('should call override handlers if they exist', async () => {
161
+ const overrideHandlers = {
162
+ pubSub: jest.fn(),
163
+ config: jest.fn(),
164
+ logging: jest.fn(),
165
+ auth: jest.fn(),
166
+ analytics: jest.fn(),
167
+ i18n: jest.fn(),
168
+ ready: jest.fn(),
169
+ initError: jest.fn(),
170
+ };
171
+
172
+ await initialize({
173
+ messages: null,
174
+ handlers: overrideHandlers,
175
+ });
176
+
177
+ expect(overrideHandlers.pubSub).toHaveBeenCalled();
178
+ expect(overrideHandlers.config).toHaveBeenCalled();
179
+ expect(overrideHandlers.logging).toHaveBeenCalled();
180
+ expect(overrideHandlers.auth).toHaveBeenCalled();
181
+ expect(overrideHandlers.analytics).toHaveBeenCalled();
182
+ expect(overrideHandlers.i18n).toHaveBeenCalled();
183
+ expect(overrideHandlers.ready).toHaveBeenCalled();
184
+ expect(overrideHandlers.initError).not.toHaveBeenCalled();
185
+ expect(fetchAuthenticatedUser).not.toHaveBeenCalled();
186
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
187
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
188
+ expect(logError).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it('should call the default initError handler if something throws', async () => {
192
+ const overrideHandlers = {
193
+ pubSub: jest.fn(() => {
194
+ throw new Error('uhoh!');
195
+ }),
196
+ config: jest.fn(),
197
+ logging: jest.fn(),
198
+ auth: jest.fn(),
199
+ analytics: jest.fn(),
200
+ i18n: jest.fn(),
201
+ ready: jest.fn(),
202
+ };
203
+
204
+ function errorHandler(eventName, data) {
205
+ expect(eventName).toEqual(SITE_INIT_ERROR);
206
+ expect(data).toEqual(new Error('uhoh!'));
207
+ }
208
+
209
+ subscribe(SITE_INIT_ERROR, errorHandler);
210
+
211
+ await initialize({
212
+ messages: null,
213
+ handlers: overrideHandlers,
214
+ });
215
+
216
+ expect(overrideHandlers.pubSub).toHaveBeenCalled();
217
+ expect(overrideHandlers.config).not.toHaveBeenCalled();
218
+ expect(overrideHandlers.logging).not.toHaveBeenCalled();
219
+ expect(overrideHandlers.auth).not.toHaveBeenCalled();
220
+ expect(overrideHandlers.analytics).not.toHaveBeenCalled();
221
+ expect(overrideHandlers.i18n).not.toHaveBeenCalled();
222
+ expect(overrideHandlers.ready).not.toHaveBeenCalled();
223
+ expect(logError).toHaveBeenCalledWith(new Error('uhoh!'));
224
+ });
225
+
226
+ it('should call the override initError handler if something throws', async () => {
227
+ const overrideHandlers = {
228
+ pubSub: jest.fn(() => {
229
+ throw new Error('uhoh!');
230
+ }),
231
+ config: jest.fn(),
232
+ logging: jest.fn(),
233
+ auth: jest.fn(),
234
+ analytics: jest.fn(),
235
+ i18n: jest.fn(),
236
+ ready: jest.fn(),
237
+ initError: jest.fn(),
238
+ };
239
+
240
+ function errorHandler(eventName, data) {
241
+ expect(eventName).toEqual(SITE_INIT_ERROR);
242
+ expect(data).toEqual(new Error('uhoh!'));
243
+ }
244
+
245
+ subscribe(SITE_INIT_ERROR, errorHandler);
246
+
247
+ await initialize({
248
+ messages: null,
249
+ handlers: overrideHandlers,
250
+ });
251
+
252
+ expect(overrideHandlers.pubSub).toHaveBeenCalled();
253
+ expect(overrideHandlers.config).not.toHaveBeenCalled();
254
+ expect(overrideHandlers.logging).not.toHaveBeenCalled();
255
+ expect(overrideHandlers.auth).not.toHaveBeenCalled();
256
+ expect(overrideHandlers.analytics).not.toHaveBeenCalled();
257
+ expect(overrideHandlers.i18n).not.toHaveBeenCalled();
258
+ expect(overrideHandlers.ready).not.toHaveBeenCalled();
259
+ expect(overrideHandlers.initError).toHaveBeenCalledWith(new Error('uhoh!'));
260
+ });
261
+
262
+ it('should initialize the app with runtime configuration', async () => {
263
+ configureCache.mockReturnValueOnce(Promise.resolve({
264
+ get: (url) => {
265
+ const params = new URL(url).search;
266
+ const mfe = new URLSearchParams(params).get('mfe');
267
+ return ({ data: { ...newConfig.common, ...newConfig[mfe] } });
268
+ },
269
+ }));
270
+
271
+ const messages = { i_am: 'a message' };
272
+ await initialize({
273
+ messages,
274
+ handlers: {
275
+ config: () => {
276
+ mergeSiteConfig({
277
+ mfeConfigApiUrl: 'http://localhost:18000/api/mfe/v1/config',
278
+ siteId: 'auth',
279
+ });
280
+ }
281
+ }
282
+ });
283
+
284
+ expect(configureCache).toHaveBeenCalled();
285
+ expect(configureLogging).toHaveBeenCalledWith(NewRelicLoggingService, { config });
286
+ expect(configureAuth).toHaveBeenCalledWith(AxiosJwtAuthService, {
287
+ loggingService: getLoggingService(),
288
+ config,
289
+ middleware: [],
290
+ });
291
+ expect(configureAnalytics).toHaveBeenCalledWith(SegmentAnalyticsService, {
292
+ config,
293
+ loggingService: getLoggingService(),
294
+ httpClient: getAuthenticatedHttpClient(),
295
+ });
296
+ expect(configureI18n).toHaveBeenCalledWith({
297
+ messages,
298
+ });
299
+
300
+ expect(fetchAuthenticatedUser).toHaveBeenCalled();
301
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
302
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
303
+ expect(logError).not.toHaveBeenCalled();
304
+ expect(getSiteConfig().siteName).toBe(newConfig.common.siteName);
305
+ expect(getSiteConfig().INFO_EMAIL).toBe(newConfig.auth.INFO_EMAIL);
306
+ expect(Object.values(getSiteConfig()).includes(newConfig.learning.DISCUSSIONS_MFE_BASE_URL)).toBeFalsy();
307
+ });
308
+
309
+ describe('with mocked console.error', () => {
310
+ beforeEach(() => {
311
+ console.error = jest.fn();
312
+ });
313
+
314
+ afterAll(() => {
315
+ console.error.mockRestore();
316
+ });
317
+
318
+ it('should initialize the app with the build config when runtime configuration fails', async () => {
319
+ configureCache.mockRejectedValueOnce(new Error('Api fails'));
320
+
321
+ const messages = { i_am: 'a message' };
322
+ await initialize({
323
+ messages,
324
+ handlers: {
325
+ config: () => {
326
+ mergeSiteConfig({
327
+ mfeConfigApiUrl: 'http://localhost:18000/api/mfe/v1/config',
328
+ siteId: 'auth',
329
+ });
330
+ }
331
+ }
332
+ });
333
+
334
+ expect(configureCache).toHaveBeenCalled();
335
+ expect(console.error).toHaveBeenCalledWith('Error with config API', 'Api fails');
336
+ expect(configureLogging).toHaveBeenCalledWith(NewRelicLoggingService, { config });
337
+ expect(configureAuth).toHaveBeenCalledWith(AxiosJwtAuthService, {
338
+ loggingService: getLoggingService(),
339
+ config,
340
+ middleware: [],
341
+ });
342
+ expect(configureAnalytics).toHaveBeenCalledWith(SegmentAnalyticsService, {
343
+ config,
344
+ loggingService: getLoggingService(),
345
+ httpClient: getAuthenticatedHttpClient(),
346
+ });
347
+ expect(configureI18n).toHaveBeenCalledWith({
348
+ messages,
349
+ });
350
+ expect(fetchAuthenticatedUser).toHaveBeenCalled();
351
+ expect(ensureAuthenticatedUser).not.toHaveBeenCalled();
352
+ expect(hydrateAuthenticatedUser).not.toHaveBeenCalled();
353
+ expect(logError).not.toHaveBeenCalled();
354
+ });
355
+ });
356
+ });
@@ -0,0 +1,214 @@
1
+ import NewRelicLoggingService, { MAX_ERROR_LENGTH } from './NewRelicLoggingService';
2
+
3
+ global.newrelic = {
4
+ addPageAction: jest.fn(),
5
+ noticeError: jest.fn(),
6
+ setCustomAttribute: jest.fn(),
7
+ };
8
+
9
+ let service = null;
10
+ const configWithIgnoredErrors = {
11
+ config: {
12
+ ignoredErrorRegex: /^Ignore this error|very minor/,
13
+ },
14
+ };
15
+ const configWithNullIgnoredErrors = {
16
+ config: {
17
+ ignoredErrorRegex: null,
18
+ },
19
+ };
20
+ const configWithEmptyIgnoredErrors = {
21
+ config: {
22
+ ignoredErrorRegex: '',
23
+ },
24
+ };
25
+ const configWithWhitespaceIgnoredErrors = {
26
+ config: {
27
+ ignoredErrorRegex: ' ',
28
+ },
29
+ };
30
+ const configWithMissingIgnoredErrors = {
31
+ config: {},
32
+ };
33
+
34
+ describe('NewRelicLoggingService', () => {
35
+ beforeEach(() => {
36
+ service = new NewRelicLoggingService(configWithIgnoredErrors);
37
+ });
38
+
39
+ describe('logInfo', () => {
40
+ beforeEach(() => {
41
+ global.newrelic.addPageAction.mockReset();
42
+ });
43
+
44
+ it('calls New Relic client to log message if the client is available', () => {
45
+ const message = 'Test log';
46
+ service.logInfo(message);
47
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', { message });
48
+ });
49
+
50
+ it('handles plain string message properly with custom attributes', () => {
51
+ const message = 'Test log';
52
+ const attrs = { a: 1, b: 'red', c: 3 };
53
+ service.logInfo(message, attrs);
54
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', {
55
+ message, ...attrs,
56
+ });
57
+ });
58
+
59
+ it('handles plain string message properly with no custom attributes', () => {
60
+ const message = 'Test log';
61
+ service.logInfo(message);
62
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', {
63
+ message,
64
+ });
65
+ });
66
+
67
+ it('handles error object properly with custom attributes', () => {
68
+ const message = 'Test log';
69
+ const attrs = { a: 1, b: 'red', c: 3 };
70
+ const err = { message, customAttributes: attrs };
71
+ service.logInfo(err);
72
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', {
73
+ message, ...attrs,
74
+ });
75
+ });
76
+
77
+ it('handles error object properly with no custom attributes', () => {
78
+ const message = 'Test log';
79
+ const err = { message };
80
+ service.logInfo(err);
81
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', {
82
+ message,
83
+ });
84
+ });
85
+
86
+ it('handles error object properly with custom attributes in object and param', () => {
87
+ const message = 'Test log';
88
+ const attrsObj = { a: 1, b: 'red', c: 3 };
89
+ const attrsParam = { x: 99, y: 'blue', z: 987 };
90
+ const err = { message, customAttributes: attrsObj };
91
+ service.logInfo(err, attrsParam);
92
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', {
93
+ message, ...attrsObj, ...attrsParam,
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('logError', () => {
99
+ beforeEach(() => {
100
+ global.newrelic.noticeError.mockReset();
101
+ });
102
+
103
+ it('calls New Relic client to log error if the client is available', () => {
104
+ const error = new Error('Failed!');
105
+ service.logError(error);
106
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
107
+ });
108
+
109
+ it('calls New Relic client to log error if the client is available', () => {
110
+ const error = new Error('Failed!');
111
+ service.logError(error);
112
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
113
+ });
114
+
115
+ it('calls New Relic client to log error and merges in customAttributes from the error', () => {
116
+ const error = new Error('Failed!');
117
+ error.customAttributes = {
118
+ boo: 'yah',
119
+ foo: 'gah',
120
+ };
121
+ service.logError(error, { foo: 'wins', bar: 'baz' });
122
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, { boo: 'yah', foo: 'wins', bar: 'baz' });
123
+ });
124
+
125
+ it('calls New Relic client with truncated error string', () => {
126
+ const error = new Array(MAX_ERROR_LENGTH + 500 + 1).join('0');
127
+ const expectedError = new Array(MAX_ERROR_LENGTH + 1).join('0');
128
+ service.logError(error);
129
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(expectedError, undefined);
130
+ });
131
+
132
+ it('calls New Relic client with truncated error', () => {
133
+ const error = {
134
+ message: new Array(MAX_ERROR_LENGTH + 500 + 1).join('0'),
135
+ };
136
+ const expectedError = {
137
+ message: new Array(MAX_ERROR_LENGTH + 1).join('0'),
138
+ };
139
+ service.logError(error);
140
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(expectedError, undefined);
141
+ });
142
+ });
143
+
144
+ describe('setCustomAttribute', () => {
145
+ beforeEach(() => {
146
+ global.newrelic.setCustomAttribute.mockReset();
147
+ });
148
+
149
+ it('calls New Relic client with name and value', () => {
150
+ service.setCustomAttribute('foo', 'bar');
151
+ expect(global.newrelic.setCustomAttribute).toHaveBeenCalledWith('foo', 'bar');
152
+ });
153
+ });
154
+
155
+ describe('ignoredErrors', () => {
156
+ beforeEach(() => {
157
+ global.newrelic.addPageAction.mockReset();
158
+ global.newrelic.noticeError.mockReset();
159
+ });
160
+
161
+ it('calls New Relic client as error objects but ignored and sent as page action', () => {
162
+ const error1 = new Error('Ignore this error!');
163
+ error1.customAttributes = {
164
+ hi: 'hello',
165
+ };
166
+ service.logError(error1);
167
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('IGNORED_ERROR', {
168
+ message: error1.message, ...error1.customAttributes,
169
+ });
170
+
171
+ const error2 = new Error('very minor');
172
+ service.logError(error2);
173
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('IGNORED_ERROR', {
174
+ message: error2.message,
175
+ });
176
+ });
177
+
178
+ it('calls New Relic client as error string but ignored and sent as page action', () => {
179
+ const error = 'Ignore this error!';
180
+ service.logError(error);
181
+ expect(global.newrelic.addPageAction).toHaveBeenCalledWith('IGNORED_ERROR', {
182
+ message: error,
183
+ });
184
+ });
185
+
186
+ it('calls New Relic client as error object but with null ignored error config', () => {
187
+ service = new NewRelicLoggingService(configWithNullIgnoredErrors);
188
+ const error = new Error('Ignore this error!');
189
+ service.logError(error);
190
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
191
+ });
192
+
193
+ it('calls New Relic client as error object but with empty ignored error config', () => {
194
+ service = new NewRelicLoggingService(configWithEmptyIgnoredErrors);
195
+ const error = new Error('Ignore this error!');
196
+ service.logError(error);
197
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
198
+ });
199
+
200
+ it('calls New Relic client as error object but with whitespace-only ignored error config', () => {
201
+ service = new NewRelicLoggingService(configWithWhitespaceIgnoredErrors);
202
+ const error = new Error('Ignore this error!');
203
+ service.logError(error);
204
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
205
+ });
206
+
207
+ it('calls New Relic client as error object but with missing ignored error config', () => {
208
+ service = new NewRelicLoggingService(configWithMissingIgnoredErrors);
209
+ const error = new Error('Ignore this error!');
210
+ service.logError(error);
211
+ expect(global.newrelic.noticeError).toHaveBeenCalledWith(error, undefined);
212
+ });
213
+ });
214
+ });
@@ -0,0 +1,135 @@
1
+ /* eslint-disable react/jsx-no-constructed-context-values */
2
+ import { render } from '@testing-library/react';
3
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
4
+ import { sendPageEvent } from '../analytics';
5
+ import { getAuthenticatedUser, getLoginRedirectUrl } from '../auth';
6
+ import { getSiteConfig } from '../config';
7
+ import SiteContext from './SiteContext';
8
+ import AuthenticatedPageRoute from './AuthenticatedPageRoute';
9
+
10
+ jest.mock('../subscriptions');
11
+ jest.mock('../analytics');
12
+ jest.mock('../auth');
13
+
14
+ describe('AuthenticatedPageRoute', () => {
15
+ const { location } = global;
16
+
17
+ beforeEach(() => {
18
+ delete global.location;
19
+ global.location = {
20
+ assign: jest.fn(),
21
+ };
22
+ sendPageEvent.mockReset();
23
+ getLoginRedirectUrl.mockReset();
24
+ getAuthenticatedUser.mockReset();
25
+ });
26
+
27
+ afterEach(() => {
28
+ global.location = location;
29
+ });
30
+
31
+ it('should redirect to login if not authenticated', () => {
32
+ getAuthenticatedUser.mockReturnValue(null);
33
+ getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
34
+ const component = (
35
+ <SiteContext.Provider
36
+ value={{
37
+ authenticatedUser: getAuthenticatedUser(),
38
+ siteConfig: getSiteConfig(),
39
+ }}
40
+ >
41
+ <MemoryRouter initialEntries={['/authenticated']}>
42
+ <Routes>
43
+ <Route path="/" element={() => <p>Anonymous</p>} />
44
+ <Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
45
+ </Routes>
46
+ </MemoryRouter>
47
+ </SiteContext.Provider>
48
+ );
49
+ global.location.href = 'http://localhost/authenticated';
50
+ render(component);
51
+ expect(getLoginRedirectUrl).toHaveBeenCalledWith('http://localhost/authenticated');
52
+ expect(sendPageEvent).not.toHaveBeenCalled();
53
+ expect(global.location.assign).toHaveBeenCalledWith('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
54
+ });
55
+
56
+ it('should redirect to custom redirect URL if not authenticated', () => {
57
+ getAuthenticatedUser.mockReturnValue(null);
58
+ getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
59
+ const authenticatedElement = (
60
+ <AuthenticatedPageRoute redirectUrl="http://localhost/elsewhere">
61
+ <p>Authenticated</p>
62
+ </AuthenticatedPageRoute>
63
+ );
64
+ const component = (
65
+ <SiteContext.Provider
66
+ value={{
67
+ authenticatedUser: getAuthenticatedUser(),
68
+ siteConfig: getSiteConfig(),
69
+ }}
70
+ >
71
+ <MemoryRouter initialEntries={['/authenticated']}>
72
+ <Routes>
73
+ <Route path="/" component={() => <p>Anonymous</p>} />
74
+ <Route path="/authenticated" element={authenticatedElement} />
75
+ </Routes>
76
+ </MemoryRouter>
77
+ </SiteContext.Provider>
78
+ );
79
+ render(component);
80
+ expect(getLoginRedirectUrl).not.toHaveBeenCalled();
81
+ expect(sendPageEvent).not.toHaveBeenCalled();
82
+ expect(global.location.assign).toHaveBeenCalledWith('http://localhost/elsewhere');
83
+ });
84
+
85
+ it('should not call login if not the current route', () => {
86
+ getAuthenticatedUser.mockReturnValue(null);
87
+ getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
88
+ const component = (
89
+ <SiteContext.Provider
90
+ value={{
91
+ authenticatedUser: getAuthenticatedUser(),
92
+ siteConfig: getSiteConfig(),
93
+ }}
94
+ >
95
+ <MemoryRouter>
96
+ <Routes>
97
+ <Route path="/" element={<p>Anonymous</p>} />
98
+ <Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
99
+ </Routes>
100
+ </MemoryRouter>
101
+ </SiteContext.Provider>
102
+ );
103
+ const wrapper = render(component);
104
+
105
+ expect(getLoginRedirectUrl).not.toHaveBeenCalled();
106
+ expect(global.location.assign).not.toHaveBeenCalled();
107
+ expect(sendPageEvent).not.toHaveBeenCalled();
108
+ const element = wrapper.container.querySelector('p');
109
+ expect(element.textContent).toEqual('Anonymous'); // This is just a sanity check on our setup.
110
+ });
111
+
112
+ it('should render authenticated route if authenticated', () => {
113
+ const component = (
114
+ <SiteContext.Provider
115
+ value={{
116
+ authenticatedUser: { userId: 12345, username: 'edx' },
117
+ siteConfig: getSiteConfig(),
118
+ }}
119
+ >
120
+ <MemoryRouter initialEntries={['/authenticated']}>
121
+ <Routes>
122
+ <Route path="/" element={<p>Anonymous</p>} />
123
+ <Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
124
+ </Routes>
125
+ </MemoryRouter>
126
+ </SiteContext.Provider>
127
+ );
128
+ const wrapper = render(component);
129
+ expect(getLoginRedirectUrl).not.toHaveBeenCalled();
130
+ expect(global.location.assign).not.toHaveBeenCalled();
131
+ expect(sendPageEvent).toHaveBeenCalled();
132
+ const element = wrapper.container.querySelector('p');
133
+ expect(element.textContent).toEqual('Authenticated');
134
+ });
135
+ });