@exdst-sitecore-content-sdk/astro 0.0.16 → 0.0.19
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.
- package/README.md +3 -3
- package/package.json +41 -42
- package/src/client/index.ts +1 -1
- package/src/client/sitecore-astro-client.test.ts +41 -20
- package/src/client/sitecore-astro-client.ts +74 -57
- package/src/components/AstroImage.astro +2 -2
- package/src/components/AstroImage.astro.test.ts +542 -0
- package/src/components/Date.astro +5 -1
- package/src/components/Date.astro.test.ts +197 -0
- package/src/components/DefaultEmptyFieldEditingComponentImage.astro +4 -0
- package/src/components/DefaultEmptyFieldEditingComponentText.astro +4 -0
- package/src/components/EditingScripts.astro +2 -2
- package/src/components/EditingScripts.astro.test.ts +267 -0
- package/src/components/ErrorBoundary.astro +8 -9
- package/src/components/ErrorBoundary.astro.test.ts +252 -0
- package/src/components/ErrorComponent.astro +16 -0
- package/src/components/ErrorComponent.astro.test.ts +31 -0
- package/src/components/FieldMetadata.astro +1 -1
- package/src/components/FieldMetadata.astro.test.ts +40 -0
- package/src/components/File.astro +5 -1
- package/src/components/File.astro.test.ts +68 -0
- package/src/components/HiddenRendering.astro.test.ts +36 -0
- package/src/components/Image.astro +18 -4
- package/src/components/Image.astro.test.ts +438 -0
- package/src/components/Link.astro +13 -1
- package/src/components/Link.astro.test.ts +261 -0
- package/src/components/MissingComponent.astro.test.ts +21 -0
- package/src/components/Placeholder/Placeholder.astro +18 -23
- package/src/components/Placeholder/Placeholder.astro.test.ts +1088 -0
- package/src/components/Placeholder/PlaceholderMetadata.astro +24 -18
- package/src/components/Placeholder/PlaceholderMetadata.astro.test.ts +228 -0
- package/src/components/Placeholder/PlaceholderUtils.astro +21 -40
- package/src/components/Placeholder/PlaceholderUtils.astro.test.ts +149 -0
- package/src/components/Placeholder/models.ts +26 -17
- package/src/components/Placeholder/placeholder-utils.test.ts +153 -6
- package/src/components/Placeholder/placeholder-utils.ts +33 -11
- package/src/components/RichText.astro +9 -1
- package/src/components/RichText.astro.test.ts +205 -0
- package/src/components/Text.astro +15 -3
- package/src/components/Text.astro.test.ts +273 -0
- package/src/config/define-config.test.ts +5 -5
- package/src/config/define-config.ts +22 -42
- package/src/config-cli/define-cli-config.test.ts +5 -12
- package/src/config-cli/define-cli-config.ts +4 -8
- package/src/context.ts +42 -11
- package/src/debug.ts +13 -0
- package/src/editing/editing-config-middleware.test.ts +5 -7
- package/src/editing/editing-config-middleware.ts +11 -7
- package/src/editing/editing-render-middleware.test.ts +366 -24
- package/src/editing/editing-render-middleware.ts +34 -12
- package/src/editing/index.ts +2 -0
- package/src/editing/render-middleware.test.ts +1 -1
- package/src/editing/render-middleware.ts +1 -1
- package/src/editing/types.ts +39 -0
- package/src/editing/utils.test.ts +364 -4
- package/src/editing/utils.ts +82 -24
- package/src/enhancers/WithEmptyFieldEditingComponent.astro +1 -1
- package/src/enhancers/WithEmptyFieldEditingComponent.astro.test.ts +380 -0
- package/src/enhancers/WithFieldMetadata.astro.test.ts +113 -0
- package/src/index.ts +10 -7
- package/src/middleware/index.ts +4 -12
- package/src/middleware/middleware.test.ts +13 -0
- package/src/middleware/middleware.ts +12 -3
- package/src/middleware/multisite-middleware.test.ts +45 -50
- package/src/middleware/multisite-middleware.ts +33 -6
- package/src/middleware/robots-middleware.test.ts +20 -4
- package/src/middleware/robots-middleware.ts +10 -3
- package/src/middleware/sitemap-middleware.test.ts +35 -3
- package/src/middleware/sitemap-middleware.ts +7 -6
- package/src/services/component-props-service.ts +7 -6
- package/src/sharedTypes/component-props.ts +15 -4
- package/src/site/index.ts +1 -1
- package/src/tests/astro-helpers.ts +61 -0
- package/src/tests/test-components/CustomErrorComponent.astro +3 -0
- package/src/tests/test-components/CustomHiddenRendering.astro +10 -0
- package/src/tests/test-components/CustomMissingComponent.astro +9 -0
- package/src/tests/test-components/DownloadCallout.astro +12 -0
- package/src/tests/test-components/EmptyFieldEditingComponent.astro +5 -0
- package/src/tests/test-components/ErrorBoundaryWithError.astro +10 -0
- package/src/tests/test-components/Home.astro +12 -0
- package/src/tests/test-components/SxaRichText.astro +23 -0
- package/src/tests/test-components/SxaRichTextDefault.astro +7 -0
- package/src/tests/test-components/SxaRichTextWithTitle.astro +8 -0
- package/src/tests/test-components/TestComponent.astro +9 -0
- package/src/tests/test-components/TestComponentWithError.astro +4 -0
- package/src/tests/test-components/TestComponentWithField.astro +17 -0
- package/src/tests/test-components/TestHeader.astro +8 -0
- package/src/tests/test-components/TestLogo.astro +5 -0
- package/src/tests/test-components/TestParentWrapperComponent.astro +5 -0
- package/src/tests/test-components/TestWrapperComponent.astro +5 -0
- package/src/tests/test-data/metadata-data.ts +86 -0
- package/src/tests/test-data/normal-mode-data.ts +466 -0
- package/src/tests/vitest.setup.ts +4 -0
- package/src/tools/generate-map.ts +4 -3
- package/src/tools/index.ts +2 -4
- package/src/tools/templating/components.test.ts +100 -87
- package/src/tools/templating/components.ts +2 -1
- package/src/tools/templating/default-component.ts +3 -8
- package/src/utils/utils.ts +20 -2
- /package/src/{test-data → tests}/helpers.ts +0 -0
- /package/src/{test-data → tests}/personalizeData.ts +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/Bar.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/Baz.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/Foo.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/Hero.variant.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/NotComponent.bsx +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/Qux.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/folded/Folded.astro +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/folded/random-file-2.docx +0 -0
- /package/src/{test-data/components → tests/test-components/map-components}/random-file.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable dot-notation */
|
|
2
2
|
/* eslint-disable no-unused-expressions */
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
4
|
import { expect, use } from 'chai';
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
QUERY_PARAM_EDITING_SECRET,
|
|
8
8
|
EditingRenderQueryParams,
|
|
9
9
|
DesignLibraryMode,
|
|
10
|
-
} from '@sitecore-content-sdk/
|
|
10
|
+
} from '@sitecore-content-sdk/content/editing';
|
|
11
11
|
import { EditingRenderMiddleware } from './editing-render-middleware';
|
|
12
12
|
import sinonChai from 'sinon-chai';
|
|
13
13
|
import sinon from 'sinon';
|
|
14
|
-
import { mockRequest as MockRequest, Query } from '../
|
|
14
|
+
import { mockRequest as MockRequest, Query } from '../tests/helpers';
|
|
15
15
|
import {
|
|
16
16
|
QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
|
|
17
17
|
QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
|
|
@@ -49,7 +49,7 @@ const toQuery = (params: Query | EditingRenderQueryParams): Query => {
|
|
|
49
49
|
|
|
50
50
|
Object.entries(params).forEach(([key, value]) => {
|
|
51
51
|
if (value !== undefined && value !== null) {
|
|
52
|
-
query[key] = String(value);
|
|
52
|
+
query[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
|
|
@@ -106,15 +106,17 @@ describe('EditingRenderMiddleware', () => {
|
|
|
106
106
|
expect(res.body).to.equal(null);
|
|
107
107
|
|
|
108
108
|
expect(res.headers.has('Access-Control-Allow-Origin')).to.be.true;
|
|
109
|
-
expect(res.headers.get('Access-Control-Allow-Origin')).to.
|
|
109
|
+
expect(res.headers.get('Access-Control-Allow-Origin')).to.include(allowedOrigin);
|
|
110
110
|
|
|
111
111
|
expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
|
|
112
|
-
expect(res.headers.get('Access-Control-Allow-Methods')).to.
|
|
112
|
+
expect(res.headers.get('Access-Control-Allow-Methods')).to.include(
|
|
113
113
|
'GET, POST, OPTIONS, DELETE, PUT, PATCH'
|
|
114
114
|
);
|
|
115
115
|
|
|
116
116
|
expect(res.headers.has('Access-Control-Allow-Headers')).to.be.true;
|
|
117
|
-
expect(res.headers.get('Access-Control-Allow-Headers')).to.
|
|
117
|
+
expect(res.headers.get('Access-Control-Allow-Headers')).to.include(
|
|
118
|
+
'Content-Type, Authorization'
|
|
119
|
+
);
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
it('should respond with 401 for invalid secret', async () => {
|
|
@@ -295,6 +297,7 @@ describe('EditingRenderMiddleware', () => {
|
|
|
295
297
|
expect(res.headers.get('Content-Security-Policy')).to.equal(
|
|
296
298
|
`frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
|
|
297
299
|
);
|
|
300
|
+
expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
|
|
298
301
|
});
|
|
299
302
|
|
|
300
303
|
it('should use custom resolvePageUrl', async () => {
|
|
@@ -377,6 +380,7 @@ describe('EditingRenderMiddleware', () => {
|
|
|
377
380
|
expect(res.headers.get('Content-Security-Policy')).to.equal(
|
|
378
381
|
`frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
|
|
379
382
|
);
|
|
383
|
+
expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
|
|
380
384
|
});
|
|
381
385
|
|
|
382
386
|
it('should response with 400 for missing query params', async () => {
|
|
@@ -440,6 +444,332 @@ describe('EditingRenderMiddleware', () => {
|
|
|
440
444
|
expect(fetchRequestUrl.includes('someOtherParam=shouldNotBeIncluded')).to.be.false;
|
|
441
445
|
});
|
|
442
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
|
+
|
|
443
773
|
it('should issue intrnal request propagating allowed headers', async () => {
|
|
444
774
|
const req = mockRequest({
|
|
445
775
|
query,
|
|
@@ -485,6 +815,7 @@ describe('EditingRenderMiddleware', () => {
|
|
|
485
815
|
const res = await handler(req);
|
|
486
816
|
|
|
487
817
|
expect(res.status).to.equal(200);
|
|
818
|
+
expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
|
|
488
819
|
});
|
|
489
820
|
|
|
490
821
|
it('should remove preview cookies before responding to browser', async () => {
|
|
@@ -503,32 +834,42 @@ describe('EditingRenderMiddleware', () => {
|
|
|
503
834
|
expect(res.status).to.equal(200);
|
|
504
835
|
});
|
|
505
836
|
|
|
506
|
-
|
|
507
|
-
|
|
837
|
+
describe('error handling', () => {
|
|
838
|
+
beforeEach(() => {
|
|
839
|
+
sinon.stub(console, 'error');
|
|
840
|
+
});
|
|
508
841
|
|
|
509
|
-
|
|
510
|
-
|
|
842
|
+
afterEach(() => {
|
|
843
|
+
sinon.restore();
|
|
844
|
+
});
|
|
511
845
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.resolves({ status: 200, statusText: 'success', data: '' });
|
|
846
|
+
it('should respondWith 500 if rendered html empty', async () => {
|
|
847
|
+
const req = mockRequest({ query });
|
|
515
848
|
|
|
516
|
-
|
|
849
|
+
const middleware = new EditingRenderMiddleware();
|
|
850
|
+
const handler = middleware.getHandler();
|
|
517
851
|
|
|
518
|
-
|
|
519
|
-
|
|
852
|
+
sinon
|
|
853
|
+
.stub(middleware['dataFetcher'], 'get')
|
|
854
|
+
.resolves({ status: 200, statusText: 'success', data: '' });
|
|
520
855
|
|
|
521
|
-
|
|
522
|
-
const req = mockRequest({ query });
|
|
856
|
+
const res = await handler(req);
|
|
523
857
|
|
|
524
|
-
|
|
525
|
-
|
|
858
|
+
expect(res.status).to.equal(500);
|
|
859
|
+
});
|
|
526
860
|
|
|
527
|
-
|
|
861
|
+
it('should respondWith 500 if internal request fails', async () => {
|
|
862
|
+
const req = mockRequest({ query });
|
|
528
863
|
|
|
529
|
-
|
|
864
|
+
const middleware = new EditingRenderMiddleware();
|
|
865
|
+
const handler = middleware.getHandler();
|
|
866
|
+
|
|
867
|
+
sinon.stub(middleware['dataFetcher'], 'get').throws(new Error('Request failed'));
|
|
530
868
|
|
|
531
|
-
|
|
869
|
+
const res = await handler(req);
|
|
870
|
+
|
|
871
|
+
expect(res.status).to.equal(500);
|
|
872
|
+
});
|
|
532
873
|
});
|
|
533
874
|
|
|
534
875
|
describe('Design Library handling', () => {
|
|
@@ -708,6 +1049,7 @@ describe('EditingRenderMiddleware', () => {
|
|
|
708
1049
|
|
|
709
1050
|
expect(res.status).to.equal(200);
|
|
710
1051
|
expect(body).to.equal('<div>some html</div>');
|
|
1052
|
+
expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
|
|
711
1053
|
});
|
|
712
1054
|
});
|
|
713
1055
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NativeDataFetcher } from '@sitecore-content-sdk/core';
|
|
2
2
|
import {
|
|
3
3
|
QUERY_PARAM_EDITING_SECRET,
|
|
4
4
|
EDITING_ALLOWED_ORIGINS,
|
|
5
|
-
|
|
5
|
+
INVALID_SECRET_HTML_MESSAGE,
|
|
6
|
+
} from '@sitecore-content-sdk/content/editing';
|
|
6
7
|
import { getEditingSecret } from '../utils';
|
|
7
|
-
import { getEnforcedCorsHeaders } from '@sitecore-content-sdk/core/
|
|
8
|
-
import { LayoutServicePageState } from '@sitecore-content-sdk/
|
|
8
|
+
import { getEnforcedCorsHeaders } from '@sitecore-content-sdk/core/tools';
|
|
9
|
+
import { LayoutServicePageState } from '@sitecore-content-sdk/content/layout';
|
|
9
10
|
import { RenderMiddlewareBase } from './render-middleware';
|
|
11
|
+
import debug from '../debug';
|
|
10
12
|
import {
|
|
11
13
|
cleanupPreviewCookies,
|
|
12
14
|
getCSPHeader,
|
|
@@ -19,11 +21,14 @@ import {
|
|
|
19
21
|
mapEditingParams,
|
|
20
22
|
PreviewCookies,
|
|
21
23
|
resolveServerUrl,
|
|
24
|
+
getAllowedQueryParams,
|
|
22
25
|
} from './utils';
|
|
26
|
+
import type { AllowedQueryParams } from './types';
|
|
23
27
|
import * as cookie from 'cookie';
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* Configuration for the Editing Render Middleware.
|
|
31
|
+
* @public
|
|
27
32
|
*/
|
|
28
33
|
export type EditingRenderMiddlewareConfig = {
|
|
29
34
|
/**
|
|
@@ -38,11 +43,18 @@ export type EditingRenderMiddlewareConfig = {
|
|
|
38
43
|
* The internal host URL for the application, used for server-side requests for page rendering during editing.
|
|
39
44
|
*/
|
|
40
45
|
sitecoreInternalEditingHostUrl?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Query string parameters to allow and include in the preview data.
|
|
48
|
+
* - Array: each item is a parameter name (string) or an object `{ name, required? }`.
|
|
49
|
+
* - Function: receives the request's query parameter names and returns the list of allowed parameters.
|
|
50
|
+
*/
|
|
51
|
+
allowedQueryParams?: AllowedQueryParams;
|
|
41
52
|
};
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
55
|
* Middleware / handler for use in the editing render API route (e.g. '/api/editing/render')
|
|
45
56
|
* which is required for Sitecore editing support.
|
|
57
|
+
* @public
|
|
46
58
|
*/
|
|
47
59
|
export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
48
60
|
private dataFetcher: NativeDataFetcher;
|
|
@@ -85,7 +97,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
|
85
97
|
|
|
86
98
|
private handler = async (_req: Request): Promise<Response> => {
|
|
87
99
|
const { method, headers } = _req;
|
|
88
|
-
const url = new URL(_req.url
|
|
100
|
+
const url = new URL(_req.url);
|
|
89
101
|
const query = getEditingRenderQueryParams(url.searchParams);
|
|
90
102
|
|
|
91
103
|
debug.editing('editing render middleware start: %o', {
|
|
@@ -132,7 +144,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
|
132
144
|
|
|
133
145
|
return new Response(
|
|
134
146
|
JSON.stringify({
|
|
135
|
-
html:
|
|
147
|
+
html: INVALID_SECRET_HTML_MESSAGE,
|
|
136
148
|
}),
|
|
137
149
|
{
|
|
138
150
|
status: 401,
|
|
@@ -144,7 +156,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
|
144
156
|
if (_req.method === 'OPTIONS') {
|
|
145
157
|
debug.editing('preflight request');
|
|
146
158
|
|
|
147
|
-
// CORS headers are set by
|
|
159
|
+
// CORS headers are set by getEnforcedCorsHeaders
|
|
148
160
|
return new Response(null, {
|
|
149
161
|
status: 204,
|
|
150
162
|
headers: _res.headers,
|
|
@@ -175,15 +187,24 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
|
175
187
|
|
|
176
188
|
const missingQueryParams = requiredQueryParams.filter((param) => !query[param]);
|
|
177
189
|
|
|
190
|
+
const { allowedQueryParams, missingAllowedParams } = getAllowedQueryParams(
|
|
191
|
+
query,
|
|
192
|
+
this.config?.allowedQueryParams
|
|
193
|
+
);
|
|
194
|
+
|
|
178
195
|
// Validate query parameters
|
|
179
|
-
if (missingQueryParams.length) {
|
|
180
|
-
debug.editing('missing required query parameters: %o',
|
|
196
|
+
if (missingQueryParams.length || missingAllowedParams.length) {
|
|
197
|
+
debug.editing('missing required query parameters: %o', [
|
|
198
|
+
...missingQueryParams,
|
|
199
|
+
...missingAllowedParams,
|
|
200
|
+
]);
|
|
181
201
|
|
|
182
202
|
return new Response(
|
|
183
203
|
JSON.stringify({
|
|
184
|
-
html: `<html><body>Missing required query parameters: ${
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
html: `<html><body>Missing required query parameters: ${[
|
|
205
|
+
...missingQueryParams,
|
|
206
|
+
...missingAllowedParams,
|
|
207
|
+
].join(', ')}</body></html>`,
|
|
187
208
|
}),
|
|
188
209
|
{
|
|
189
210
|
status: 400,
|
|
@@ -196,6 +217,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
|
|
|
196
217
|
|
|
197
218
|
const previewDataCookies = this.getPreviewDataCookies({
|
|
198
219
|
...previewDataParams,
|
|
220
|
+
...allowedQueryParams,
|
|
199
221
|
variantIds: previewDataParams.variantIds?.split(','),
|
|
200
222
|
});
|
|
201
223
|
_res.headers.append('Set-Cookie', previewDataCookies);
|
package/src/editing/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable dot-notation */
|
|
2
2
|
import chai from 'chai';
|
|
3
3
|
import chaiString from 'chai-string';
|
|
4
|
-
import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-content-sdk/
|
|
4
|
+
import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-content-sdk/content/editing';
|
|
5
5
|
import { RenderMiddlewareBase } from './render-middleware';
|
|
6
6
|
import {
|
|
7
7
|
QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Base class for middleware that handles pages and components rendering in Sitecore Editors.
|
|
9
|
-
* @
|
|
9
|
+
* @internal
|
|
10
10
|
*/
|
|
11
11
|
export abstract class RenderMiddlewareBase {
|
|
12
12
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an allowed query parameter.
|
|
3
|
+
* @public
|
|
4
|
+
*/
|
|
5
|
+
export interface AllowedQueryParam {
|
|
6
|
+
/**
|
|
7
|
+
* The name of the query parameter to allow.
|
|
8
|
+
*/
|
|
9
|
+
name: string;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the query parameter is required.
|
|
12
|
+
*/
|
|
13
|
+
required?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolver function for allowed query parameters, which can be used to extract additional parameters from the query string beyond the required editing parameters.
|
|
18
|
+
* @param {string[]} queryParams Array of query parameters from incoming URL.
|
|
19
|
+
* @returns {Array<AllowedQueryParam | string>} Allowed query editing parameters.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export type AllowedQueryParamsResolver = (
|
|
23
|
+
queryParams: string[]
|
|
24
|
+
) => Array<AllowedQueryParam | string>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Allowed query parameters which can be defined as an array of parameter names or objects, or a resolver function which can be used to extract additional parameters from the query string beyond the required editing parameters.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type AllowedQueryParams = Array<AllowedQueryParam | string> | AllowedQueryParamsResolver;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of processing allowed query parameters, including any missing required parameters and the set of allowed parameters that were extracted from the query string.
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export interface GetAllowedQueryParamsResult {
|
|
37
|
+
missingAllowedParams: string[];
|
|
38
|
+
allowedQueryParams: { [key: string]: unknown };
|
|
39
|
+
}
|