@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.
- package/LICENSE.txt +202 -0
- package/README.md +3 -0
- package/package.json +101 -0
- package/src/client/index.ts +12 -0
- package/src/client/sitecore-astro-client.test.ts +271 -0
- package/src/client/sitecore-astro-client.ts +137 -0
- package/src/components/AstroImage.astro +114 -0
- package/src/components/Date.astro +76 -0
- package/src/components/DefaultEmptyFieldEditingComponentImage.astro +24 -0
- package/src/components/DefaultEmptyFieldEditingComponentText.astro +12 -0
- package/src/components/EditingScripts.astro +49 -0
- package/src/components/EmptyRendering.astro +3 -0
- package/src/components/ErrorBoundary.astro +77 -0
- package/src/components/FieldMetadata.astro +30 -0
- package/src/components/File.astro +46 -0
- package/src/components/HiddenRendering.astro +22 -0
- package/src/components/Image.astro +155 -0
- package/src/components/Link.astro +105 -0
- package/src/components/MissingComponent.astro +39 -0
- package/src/components/Placeholder/EmptyPlaceholder.astro +9 -0
- package/src/components/Placeholder/Placeholder.astro +100 -0
- package/src/components/Placeholder/PlaceholderMetadata.astro +102 -0
- package/src/components/Placeholder/PlaceholderUtils.astro +153 -0
- package/src/components/Placeholder/index.ts +5 -0
- package/src/components/Placeholder/models.ts +82 -0
- package/src/components/Placeholder/placeholder-utils.test.ts +162 -0
- package/src/components/Placeholder/placeholder-utils.ts +80 -0
- package/src/components/RenderWrapper.astro +31 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +97 -0
- package/src/components/sharedTypes/index.ts +1 -0
- package/src/components/sharedTypes/props.ts +17 -0
- package/src/config/define-config.test.ts +526 -0
- package/src/config/define-config.ts +99 -0
- package/src/config/index.ts +1 -0
- package/src/config-cli/define-cli-config.test.ts +95 -0
- package/src/config-cli/define-cli-config.ts +50 -0
- package/src/config-cli/index.ts +1 -0
- package/src/context.ts +68 -0
- package/src/editing/constants.ts +8 -0
- package/src/editing/editing-config-middleware.test.ts +166 -0
- package/src/editing/editing-config-middleware.ts +111 -0
- package/src/editing/editing-render-middleware.test.ts +801 -0
- package/src/editing/editing-render-middleware.ts +288 -0
- package/src/editing/index.ts +16 -0
- package/src/editing/render-middleware.test.ts +57 -0
- package/src/editing/render-middleware.ts +51 -0
- package/src/editing/utils.test.ts +852 -0
- package/src/editing/utils.ts +308 -0
- package/src/enhancers/WithEmptyFieldEditingComponent.astro +56 -0
- package/src/enhancers/WithFieldMetadata.astro +31 -0
- package/src/env.d.ts +12 -0
- package/src/index.ts +16 -0
- package/src/middleware/index.ts +24 -0
- package/src/middleware/middleware.test.ts +507 -0
- package/src/middleware/middleware.ts +167 -0
- package/src/middleware/multisite-middleware.test.ts +672 -0
- package/src/middleware/multisite-middleware.ts +147 -0
- package/src/middleware/robots-middleware.test.ts +113 -0
- package/src/middleware/robots-middleware.ts +47 -0
- package/src/middleware/sitemap-middleware.test.ts +152 -0
- package/src/middleware/sitemap-middleware.ts +65 -0
- package/src/services/component-props-service.ts +182 -0
- package/src/sharedTypes/component-props.ts +17 -0
- package/src/site/index.ts +1 -0
- package/src/test-data/components/Bar.astro +0 -0
- package/src/test-data/components/Baz.astro +0 -0
- package/src/test-data/components/Foo.astro +0 -0
- package/src/test-data/components/Hero.variant.astro +0 -0
- package/src/test-data/components/NotComponent.bsx +0 -0
- package/src/test-data/components/Qux.astro +0 -0
- package/src/test-data/components/folded/Folded.astro +0 -0
- package/src/test-data/components/folded/random-file-2.docx +0 -0
- package/src/test-data/components/random-file.txt +0 -0
- package/src/test-data/helpers.ts +46 -0
- package/src/test-data/personalizeData.ts +63 -0
- package/src/tools/generate-map.ts +83 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/templating/components.test.ts +305 -0
- package/src/tools/templating/components.ts +49 -0
- package/src/tools/templating/constants.ts +4 -0
- package/src/tools/templating/default-component.test.ts +31 -0
- package/src/tools/templating/default-component.ts +63 -0
- package/src/tools/templating/index.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.test.ts +48 -0
- 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
|
+
});
|