@exdst-sitecore-content-sdk/astro 0.0.23 → 0.0.25

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 (72) hide show
  1. package/package.json +9 -14
  2. package/src/client/sitecore-astro-client.ts +0 -45
  3. package/src/context.ts +14 -16
  4. package/src/env.d.ts +1 -1
  5. package/src/sharedTypes/component-props.ts +0 -10
  6. package/src/tools/generate-map.ts +9 -21
  7. package/src/client/sitecore-astro-client.test.ts +0 -292
  8. package/src/components/AstroImage.astro.test.ts +0 -541
  9. package/src/components/Date.astro.test.ts +0 -197
  10. package/src/components/EditingScripts.astro.test.ts +0 -267
  11. package/src/components/ErrorBoundary.astro.test.ts +0 -252
  12. package/src/components/ErrorComponent.astro.test.ts +0 -31
  13. package/src/components/FieldMetadata.astro.test.ts +0 -40
  14. package/src/components/File.astro.test.ts +0 -68
  15. package/src/components/HiddenRendering.astro.test.ts +0 -36
  16. package/src/components/Image.astro.test.ts +0 -438
  17. package/src/components/Link.astro.test.ts +0 -261
  18. package/src/components/MissingComponent.astro.test.ts +0 -21
  19. package/src/components/Placeholder/Placeholder.astro.test.ts +0 -1088
  20. package/src/components/Placeholder/PlaceholderMetadata.astro.test.ts +0 -228
  21. package/src/components/Placeholder/PlaceholderUtils.astro.test.ts +0 -149
  22. package/src/components/Placeholder/placeholder-utils.test.ts +0 -309
  23. package/src/components/RichText.astro.test.ts +0 -205
  24. package/src/components/Text.astro.test.ts +0 -273
  25. package/src/config/define-config.test.ts +0 -526
  26. package/src/config-cli/define-cli-config.test.ts +0 -88
  27. package/src/editing/editing-config-middleware.test.ts +0 -164
  28. package/src/editing/editing-render-middleware.test.ts +0 -1143
  29. package/src/editing/render-middleware.test.ts +0 -57
  30. package/src/editing/utils.test.ts +0 -1212
  31. package/src/enhancers/WithEmptyFieldEditingComponent.astro.test.ts +0 -380
  32. package/src/enhancers/WithFieldMetadata.astro.test.ts +0 -113
  33. package/src/middleware/middleware.test.ts +0 -520
  34. package/src/middleware/multisite-middleware.test.ts +0 -667
  35. package/src/middleware/robots-middleware.test.ts +0 -129
  36. package/src/middleware/sitemap-middleware.test.ts +0 -184
  37. package/src/services/component-props-service.ts +0 -183
  38. package/src/tests/astro-helpers.ts +0 -61
  39. package/src/tests/helpers.ts +0 -46
  40. package/src/tests/personalizeData.ts +0 -63
  41. package/src/tests/test-components/CustomErrorComponent.astro +0 -3
  42. package/src/tests/test-components/CustomHiddenRendering.astro +0 -10
  43. package/src/tests/test-components/CustomMissingComponent.astro +0 -9
  44. package/src/tests/test-components/DownloadCallout.astro +0 -12
  45. package/src/tests/test-components/EmptyFieldEditingComponent.astro +0 -5
  46. package/src/tests/test-components/ErrorBoundaryWithError.astro +0 -10
  47. package/src/tests/test-components/Home.astro +0 -12
  48. package/src/tests/test-components/SxaRichText.astro +0 -23
  49. package/src/tests/test-components/SxaRichTextDefault.astro +0 -7
  50. package/src/tests/test-components/SxaRichTextWithTitle.astro +0 -8
  51. package/src/tests/test-components/TestComponent.astro +0 -9
  52. package/src/tests/test-components/TestComponentWithError.astro +0 -4
  53. package/src/tests/test-components/TestComponentWithField.astro +0 -17
  54. package/src/tests/test-components/TestHeader.astro +0 -8
  55. package/src/tests/test-components/TestLogo.astro +0 -5
  56. package/src/tests/test-components/TestParentWrapperComponent.astro +0 -5
  57. package/src/tests/test-components/TestWrapperComponent.astro +0 -5
  58. package/src/tests/test-components/map-components/Bar.astro +0 -0
  59. package/src/tests/test-components/map-components/Baz.astro +0 -0
  60. package/src/tests/test-components/map-components/Foo.astro +0 -0
  61. package/src/tests/test-components/map-components/Hero.variant.astro +0 -0
  62. package/src/tests/test-components/map-components/NotComponent.bsx +0 -0
  63. package/src/tests/test-components/map-components/Qux.astro +0 -0
  64. package/src/tests/test-components/map-components/folded/Folded.astro +0 -0
  65. package/src/tests/test-components/map-components/folded/random-file-2.docx +0 -0
  66. package/src/tests/test-components/map-components/random-file.txt +0 -0
  67. package/src/tests/test-data/metadata-data.ts +0 -86
  68. package/src/tests/test-data/normal-mode-data.ts +0 -466
  69. package/src/tests/vitest.setup.ts +0 -4
  70. package/src/tools/templating/components.test.ts +0 -318
  71. package/src/tools/templating/default-component.test.ts +0 -31
  72. package/src/utils/utils.test.ts +0 -48
@@ -1,1143 +0,0 @@
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/content/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 '../tests/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] = Array.isArray(value) ? value.map(String) : 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.include(allowedOrigin);
110
-
111
- expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
112
- expect(res.headers.get('Access-Control-Allow-Methods')).to.include(
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.include(
118
- 'Content-Type, Authorization'
119
- );
120
- });
121
-
122
- it('should respond with 401 for invalid secret', async () => {
123
- const query = {} as Query;
124
- query[QUERY_PARAM_EDITING_SECRET] = 'nope';
125
- const req = mockRequest({
126
- query,
127
- });
128
-
129
- const middleware = new EditingRenderMiddleware();
130
- const handler = middleware.getHandler();
131
-
132
- const res = await handler(req);
133
-
134
- const body = await res.json();
135
-
136
- expect(res.status).to.equal(401);
137
- expect(body).to.deep.equal({
138
- html: '<html><body>Missing or invalid secret</body></html>',
139
- });
140
- });
141
-
142
- it('should stop request and return 401 when CORS match is not met', async () => {
143
- const req = mockRequest({
144
- headers: { origin: 'https://notallowed.com' },
145
- });
146
- const middleware = new EditingRenderMiddleware();
147
- const handler = middleware.getHandler();
148
-
149
- const res = await handler(req);
150
-
151
- const body = await res.json();
152
-
153
- expect(res.status).to.equal(401);
154
- expect(body).to.deep.equal({
155
- html: '<html><body>Requests from origin https://notallowed.com not allowed</body></html>',
156
- });
157
- });
158
-
159
- it('should respond with 401 for missing secret', async () => {
160
- const query = {} as Query;
161
- const req = mockRequest({ query });
162
-
163
- const middleware = new EditingRenderMiddleware();
164
- const handler = middleware.getHandler();
165
-
166
- const res = await handler(req);
167
-
168
- const body = await res.json();
169
-
170
- expect(res.status).to.equal(401);
171
- expect(body).to.deep.equal({
172
- html: '<html><body>Missing or invalid secret</body></html>',
173
- });
174
- });
175
-
176
- const query = {
177
- mode: 'edit',
178
- route: '/styleguide',
179
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
180
- sc_lang: 'en',
181
- sc_site: 'website',
182
- sc_variant: 'dev',
183
- sc_version: 'latest',
184
- secret: secret,
185
- sc_layoutKind: 'shared',
186
- } as EditingRenderQueryParams;
187
-
188
- it('should handle request', async () => {
189
- const req = mockRequest({ query });
190
-
191
- const middleware = new EditingRenderMiddleware();
192
-
193
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
194
-
195
- const handler = middleware.getHandler();
196
-
197
- sinon
198
- .stub(middleware['dataFetcher'], 'get')
199
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
200
- const res = await handler(req);
201
-
202
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
203
- site: 'website',
204
- itemId: '{11111111-1111-1111-1111-111111111111}',
205
- language: 'en',
206
- variantIds: ['dev'],
207
- version: 'latest',
208
- mode: 'edit',
209
- layoutKind: 'shared',
210
- });
211
-
212
- const body = await res.text();
213
-
214
- expect(res.status).to.equal(200);
215
- expect(body).to.equal('<div>some html</div>');
216
-
217
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
218
- expect(res.headers.get('Content-Security-Policy')).to.equal(
219
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
220
- );
221
- });
222
-
223
- it('should pass multiple variant ids into setPreviewData when sc_variantId parameter has many values', async () => {
224
- const query = {
225
- mode: 'edit',
226
- route: '/styleguide',
227
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
228
- sc_lang: 'en',
229
- sc_site: 'website',
230
- secret: secret,
231
- sc_variant: 'id-1,id-2,id-3',
232
- } as EditingRenderQueryParams;
233
-
234
- const req = mockRequest({ query });
235
-
236
- const middleware = new EditingRenderMiddleware();
237
-
238
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
239
-
240
- const handler = middleware.getHandler();
241
-
242
- sinon
243
- .stub(middleware['dataFetcher'], 'get')
244
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
245
- await handler(req);
246
-
247
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
248
- site: 'website',
249
- itemId: '{11111111-1111-1111-1111-111111111111}',
250
- language: 'en',
251
- variantIds: ['id-1', 'id-2', 'id-3'],
252
- version: undefined,
253
- mode: 'edit',
254
- layoutKind: undefined,
255
- });
256
- });
257
-
258
- it('should handle request with missing optional parameters', async () => {
259
- const queryWithoutOptionalParams = {
260
- mode: 'edit',
261
- route: '/styleguide',
262
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
263
- sc_lang: 'en',
264
- sc_site: 'website',
265
- secret: secret,
266
- } as EditingRenderQueryParams;
267
- const req = mockRequest({ query: queryWithoutOptionalParams });
268
-
269
- const middleware = new EditingRenderMiddleware();
270
-
271
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
272
-
273
- const handler = middleware.getHandler();
274
-
275
- sinon
276
- .stub(middleware['dataFetcher'], 'get')
277
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
278
-
279
- const res = await handler(req);
280
-
281
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
282
- site: 'website',
283
- itemId: '{11111111-1111-1111-1111-111111111111}',
284
- language: 'en',
285
- variantIds: ['_default'],
286
- version: undefined,
287
- mode: 'edit',
288
- layoutKind: undefined,
289
- });
290
-
291
- const body = await res.text();
292
-
293
- expect(res.status).to.equal(200);
294
- expect(body).to.equal('<div>some html</div>');
295
-
296
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
297
- expect(res.headers.get('Content-Security-Policy')).to.equal(
298
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
299
- );
300
- expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
301
- });
302
-
303
- it('should use custom resolvePageUrl', async () => {
304
- const req = mockRequest({ query });
305
-
306
- const middleware = new EditingRenderMiddleware({
307
- resolvePageUrl: (itemPath) => {
308
- return `/custom/path${itemPath}`;
309
- },
310
- });
311
-
312
- const handler = middleware.getHandler();
313
-
314
- sinon
315
- .stub(middleware['dataFetcher'], 'get')
316
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
317
-
318
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
319
-
320
- const res = await handler(req);
321
-
322
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
323
- site: 'website',
324
- itemId: '{11111111-1111-1111-1111-111111111111}',
325
- language: 'en',
326
- variantIds: ['dev'],
327
- version: 'latest',
328
- mode: 'edit',
329
- layoutKind: 'shared',
330
- });
331
-
332
- const body = await res.text();
333
-
334
- expect(res.status).to.equal(200);
335
- expect(body).to.equal('<div>some html</div>');
336
- });
337
-
338
- it('should handle request with special characters in route', async () => {
339
- const query = {
340
- mode: 'edit',
341
- route: '/Åbout',
342
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
343
- sc_lang: 'en',
344
- sc_site: 'website',
345
- sc_variant: 'dev',
346
- sc_version: 'latest',
347
- secret: secret,
348
- sc_layoutKind: 'shared',
349
- } as EditingRenderQueryParams;
350
-
351
- const req = mockRequest({ query });
352
-
353
- const middleware = new EditingRenderMiddleware();
354
- const handler = middleware.getHandler();
355
-
356
- sinon
357
- .stub(middleware['dataFetcher'], 'get')
358
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
359
-
360
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
361
-
362
- const res = await handler(req);
363
-
364
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
365
- site: 'website',
366
- itemId: '{11111111-1111-1111-1111-111111111111}',
367
- language: 'en',
368
- variantIds: ['dev'],
369
- version: 'latest',
370
- mode: 'edit',
371
- layoutKind: 'shared',
372
- });
373
-
374
- const body = await res.text();
375
-
376
- expect(res.status).to.equal(200);
377
- expect(body).to.equal('<div>some html</div>');
378
-
379
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
380
- expect(res.headers.get('Content-Security-Policy')).to.equal(
381
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
382
- );
383
- expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
384
- });
385
-
386
- it('should response with 400 for missing query params', async () => {
387
- const req = mockRequest({ query: { sc_site: 'website', secret } });
388
-
389
- const middleware = new EditingRenderMiddleware();
390
- const handler = middleware.getHandler();
391
-
392
- const res = await handler(req);
393
-
394
- const body = await res.json();
395
-
396
- expect(res.status).to.equal(400);
397
- expect(body).to.deep.equal({
398
- html: '<html><body>Missing required query parameters: sc_itemid, sc_lang, route, mode</body></html>',
399
- });
400
- });
401
-
402
- it('should set allowed origins when multiple allowed origins are provided in env variable', async () => {
403
- process.env.JSS_ALLOWED_ORIGINS = 'https://allowed.com,https://anotherallowed.com';
404
- const req = mockRequest({ query });
405
-
406
- const middleware = new EditingRenderMiddleware();
407
- const handler = middleware.getHandler();
408
-
409
- sinon
410
- .stub(middleware['dataFetcher'], 'get')
411
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
412
-
413
- const res = await handler(req);
414
-
415
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
416
- expect(res.headers.get('Content-Security-Policy')).to.equal(
417
- `frame-ancestors 'self' https://allowed.com https://anotherallowed.com ${EDITING_ALLOWED_ORIGINS.join(
418
- ' '
419
- )}`
420
- );
421
- });
422
-
423
- it('should issue internal request propagating allowed query parameters', async () => {
424
- const protectedQuery = {} as Query;
425
- protectedQuery[QUERY_PARAM_VERCEL_PROTECTION_BYPASS] = 'bypass123';
426
- protectedQuery[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE] = 'true';
427
- protectedQuery['someOtherParam'] = 'shouldNotBeIncluded';
428
- const req = mockRequest({ query: { ...query, ...protectedQuery } });
429
-
430
- const middleware = new EditingRenderMiddleware();
431
-
432
- const handler = middleware.getHandler();
433
-
434
- const fetcherGetStub = sinon
435
- .stub(middleware['dataFetcher'], 'get')
436
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
437
-
438
- await handler(req);
439
-
440
- const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
441
- expect(fetchRequestUrl.includes(`${QUERY_PARAM_VERCEL_PROTECTION_BYPASS}=bypass123`)).to.be
442
- .true;
443
- expect(fetchRequestUrl.includes(`${QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE}=true`)).to.be.true;
444
- expect(fetchRequestUrl.includes('someOtherParam=shouldNotBeIncluded')).to.be.false;
445
- });
446
-
447
- describe('allowedQueryParams configuration', () => {
448
- it('should not include additional query params when allowedQueryParams is not configured', async () => {
449
- const customQuery = {
450
- ...query,
451
- customParam1: 'value1',
452
- customParam2: 'value2',
453
- };
454
- const req = mockRequest({ query: customQuery });
455
-
456
- const middleware = new EditingRenderMiddleware();
457
- const handler = middleware.getHandler();
458
-
459
- sinon
460
- .stub(middleware['dataFetcher'], 'get')
461
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
462
-
463
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
464
-
465
- await handler(req);
466
-
467
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
468
- site: 'website',
469
- itemId: '{11111111-1111-1111-1111-111111111111}',
470
- language: 'en',
471
- variantIds: ['dev'],
472
- version: 'latest',
473
- mode: 'edit',
474
- layoutKind: 'shared',
475
- });
476
- });
477
-
478
- it('should include allowed query params when configured as array (objects and strings)', async () => {
479
- const customQuery = {
480
- ...query,
481
- customParam1: 'value1',
482
- customParam2: 'value2',
483
- stringParam: 'string-value',
484
- notAllowed: 'shouldNotBeIncluded',
485
- };
486
- const req = mockRequest({ query: customQuery });
487
-
488
- const middleware = new EditingRenderMiddleware({
489
- allowedQueryParams: [
490
- { name: 'customParam1' },
491
- { name: 'customParam2' },
492
- 'stringParam',
493
- 'missingStringParam',
494
- ],
495
- });
496
-
497
- const handler = middleware.getHandler();
498
-
499
- sinon
500
- .stub(middleware['dataFetcher'], 'get')
501
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
502
-
503
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
504
-
505
- await handler(req);
506
-
507
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
508
- site: 'website',
509
- itemId: '{11111111-1111-1111-1111-111111111111}',
510
- language: 'en',
511
- variantIds: ['dev'],
512
- version: 'latest',
513
- mode: 'edit',
514
- layoutKind: 'shared',
515
- customParam1: 'value1',
516
- customParam2: 'value2',
517
- stringParam: 'string-value',
518
- });
519
- });
520
-
521
- it('should return 400 when required allowed query param is missing (mixed types)', async () => {
522
- const customQuery = {
523
- ...query,
524
- customParam1: 'value1',
525
- stringParam: 'value',
526
- };
527
- const req = mockRequest({ query: customQuery });
528
-
529
- const middleware = new EditingRenderMiddleware({
530
- allowedQueryParams: [
531
- { name: 'customParam1', required: true },
532
- { name: 'customParam2', required: true },
533
- 'stringParam',
534
- ],
535
- });
536
- const handler = middleware.getHandler();
537
-
538
- const res = await handler(req);
539
-
540
- const body = await res.json();
541
-
542
- expect(res.status).to.equal(400);
543
- expect(body).to.deep.equal({
544
- html: '<html><body>Missing required query parameters: customParam2</body></html>',
545
- });
546
- });
547
-
548
- it('should handle optional allowed query params correctly', async () => {
549
- const customQuery = {
550
- ...query,
551
- requiredParam: 'required-value',
552
- // optionalParam is not provided
553
- };
554
- const req = mockRequest({ query: customQuery });
555
-
556
- const middleware = new EditingRenderMiddleware({
557
- allowedQueryParams: [
558
- { name: 'requiredParam', required: true },
559
- { name: 'optionalParam', required: false },
560
- ],
561
- });
562
- const handler = middleware.getHandler();
563
-
564
- sinon
565
- .stub(middleware['dataFetcher'], 'get')
566
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
567
-
568
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
569
-
570
- const res = await handler(req);
571
-
572
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
573
- site: 'website',
574
- itemId: '{11111111-1111-1111-1111-111111111111}',
575
- language: 'en',
576
- variantIds: ['dev'],
577
- version: 'latest',
578
- mode: 'edit',
579
- layoutKind: 'shared',
580
- requiredParam: 'required-value',
581
- });
582
- expect(res.status).to.equal(200);
583
- });
584
-
585
- it('should use resolver function returning strings and objects', async () => {
586
- const customQuery = {
587
- ...query,
588
- prefixedParam1: 'value1',
589
- prefixedParam2: 'value2',
590
- requiredParam: 'required-value',
591
- otherParam: 'shouldNotBeIncluded',
592
- };
593
- const req = mockRequest({ query: customQuery });
594
-
595
- const resolver = (queryParamKeys: string[]) => {
596
- const result: Array<string | { name: string; required?: boolean }> = [];
597
- queryParamKeys
598
- .filter((key) => key.startsWith('prefixed'))
599
- .forEach((key) => result.push(key));
600
-
601
- if (queryParamKeys.includes('requiredParam')) {
602
- result.push({ name: 'requiredParam', required: true });
603
- }
604
- return result;
605
- };
606
-
607
- const middleware = new EditingRenderMiddleware({
608
- allowedQueryParams: resolver,
609
- });
610
- const handler = middleware.getHandler();
611
-
612
- sinon
613
- .stub(middleware['dataFetcher'], 'get')
614
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
615
-
616
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
617
-
618
- await handler(req);
619
-
620
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
621
- site: 'website',
622
- itemId: '{11111111-1111-1111-1111-111111111111}',
623
- language: 'en',
624
- variantIds: ['dev'],
625
- version: 'latest',
626
- mode: 'edit',
627
- layoutKind: 'shared',
628
- prefixedParam1: 'value1',
629
- prefixedParam2: 'value2',
630
- requiredParam: 'required-value',
631
- });
632
- });
633
-
634
- it('should return 400 when resolver returns mixed types with missing required param', async () => {
635
- const customQuery = {
636
- ...query,
637
- presentParam: 'value1',
638
- stringParam: 'string-value',
639
- };
640
- const req = mockRequest({ query: customQuery });
641
-
642
- const resolver = () => {
643
- return [
644
- 'stringParam',
645
- { name: 'presentParam', required: true },
646
- { name: 'missingRequiredParam', required: true },
647
- ];
648
- };
649
-
650
- const middleware = new EditingRenderMiddleware({
651
- allowedQueryParams: resolver,
652
- });
653
- const handler = middleware.getHandler();
654
-
655
- const res = await handler(req);
656
-
657
- const body = await res.json();
658
-
659
- expect(res.status).to.equal(400);
660
- expect(body).to.deep.equal({
661
- html: '<html><body>Missing required query parameters: missingRequiredParam</body></html>',
662
- });
663
- });
664
-
665
- it('should handle resolver function returning empty array', async () => {
666
- const customQuery = {
667
- ...query,
668
- customParam: 'value',
669
- };
670
- const req = mockRequest({ query: customQuery });
671
-
672
- const resolver = () => {
673
- return []; // No additional params allowed
674
- };
675
-
676
- const middleware = new EditingRenderMiddleware({
677
- allowedQueryParams: resolver,
678
- });
679
- const handler = middleware.getHandler();
680
-
681
- sinon
682
- .stub(middleware['dataFetcher'], 'get')
683
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
684
-
685
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
686
-
687
- const res = await handler(req);
688
-
689
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
690
- site: 'website',
691
- itemId: '{11111111-1111-1111-1111-111111111111}',
692
- language: 'en',
693
- variantIds: ['dev'],
694
- version: 'latest',
695
- mode: 'edit',
696
- layoutKind: 'shared',
697
- });
698
- expect(res.status).to.equal(200);
699
- });
700
-
701
- it('should handle various data types in allowed query params', async () => {
702
- const customQuery = {
703
- ...query,
704
- stringParam: 'string-value',
705
- numberParam: '123',
706
- booleanParam: 'true',
707
- arrayParam: ['val1', 'val2'],
708
- };
709
- const req = mockRequest({ query: customQuery });
710
-
711
- const middleware = new EditingRenderMiddleware({
712
- allowedQueryParams: [
713
- { name: 'stringParam' },
714
- { name: 'numberParam' },
715
- { name: 'booleanParam' },
716
- { name: 'arrayParam' },
717
- ],
718
- });
719
- const handler = middleware.getHandler();
720
-
721
- sinon
722
- .stub(middleware['dataFetcher'], 'get')
723
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
724
-
725
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
726
-
727
- await handler(req);
728
-
729
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
730
- site: 'website',
731
- itemId: '{11111111-1111-1111-1111-111111111111}',
732
- language: 'en',
733
- variantIds: ['dev'],
734
- version: 'latest',
735
- mode: 'edit',
736
- layoutKind: 'shared',
737
- stringParam: 'string-value',
738
- numberParam: '123',
739
- booleanParam: 'true',
740
- arrayParam: ['val1', 'val2'],
741
- });
742
- });
743
-
744
- it('should combine missing required editing params and missing required allowed params in error message', async () => {
745
- const customQuery = {
746
- sc_site: 'website',
747
- secret: secret,
748
- // missing: sc_itemid, sc_lang, route, mode
749
- // missing: requiredAllowedParam
750
- };
751
- const req = mockRequest({ query: customQuery });
752
-
753
- const middleware = new EditingRenderMiddleware({
754
- allowedQueryParams: [{ name: 'requiredAllowedParam', required: true }],
755
- });
756
- const handler = middleware.getHandler();
757
-
758
- const res = await handler(req);
759
-
760
- const body = await res.json();
761
- const html = body.html;
762
-
763
- expect(res.status).to.equal(400);
764
- expect(html).to.include('Missing required query parameters:');
765
- expect(html).to.include('sc_itemid');
766
- expect(html).to.include('sc_lang');
767
- expect(html).to.include('route');
768
- expect(html).to.include('mode');
769
- expect(html).to.include('requiredAllowedParam');
770
- });
771
- });
772
-
773
- it('should issue intrnal request propagating allowed headers', async () => {
774
- const req = mockRequest({
775
- query,
776
- headers: {
777
- authorization: 'yes',
778
- cookie: 'sc_another_cookie=12345',
779
- otherHeader: 'shouldNotBeIncluded',
780
- },
781
- });
782
-
783
- const middleware = new EditingRenderMiddleware();
784
- const handler = middleware.getHandler();
785
-
786
- sinon.stub(middleware as any, 'getPreviewDataCookies').returns(mockPreviewCookies);
787
-
788
- const fetcherGetStub = sinon
789
- .stub(middleware['dataFetcher'], 'get')
790
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
791
-
792
- await handler(req);
793
-
794
- const fetchRequestHeaders = fetcherGetStub.getCall(0).args[1]?.headers as Headers;
795
-
796
- expect(fetchRequestHeaders).to.not.be.undefined;
797
- expect(fetchRequestHeaders).to.have.property(
798
- 'cookie',
799
- 'sc_another_cookie=12345;_preview_data=1122334455; Max-Age=3; Path=/; HttpOnly; Secure; SameSite=None'
800
- );
801
- expect(fetchRequestHeaders).to.have.property('authorization', 'yes');
802
- expect(fetchRequestHeaders).to.not.have.property('otherHeader');
803
- });
804
-
805
- it('should return 200 if internal request successful', async () => {
806
- const req = mockRequest({ query });
807
-
808
- const middleware = new EditingRenderMiddleware();
809
- const handler = middleware.getHandler();
810
-
811
- sinon
812
- .stub(middleware['dataFetcher'], 'get')
813
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
814
-
815
- const res = await handler(req);
816
-
817
- expect(res.status).to.equal(200);
818
- expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
819
- });
820
-
821
- it('should remove preview cookies before responding to browser', async () => {
822
- const req = mockRequest({ query });
823
-
824
- const middleware = new EditingRenderMiddleware();
825
- const handler = middleware.getHandler();
826
-
827
- sinon
828
- .stub(middleware['dataFetcher'], 'get')
829
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
830
-
831
- const res = await handler(req);
832
-
833
- expect(res.headers.has('Set-Cookie')).to.be.false;
834
- expect(res.status).to.equal(200);
835
- });
836
-
837
- describe('error handling', () => {
838
- beforeEach(() => {
839
- sinon.stub(console, 'error');
840
- });
841
-
842
- afterEach(() => {
843
- sinon.restore();
844
- });
845
-
846
- it('should respondWith 500 if rendered html empty', async () => {
847
- const req = mockRequest({ query });
848
-
849
- const middleware = new EditingRenderMiddleware();
850
- const handler = middleware.getHandler();
851
-
852
- sinon
853
- .stub(middleware['dataFetcher'], 'get')
854
- .resolves({ status: 200, statusText: 'success', data: '' });
855
-
856
- const res = await handler(req);
857
-
858
- expect(res.status).to.equal(500);
859
- });
860
-
861
- it('should respondWith 500 if internal request fails', async () => {
862
- const req = mockRequest({ query });
863
-
864
- const middleware = new EditingRenderMiddleware();
865
- const handler = middleware.getHandler();
866
-
867
- sinon.stub(middleware['dataFetcher'], 'get').throws(new Error('Request failed'));
868
-
869
- const res = await handler(req);
870
-
871
- expect(res.status).to.equal(500);
872
- });
873
- });
874
-
875
- describe('Design Library handling', () => {
876
- const query = {
877
- mode: DesignLibraryMode.Normal,
878
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
879
- sc_lang: 'en',
880
- sc_site: 'website',
881
- sc_variant: 'dev',
882
- sc_version: 'latest',
883
- secret: secret,
884
- sc_renderingId: '123',
885
- dataSourceId: '456',
886
- sc_uid: '789',
887
- generation: 'variant',
888
- };
889
-
890
- it('should handle request with mode=library', async () => {
891
- const req = mockRequest({ query });
892
-
893
- const middleware = new EditingRenderMiddleware();
894
-
895
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
896
-
897
- const handler = middleware.getHandler();
898
-
899
- sinon
900
- .stub(middleware['dataFetcher'], 'get')
901
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
902
-
903
- const res = await handler(req);
904
-
905
- expect(getPreviewDataCookiesSpy).to.have.been.calledWithMatch({
906
- itemId: query.sc_itemid,
907
- componentUid: query.sc_uid,
908
- renderingId: query.sc_renderingId,
909
- language: query.sc_lang,
910
- site: query.sc_site,
911
- mode: DesignLibraryMode.Normal,
912
- dataSourceId: query.dataSourceId,
913
- version: query.sc_version,
914
- generation: query.generation,
915
- });
916
-
917
- const body = await res.text();
918
-
919
- expect(res.status).to.equal(200);
920
- expect(body).to.equal('<div>some html</div>');
921
-
922
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
923
- expect(res.headers.get('Content-Security-Policy')).to.equal(
924
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
925
- );
926
- });
927
-
928
- it('should handle request with mode=library-metadata', async () => {
929
- const req = mockRequest({
930
- query: { ...query, mode: DesignLibraryMode.Metadata },
931
- });
932
-
933
- const middleware = new EditingRenderMiddleware();
934
-
935
- const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
936
-
937
- const handler = middleware.getHandler();
938
-
939
- sinon
940
- .stub(middleware['dataFetcher'], 'get')
941
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
942
-
943
- const res = await handler(req);
944
-
945
- expect(getPreviewDataCookiesSpy).to.have.been.calledWithMatch({
946
- itemId: query.sc_itemid,
947
- componentUid: query.sc_uid,
948
- renderingId: query.sc_renderingId,
949
- language: query.sc_lang,
950
- site: query.sc_site,
951
- mode: DesignLibraryMode.Metadata,
952
- dataSourceId: query.dataSourceId,
953
- version: query.sc_version,
954
- generation: query.generation,
955
- });
956
-
957
- const body = await res.text();
958
-
959
- expect(res.status).to.equal(200);
960
- expect(body).to.equal('<div>some html</div>');
961
-
962
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
963
- expect(res.headers.get('Content-Security-Policy')).to.equal(
964
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
965
- );
966
- });
967
-
968
- it('should response with 400 for missing query params', async () => {
969
- const req = mockRequest({
970
- query: { sc_site: 'website', secret },
971
- });
972
-
973
- const middleware = new EditingRenderMiddleware();
974
- const handler = middleware.getHandler();
975
-
976
- const res = await handler(req);
977
-
978
- const body = await res.json();
979
-
980
- expect(res.status).to.equal(400);
981
- expect(body).to.deep.equal({
982
- html: '<html><body>Missing required query parameters: sc_itemid, sc_lang, route, mode</body></html>',
983
- });
984
- });
985
- });
986
-
987
- describe('Sitecore Preview handling', () => {
988
- const query = {
989
- mode: 'preview',
990
- route: '/styleguide',
991
- sc_itemid: '{11111111-1111-1111-1111-111111111111}',
992
- sc_lang: 'en',
993
- sc_site: 'website',
994
- sc_variant: 'dev',
995
- sc_version: 'latest',
996
- secret: secret,
997
- sc_layoutKind: 'final',
998
- } as EditingRenderQueryParams;
999
-
1000
- it('should handle request', async () => {
1001
- const req = mockRequest({ query });
1002
-
1003
- const middleware = new EditingRenderMiddleware();
1004
-
1005
- const getPreviewDataCookiesSpy = sinon
1006
- .stub(middleware as any, 'getPreviewDataCookies')
1007
- .returns(mockPreviewCookies);
1008
-
1009
- const handler = middleware.getHandler();
1010
-
1011
- sinon
1012
- .stub(middleware['dataFetcher'], 'get')
1013
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
1014
-
1015
- const res = await handler(req);
1016
-
1017
- expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
1018
- site: 'website',
1019
- itemId: '{11111111-1111-1111-1111-111111111111}',
1020
- language: 'en',
1021
- variantIds: ['dev'],
1022
- version: 'latest',
1023
- mode: 'preview',
1024
- layoutKind: 'final',
1025
- });
1026
-
1027
- expect(res.headers.has('Access-Control-Allow-Origin')).to.be.true;
1028
- expect(res.headers.get('Access-Control-Allow-Origin')).to.equal(allowedOrigin);
1029
-
1030
- expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
1031
- expect(res.headers.get('Access-Control-Allow-Methods')).to.equal(
1032
- 'GET, POST, OPTIONS, DELETE, PUT, PATCH'
1033
- );
1034
-
1035
- expect(res.headers.has('Set-Cookie')).to.be.true;
1036
- expect(res.headers.getSetCookie()).to.have.members([
1037
- 'sc_site=website; Path=/; HttpOnly; SameSite=None; Secure',
1038
- 'sc_preview=true; Path=/; HttpOnly; SameSite=None; Secure',
1039
- ]);
1040
-
1041
- expect(res.headers.getSetCookie()).to.not.include(mockPreviewCookies);
1042
-
1043
- expect(res.headers.has('Content-Security-Policy')).to.be.true;
1044
- expect(res.headers.get('Content-Security-Policy')).to.equal(
1045
- `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
1046
- );
1047
-
1048
- const body = await res.text();
1049
-
1050
- expect(res.status).to.equal(200);
1051
- expect(body).to.equal('<div>some html</div>');
1052
- expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
1053
- });
1054
- });
1055
-
1056
- describe('internal server request host resolution', () => {
1057
- 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 () => {
1058
- const req = mockRequest({ query });
1059
- const reqHost = 'some-other-host';
1060
- req.headers.set('host', reqHost);
1061
-
1062
- const middleware = new EditingRenderMiddleware();
1063
-
1064
- const handler = middleware.getHandler();
1065
-
1066
- const fetcherGetStub = sinon
1067
- .stub(middleware['dataFetcher'], 'get')
1068
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
1069
-
1070
- await handler(req);
1071
-
1072
- const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
1073
- expect(fetchRequestUrl.includes(reqHost)).to.be.true;
1074
- });
1075
-
1076
- 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 () => {
1077
- process.env.SITECORE = 'yes';
1078
- const req = mockRequest({ query });
1079
- const expectedHost = 'http://localhost:3000';
1080
- const reqHost = 'some-other-host';
1081
- req.headers.set('host', reqHost);
1082
-
1083
- const middleware = new EditingRenderMiddleware();
1084
-
1085
- const handler = middleware.getHandler();
1086
-
1087
- const fetcherGetStub = sinon
1088
- .stub(middleware['dataFetcher'], 'get')
1089
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
1090
-
1091
- await handler(req);
1092
-
1093
- const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
1094
- expect(fetchRequestUrl.includes(expectedHost)).to.be.true;
1095
- delete process.env.SITECORE;
1096
- });
1097
-
1098
- it('should use internal editing url from env variable if provided', async () => {
1099
- const reqHostEnv = 'http://custom-internal-host-env';
1100
- process.env.SITECORE_INTERNAL_EDITING_HOST_URL = reqHostEnv;
1101
-
1102
- const req = mockRequest({ query });
1103
-
1104
- const middleware = new EditingRenderMiddleware();
1105
-
1106
- const handler = middleware.getHandler();
1107
-
1108
- const fetcherGetStub = sinon
1109
- .stub(middleware['dataFetcher'], 'get')
1110
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
1111
-
1112
- await handler(req);
1113
-
1114
- const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
1115
- expect(fetchRequestUrl.includes(reqHostEnv)).to.be.true;
1116
- delete process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
1117
- });
1118
-
1119
- it('should use internal editing url from config if provided', async () => {
1120
- const reqHostConfig = 'http://custom-internal-host-config';
1121
- const reqHostEnv = 'http://custom-internal-host-env';
1122
- process.env.SITECORE_INTERNAL_EDITING_HOST_URL = reqHostEnv;
1123
-
1124
- const req = mockRequest({ query });
1125
-
1126
- const middleware = new EditingRenderMiddleware({
1127
- sitecoreInternalEditingHostUrl: reqHostConfig,
1128
- });
1129
-
1130
- const handler = middleware.getHandler();
1131
-
1132
- const fetcherGetStub = sinon
1133
- .stub(middleware['dataFetcher'], 'get')
1134
- .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
1135
-
1136
- await handler(req);
1137
-
1138
- const fetchRequestUrl = fetcherGetStub.getCall(0).args[0];
1139
- expect(fetchRequestUrl.includes(reqHostConfig)).to.be.true;
1140
- delete process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
1141
- });
1142
- });
1143
- });