@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,852 @@
|
|
|
1
|
+
/* eslint-disable no-unused-expressions */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
3
|
+
import chai, { expect } from 'chai';
|
|
4
|
+
import sinon from 'sinon';
|
|
5
|
+
import sinonChai from 'sinon-chai';
|
|
6
|
+
import {
|
|
7
|
+
QUERY_PARAM_EDITING_SECRET,
|
|
8
|
+
DesignLibraryMode,
|
|
9
|
+
PREVIEW_KEY,
|
|
10
|
+
} from '@sitecore-content-sdk/core/editing';
|
|
11
|
+
import { DEFAULT_VARIANT } from '@sitecore-content-sdk/core/personalize';
|
|
12
|
+
import { SITE_KEY } from '@sitecore-content-sdk/core/site';
|
|
13
|
+
import { NativeDataFetcher } from '@sitecore-content-sdk/core';
|
|
14
|
+
import {
|
|
15
|
+
getEditingSecretFromRequest,
|
|
16
|
+
mapEditingParams,
|
|
17
|
+
cleanupPreviewCookies,
|
|
18
|
+
getPreviewCookies,
|
|
19
|
+
getRequiredEditingParamsList,
|
|
20
|
+
getQueryParamsForPropagation,
|
|
21
|
+
getHeadersForPropagation,
|
|
22
|
+
getEditingRequestHtml,
|
|
23
|
+
isDesignLibraryPreviewData,
|
|
24
|
+
resolveServerUrl,
|
|
25
|
+
getCSPHeader,
|
|
26
|
+
} from './utils';
|
|
27
|
+
import {
|
|
28
|
+
QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
|
|
29
|
+
QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
|
|
30
|
+
} from './constants';
|
|
31
|
+
import { mockRequest } from '../test-data/helpers';
|
|
32
|
+
|
|
33
|
+
chai.use(sinonChai);
|
|
34
|
+
|
|
35
|
+
describe('editing/utils', () => {
|
|
36
|
+
const sandbox = sinon.createSandbox();
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
sandbox.restore();
|
|
40
|
+
// Clean up environment variables
|
|
41
|
+
delete process.env.SITECORE_INTERNAL_EDITING_HOST_URL;
|
|
42
|
+
delete process.env.SITECORE;
|
|
43
|
+
delete process.env.VERCEL;
|
|
44
|
+
delete process.env.NETLIFY;
|
|
45
|
+
delete process.env.JSS_ALLOWED_ORIGINS;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('getEditingSecretFromRequest', () => {
|
|
49
|
+
it('should extract secret from request URL', () => {
|
|
50
|
+
const req = {
|
|
51
|
+
url: `https://example.com/api/editing?${QUERY_PARAM_EDITING_SECRET}=secret&other=param`,
|
|
52
|
+
} as Request;
|
|
53
|
+
|
|
54
|
+
const secret = getEditingSecretFromRequest(req);
|
|
55
|
+
expect(secret).to.equal('secret');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return undefined when no secret is present', () => {
|
|
59
|
+
const req = {
|
|
60
|
+
url: 'https://example.com/api/editing?other=param',
|
|
61
|
+
} as Request;
|
|
62
|
+
|
|
63
|
+
const secret = getEditingSecretFromRequest(req);
|
|
64
|
+
expect(secret).to.be.null;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('mapEditingParams', () => {
|
|
69
|
+
it('should return design library params when mode is design library', () => {
|
|
70
|
+
const query = {
|
|
71
|
+
mode: DesignLibraryMode.Normal,
|
|
72
|
+
sc_itemid: 'item-123',
|
|
73
|
+
sc_uid: 'component-uid',
|
|
74
|
+
sc_renderingId: 'rendering-456',
|
|
75
|
+
sc_lang: 'en',
|
|
76
|
+
sc_site: 'test-site',
|
|
77
|
+
dataSourceId: 'datasource-789',
|
|
78
|
+
sc_version: '1',
|
|
79
|
+
generation: 'variant',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const params = mapEditingParams(query);
|
|
83
|
+
|
|
84
|
+
expect(params).to.deep.equal({
|
|
85
|
+
itemId: 'item-123',
|
|
86
|
+
componentUid: 'component-uid',
|
|
87
|
+
renderingId: 'rendering-456',
|
|
88
|
+
language: 'en',
|
|
89
|
+
site: 'test-site',
|
|
90
|
+
mode: DesignLibraryMode.Normal,
|
|
91
|
+
dataSourceId: 'datasource-789',
|
|
92
|
+
version: '1',
|
|
93
|
+
generation: 'variant',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return design library params when mode is library metadata', () => {
|
|
98
|
+
const query = {
|
|
99
|
+
mode: DesignLibraryMode.Metadata,
|
|
100
|
+
sc_itemid: 'item-123',
|
|
101
|
+
sc_uid: 'component-uid',
|
|
102
|
+
sc_renderingId: 'rendering-456',
|
|
103
|
+
sc_lang: 'en',
|
|
104
|
+
sc_site: 'test-site',
|
|
105
|
+
dataSourceId: 'datasource-789',
|
|
106
|
+
sc_version: '1',
|
|
107
|
+
generation: 'variant',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const params = mapEditingParams(query);
|
|
111
|
+
|
|
112
|
+
expect(params).to.deep.equal({
|
|
113
|
+
itemId: 'item-123',
|
|
114
|
+
componentUid: 'component-uid',
|
|
115
|
+
renderingId: 'rendering-456',
|
|
116
|
+
language: 'en',
|
|
117
|
+
site: 'test-site',
|
|
118
|
+
mode: DesignLibraryMode.Metadata,
|
|
119
|
+
dataSourceId: 'datasource-789',
|
|
120
|
+
version: '1',
|
|
121
|
+
generation: 'variant',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return standard editing params when mode is not design library', () => {
|
|
126
|
+
const query = {
|
|
127
|
+
mode: 'edit',
|
|
128
|
+
sc_site: 'test-site',
|
|
129
|
+
sc_itemid: 'item-123',
|
|
130
|
+
sc_lang: 'en',
|
|
131
|
+
sc_variant: 'variant1,variant2',
|
|
132
|
+
sc_version: '1',
|
|
133
|
+
sc_layoutKind: 'mvc',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const params = mapEditingParams(query);
|
|
137
|
+
|
|
138
|
+
expect(params).to.deep.equal({
|
|
139
|
+
site: 'test-site',
|
|
140
|
+
itemId: 'item-123',
|
|
141
|
+
language: 'en',
|
|
142
|
+
variantIds: 'variant1,variant2',
|
|
143
|
+
version: '1',
|
|
144
|
+
mode: 'edit',
|
|
145
|
+
layoutKind: 'mvc',
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should use default variant when no variant is specified', () => {
|
|
150
|
+
const query = {
|
|
151
|
+
mode: 'edit',
|
|
152
|
+
sc_site: 'test-site',
|
|
153
|
+
sc_itemid: 'item-123',
|
|
154
|
+
sc_lang: 'en',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const params = mapEditingParams(query);
|
|
158
|
+
|
|
159
|
+
expect(params.variantIds).to.equal(DEFAULT_VARIANT);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle empty variant string', () => {
|
|
163
|
+
const query = {
|
|
164
|
+
mode: 'edit',
|
|
165
|
+
sc_site: 'test-site',
|
|
166
|
+
sc_itemid: 'item-123',
|
|
167
|
+
sc_lang: 'en',
|
|
168
|
+
sc_variant: '',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const params = mapEditingParams(query);
|
|
172
|
+
|
|
173
|
+
expect(params.variantIds).to.equal(DEFAULT_VARIANT);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle undefined values gracefully', () => {
|
|
177
|
+
const query = {
|
|
178
|
+
mode: 'edit',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const params = mapEditingParams(query);
|
|
182
|
+
|
|
183
|
+
expect(params).to.have.property('site', undefined);
|
|
184
|
+
expect(params).to.have.property('itemId', undefined);
|
|
185
|
+
expect(params).to.have.property('language', undefined);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('cleanupPreviewCookies', () => {
|
|
190
|
+
it('should return null when cookies is null', () => {
|
|
191
|
+
const result = cleanupPreviewCookies(null);
|
|
192
|
+
expect(result).to.be.null;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return null when cookies is undefined', () => {
|
|
196
|
+
const result = cleanupPreviewCookies(undefined as any);
|
|
197
|
+
expect(result).to.be.null;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should filter out preview cookies from string', () => {
|
|
201
|
+
const cookies = 'normalCookie=value,_preview_data=preview123,anotherCookie=value2';
|
|
202
|
+
|
|
203
|
+
const result = cleanupPreviewCookies(cookies);
|
|
204
|
+
|
|
205
|
+
expect(result).to.deep.equal(['normalCookie=value', 'anotherCookie=value2']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should filter out preview cookies from array', () => {
|
|
209
|
+
const cookies = ['normalCookie=value', '_preview_data=preview123', 'anotherCookie=value2'];
|
|
210
|
+
|
|
211
|
+
const result = cleanupPreviewCookies(cookies);
|
|
212
|
+
|
|
213
|
+
expect(result).to.deep.equal(['normalCookie=value', 'anotherCookie=value2']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should return all cookies when no preview cookies are present', () => {
|
|
217
|
+
const cookies = ['normalCookie=value', 'anotherCookie=value2'];
|
|
218
|
+
|
|
219
|
+
const result = cleanupPreviewCookies(cookies);
|
|
220
|
+
|
|
221
|
+
expect(result).to.deep.equal(cookies);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle empty string', () => {
|
|
225
|
+
const result = cleanupPreviewCookies('');
|
|
226
|
+
|
|
227
|
+
expect(result).to.be.null;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle empty array', () => {
|
|
231
|
+
const result = cleanupPreviewCookies([]);
|
|
232
|
+
|
|
233
|
+
expect(result).to.deep.equal([]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle string with only preview cookies', () => {
|
|
237
|
+
const cookies = '_preview_data=preview123';
|
|
238
|
+
|
|
239
|
+
const result = cleanupPreviewCookies(cookies);
|
|
240
|
+
|
|
241
|
+
expect(result).to.deep.equal([]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('getPreviewCookies', () => {
|
|
246
|
+
it('should generate correct preview cookies for a site', () => {
|
|
247
|
+
const siteName = 'test-site';
|
|
248
|
+
|
|
249
|
+
const cookies = getPreviewCookies(siteName);
|
|
250
|
+
|
|
251
|
+
expect(cookies).to.have.length(2);
|
|
252
|
+
expect(cookies[0]).to.equal(`${SITE_KEY}=test-site; Path=/; HttpOnly; SameSite=None; Secure`);
|
|
253
|
+
expect(cookies[1]).to.equal(`${PREVIEW_KEY}=true; Path=/; HttpOnly; SameSite=None; Secure`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle empty site name', () => {
|
|
257
|
+
const cookies = getPreviewCookies('');
|
|
258
|
+
|
|
259
|
+
expect(cookies).to.have.length(2);
|
|
260
|
+
expect(cookies[0]).to.equal(`${SITE_KEY}=; Path=/; HttpOnly; SameSite=None; Secure`);
|
|
261
|
+
expect(cookies[1]).to.equal(`${PREVIEW_KEY}=true; Path=/; HttpOnly; SameSite=None; Secure`);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle site name with special characters', () => {
|
|
265
|
+
const siteName = 'test-site-with_special.chars';
|
|
266
|
+
|
|
267
|
+
const cookies = getPreviewCookies(siteName);
|
|
268
|
+
|
|
269
|
+
expect(cookies[0]).to.equal(
|
|
270
|
+
`${SITE_KEY}=test-site-with_special.chars; Path=/; HttpOnly; SameSite=None; Secure`
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('getRequiredEditingParamsList', () => {
|
|
276
|
+
it('should return component required params for design library mode', () => {
|
|
277
|
+
const params = getRequiredEditingParamsList(DesignLibraryMode.Normal);
|
|
278
|
+
|
|
279
|
+
expect(params).to.deep.equal([
|
|
280
|
+
'sc_site',
|
|
281
|
+
'sc_itemid',
|
|
282
|
+
'sc_renderingId',
|
|
283
|
+
'sc_uid',
|
|
284
|
+
'sc_lang',
|
|
285
|
+
'mode',
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should return component required params for library metadata mode', () => {
|
|
290
|
+
const params = getRequiredEditingParamsList(DesignLibraryMode.Metadata);
|
|
291
|
+
|
|
292
|
+
expect(params).to.deep.equal([
|
|
293
|
+
'sc_site',
|
|
294
|
+
'sc_itemid',
|
|
295
|
+
'sc_renderingId',
|
|
296
|
+
'sc_uid',
|
|
297
|
+
'sc_lang',
|
|
298
|
+
'mode',
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return editing required params for edit mode', () => {
|
|
303
|
+
const params = getRequiredEditingParamsList('edit');
|
|
304
|
+
|
|
305
|
+
expect(params).to.deep.equal(['sc_site', 'sc_itemid', 'sc_lang', 'route', 'mode']);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should return editing required params for preview mode', () => {
|
|
309
|
+
const params = getRequiredEditingParamsList('preview');
|
|
310
|
+
|
|
311
|
+
expect(params).to.deep.equal(['sc_site', 'sc_itemid', 'sc_lang', 'route', 'mode']);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should return editing required params for undefined mode', () => {
|
|
315
|
+
const params = getRequiredEditingParamsList(undefined as any);
|
|
316
|
+
|
|
317
|
+
expect(params).to.deep.equal(['sc_site', 'sc_itemid', 'sc_lang', 'route', 'mode']);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('getQueryParamsForPropagation', () => {
|
|
322
|
+
it('should include Vercel protection bypass parameter when present', () => {
|
|
323
|
+
const query = {
|
|
324
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: 'bypass-token',
|
|
325
|
+
otherParam: 'ignored',
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = getQueryParamsForPropagation(query);
|
|
329
|
+
|
|
330
|
+
expect(result).to.deep.equal({
|
|
331
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: 'bypass-token',
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should include Vercel set bypass cookie parameter when present', () => {
|
|
336
|
+
const query = {
|
|
337
|
+
[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: 'cookie-value',
|
|
338
|
+
otherParam: 'ignored',
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const result = getQueryParamsForPropagation(query);
|
|
342
|
+
|
|
343
|
+
expect(result).to.deep.equal({
|
|
344
|
+
[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: 'cookie-value',
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should include both Vercel parameters when present', () => {
|
|
349
|
+
const query = {
|
|
350
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: 'bypass-token',
|
|
351
|
+
[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: 'cookie-value',
|
|
352
|
+
otherParam: 'ignored',
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = getQueryParamsForPropagation(query);
|
|
356
|
+
|
|
357
|
+
expect(result).to.deep.equal({
|
|
358
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: 'bypass-token',
|
|
359
|
+
[QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE]: 'cookie-value',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return empty object when no relevant parameters are present', () => {
|
|
364
|
+
const query = {
|
|
365
|
+
otherParam: 'ignored',
|
|
366
|
+
anotherParam: 'also ignored',
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const result = getQueryParamsForPropagation(query);
|
|
370
|
+
|
|
371
|
+
expect(result).to.deep.equal({});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should handle empty query object', () => {
|
|
375
|
+
const result = getQueryParamsForPropagation({});
|
|
376
|
+
|
|
377
|
+
expect(result).to.deep.equal({});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle array values by using first value', () => {
|
|
381
|
+
const query = {
|
|
382
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: ['first', 'second'] as any,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const result = getQueryParamsForPropagation(query);
|
|
386
|
+
|
|
387
|
+
expect(result).to.deep.equal({
|
|
388
|
+
[QUERY_PARAM_VERCEL_PROTECTION_BYPASS]: ['first', 'second'],
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('getHeadersForPropagation', () => {
|
|
394
|
+
it('should filter and return approved headers from IncomingHttpHeaders', () => {
|
|
395
|
+
const headers = {
|
|
396
|
+
authorization: 'Bearer token123',
|
|
397
|
+
cookie: 'session=abc123',
|
|
398
|
+
'content-type': 'application/json',
|
|
399
|
+
'user-agent': 'Mozilla/5.0',
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const result = getHeadersForPropagation(headers);
|
|
403
|
+
|
|
404
|
+
expect(result).to.deep.equal({
|
|
405
|
+
authorization: 'Bearer token123',
|
|
406
|
+
cookie: 'session=abc123',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should filter and return approved headers from Headers object', () => {
|
|
411
|
+
const headers = new Headers();
|
|
412
|
+
headers.set('authorization', 'Bearer token123');
|
|
413
|
+
headers.set('cookie', 'session=abc123');
|
|
414
|
+
headers.set('content-type', 'application/json');
|
|
415
|
+
headers.set('user-agent', 'Mozilla/5.0');
|
|
416
|
+
|
|
417
|
+
const result = getHeadersForPropagation(headers);
|
|
418
|
+
|
|
419
|
+
expect(result).to.deep.equal({
|
|
420
|
+
authorization: 'Bearer token123',
|
|
421
|
+
cookie: 'session=abc123',
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should handle array values in IncomingHttpHeaders', () => {
|
|
426
|
+
const headers = {
|
|
427
|
+
authorization: ['Bearer token1', 'Bearer token2'],
|
|
428
|
+
cookie: 'session=abc123',
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const result = getHeadersForPropagation(headers);
|
|
432
|
+
|
|
433
|
+
expect(result).to.deep.equal({
|
|
434
|
+
authorization: 'Bearer token1, Bearer token2',
|
|
435
|
+
cookie: 'session=abc123',
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should return empty object when no approved headers are present', () => {
|
|
440
|
+
const headers = {
|
|
441
|
+
'content-type': 'application/json',
|
|
442
|
+
'user-agent': 'Mozilla/5.0',
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const result = getHeadersForPropagation(headers);
|
|
446
|
+
|
|
447
|
+
expect(result).to.deep.equal({});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should handle empty headers object', () => {
|
|
451
|
+
const result = getHeadersForPropagation({});
|
|
452
|
+
|
|
453
|
+
expect(result).to.deep.equal({});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should ignore headers with undefined/null values', () => {
|
|
457
|
+
const headers = {
|
|
458
|
+
authorization: undefined,
|
|
459
|
+
cookie: null,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const result = getHeadersForPropagation(headers as any);
|
|
463
|
+
|
|
464
|
+
expect(result).to.deep.equal({});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should handle case sensitivity correctly', () => {
|
|
468
|
+
const headers = {
|
|
469
|
+
Authorization: 'Bearer token123',
|
|
470
|
+
Cookie: 'session=abc123',
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const result = getHeadersForPropagation(headers);
|
|
474
|
+
|
|
475
|
+
// Should not match due to case sensitivity
|
|
476
|
+
expect(result).to.deep.equal({});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('getEditingRequestHtml', () => {
|
|
481
|
+
let mockDataFetcher: sinon.SinonStubbedInstance<NativeDataFetcher>;
|
|
482
|
+
let requestUrl: URL;
|
|
483
|
+
|
|
484
|
+
beforeEach(() => {
|
|
485
|
+
mockDataFetcher = sandbox.createStubInstance(NativeDataFetcher);
|
|
486
|
+
requestUrl = new URL('https://example.com/page');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should successfully fetch and process HTML', async () => {
|
|
490
|
+
const mockHtml = `<html><body><div phkey="test">Content</div><script>static</script></body></html>`;
|
|
491
|
+
const expectedHtml = `<html><body><div key="test">Content</div><script>static</script></body></html>`;
|
|
492
|
+
|
|
493
|
+
mockDataFetcher.get.resolves({
|
|
494
|
+
data: mockHtml,
|
|
495
|
+
} as any);
|
|
496
|
+
|
|
497
|
+
const propagatedQsParams = { param1: 'value1' };
|
|
498
|
+
const propagatedHeaders = { authorization: 'Bearer token' };
|
|
499
|
+
const cookies = ['session=abc123', 'other=xyz789'];
|
|
500
|
+
|
|
501
|
+
const result = await getEditingRequestHtml(
|
|
502
|
+
requestUrl,
|
|
503
|
+
propagatedQsParams,
|
|
504
|
+
propagatedHeaders,
|
|
505
|
+
cookies,
|
|
506
|
+
mockDataFetcher as any
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(result).to.equal(expectedHtml);
|
|
510
|
+
expect(mockDataFetcher.get).to.have.been.calledOnce;
|
|
511
|
+
|
|
512
|
+
// Verify URL was modified with query params and timestamp
|
|
513
|
+
const calledUrl = mockDataFetcher.get.firstCall.args[0];
|
|
514
|
+
expect(calledUrl).to.include('param1=value1');
|
|
515
|
+
expect(calledUrl).to.include('timestamp=');
|
|
516
|
+
|
|
517
|
+
// Verify headers were modified with cookies
|
|
518
|
+
const calledOptions = mockDataFetcher.get.firstCall.args[1];
|
|
519
|
+
expect(calledOptions.headers.cookie).to.equal('session=abc123;other=xyz789');
|
|
520
|
+
expect(calledOptions.credentials).to.equal('include');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should append cookies to existing cookie header', async () => {
|
|
524
|
+
const mockHtml = '<html><body>Content</body></html>';
|
|
525
|
+
|
|
526
|
+
mockDataFetcher.get.resolves({
|
|
527
|
+
data: mockHtml,
|
|
528
|
+
} as any);
|
|
529
|
+
|
|
530
|
+
const propagatedHeaders = {
|
|
531
|
+
authorization: 'Bearer token',
|
|
532
|
+
cookie: 'existing=cookie',
|
|
533
|
+
};
|
|
534
|
+
const cookies = ['session=abc123'];
|
|
535
|
+
|
|
536
|
+
await getEditingRequestHtml(
|
|
537
|
+
requestUrl,
|
|
538
|
+
{},
|
|
539
|
+
propagatedHeaders,
|
|
540
|
+
cookies,
|
|
541
|
+
mockDataFetcher as any
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const calledOptions = mockDataFetcher.get.firstCall.args[1];
|
|
545
|
+
expect(calledOptions.headers.cookie).to.equal('existing=cookie;session=abc123');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should handle 404 response without throwing', async () => {
|
|
549
|
+
const mock404Response = {
|
|
550
|
+
data: '<html><body>404 Not Found</body></html>',
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const error = {
|
|
554
|
+
response: {
|
|
555
|
+
status: 404,
|
|
556
|
+
...mock404Response,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
mockDataFetcher.get.rejects(error);
|
|
561
|
+
|
|
562
|
+
const result = await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
563
|
+
|
|
564
|
+
expect(result).to.equal('<html><body>404 Not Found</body></html>');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should throw error for non-404 fetch failures', async () => {
|
|
568
|
+
const error = {
|
|
569
|
+
response: {
|
|
570
|
+
status: 500,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
mockDataFetcher.get.rejects(error);
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
578
|
+
expect.fail('Should have thrown an error');
|
|
579
|
+
} catch (err) {
|
|
580
|
+
expect(err).to.equal(error);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should throw error when HTML is empty', async () => {
|
|
585
|
+
mockDataFetcher.get.resolves({
|
|
586
|
+
data: '',
|
|
587
|
+
} as any);
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
591
|
+
expect.fail('Should have thrown an error');
|
|
592
|
+
} catch (err) {
|
|
593
|
+
expect(err.message).to.include('Failed to render html for');
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should throw error when HTML is null', async () => {
|
|
598
|
+
mockDataFetcher.get.resolves({
|
|
599
|
+
data: null,
|
|
600
|
+
} as any);
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
604
|
+
expect.fail('Should have thrown an error');
|
|
605
|
+
} catch (err) {
|
|
606
|
+
expect(err.message).to.include('Failed to render html for');
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should replace multiple phkey attributes', async () => {
|
|
611
|
+
const mockHtml = '<div phkey="test1"><span phkey="test2">Content</span></div>';
|
|
612
|
+
const expectedHtml = '<div key="test1"><span key="test2">Content</span></div>';
|
|
613
|
+
|
|
614
|
+
mockDataFetcher.get.resolves({
|
|
615
|
+
data: mockHtml,
|
|
616
|
+
} as any);
|
|
617
|
+
|
|
618
|
+
const result = await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
619
|
+
|
|
620
|
+
expect(result).to.equal(expectedHtml);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should add timestamp to URL', async () => {
|
|
624
|
+
const mockHtml = '<html><body>Content</body></html>';
|
|
625
|
+
mockDataFetcher.get.resolves({ data: mockHtml } as any);
|
|
626
|
+
|
|
627
|
+
const beforeTime = Date.now();
|
|
628
|
+
await getEditingRequestHtml(requestUrl, {}, {}, [], mockDataFetcher as any);
|
|
629
|
+
const afterTime = Date.now();
|
|
630
|
+
|
|
631
|
+
const calledUrl = mockDataFetcher.get.firstCall.args[0];
|
|
632
|
+
const url = new URL(calledUrl);
|
|
633
|
+
const timestamp = parseInt(url.searchParams.get('timestamp') || '0', 10);
|
|
634
|
+
expect(timestamp).to.be.at.least(beforeTime);
|
|
635
|
+
expect(timestamp).to.be.at.most(afterTime);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe('isDesignLibraryPreviewData', () => {
|
|
640
|
+
it('should return true for valid design library preview data', () => {
|
|
641
|
+
const data = {
|
|
642
|
+
mode: DesignLibraryMode.Normal,
|
|
643
|
+
otherProp: 'value',
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const result = isDesignLibraryPreviewData(data);
|
|
647
|
+
|
|
648
|
+
expect(result).to.be.true;
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should return true for library metadata mode', () => {
|
|
652
|
+
const data = {
|
|
653
|
+
mode: DesignLibraryMode.Metadata,
|
|
654
|
+
otherProp: 'value',
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const result = isDesignLibraryPreviewData(data);
|
|
658
|
+
|
|
659
|
+
expect(result).to.be.true;
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should return false for non-design library mode', () => {
|
|
663
|
+
const data = {
|
|
664
|
+
mode: 'edit',
|
|
665
|
+
otherProp: 'value',
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const result = isDesignLibraryPreviewData(data);
|
|
669
|
+
|
|
670
|
+
expect(result).to.be.false;
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should return false for null data', () => {
|
|
674
|
+
const result = isDesignLibraryPreviewData(null);
|
|
675
|
+
|
|
676
|
+
expect(result).to.be.false;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('should return false for undefined data', () => {
|
|
680
|
+
const result = isDesignLibraryPreviewData(undefined);
|
|
681
|
+
|
|
682
|
+
expect(result).to.be.false;
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('should return false for non-object data', () => {
|
|
686
|
+
const result = isDesignLibraryPreviewData('string');
|
|
687
|
+
|
|
688
|
+
expect(result).to.be.false;
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should return false for object without mode property', () => {
|
|
692
|
+
const data = {
|
|
693
|
+
otherProp: 'value',
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const result = isDesignLibraryPreviewData(data);
|
|
697
|
+
|
|
698
|
+
expect(result).to.be.false;
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('should return false for empty object', () => {
|
|
702
|
+
const result = isDesignLibraryPreviewData({});
|
|
703
|
+
|
|
704
|
+
expect(result).to.be.false;
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
describe('resolveServerUrl', () => {
|
|
709
|
+
it('should return SITECORE_INTERNAL_EDITING_HOST_URL when set', () => {
|
|
710
|
+
process.env.SITECORE_INTERNAL_EDITING_HOST_URL = 'https://custom-host.com';
|
|
711
|
+
const req = mockRequest({
|
|
712
|
+
headers: { host: 'example.com' },
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const result = resolveServerUrl(req);
|
|
716
|
+
|
|
717
|
+
expect(result).to.equal('https://custom-host.com');
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('should return localhost:3000 for XM Cloud deployments', () => {
|
|
721
|
+
process.env.SITECORE = 'true';
|
|
722
|
+
const req = mockRequest({
|
|
723
|
+
headers: { host: 'example.com' },
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const result = resolveServerUrl(req);
|
|
727
|
+
|
|
728
|
+
expect(result).to.equal('http://localhost:3000');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should use https for Vercel deployment', () => {
|
|
732
|
+
process.env.VERCEL = 'true';
|
|
733
|
+
const req = mockRequest({
|
|
734
|
+
headers: { host: 'vercel-app.vercel.app' },
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const result = resolveServerUrl(req);
|
|
738
|
+
|
|
739
|
+
expect(result).to.equal('https://vercel-app.vercel.app');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should use https for Netlify deployment', () => {
|
|
743
|
+
process.env.NETLIFY = 'true';
|
|
744
|
+
const req = mockRequest({
|
|
745
|
+
headers: { host: 'netlify-app.netlify.app' },
|
|
746
|
+
});
|
|
747
|
+
const result = resolveServerUrl(req);
|
|
748
|
+
|
|
749
|
+
expect(result).to.equal('https://netlify-app.netlify.app');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('should use http for local development', () => {
|
|
753
|
+
const req = mockRequest({
|
|
754
|
+
headers: { host: 'localhost:3000' },
|
|
755
|
+
});
|
|
756
|
+
const result = resolveServerUrl(req);
|
|
757
|
+
|
|
758
|
+
expect(result).to.equal('http://localhost:3000');
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('should handle missing host header gracefully', () => {
|
|
762
|
+
const req = mockRequest({});
|
|
763
|
+
|
|
764
|
+
const result = resolveServerUrl(req);
|
|
765
|
+
|
|
766
|
+
expect(result).to.equal('http://undefined');
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('should prioritize SITECORE_INTERNAL_EDITING_HOST_URL over environment flags', () => {
|
|
770
|
+
process.env.SITECORE_INTERNAL_EDITING_HOST_URL = 'https://priority-host.com';
|
|
771
|
+
process.env.VERCEL = 'true';
|
|
772
|
+
process.env.SITECORE = 'true';
|
|
773
|
+
|
|
774
|
+
const req = mockRequest({
|
|
775
|
+
headers: { host: 'example.com' },
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const result = resolveServerUrl(req);
|
|
779
|
+
|
|
780
|
+
expect(result).to.equal('https://priority-host.com');
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('should prioritize SITECORE over hosting platform flags', () => {
|
|
784
|
+
process.env.SITECORE = 'true';
|
|
785
|
+
process.env.VERCEL = 'true';
|
|
786
|
+
process.env.NETLIFY = 'true';
|
|
787
|
+
|
|
788
|
+
const req = mockRequest({
|
|
789
|
+
headers: { host: 'example.com' },
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const result = resolveServerUrl(req);
|
|
793
|
+
|
|
794
|
+
expect(result).to.equal('http://localhost:3000');
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
describe('getCSPHeader', () => {
|
|
799
|
+
beforeEach(() => {
|
|
800
|
+
// Clean up any existing JSS_ALLOWED_ORIGINS
|
|
801
|
+
delete process.env.JSS_ALLOWED_ORIGINS;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should return CSP header with default allowed origins', () => {
|
|
805
|
+
const result = getCSPHeader();
|
|
806
|
+
|
|
807
|
+
expect(result).to.include("frame-ancestors 'self'");
|
|
808
|
+
expect(result).to.include('https://pages.sitecorecloud.io');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should include custom allowed origins from environment', () => {
|
|
812
|
+
process.env.JSS_ALLOWED_ORIGINS = 'https://custom1.com,https://custom2.com';
|
|
813
|
+
|
|
814
|
+
const result = getCSPHeader();
|
|
815
|
+
|
|
816
|
+
expect(result).to.include("frame-ancestors 'self'");
|
|
817
|
+
expect(result).to.include('https://custom1.com');
|
|
818
|
+
expect(result).to.include('https://custom2.com');
|
|
819
|
+
// Should also include default origins
|
|
820
|
+
expect(result).to.include('https://pages.sitecorecloud.io');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('should handle empty JSS_ALLOWED_ORIGINS', () => {
|
|
824
|
+
process.env.JSS_ALLOWED_ORIGINS = '';
|
|
825
|
+
|
|
826
|
+
const result = getCSPHeader();
|
|
827
|
+
|
|
828
|
+
expect(result).to.include("frame-ancestors 'self'");
|
|
829
|
+
expect(result).to.include('https://pages.sitecorecloud.io');
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('should handle JSS_ALLOWED_ORIGINS with spaces', () => {
|
|
833
|
+
process.env.JSS_ALLOWED_ORIGINS = ' https://custom1.com , https://custom2.com ';
|
|
834
|
+
|
|
835
|
+
const result = getCSPHeader();
|
|
836
|
+
|
|
837
|
+
expect(result).to.include('https://custom1.com');
|
|
838
|
+
expect(result).to.include('https://custom2.com');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should include origins from both env and defaults even if duplicated', () => {
|
|
842
|
+
process.env.JSS_ALLOWED_ORIGINS = 'https://pages.sitecorecloud.io,https://custom.com';
|
|
843
|
+
|
|
844
|
+
const result = getCSPHeader();
|
|
845
|
+
|
|
846
|
+
// May have duplicate pages.sitecorecloud.io from both env and defaults
|
|
847
|
+
const matches = (result.match(/https:\/\/pages\.sitecorecloud\.io/g) || []).length;
|
|
848
|
+
expect(matches).to.be.at.least(1);
|
|
849
|
+
expect(result).to.include('https://custom.com');
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
});
|