@salesforce/pwa-kit-runtime 3.8.0-preview.0-basepath → 3.8.0-preview.2-basepath

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 (52) hide show
  1. package/package.json +6 -5
  2. package/ssr/server/build-remote-server.js +1159 -0
  3. package/ssr/server/build-remote-server.test.js +41 -0
  4. package/ssr/server/constants.js +37 -0
  5. package/ssr/server/express.js +462 -0
  6. package/ssr/server/express.lambda.test.js +390 -0
  7. package/ssr/server/express.test.js +963 -0
  8. package/ssr/server/test_fixtures/favicon.ico +0 -0
  9. package/ssr/server/test_fixtures/loadable-stats.json +1 -0
  10. package/ssr/server/test_fixtures/localhost.pem +45 -0
  11. package/ssr/server/test_fixtures/main.js +7 -0
  12. package/ssr/server/test_fixtures/mobify.png +0 -0
  13. package/ssr/server/test_fixtures/server-renderer.js +12 -0
  14. package/ssr/server/test_fixtures/worker.js +7 -0
  15. package/ssr/server/test_fixtures/worker.js.map +1 -0
  16. package/utils/logger-factory.js +154 -0
  17. package/utils/logger-factory.test.js +71 -0
  18. package/utils/logger-instance.js +19 -0
  19. package/utils/middleware/index.js +16 -0
  20. package/utils/middleware/security.js +111 -0
  21. package/utils/middleware/security.test.js +110 -0
  22. package/utils/morgan-stream.js +28 -0
  23. package/utils/ssr-cache.js +177 -0
  24. package/utils/ssr-cache.test.js +64 -0
  25. package/utils/ssr-config.client.js +23 -0
  26. package/utils/ssr-config.client.test.js +25 -0
  27. package/utils/ssr-config.js +20 -0
  28. package/utils/ssr-config.server.js +98 -0
  29. package/utils/ssr-config.server.test.js +50 -0
  30. package/utils/ssr-namespace-paths.js +60 -0
  31. package/utils/ssr-namespace-paths.test.js +30 -0
  32. package/utils/ssr-paths.js +51 -0
  33. package/utils/ssr-paths.test.js +49 -0
  34. package/utils/ssr-proxying.js +859 -0
  35. package/utils/ssr-proxying.test.js +593 -0
  36. package/utils/ssr-request-processing.js +164 -0
  37. package/utils/ssr-request-processing.test.js +95 -0
  38. package/utils/ssr-server/cached-response.js +116 -0
  39. package/utils/ssr-server/configure-proxy.js +304 -0
  40. package/utils/ssr-server/metrics-sender.js +204 -0
  41. package/utils/ssr-server/outgoing-request-hook.js +139 -0
  42. package/utils/ssr-server/parse-end-parameters.js +38 -0
  43. package/utils/ssr-server/process-express-response.js +56 -0
  44. package/utils/ssr-server/process-lambda-response.js +43 -0
  45. package/utils/ssr-server/update-global-agent-options.js +41 -0
  46. package/utils/ssr-server/update-global-agent-options.test.js +28 -0
  47. package/utils/ssr-server/utils.js +124 -0
  48. package/utils/ssr-server/utils.test.js +69 -0
  49. package/utils/ssr-server/wrap-response-write.js +40 -0
  50. package/utils/ssr-server.js +115 -0
  51. package/utils/ssr-server.test.js +869 -0
  52. package/utils/ssr-shared.js +183 -0
@@ -0,0 +1,963 @@
1
+ "use strict";
2
+
3
+ var _fsExtra = _interopRequireDefault(require("fs-extra"));
4
+ var _https = _interopRequireDefault(require("https"));
5
+ var _nock = _interopRequireDefault(require("nock"));
6
+ var _os = _interopRequireDefault(require("os"));
7
+ var _path = _interopRequireDefault(require("path"));
8
+ var _sinon = _interopRequireDefault(require("sinon"));
9
+ var _superagent = _interopRequireDefault(require("superagent"));
10
+ var _supertest = _interopRequireDefault(require("supertest"));
11
+ var _express = _interopRequireDefault(require("express"));
12
+ var _ssrCache = require("../../utils/ssr-cache");
13
+ var _ssrServer = require("../../utils/ssr-server");
14
+ var ssrServerUtils = _interopRequireWildcard(require("../../utils/ssr-server/utils"));
15
+ var _buildRemoteServer = require("./build-remote-server");
16
+ var _constants = require("./constants");
17
+ var _express2 = require("./express");
18
+ var _crypto = require("crypto");
19
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
20
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
21
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
22
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
23
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
24
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
25
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
26
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
27
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
28
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
29
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /*
30
+ * Copyright (c) 2022, Salesforce, Inc.
31
+ * All rights reserved.
32
+ * SPDX-License-Identifier: BSD-3-Clause
33
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
34
+ */ // We need to mock isRemote in some tests, so we need to import it directly from
35
+ // the file it was defined in, because of the way jest works.
36
+ jest.mock('../../utils/ssr-config', () => {
37
+ return {
38
+ getConfig: () => {}
39
+ };
40
+ });
41
+
42
+ // Mock static assets (require path is relative to the 'ssr' directory)
43
+ const mockStaticAssets = {};
44
+ jest.mock('../static/assets.json', () => mockStaticAssets, {
45
+ virtual: true
46
+ });
47
+ const TEST_PORT = 3444;
48
+ const testFixtures = _path.default.resolve(process.cwd(), 'src/ssr/server/test_fixtures');
49
+
50
+ /**
51
+ * An HTTPS.Agent that allows self-signed certificates
52
+ * @type {module:https.Agent}
53
+ */
54
+ const httpsAgent = new _https.default.Agent({
55
+ rejectUnauthorized: false
56
+ });
57
+ const opts = (overrides = {}) => {
58
+ const defaults = {
59
+ buildDir: './src/ssr/server/test_fixtures',
60
+ mobify: {
61
+ ssrEnabled: true,
62
+ ssrOnly: ['main.js.map', 'ssr.js', 'ssr.js.map'],
63
+ ssrShared: ['main.js', 'ssr-loader.js', 'worker.js'],
64
+ ssrParameters: {
65
+ proxyConfigs: [{
66
+ protocol: 'https',
67
+ host: 'test.proxy.com',
68
+ path: 'base'
69
+ }, {
70
+ protocol: 'https',
71
+ // This is intentionally an unreachable host
72
+ host: '0.0.0.0',
73
+ path: 'base2'
74
+ }, {
75
+ protocol: 'https',
76
+ host: 'test.proxy.com',
77
+ path: 'base3',
78
+ caching: true
79
+ }]
80
+ }
81
+ },
82
+ sslFilePath: './src/ssr/server/test_fixtures/localhost.pem',
83
+ quiet: true,
84
+ port: TEST_PORT,
85
+ protocol: 'https',
86
+ fetchAgents: {
87
+ https: httpsAgent
88
+ },
89
+ defaultCacheTimeSeconds: 123,
90
+ enableLegacyRemoteProxying: false,
91
+ useSLASPrivateClient: false
92
+ };
93
+ return _objectSpread(_objectSpread({}, defaults), overrides);
94
+ };
95
+ const mkdtempSync = () => _fsExtra.default.mkdtempSync(_path.default.resolve(_os.default.tmpdir(), 'ssr-server-tests-'));
96
+ beforeAll(() => {
97
+ // The SSR app applies patches on creation. Those patches are specific to an
98
+ // environment (Lambda or not) and we need to ensure that the non-lambda patches
99
+ // are applied for testing. Creating and immediately discarding an app in
100
+ // local mode here applies the correct patches for all tests.
101
+ _buildRemoteServer.RemoteServerFactory._createApp(opts());
102
+ });
103
+ describe('_createApp validates the options object', () => {
104
+ let savedEnvironment;
105
+ beforeEach(() => {
106
+ savedEnvironment = _extends({}, process.env);
107
+ process.env = {
108
+ LISTEN_ADDRESS: '',
109
+ EXTERNAL_DOMAIN_NAME: ''
110
+ };
111
+ });
112
+ afterEach(() => {
113
+ process.env = savedEnvironment;
114
+ });
115
+ const invalidOptions = [{
116
+ name: 'mobify',
117
+ options: opts({
118
+ mobify: undefined
119
+ })
120
+ }, {
121
+ name: 'mobify',
122
+ options: opts({
123
+ mobify: 'a string'
124
+ })
125
+ }, {
126
+ name: 'buildDir empty',
127
+ options: opts({
128
+ buildDir: ''
129
+ })
130
+ }];
131
+ invalidOptions.forEach(({
132
+ name,
133
+ options
134
+ }) => {
135
+ test(`_createApp validates missing or invalid field "${name}"`, () => {
136
+ expect(() => _buildRemoteServer.RemoteServerFactory._createApp(options)).toThrow();
137
+ });
138
+ });
139
+ });
140
+ describe('_createApp validates environment variables', () => {
141
+ let savedEnvironment;
142
+ beforeEach(() => {
143
+ savedEnvironment = _extends({}, process.env);
144
+ });
145
+ afterEach(() => {
146
+ process.env = savedEnvironment;
147
+ });
148
+ _buildRemoteServer.REMOTE_REQUIRED_ENV_VARS.forEach(envVar => {
149
+ test(`SSR Server verifies environment variable "${envVar}"`, () => {
150
+ // Set truthy values for all the env vars except the one we're testing.
151
+ const vars = _buildRemoteServer.REMOTE_REQUIRED_ENV_VARS.filter(name => name !== envVar).map(name => ({
152
+ [name]: 'value'
153
+ }));
154
+ // AWS_LAMBDA_FUNCTION_NAME indicates the server is running remotely on Lambda
155
+ vars.push({
156
+ AWS_LAMBDA_FUNCTION_NAME: 'pretend-to-be-remote'
157
+ });
158
+ process.env = _extends({}, savedEnvironment, ...vars);
159
+ expect(() => _buildRemoteServer.RemoteServerFactory._createApp(opts())).toThrow(envVar);
160
+ });
161
+ });
162
+ });
163
+ describe('SSRServer operation', () => {
164
+ const savedEnvironment = _extends({}, process.env);
165
+ const sandbox = _sinon.default.createSandbox();
166
+ afterEach(() => {
167
+ sandbox.restore();
168
+ jest.resetModules();
169
+ _nock.default.cleanAll();
170
+ });
171
+ afterAll(() => {
172
+ process.env = savedEnvironment;
173
+ });
174
+ beforeEach(() => {
175
+ _buildRemoteServer.RemoteServerFactory._setRequestId = jest.fn().mockImplementation(_app => {
176
+ _app.use((req, res, next) => {
177
+ res.locals.requestId = (0, _crypto.randomUUID)();
178
+ next();
179
+ });
180
+ });
181
+ // Ensure the environment is clean
182
+ process.env = {
183
+ LISTEN_ADDRESS: '',
184
+ EXTERNAL_DOMAIN_NAME: ''
185
+ };
186
+ });
187
+ test('_createApp creates an express app', () => {
188
+ const options = opts();
189
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(options);
190
+ const expected = `max-age=${options.defaultCacheTimeSeconds}, s-maxage=${options.defaultCacheTimeSeconds}`;
191
+ expect(app.options.defaultCacheControl).toEqual(expected);
192
+ });
193
+ test('SSRServer tracks responses', () => {
194
+ const route = jest.fn().mockImplementation((req, res) => {
195
+ res.send('<div>hello world</div>');
196
+ return Promise.resolve();
197
+ });
198
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
199
+ app.get('/*', route);
200
+ const response1 = {
201
+ locals: {
202
+ requestId: 1
203
+ },
204
+ once: () => null
205
+ };
206
+ const response2 = {
207
+ locals: {
208
+ requestId: 2
209
+ },
210
+ once: () => null
211
+ };
212
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([]);
213
+ app._requestMonitor._responseFinished(response1);
214
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([]);
215
+ const promise1 = app._requestMonitor._waitForResponses();
216
+ expect(promise1).toBe(_express2.RESOLVED_PROMISE);
217
+ app._requestMonitor._responseStarted(response1);
218
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([1]);
219
+ const promise2 = app._requestMonitor._waitForResponses();
220
+ expect(promise2).not.toBe(_express2.RESOLVED_PROMISE);
221
+ app._requestMonitor._responseStarted(response2);
222
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([1, 2]);
223
+ const promise3 = app._requestMonitor._waitForResponses();
224
+ expect(promise3).toBe(promise2);
225
+ app._requestMonitor._responseFinished(response1);
226
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([2]);
227
+ app._requestMonitor._responseFinished(response1);
228
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([2]);
229
+ app._requestMonitor._responseFinished(response2);
230
+ expect(app._requestMonitor._pendingResponses.ids).toEqual([]);
231
+
232
+ // If the promise doesn't resolve, this test will timeout
233
+ return promise2;
234
+ });
235
+ test(`The Remote SSRServer always uses https`, () => {
236
+ _buildRemoteServer.REMOTE_REQUIRED_ENV_VARS.forEach(envVar => {
237
+ process.env[envVar] = 'value';
238
+ });
239
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
240
+ protocol: 'http'
241
+ }));
242
+ expect(app.options.protocol).toBe('https');
243
+ process.env = savedEnvironment;
244
+ });
245
+ test('SSRServer renders correctly', () => {
246
+ const body = '<div>hello world</div>';
247
+ const route = jest.fn().mockImplementation((req, res) => {
248
+ res.send(body);
249
+ });
250
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
251
+ app.get('/*', route);
252
+ return (0, _supertest.default)(app).get('/').expect(200).then(res => {
253
+ expect(res.headers['x-powered-by']).toBeUndefined();
254
+ expect(res.text).toBe(body);
255
+ expect(route).toHaveBeenCalled();
256
+ });
257
+ });
258
+ test('SSRServer renders with the react rendering', () => {
259
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
260
+ app.get('/*', _buildRemoteServer.RemoteServerFactory.render);
261
+ expect(app.__renderer).toBeUndefined();
262
+ return (0, _supertest.default)(app).get('/').expect(200).then(res => {
263
+ expect(res.text).toBe('OK');
264
+ expect(app.__renderer).toBeDefined();
265
+ });
266
+ });
267
+ test('SSRServer rendering blocks cookie setting by default', () => {
268
+ const route = (req, res) => {
269
+ res.setHeader('set-cookie', 'blah123');
270
+ res.sendStatus(200);
271
+ };
272
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
273
+ app.get('/*', route);
274
+ jest.spyOn(console, 'warn');
275
+ return (0, _supertest.default)(app).get('/').expect(200).then(res => {
276
+ expect(console.warn.mock.calls[0][0]).toContain(`Discarding "Set-Cookie: blah123"`);
277
+ expect(res.headers['Set-Cookie']).toBeUndefined();
278
+ expect(res.headers['set-cookie']).toBeUndefined();
279
+ });
280
+ });
281
+ test('SSRServer rendering allows setting cookies with MRT_ALLOW_COOKIES env', () => {
282
+ process.env = {
283
+ MRT_ALLOW_COOKIES: 'true'
284
+ };
285
+ const route = (req, res) => {
286
+ res.setHeader('set-cookie', 'blah123');
287
+ res.sendStatus(200);
288
+ };
289
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
290
+ app.get('/*', route);
291
+ return (0, _supertest.default)(app).get('/').expect(200).then(res => {
292
+ expect(res.headers['set-cookie']).toEqual(['blah123']);
293
+ });
294
+ });
295
+ test('SSRServer does not allow multi-value headers', () => {
296
+ const route = (req, res) => {
297
+ res.set('content-type', 'application/octet-stream');
298
+ res.set('content-type', 'text/plain');
299
+ res.send('<div>hello world</div>');
300
+ };
301
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
302
+ app.get('*', route);
303
+ return (0, _supertest.default)(app).get('/').expect(200).then(res => {
304
+ expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
305
+ });
306
+ });
307
+ test('SSRServer honours any x-mobify-querystring header', () => {
308
+ const route = jest.fn().mockImplementation((req, res) => {
309
+ res.send('<div> Hello world </div>');
310
+ });
311
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
312
+ app.get('/*', route);
313
+ return (0, _supertest.default)(app).get('/').set(_constants.X_MOBIFY_QUERYSTRING, 'z=1&y=2&x=3').expect(200).then(() => {
314
+ expect(route.mock.calls[0][0].query).toEqual({
315
+ z: '1',
316
+ y: '2',
317
+ x: '3'
318
+ });
319
+ });
320
+ });
321
+ describe('Running remotely', () => {
322
+ let isRemoteMock;
323
+ let savedEnvironment;
324
+ beforeEach(() => {
325
+ isRemoteMock = jest.spyOn(ssrServerUtils, 'isRemote').mockImplementation(() => true);
326
+ savedEnvironment = _extends({}, process.env);
327
+ _extends(process.env, {
328
+ BUNDLE_ID: 1,
329
+ DEPLOY_TARGET: 1,
330
+ EXTERNAL_DOMAIN_NAME: 'http://www.example.com',
331
+ MOBIFY_PROPERTY_ID: 'example'
332
+ });
333
+ });
334
+ afterEach(() => {
335
+ isRemoteMock.mockRestore();
336
+ process.env = savedEnvironment;
337
+ });
338
+ test('should not proxy', () => {
339
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
340
+ return (0, _supertest.default)(app).get('/mobify/proxy/base/test/path').expect(501);
341
+ });
342
+ });
343
+ test('SSRServer handles /mobify/ping', () => {
344
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
345
+ return (0, _supertest.default)(app).get('/mobify/ping').expect(200);
346
+ });
347
+ describe('SSRServer worker.js handling', () => {
348
+ let tmpDir;
349
+ beforeEach(() => {
350
+ tmpDir = mkdtempSync();
351
+ });
352
+ afterEach(() => {
353
+ _fsExtra.default.removeSync(tmpDir);
354
+ });
355
+ const cases = [{
356
+ file: 'worker.js',
357
+ content: '// a service worker',
358
+ name: 'Should serve the service worker',
359
+ requestPath: '/worker.js'
360
+ }, {
361
+ file: 'worker.js.map',
362
+ content: '// a service worker source map',
363
+ name: 'Should serve the service worker source map',
364
+ requestPath: '/worker.js.map'
365
+ }];
366
+ cases.forEach(({
367
+ file,
368
+ content,
369
+ name,
370
+ requestPath
371
+ }) => {
372
+ test(`${name}`, () => {
373
+ const fixture = _path.default.join(__dirname, 'test_fixtures');
374
+ const buildDir = _path.default.join(tmpDir, 'build');
375
+ _fsExtra.default.copySync(fixture, buildDir);
376
+ const updatedFile = _path.default.resolve(buildDir, file);
377
+ _fsExtra.default.writeFileSync(updatedFile, content);
378
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
379
+ buildDir
380
+ }));
381
+ app.get('/worker.js(.map)?', _buildRemoteServer.RemoteServerFactory.serveServiceWorker);
382
+ return (0, _supertest.default)(app).get(requestPath).expect(200).then(res => expect(res.text).toEqual(content));
383
+ });
384
+ test(`${name} (and handle 404s correctly)`, () => {
385
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
386
+ buildDir: tmpDir
387
+ }));
388
+ app.get('/worker.js(.map)?', _buildRemoteServer.RemoteServerFactory.serveServiceWorker);
389
+ return (0, _supertest.default)(app).get(requestPath).expect(404);
390
+ });
391
+ });
392
+ });
393
+ test('SSRServer creates cache on demand', () => {
394
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
395
+ expect(app._applicationCache).toBeUndefined();
396
+ expect(app.applicationCache).toBeInstanceOf(_ssrCache.PersistentCache);
397
+ expect(app._applicationCache).toBe(app.applicationCache);
398
+ });
399
+ test('should support redirects to bundle assets', () => {
400
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
401
+ const route = (req, res) => {
402
+ (0, _express2.respondFromBundle)({
403
+ req,
404
+ res
405
+ });
406
+ };
407
+ app.get('/*', route);
408
+ return (0, _supertest.default)(app).get('/some-bundle-path.jpg').then(response => {
409
+ expect(response.status).toBe(301);
410
+ expect(response.headers['location'].endsWith('/mobify/bundle/development/some-bundle-path.jpg')).toBe(true);
411
+ });
412
+ });
413
+ test('should support other redirects', () => {
414
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
415
+ const route = (req, res) => {
416
+ res.redirect(302, '/elsewhere');
417
+ };
418
+ app.get('/*', route);
419
+ return (0, _supertest.default)(app).get('/some-bundle-path.jpg').then(response => {
420
+ expect(response.status).toBe(302);
421
+ expect(response.headers['location'].endsWith('/elsewhere')).toBe(true);
422
+ });
423
+ });
424
+ test('should warn about non-strict SSL', () => {
425
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
426
+ const route = (req, res) => {
427
+ res.redirect(302, '/elsewhere');
428
+ };
429
+ app.get('/*', route);
430
+ return (0, _supertest.default)(app).get('/some-bundle-path.jpg').then(response => {
431
+ expect(response.status).toBe(302);
432
+ expect(response.headers['location'].endsWith('/elsewhere')).toBe(true);
433
+ });
434
+ });
435
+ test('should support error codes', () => {
436
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
437
+ const route = (request, response) => {
438
+ response.sendStatus(500);
439
+ };
440
+ app.get('/*', route);
441
+ return (0, _supertest.default)(app).get('/').then(response => {
442
+ expect(response.status).toBe(500);
443
+ });
444
+ });
445
+ test('should strip cookies before passing the request to the handler by default', () => {
446
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
447
+ const route = (req, res) => {
448
+ expect(req.headers.cookie).toBeUndefined();
449
+ res.sendStatus(200);
450
+ };
451
+ app.get('/*', route);
452
+ return (0, _supertest.default)(app).get('/').set('cookie', 'xyz=456').then(response => {
453
+ expect(response.status).toBe(200);
454
+ expect(response.headers['set-cookie']).toBeUndefined();
455
+ });
456
+ });
457
+ test('should allow cookies in the request with MRT_ALLOW_COOKIES env', () => {
458
+ process.env = {
459
+ MRT_ALLOW_COOKIES: 'true'
460
+ };
461
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
462
+ const route = (req, res) => {
463
+ expect(req.headers.cookie).toBe('xyz=456');
464
+ res.sendStatus(200);
465
+ };
466
+ app.get('/*', route);
467
+ return (0, _supertest.default)(app).get('/').set('cookie', 'xyz=456').then(response => {
468
+ expect(response.status).toBe(200);
469
+ expect(response.headers['set-cookie']).toBeUndefined();
470
+ });
471
+ });
472
+ test('should fix host and origin headers before passing the request to the handler', () => {
473
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
474
+ const route = (req, res) => {
475
+ expect(req.headers.host).toEqual(app.options.appHostname);
476
+ expect(req.headers.origin).toEqual(app.options.appOrigin);
477
+ res.sendStatus(200);
478
+ };
479
+ app.get('/*', route);
480
+ return (0, _supertest.default)(app).get('/').set({
481
+ host: 'somewhere.over.the.rainbow',
482
+ origin: 'https://somewhere.over.the.rainbow'
483
+ }).then(response => {
484
+ expect(response.status).toBe(200);
485
+ expect(response.headers['set-cookie']).toBeUndefined();
486
+ });
487
+ });
488
+ test(`should reject POST requests to /`, () => {
489
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
490
+ const route = (req, res) => {
491
+ res.status(200).end();
492
+ };
493
+ app.get('/*', route);
494
+ return (0, _supertest.default)(app).post('/').then(response => {
495
+ expect(response.status).toBe(405);
496
+ });
497
+ });
498
+ test('serveStaticFile serves static files from the build directory', () => {
499
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
500
+ const faviconPath = _path.default.resolve(testFixtures, 'favicon.ico');
501
+ app.get('/thing', _buildRemoteServer.RemoteServerFactory.serveStaticFile('favicon.ico'));
502
+ return (0, _supertest.default)(app).get('/thing').buffer(true).parse(_superagent.default.parse.image).expect(200).then(res => {
503
+ const iconData = _fsExtra.default.readFileSync(faviconPath);
504
+ expect(res.body).toEqual(iconData);
505
+ });
506
+ });
507
+ test('serveStaticFile returns 404 if the file does not exist', () => {
508
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
509
+ app.get('/thing', _buildRemoteServer.RemoteServerFactory.serveStaticFile('this-does-not-exist.ico'));
510
+ return (0, _supertest.default)(app).get('/thing').expect(404);
511
+ });
512
+ });
513
+ describe('SSRServer persistent caching', () => {
514
+ const namespace = 'test';
515
+ const keyFromURL = url => encodeURIComponent(url);
516
+
517
+ /**
518
+ * A cache decorator for a route function that uses the percent-encoded req.url
519
+ * as keys for all cache entries (this makes testing easier).
520
+ */
521
+ const cachedRoute = route => (req, res) => {
522
+ const shouldCache = !req.query.noCache;
523
+ const cacheArgs = {
524
+ req,
525
+ res,
526
+ namespace,
527
+ key: keyFromURL(req.url)
528
+ };
529
+ const shouldCacheResponse = (req, res) => res.statusCode >= 200 && res.statusCode < 300;
530
+ return Promise.resolve().then(() => (0, _express2.getResponseFromCache)(cacheArgs)).then(entry => {
531
+ if (entry.found) {
532
+ (0, _express2.sendCachedResponse)(entry);
533
+ } else {
534
+ if (shouldCache) {
535
+ (0, _express2.cacheResponseWhenDone)(_objectSpread({
536
+ shouldCacheResponse
537
+ }, cacheArgs));
538
+ }
539
+ return route(req, res);
540
+ }
541
+ });
542
+ };
543
+
544
+ /**
545
+ * A test route that returns different content types based on query params.
546
+ */
547
+ const routeImplementation = (req, res) => {
548
+ const status = parseInt(req.query.status || 200);
549
+ switch (req.query.type) {
550
+ case 'image':
551
+ res.status(status);
552
+ res.setHeader('content-type', 'image/png');
553
+ res.send(_fsExtra.default.readFileSync(_path.default.join(testFixtures, 'mobify.png')));
554
+ break;
555
+ case 'html':
556
+ res.status(status);
557
+ res.setHeader('x-rendered', 'true');
558
+ res.setHeader('cache-control', 's-maxage=60');
559
+ res.send('<div> Hello world </div>');
560
+ break;
561
+ case '500':
562
+ res.sendStatus(500);
563
+ break;
564
+ case '400':
565
+ res.sendStatus(400);
566
+ break;
567
+ default:
568
+ res.sendStatus(status);
569
+ break;
570
+ }
571
+ };
572
+ const sandbox = _sinon.default.createSandbox();
573
+ let app, route;
574
+ beforeEach(() => {
575
+ route = jest.fn().mockImplementation(routeImplementation);
576
+ const withCaching = cachedRoute(route);
577
+ app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
578
+ app.get('/*', withCaching);
579
+ });
580
+ afterEach(() => {
581
+ sandbox.restore();
582
+ app.applicationCache.close();
583
+ app = null;
584
+ route = null;
585
+ });
586
+ const testCases = [{
587
+ name: 'Should put HTML responses into the cache after rendering',
588
+ url: '/cacheme/?type=html',
589
+ expectOk: true,
590
+ expectHeaders: {
591
+ 'x-mobify-from-cache': 'false',
592
+ 'x-rendered': 'true',
593
+ 'content-type': 'text/html; charset=utf-8'
594
+ },
595
+ expectToBeCached: false,
596
+ expectRenderCallCount: 1
597
+ }, {
598
+ name: 'Should put binary responses into the cache after rendering',
599
+ url: '/cacheme/?type=image',
600
+ expectOk: true,
601
+ expectHeaders: {
602
+ 'x-mobify-from-cache': 'false',
603
+ 'content-type': 'image/png'
604
+ },
605
+ expectToBeCached: false,
606
+ expectRenderCallCount: 1
607
+ }, {
608
+ name: 'Should skip putting responses into the cache when noCache is set',
609
+ url: '/cacheme/?type=image&noCache=1',
610
+ expectOk: true,
611
+ expectHeaders: {
612
+ 'x-mobify-from-cache': 'false',
613
+ 'content-type': 'image/png'
614
+ },
615
+ expectToBeCached: false,
616
+ expectRenderCallCount: 1
617
+ }, {
618
+ name: 'Should return a response even when the cache put fails',
619
+ url: '/cacheme/?type=image&a=1',
620
+ expectOk: true,
621
+ expectHeaders: {
622
+ 'x-mobify-from-cache': 'false',
623
+ 'content-type': 'image/png'
624
+ },
625
+ expectToBeCached: false,
626
+ expectRenderCallCount: 1,
627
+ forcePutFailure: true
628
+ }, {
629
+ name: 'Should serve responses from the cache, including HTTP headers',
630
+ url: '/cacheme/?type=html',
631
+ expectOk: true,
632
+ expectHeaders: {
633
+ 'x-mobify-from-cache': 'false',
634
+ 'content-type': 'text/html; charset=utf-8'
635
+ },
636
+ expectToBeCached: false,
637
+ expectRenderCallCount: 1,
638
+ preCache: {
639
+ data: Buffer.from('<html>456</html>'),
640
+ metadata: {
641
+ status: 200,
642
+ headers: {
643
+ 'x-precached': 'false',
644
+ 'content-type': 'text/html; charset=utf-8'
645
+ }
646
+ }
647
+ }
648
+ }, {
649
+ name: 'Should serve responses from the cache without cached HTTP headers',
650
+ url: '/cacheme/?type=html',
651
+ expectOk: true,
652
+ expectHeaders: {
653
+ 'x-mobify-from-cache': 'false'
654
+ },
655
+ expectToBeCached: false,
656
+ expectRenderCallCount: 1,
657
+ preCache: {
658
+ data: Buffer.from('<html>123</html>')
659
+ }
660
+ }, {
661
+ name: 'Should serve empty responses from the cache without errors',
662
+ url: '/cacheme/?type=none',
663
+ expectOk: true,
664
+ expectHeaders: {
665
+ 'x-mobify-from-cache': 'false'
666
+ },
667
+ expectToBeCached: false,
668
+ expectRenderCallCount: 1,
669
+ preCache: {
670
+ data: undefined,
671
+ metadata: {
672
+ status: 200,
673
+ headers: {
674
+ 'x-precached': 'true'
675
+ }
676
+ }
677
+ }
678
+ }];
679
+ testCases.forEach(testCase => test(`${testCase.name}`, () => {
680
+ let url = testCase.url;
681
+ return Promise.resolve().then(() => {
682
+ const preCache = testCase.preCache;
683
+ if (preCache) {
684
+ return app.applicationCache.put({
685
+ namespace,
686
+ key: keyFromURL(url),
687
+ metadata: preCache.metadata,
688
+ data: preCache.data
689
+ });
690
+ }
691
+ }).then(() => {
692
+ sandbox.stub(app.applicationCache, 'put');
693
+ if (testCase.forcePutFailure) {
694
+ app.applicationCache.put.onFirstCall().callsFake(() => Promise.reject('Fake put error'));
695
+ }
696
+ app.applicationCache.put.callThrough();
697
+ }).then(() => {
698
+ // Buffer and parse everything as binary for easy comparison
699
+ // across content types.
700
+ return (0, _supertest.default)(app).get(url).buffer(true).parse(_superagent.default.parse['application/octet-stream']);
701
+ })
702
+ // Wait for any caching to complete
703
+ .then(response => app._requestMonitor._waitForResponses().then(() => response))
704
+ // Handle and verify the response
705
+ .then(response => {
706
+ expect(response.ok).toEqual(testCase.expectOk);
707
+ expect(route.mock.calls).toHaveLength(testCase.expectRenderCallCount);
708
+ expect(response.headers).toMatchObject(testCase.expectHeaders);
709
+ return Promise.all([response, app.applicationCache.get({
710
+ key: keyFromURL(url),
711
+ namespace
712
+ })]);
713
+ }).then(([, entry]) => {
714
+ // Verify the response data against the cache
715
+ expect(entry.found).toBe(false);
716
+ });
717
+ }));
718
+ const errorCases = [{
719
+ url: '/?type=500',
720
+ status: 500
721
+ }, {
722
+ url: '/?type=400',
723
+ status: 400
724
+ }];
725
+ errorCases.forEach(({
726
+ url,
727
+ status
728
+ }) => {
729
+ test(`should not cache responses with ${status} status codes`, () => {
730
+ return (0, _supertest.default)(app).get(url).then(res => app._requestMonitor._waitForResponses().then(() => res)).then(res => {
731
+ expect(res.status).toBe(status);
732
+ expect(res.headers['x-mobify-from-cache']).toBe('false');
733
+ }).then(() => app.applicationCache.get({
734
+ key: keyFromURL(url),
735
+ namespace
736
+ })).then(entry => expect(entry.found).toBe(false));
737
+ });
738
+ });
739
+ test('Try to send non-cached response', () => {
740
+ expect(() => (0, _express2.sendCachedResponse)(new _ssrServer.CachedResponse({}))).toThrow('non-cached');
741
+ });
742
+ });
743
+ describe('generateCacheKey', () => {
744
+ const mockRequest = overrides => {
745
+ return _objectSpread({
746
+ url: '/test?a=1',
747
+ query: {},
748
+ headers: {},
749
+ get: function (key) {
750
+ return this.headers[key];
751
+ }
752
+ }, overrides);
753
+ };
754
+ test('returns expected results', () => {
755
+ expect((0, _express2.generateCacheKey)(mockRequest({
756
+ url: '/test/1?id=abc'
757
+ })).indexOf('/test/1')).toBe(0);
758
+ });
759
+ test('path affects key', () => {
760
+ const result1 = (0, _express2.generateCacheKey)(mockRequest({
761
+ url: '/test2a/'
762
+ }));
763
+ expect((0, _express2.generateCacheKey)(mockRequest({
764
+ url: '/testab/'
765
+ }))).not.toEqual(result1);
766
+ });
767
+ test('query affects key', () => {
768
+ const result1 = (0, _express2.generateCacheKey)(mockRequest({
769
+ url: '/test3?a=1'
770
+ }));
771
+ expect((0, _express2.generateCacheKey)(mockRequest({
772
+ url: '/test3?a=2'
773
+ }))).not.toEqual(result1);
774
+ });
775
+ test('request class affects key', () => {
776
+ const result1 = (0, _express2.generateCacheKey)(mockRequest());
777
+ const request2 = mockRequest({
778
+ headers: {
779
+ 'x-mobify-request-class': 'bot'
780
+ }
781
+ });
782
+ expect((0, _express2.generateCacheKey)(request2)).not.toEqual(result1);
783
+ expect((0, _express2.generateCacheKey)(request2, {
784
+ ignoreRequestClass: true
785
+ })).toEqual(result1);
786
+ });
787
+ test('extras affect key', () => {
788
+ const result1 = (0, _express2.generateCacheKey)(mockRequest());
789
+ expect((0, _express2.generateCacheKey)(mockRequest(), {
790
+ extras: ['123']
791
+ })).not.toEqual(result1);
792
+ });
793
+ });
794
+ describe('getRuntime', () => {
795
+ let originalEnv;
796
+ let originalEval = global.eval;
797
+
798
+ // Mock the DevSeverFactory via `eval` so we don't have to include it as a dev
799
+ // dependency which will cause circular dependency warnings.
800
+ const MockDevServerFactory = {
801
+ name: 'MockDevServerFactory',
802
+ returnMyName() {
803
+ return this.name;
804
+ }
805
+ };
806
+ const mockEval = () => ({
807
+ main: {
808
+ require: () => ({
809
+ DevServerFactory: MockDevServerFactory
810
+ })
811
+ }
812
+ });
813
+ const matchExceptFunctionValues = obj => {
814
+ const entries = Object.entries(obj);
815
+ const matchers = entries.map(([key, value]) => {
816
+ const matcher = typeof value === 'function' ? expect.any(Function) : value;
817
+ return [key, matcher];
818
+ });
819
+ return Object.fromEntries(matchers);
820
+ };
821
+ const cases = [{
822
+ env: {},
823
+ expectedRuntime: MockDevServerFactory,
824
+ msg: 'when running locally'
825
+ }, {
826
+ env: {
827
+ AWS_LAMBDA_FUNCTION_NAME: 'this-makes-it-remote'
828
+ },
829
+ expectedRuntime: _buildRemoteServer.RemoteServerFactory,
830
+ msg: 'when running remotely'
831
+ }];
832
+ beforeAll(() => {
833
+ global.eval = mockEval;
834
+ });
835
+ afterAll(() => {
836
+ global.eval = originalEval;
837
+ });
838
+ beforeEach(() => {
839
+ originalEnv = process.env;
840
+ });
841
+ afterEach(() => {
842
+ process.env = originalEnv;
843
+ });
844
+ test.each(cases)('should return a remote/development runtime $msg', ({
845
+ env,
846
+ expectedRuntime
847
+ }) => {
848
+ process.env = _objectSpread(_objectSpread({}, process.env), env);
849
+ expect((0, _express2.getRuntime)()).toMatchObject(matchExceptFunctionValues(expectedRuntime));
850
+ });
851
+ test('should return a remote/development runtime bound to the correct context', () => {
852
+ const mockDevRuntime = (0, _express2.getRuntime)();
853
+ const func = mockDevRuntime.returnMyName;
854
+ expect(func()).toBe(MockDevServerFactory.name);
855
+ });
856
+ });
857
+ describe('DevServer middleware', () => {
858
+ afterEach(() => {
859
+ jest.restoreAllMocks();
860
+ });
861
+ test('_validateConfiguration protocol', () => {
862
+ let protocol = 'ftp';
863
+ let error = `Invalid local development server protocol ${protocol}. Valid protocols are http and https.`;
864
+ expect(() => {
865
+ _buildRemoteServer.RemoteServerFactory._validateConfiguration(opts({
866
+ protocol
867
+ }));
868
+ }).toThrow(error);
869
+ });
870
+ test('_validateConfiguration sslFilePath', () => {
871
+ let sslFilePath = './does/not/exist';
872
+ let error = 'The sslFilePath option passed to the SSR server constructor ' + 'must be a path to an SSL certificate file ' + 'in PEM format, whose name ends with ".pem". ' + 'See the "cert" and "key" options on ' + 'https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options';
873
+ expect(() => {
874
+ _buildRemoteServer.RemoteServerFactory._validateConfiguration(opts({
875
+ sslFilePath
876
+ }));
877
+ }).toThrow(error);
878
+ });
879
+ test('_validateConfiguration strictSSL', () => {
880
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
881
+ _buildRemoteServer.RemoteServerFactory._validateConfiguration(opts({
882
+ strictSSL: false
883
+ }));
884
+ expect(warn.mock.calls).toEqual([['The SSR Server has _strictSSL turned off for https requests']]);
885
+ });
886
+ });
887
+ describe('SLAS private client proxy', () => {
888
+ const savedEnvironment = _extends({}, process.env);
889
+ let proxyApp;
890
+ const proxyPort = 12345;
891
+ const proxyPath = '/responseHeaders';
892
+ const slasTarget = `http://localhost:${proxyPort}${proxyPath}`;
893
+ beforeAll(() => {
894
+ // by setting slasTarget, rather than forwarding the request to SLAS,
895
+ // we send the proxy request here so we can return the request headers
896
+ proxyApp = (0, _express.default)();
897
+ proxyApp.use(proxyPath, (req, res) => {
898
+ res.send(req.headers);
899
+ });
900
+ proxyApp.listen(proxyPort);
901
+ });
902
+ afterEach(() => {
903
+ process.env = savedEnvironment;
904
+ });
905
+ afterAll(() => {
906
+ proxyApp.close();
907
+ });
908
+ test('should not create proxy by default', () => {
909
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
910
+ return (0, _supertest.default)(app).get('/mobify/slas/private').expect(404);
911
+ });
912
+ test('should return HTTP 501 if PWA_KIT_SLAS_CLIENT_SECRET env var not set', () => {
913
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
914
+ useSLASPrivateClient: true
915
+ }));
916
+ return (0, _supertest.default)(app).get('/mobify/slas/private').expect(501);
917
+ });
918
+ test('does not insert client secret if request not for /oauth2/token', /*#__PURE__*/_asyncToGenerator(function* () {
919
+ process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
920
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
921
+ mobify: {
922
+ app: {
923
+ commerceAPI: {
924
+ parameters: {
925
+ clientId: 'clientId',
926
+ shortCode: 'shortCode'
927
+ }
928
+ }
929
+ }
930
+ },
931
+ useSLASPrivateClient: true,
932
+ slasTarget: slasTarget
933
+ }));
934
+ return yield (0, _supertest.default)(app).get('/mobify/slas/private/somePath').then(response => {
935
+ expect(response.body.authorization).toBeUndefined();
936
+ expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
937
+ expect(response.body['x-mobify']).toBe('true');
938
+ });
939
+ }), 15000);
940
+ test('inserts client secret if request is for /oauth2/token', /*#__PURE__*/_asyncToGenerator(function* () {
941
+ process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
942
+ const encodedCredentials = Buffer.from('clientId:a secret').toString('base64');
943
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
944
+ mobify: {
945
+ app: {
946
+ commerceAPI: {
947
+ parameters: {
948
+ clientId: 'clientId',
949
+ shortCode: 'shortCode'
950
+ }
951
+ }
952
+ }
953
+ },
954
+ useSLASPrivateClient: true,
955
+ slasTarget: slasTarget
956
+ }));
957
+ return yield (0, _supertest.default)(app).get('/mobify/slas/private/oauth2/token').then(response => {
958
+ expect(response.body.authorization).toBe(`Basic ${encodedCredentials}`);
959
+ expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
960
+ expect(response.body['x-mobify']).toBe('true');
961
+ });
962
+ }), 15000);
963
+ });