@exdst-sitecore-content-sdk/astro 0.0.1

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 (87) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +3 -0
  3. package/package.json +101 -0
  4. package/src/client/index.ts +12 -0
  5. package/src/client/sitecore-astro-client.test.ts +271 -0
  6. package/src/client/sitecore-astro-client.ts +137 -0
  7. package/src/components/AstroImage.astro +114 -0
  8. package/src/components/Date.astro +76 -0
  9. package/src/components/DefaultEmptyFieldEditingComponentImage.astro +24 -0
  10. package/src/components/DefaultEmptyFieldEditingComponentText.astro +12 -0
  11. package/src/components/EditingScripts.astro +49 -0
  12. package/src/components/EmptyRendering.astro +3 -0
  13. package/src/components/ErrorBoundary.astro +77 -0
  14. package/src/components/FieldMetadata.astro +30 -0
  15. package/src/components/File.astro +46 -0
  16. package/src/components/HiddenRendering.astro +22 -0
  17. package/src/components/Image.astro +155 -0
  18. package/src/components/Link.astro +105 -0
  19. package/src/components/MissingComponent.astro +39 -0
  20. package/src/components/Placeholder/EmptyPlaceholder.astro +9 -0
  21. package/src/components/Placeholder/Placeholder.astro +100 -0
  22. package/src/components/Placeholder/PlaceholderMetadata.astro +102 -0
  23. package/src/components/Placeholder/PlaceholderUtils.astro +153 -0
  24. package/src/components/Placeholder/index.ts +5 -0
  25. package/src/components/Placeholder/models.ts +82 -0
  26. package/src/components/Placeholder/placeholder-utils.test.ts +162 -0
  27. package/src/components/Placeholder/placeholder-utils.ts +80 -0
  28. package/src/components/RenderWrapper.astro +31 -0
  29. package/src/components/RichText.astro +59 -0
  30. package/src/components/Text.astro +97 -0
  31. package/src/components/sharedTypes/index.ts +1 -0
  32. package/src/components/sharedTypes/props.ts +17 -0
  33. package/src/config/define-config.test.ts +526 -0
  34. package/src/config/define-config.ts +99 -0
  35. package/src/config/index.ts +1 -0
  36. package/src/config-cli/define-cli-config.test.ts +95 -0
  37. package/src/config-cli/define-cli-config.ts +50 -0
  38. package/src/config-cli/index.ts +1 -0
  39. package/src/context.ts +68 -0
  40. package/src/editing/constants.ts +8 -0
  41. package/src/editing/editing-config-middleware.test.ts +166 -0
  42. package/src/editing/editing-config-middleware.ts +111 -0
  43. package/src/editing/editing-render-middleware.test.ts +801 -0
  44. package/src/editing/editing-render-middleware.ts +288 -0
  45. package/src/editing/index.ts +16 -0
  46. package/src/editing/render-middleware.test.ts +57 -0
  47. package/src/editing/render-middleware.ts +51 -0
  48. package/src/editing/utils.test.ts +852 -0
  49. package/src/editing/utils.ts +308 -0
  50. package/src/enhancers/WithEmptyFieldEditingComponent.astro +56 -0
  51. package/src/enhancers/WithFieldMetadata.astro +31 -0
  52. package/src/env.d.ts +12 -0
  53. package/src/index.ts +16 -0
  54. package/src/middleware/index.ts +24 -0
  55. package/src/middleware/middleware.test.ts +507 -0
  56. package/src/middleware/middleware.ts +167 -0
  57. package/src/middleware/multisite-middleware.test.ts +672 -0
  58. package/src/middleware/multisite-middleware.ts +147 -0
  59. package/src/middleware/robots-middleware.test.ts +113 -0
  60. package/src/middleware/robots-middleware.ts +47 -0
  61. package/src/middleware/sitemap-middleware.test.ts +152 -0
  62. package/src/middleware/sitemap-middleware.ts +65 -0
  63. package/src/services/component-props-service.ts +182 -0
  64. package/src/sharedTypes/component-props.ts +17 -0
  65. package/src/site/index.ts +1 -0
  66. package/src/test-data/components/Bar.astro +0 -0
  67. package/src/test-data/components/Baz.astro +0 -0
  68. package/src/test-data/components/Foo.astro +0 -0
  69. package/src/test-data/components/Hero.variant.astro +0 -0
  70. package/src/test-data/components/NotComponent.bsx +0 -0
  71. package/src/test-data/components/Qux.astro +0 -0
  72. package/src/test-data/components/folded/Folded.astro +0 -0
  73. package/src/test-data/components/folded/random-file-2.docx +0 -0
  74. package/src/test-data/components/random-file.txt +0 -0
  75. package/src/test-data/helpers.ts +46 -0
  76. package/src/test-data/personalizeData.ts +63 -0
  77. package/src/tools/generate-map.ts +83 -0
  78. package/src/tools/index.ts +8 -0
  79. package/src/tools/templating/components.test.ts +305 -0
  80. package/src/tools/templating/components.ts +49 -0
  81. package/src/tools/templating/constants.ts +4 -0
  82. package/src/tools/templating/default-component.test.ts +31 -0
  83. package/src/tools/templating/default-component.ts +63 -0
  84. package/src/tools/templating/index.ts +2 -0
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/utils.test.ts +48 -0
  87. package/src/utils/utils.ts +52 -0
@@ -0,0 +1,801 @@
1
+ /* eslint-disable dot-notation */
2
+ /* eslint-disable no-unused-expressions */
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+ import { expect, use } from 'chai';
5
+ import {
6
+ EDITING_ALLOWED_ORIGINS,
7
+ QUERY_PARAM_EDITING_SECRET,
8
+ EditingRenderQueryParams,
9
+ DesignLibraryMode,
10
+ } from '@sitecore-content-sdk/core/editing';
11
+ import { EditingRenderMiddleware } from './editing-render-middleware';
12
+ import sinonChai from 'sinon-chai';
13
+ import sinon from 'sinon';
14
+ import { mockRequest as MockRequest, Query } from '../test-data/helpers';
15
+ import {
16
+ QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
17
+ QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
18
+ } from './constants';
19
+
20
+ use(sinonChai);
21
+
22
+ const mockPreviewCookies =
23
+ '_preview_data=1122334455; Max-Age=3; Path=/; HttpOnly; Secure; SameSite=None';
24
+
25
+ const allowedOrigin = 'https://allowed.com';
26
+
27
+ const mockRequest = ({
28
+ query,
29
+ method,
30
+ headers,
31
+ }: {
32
+ query?: Query | EditingRenderQueryParams;
33
+ method?: string;
34
+ headers?: { [key: string]: string };
35
+ }) => {
36
+ return MockRequest({
37
+ query: query ? toQuery(query) : query,
38
+ method: method ?? 'GET',
39
+ headers: {
40
+ host: 'localhost:3000',
41
+ origin: allowedOrigin,
42
+ ...headers,
43
+ },
44
+ });
45
+ };
46
+
47
+ const toQuery = (params: Query | EditingRenderQueryParams): Query => {
48
+ const query: Query = {};
49
+
50
+ Object.entries(params).forEach(([key, value]) => {
51
+ if (value !== undefined && value !== null) {
52
+ query[key] = String(value);
53
+ }
54
+ });
55
+
56
+ return query;
57
+ };
58
+
59
+ describe('EditingRenderMiddleware', () => {
60
+ const secret = 'secret1234';
61
+
62
+ beforeEach(() => {
63
+ process.env.SITECORE_EDITING_SECRET = secret;
64
+ process.env.JSS_ALLOWED_ORIGINS = allowedOrigin;
65
+ delete process.env.VERCEL;
66
+ });
67
+
68
+ afterEach(() => {
69
+ delete process.env.SITECORE_EDITING_SECRET;
70
+ delete process.env.VERCEL;
71
+ delete process.env.JSS_ALLOWED_ORIGINS;
72
+ });
73
+
74
+ it('should respond with 405 for unsupported method', async () => {
75
+ const query = {} as Query;
76
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
77
+ const req = mockRequest({
78
+ query,
79
+ method: 'PUT',
80
+ });
81
+
82
+ const middleware = new EditingRenderMiddleware();
83
+ const handler = middleware.getHandler();
84
+
85
+ const res = await handler(req);
86
+
87
+ expect(res.headers.has('Allow')).to.be.true;
88
+ expect(res.headers.get('Allow')).to.equal('GET');
89
+ expect(res.status).to.equal(405);
90
+ });
91
+
92
+ it('should respond with 204 for OPTIONS method', async () => {
93
+ const query = {} as Query;
94
+ query[QUERY_PARAM_EDITING_SECRET] = secret;
95
+ const req = mockRequest({
96
+ query,
97
+ method: 'OPTIONS',
98
+ });
99
+
100
+ const middleware = new EditingRenderMiddleware();
101
+ const handler = middleware.getHandler();
102
+
103
+ const res = await handler(req);
104
+
105
+ expect(res.status).to.equal(204);
106
+ expect(res.body).to.equal(null);
107
+
108
+ expect(res.headers.has('Access-Control-Allow-Origin')).to.be.true;
109
+ expect(res.headers.get('Access-Control-Allow-Origin')).to.equal(allowedOrigin);
110
+
111
+ expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
112
+ expect(res.headers.get('Access-Control-Allow-Methods')).to.equal(
113
+ 'GET, POST, OPTIONS, DELETE, PUT, PATCH'
114
+ );
115
+
116
+ expect(res.headers.has('Access-Control-Allow-Headers')).to.be.true;
117
+ expect(res.headers.get('Access-Control-Allow-Headers')).to.equal('Content-Type, Authorization');
118
+ });
119
+
120
+ it('should respond with 401 for invalid secret', async () => {
121
+ const query = {} as Query;
122
+ query[QUERY_PARAM_EDITING_SECRET] = 'nope';
123
+ const req = mockRequest({
124
+ query,
125
+ });
126
+
127
+ const middleware = new EditingRenderMiddleware();
128
+ const handler = middleware.getHandler();
129
+
130
+ const res = await handler(req);
131
+
132
+ const body = await res.json();
133
+
134
+ expect(res.status).to.equal(401);
135
+ expect(body).to.deep.equal({
136
+ html: '<html><body>Missing or invalid secret</body></html>',
137
+ });
138
+ });
139
+
140
+ it('should stop request and return 401 when CORS match is not met', async () => {
141
+ const req = mockRequest({
142
+ headers: { origin: 'https://notallowed.com' },
143
+ });
144
+ const middleware = new EditingRenderMiddleware();
145
+ const handler = middleware.getHandler();
146
+
147
+ const res = await handler(req);
148
+
149
+ const body = await res.json();
150
+
151
+ expect(res.status).to.equal(401);
152
+ expect(body).to.deep.equal({
153
+ html: '<html><body>Requests from origin https://notallowed.com not allowed</body></html>',
154
+ });
155
+ });
156
+
157
+ it('should respond with 401 for missing secret', async () => {
158
+ const query = {} as Query;
159
+ const req = mockRequest({ query });
160
+
161
+ const middleware = new EditingRenderMiddleware();
162
+ const handler = middleware.getHandler();
163
+
164
+ const res = await handler(req);
165
+
166
+ const body = await res.json();
167
+
168
+ expect(res.status).to.equal(401);
169
+ expect(body).to.deep.equal({
170
+ html: '<html><body>Missing or invalid secret</body></html>',
171
+ });
172
+ });
173
+
174
+ const query = {
175
+ mode: 'edit',
176
+ route: '/styleguide',
177
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
178
+ sc_lang: 'en',
179
+ sc_site: 'website',
180
+ sc_variant: 'dev',
181
+ sc_version: 'latest',
182
+ secret: secret,
183
+ sc_layoutKind: 'shared',
184
+ } as EditingRenderQueryParams;
185
+
186
+ it('should handle request', async () => {
187
+ const req = mockRequest({ query });
188
+
189
+ const middleware = new EditingRenderMiddleware();
190
+
191
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
192
+
193
+ const handler = middleware.getHandler();
194
+
195
+ sinon
196
+ .stub(middleware['dataFetcher'], 'get')
197
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
198
+ const res = await handler(req);
199
+
200
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
201
+ site: 'website',
202
+ itemId: '{11111111-1111-1111-1111-111111111111}',
203
+ language: 'en',
204
+ variantIds: ['dev'],
205
+ version: 'latest',
206
+ mode: 'edit',
207
+ layoutKind: 'shared',
208
+ });
209
+
210
+ const body = await res.text();
211
+
212
+ expect(res.status).to.equal(200);
213
+ expect(body).to.equal('<div>some html</div>');
214
+
215
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
216
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
217
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
218
+ );
219
+ });
220
+
221
+ it('should pass multiple variant ids into setPreviewData when sc_variantId parameter has many values', async () => {
222
+ const query = {
223
+ mode: 'edit',
224
+ route: '/styleguide',
225
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
226
+ sc_lang: 'en',
227
+ sc_site: 'website',
228
+ secret: secret,
229
+ sc_variant: 'id-1,id-2,id-3',
230
+ } as EditingRenderQueryParams;
231
+
232
+ const req = mockRequest({ query });
233
+
234
+ const middleware = new EditingRenderMiddleware();
235
+
236
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
237
+
238
+ const handler = middleware.getHandler();
239
+
240
+ sinon
241
+ .stub(middleware['dataFetcher'], 'get')
242
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
243
+ await handler(req);
244
+
245
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
246
+ site: 'website',
247
+ itemId: '{11111111-1111-1111-1111-111111111111}',
248
+ language: 'en',
249
+ variantIds: ['id-1', 'id-2', 'id-3'],
250
+ version: undefined,
251
+ mode: 'edit',
252
+ layoutKind: undefined,
253
+ });
254
+ });
255
+
256
+ it('should handle request with missing optional parameters', async () => {
257
+ const queryWithoutOptionalParams = {
258
+ mode: 'edit',
259
+ route: '/styleguide',
260
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
261
+ sc_lang: 'en',
262
+ sc_site: 'website',
263
+ secret: secret,
264
+ } as EditingRenderQueryParams;
265
+ const req = mockRequest({ query: queryWithoutOptionalParams });
266
+
267
+ const middleware = new EditingRenderMiddleware();
268
+
269
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
270
+
271
+ const handler = middleware.getHandler();
272
+
273
+ sinon
274
+ .stub(middleware['dataFetcher'], 'get')
275
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
276
+
277
+ const res = await handler(req);
278
+
279
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
280
+ site: 'website',
281
+ itemId: '{11111111-1111-1111-1111-111111111111}',
282
+ language: 'en',
283
+ variantIds: ['_default'],
284
+ version: undefined,
285
+ mode: 'edit',
286
+ layoutKind: undefined,
287
+ });
288
+
289
+ const body = await res.text();
290
+
291
+ expect(res.status).to.equal(200);
292
+ expect(body).to.equal('<div>some html</div>');
293
+
294
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
295
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
296
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
297
+ );
298
+ });
299
+
300
+ it('should use custom resolvePageUrl', async () => {
301
+ const req = mockRequest({ query });
302
+
303
+ const middleware = new EditingRenderMiddleware({
304
+ resolvePageUrl: (itemPath) => {
305
+ return `/custom/path${itemPath}`;
306
+ },
307
+ });
308
+
309
+ const handler = middleware.getHandler();
310
+
311
+ sinon
312
+ .stub(middleware['dataFetcher'], 'get')
313
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
314
+
315
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
316
+
317
+ const res = await handler(req);
318
+
319
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
320
+ site: 'website',
321
+ itemId: '{11111111-1111-1111-1111-111111111111}',
322
+ language: 'en',
323
+ variantIds: ['dev'],
324
+ version: 'latest',
325
+ mode: 'edit',
326
+ layoutKind: 'shared',
327
+ });
328
+
329
+ const body = await res.text();
330
+
331
+ expect(res.status).to.equal(200);
332
+ expect(body).to.equal('<div>some html</div>');
333
+ });
334
+
335
+ it('should handle request with special characters in route', async () => {
336
+ const query = {
337
+ mode: 'edit',
338
+ route: '/Åbout',
339
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
340
+ sc_lang: 'en',
341
+ sc_site: 'website',
342
+ sc_variant: 'dev',
343
+ sc_version: 'latest',
344
+ secret: secret,
345
+ sc_layoutKind: 'shared',
346
+ } as EditingRenderQueryParams;
347
+
348
+ const req = mockRequest({ query });
349
+
350
+ const middleware = new EditingRenderMiddleware();
351
+ const handler = middleware.getHandler();
352
+
353
+ sinon
354
+ .stub(middleware['dataFetcher'], 'get')
355
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
356
+
357
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
358
+
359
+ const res = await handler(req);
360
+
361
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
362
+ site: 'website',
363
+ itemId: '{11111111-1111-1111-1111-111111111111}',
364
+ language: 'en',
365
+ variantIds: ['dev'],
366
+ version: 'latest',
367
+ mode: 'edit',
368
+ layoutKind: 'shared',
369
+ });
370
+
371
+ const body = await res.text();
372
+
373
+ expect(res.status).to.equal(200);
374
+ expect(body).to.equal('<div>some html</div>');
375
+
376
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
377
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
378
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
379
+ );
380
+ });
381
+
382
+ it('should response with 400 for missing query params', async () => {
383
+ const req = mockRequest({ query: { sc_site: 'website', secret } });
384
+
385
+ const middleware = new EditingRenderMiddleware();
386
+ const handler = middleware.getHandler();
387
+
388
+ const res = await handler(req);
389
+
390
+ const body = await res.json();
391
+
392
+ expect(res.status).to.equal(400);
393
+ expect(body).to.deep.equal({
394
+ html: '<html><body>Missing required query parameters: sc_itemid, sc_lang, route, mode</body></html>',
395
+ });
396
+ });
397
+
398
+ it('should set allowed origins when multiple allowed origins are provided in env variable', async () => {
399
+ process.env.JSS_ALLOWED_ORIGINS = 'https://allowed.com,https://anotherallowed.com';
400
+ const req = mockRequest({ query });
401
+
402
+ const middleware = new EditingRenderMiddleware();
403
+ const handler = middleware.getHandler();
404
+
405
+ sinon
406
+ .stub(middleware['dataFetcher'], 'get')
407
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
408
+
409
+ const res = await handler(req);
410
+
411
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
412
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
413
+ `frame-ancestors 'self' https://allowed.com https://anotherallowed.com ${EDITING_ALLOWED_ORIGINS.join(
414
+ ' '
415
+ )}`
416
+ );
417
+ });
418
+
419
+ it('should issue internal request propagating allowed query parameters', async () => {
420
+ const protectedQuery = {} as Query;
421
+ protectedQuery[QUERY_PARAM_VERCEL_PROTECTION_BYPASS] = 'bypass123';
422
+ protectedQuery[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE] = 'true';
423
+ protectedQuery['someOtherParam'] = 'shouldNotBeIncluded';
424
+ const req = mockRequest({ query: { ...query, ...protectedQuery } });
425
+
426
+ const middleware = new EditingRenderMiddleware();
427
+
428
+ const handler = middleware.getHandler();
429
+
430
+ const fetcherGetStub = sinon
431
+ .stub(middleware['dataFetcher'], 'get')
432
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
433
+
434
+ await handler(req);
435
+
436
+ const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
437
+ expect(fetchRequestUrl.includes(`${QUERY_PARAM_VERCEL_PROTECTION_BYPASS}=bypass123`)).to.be
438
+ .true;
439
+ expect(fetchRequestUrl.includes(`${QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE}=true`)).to.be.true;
440
+ expect(fetchRequestUrl.includes('someOtherParam=shouldNotBeIncluded')).to.be.false;
441
+ });
442
+
443
+ it('should issue intrnal request propagating allowed headers', async () => {
444
+ const req = mockRequest({
445
+ query,
446
+ headers: {
447
+ authorization: 'yes',
448
+ cookie: 'sc_another_cookie=12345',
449
+ otherHeader: 'shouldNotBeIncluded',
450
+ },
451
+ });
452
+
453
+ const middleware = new EditingRenderMiddleware();
454
+ const handler = middleware.getHandler();
455
+
456
+ sinon.stub(middleware as any, 'getPreviewDataCookies').returns(mockPreviewCookies);
457
+
458
+ const fetcherGetStub = sinon
459
+ .stub(middleware['dataFetcher'], 'get')
460
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
461
+
462
+ await handler(req);
463
+
464
+ const fetchRequestHeaders = fetcherGetStub.getCall(0).args[1]?.headers as Headers;
465
+
466
+ expect(fetchRequestHeaders).to.not.be.undefined;
467
+ expect(fetchRequestHeaders).to.have.property(
468
+ 'cookie',
469
+ 'sc_another_cookie=12345;_preview_data=1122334455; Max-Age=3; Path=/; HttpOnly; Secure; SameSite=None'
470
+ );
471
+ expect(fetchRequestHeaders).to.have.property('authorization', 'yes');
472
+ expect(fetchRequestHeaders).to.not.have.property('otherHeader');
473
+ });
474
+
475
+ it('should return 200 if internal request successful', async () => {
476
+ const req = mockRequest({ query });
477
+
478
+ const middleware = new EditingRenderMiddleware();
479
+ const handler = middleware.getHandler();
480
+
481
+ sinon
482
+ .stub(middleware['dataFetcher'], 'get')
483
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
484
+
485
+ const res = await handler(req);
486
+
487
+ expect(res.status).to.equal(200);
488
+ });
489
+
490
+ it('should remove preview cookies before responding to browser', async () => {
491
+ const req = mockRequest({ query });
492
+
493
+ const middleware = new EditingRenderMiddleware();
494
+ const handler = middleware.getHandler();
495
+
496
+ sinon
497
+ .stub(middleware['dataFetcher'], 'get')
498
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
499
+
500
+ const res = await handler(req);
501
+
502
+ expect(res.headers.has('Set-Cookie')).to.be.false;
503
+ expect(res.status).to.equal(200);
504
+ });
505
+
506
+ it('should respondWith 500 if rendered html empty', async () => {
507
+ const req = mockRequest({ query });
508
+
509
+ const middleware = new EditingRenderMiddleware();
510
+ const handler = middleware.getHandler();
511
+
512
+ sinon
513
+ .stub(middleware['dataFetcher'], 'get')
514
+ .resolves({ status: 200, statusText: 'success', data: '' });
515
+
516
+ const res = await handler(req);
517
+
518
+ expect(res.status).to.equal(500);
519
+ });
520
+
521
+ it('should respondWith 500 if internal request fails', async () => {
522
+ const req = mockRequest({ query });
523
+
524
+ const middleware = new EditingRenderMiddleware();
525
+ const handler = middleware.getHandler();
526
+
527
+ sinon.stub(middleware['dataFetcher'], 'get').throws(new Error('Request failed'));
528
+
529
+ const res = await handler(req);
530
+
531
+ expect(res.status).to.equal(500);
532
+ });
533
+
534
+ describe('Design Library handling', () => {
535
+ const query = {
536
+ mode: DesignLibraryMode.Normal,
537
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
538
+ sc_lang: 'en',
539
+ sc_site: 'website',
540
+ sc_variant: 'dev',
541
+ sc_version: 'latest',
542
+ secret: secret,
543
+ sc_renderingId: '123',
544
+ dataSourceId: '456',
545
+ sc_uid: '789',
546
+ generation: 'variant',
547
+ };
548
+
549
+ it('should handle request with mode=library', async () => {
550
+ const req = mockRequest({ query });
551
+
552
+ const middleware = new EditingRenderMiddleware();
553
+
554
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
555
+
556
+ const handler = middleware.getHandler();
557
+
558
+ sinon
559
+ .stub(middleware['dataFetcher'], 'get')
560
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
561
+
562
+ const res = await handler(req);
563
+
564
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWithMatch({
565
+ itemId: query.sc_itemid,
566
+ componentUid: query.sc_uid,
567
+ renderingId: query.sc_renderingId,
568
+ language: query.sc_lang,
569
+ site: query.sc_site,
570
+ mode: DesignLibraryMode.Normal,
571
+ dataSourceId: query.dataSourceId,
572
+ version: query.sc_version,
573
+ generation: query.generation,
574
+ });
575
+
576
+ const body = await res.text();
577
+
578
+ expect(res.status).to.equal(200);
579
+ expect(body).to.equal('<div>some html</div>');
580
+
581
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
582
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
583
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
584
+ );
585
+ });
586
+
587
+ it('should handle request with mode=library-metadata', async () => {
588
+ const req = mockRequest({
589
+ query: { ...query, mode: DesignLibraryMode.Metadata },
590
+ });
591
+
592
+ const middleware = new EditingRenderMiddleware();
593
+
594
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
595
+
596
+ const handler = middleware.getHandler();
597
+
598
+ sinon
599
+ .stub(middleware['dataFetcher'], 'get')
600
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
601
+
602
+ const res = await handler(req);
603
+
604
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWithMatch({
605
+ itemId: query.sc_itemid,
606
+ componentUid: query.sc_uid,
607
+ renderingId: query.sc_renderingId,
608
+ language: query.sc_lang,
609
+ site: query.sc_site,
610
+ mode: DesignLibraryMode.Metadata,
611
+ dataSourceId: query.dataSourceId,
612
+ version: query.sc_version,
613
+ generation: query.generation,
614
+ });
615
+
616
+ const body = await res.text();
617
+
618
+ expect(res.status).to.equal(200);
619
+ expect(body).to.equal('<div>some html</div>');
620
+
621
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
622
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
623
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
624
+ );
625
+ });
626
+
627
+ it('should response with 400 for missing query params', async () => {
628
+ const req = mockRequest({
629
+ query: { sc_site: 'website', secret },
630
+ });
631
+
632
+ const middleware = new EditingRenderMiddleware();
633
+ const handler = middleware.getHandler();
634
+
635
+ const res = await handler(req);
636
+
637
+ const body = await res.json();
638
+
639
+ expect(res.status).to.equal(400);
640
+ expect(body).to.deep.equal({
641
+ html: '<html><body>Missing required query parameters: sc_itemid, sc_lang, route, mode</body></html>',
642
+ });
643
+ });
644
+ });
645
+
646
+ describe('Sitecore Preview handling', () => {
647
+ const query = {
648
+ mode: 'preview',
649
+ route: '/styleguide',
650
+ sc_itemid: '{11111111-1111-1111-1111-111111111111}',
651
+ sc_lang: 'en',
652
+ sc_site: 'website',
653
+ sc_variant: 'dev',
654
+ sc_version: 'latest',
655
+ secret: secret,
656
+ sc_layoutKind: 'final',
657
+ } as EditingRenderQueryParams;
658
+
659
+ it('should handle request', async () => {
660
+ const req = mockRequest({ query });
661
+
662
+ const middleware = new EditingRenderMiddleware();
663
+
664
+ const getPreviewDataCookiesSpy = sinon
665
+ .stub(middleware as any, 'getPreviewDataCookies')
666
+ .returns(mockPreviewCookies);
667
+
668
+ const handler = middleware.getHandler();
669
+
670
+ sinon
671
+ .stub(middleware['dataFetcher'], 'get')
672
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
673
+
674
+ const res = await handler(req);
675
+
676
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
677
+ site: 'website',
678
+ itemId: '{11111111-1111-1111-1111-111111111111}',
679
+ language: 'en',
680
+ variantIds: ['dev'],
681
+ version: 'latest',
682
+ mode: 'preview',
683
+ layoutKind: 'final',
684
+ });
685
+
686
+ expect(res.headers.has('Access-Control-Allow-Origin')).to.be.true;
687
+ expect(res.headers.get('Access-Control-Allow-Origin')).to.equal(allowedOrigin);
688
+
689
+ expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
690
+ expect(res.headers.get('Access-Control-Allow-Methods')).to.equal(
691
+ 'GET, POST, OPTIONS, DELETE, PUT, PATCH'
692
+ );
693
+
694
+ expect(res.headers.has('Set-Cookie')).to.be.true;
695
+ expect(res.headers.getSetCookie()).to.have.members([
696
+ 'sc_site=website; Path=/; HttpOnly; SameSite=None; Secure',
697
+ 'sc_preview=true; Path=/; HttpOnly; SameSite=None; Secure',
698
+ ]);
699
+
700
+ expect(res.headers.getSetCookie()).to.not.include(mockPreviewCookies);
701
+
702
+ expect(res.headers.has('Content-Security-Policy')).to.be.true;
703
+ expect(res.headers.get('Content-Security-Policy')).to.equal(
704
+ `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
705
+ );
706
+
707
+ const body = await res.text();
708
+
709
+ expect(res.status).to.equal(200);
710
+ expect(body).to.equal('<div>some html</div>');
711
+ });
712
+ });
713
+
714
+ describe('internal server request host resolution', () => {
715
+ it('should use host header for making the internal request if config setting or env is not provided and we are not in XMC env', async () => {
716
+ const req = mockRequest({ query });
717
+ const reqHost = 'some-other-host';
718
+ req.headers.set('host', reqHost);
719
+
720
+ const middleware = new EditingRenderMiddleware();
721
+
722
+ const handler = middleware.getHandler();
723
+
724
+ const fetcherGetStub = sinon
725
+ .stub(middleware['dataFetcher'], 'get')
726
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
727
+
728
+ await handler(req);
729
+
730
+ const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
731
+ expect(fetchRequestUrl.includes(reqHost)).to.be.true;
732
+ });
733
+
734
+ it('should use http://localhost:3000 for making the internal request if config setting or env is not provided and we are in XMC', async () => {
735
+ process.env.SITECORE = 'yes';
736
+ const req = mockRequest({ query });
737
+ const expectedHost = 'http://localhost:3000';
738
+ const reqHost = 'some-other-host';
739
+ req.headers.set('host', reqHost);
740
+
741
+ const middleware = new EditingRenderMiddleware();
742
+
743
+ const handler = middleware.getHandler();
744
+
745
+ const fetcherGetStub = sinon
746
+ .stub(middleware['dataFetcher'], 'get')
747
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
748
+
749
+ await handler(req);
750
+
751
+ const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
752
+ expect(fetchRequestUrl.includes(expectedHost)).to.be.true;
753
+ delete process.env.SITECORE;
754
+ });
755
+
756
+ it('should use internal editing url from env variable if provided', async () => {
757
+ const reqHostEnv = 'http://custom-internal-host-env';
758
+ process.env.SITECORE_INTERNAL_EDITING_HOST_URL = reqHostEnv;
759
+
760
+ const req = mockRequest({ query });
761
+
762
+ const middleware = new EditingRenderMiddleware();
763
+
764
+ const handler = middleware.getHandler();
765
+
766
+ const fetcherGetStub = sinon
767
+ .stub(middleware['dataFetcher'], 'get')
768
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
769
+
770
+ await handler(req);
771
+
772
+ const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
773
+ expect(fetchRequestUrl.includes(reqHostEnv)).to.be.true;
774
+ delete process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
775
+ });
776
+
777
+ it('should use internal editing url from config if provided', async () => {
778
+ const reqHostConfig = 'http://custom-internal-host-config';
779
+ const reqHostEnv = 'http://custom-internal-host-env';
780
+ process.env.SITECORE_INTERNAL_EDITING_HOST_URL = reqHostEnv;
781
+
782
+ const req = mockRequest({ query });
783
+
784
+ const middleware = new EditingRenderMiddleware({
785
+ sitecoreInternalEditingHostUrl: reqHostConfig,
786
+ });
787
+
788
+ const handler = middleware.getHandler();
789
+
790
+ const fetcherGetStub = sinon
791
+ .stub(middleware['dataFetcher'], 'get')
792
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
793
+
794
+ await handler(req);
795
+
796
+ const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
797
+ expect(fetchRequestUrl.includes(reqHostConfig)).to.be.true;
798
+ delete process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
799
+ });
800
+ });
801
+ });