@adobe/spacecat-shared-tokowaka-client 1.1.1 → 1.2.0

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.
@@ -0,0 +1,409 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import { mergePatches, removePatchesBySuggestionIds } from '../../src/utils/patch-utils.js';
17
+
18
+ describe('Patch Utils', () => {
19
+ describe('mergePatches', () => {
20
+ it('should merge individual patches with same key', () => {
21
+ const existingPatches = [
22
+ {
23
+ opportunityId: 'opp-1',
24
+ suggestionId: 'sugg-1',
25
+ op: 'replace',
26
+ value: 'old-value',
27
+ },
28
+ ];
29
+
30
+ const newPatches = [
31
+ {
32
+ opportunityId: 'opp-1',
33
+ suggestionId: 'sugg-1',
34
+ op: 'replace',
35
+ value: 'new-value',
36
+ },
37
+ ];
38
+
39
+ const result = mergePatches(existingPatches, newPatches);
40
+
41
+ expect(result.patches).to.have.lengthOf(1);
42
+ expect(result.patches[0].value).to.equal('new-value');
43
+ expect(result.updateCount).to.equal(1);
44
+ expect(result.addCount).to.equal(0);
45
+ });
46
+
47
+ it('should keep individual patches with different keys', () => {
48
+ const existingPatches = [
49
+ {
50
+ opportunityId: 'opp-1',
51
+ suggestionId: 'sugg-1',
52
+ op: 'replace',
53
+ value: 'value-1',
54
+ },
55
+ ];
56
+
57
+ const newPatches = [
58
+ {
59
+ opportunityId: 'opp-1',
60
+ suggestionId: 'sugg-2',
61
+ op: 'replace',
62
+ value: 'value-2',
63
+ },
64
+ ];
65
+
66
+ const result = mergePatches(existingPatches, newPatches);
67
+
68
+ expect(result.patches).to.have.lengthOf(2);
69
+ expect(result.updateCount).to.equal(0);
70
+ expect(result.addCount).to.equal(1);
71
+ });
72
+
73
+ it('should handle empty existing patches', () => {
74
+ const existingPatches = [];
75
+ const newPatches = [
76
+ {
77
+ opportunityId: 'opp-1',
78
+ suggestionId: 'sugg-1',
79
+ op: 'replace',
80
+ value: 'value-1',
81
+ },
82
+ ];
83
+
84
+ const result = mergePatches(existingPatches, newPatches);
85
+
86
+ expect(result.patches).to.have.lengthOf(1);
87
+ expect(result.updateCount).to.equal(0);
88
+ expect(result.addCount).to.equal(1);
89
+ });
90
+
91
+ it('should handle empty new patches', () => {
92
+ const existingPatches = [
93
+ {
94
+ opportunityId: 'opp-1',
95
+ suggestionId: 'sugg-1',
96
+ op: 'replace',
97
+ value: 'value-1',
98
+ },
99
+ ];
100
+ const newPatches = [];
101
+
102
+ const result = mergePatches(existingPatches, newPatches);
103
+
104
+ expect(result.patches).to.have.lengthOf(1);
105
+ expect(result.updateCount).to.equal(0);
106
+ expect(result.addCount).to.equal(0);
107
+ });
108
+
109
+ it('should handle patch without suggestionId (heading patch)', () => {
110
+ const existingPatches = [
111
+ {
112
+ opportunityId: 'opp-faq',
113
+ op: 'appendChild',
114
+ value: 'Old Heading',
115
+ },
116
+ ];
117
+
118
+ const newPatches = [
119
+ {
120
+ opportunityId: 'opp-faq',
121
+ op: 'appendChild',
122
+ value: 'New Heading',
123
+ },
124
+ ];
125
+
126
+ const result = mergePatches(existingPatches, newPatches);
127
+
128
+ expect(result.patches).to.have.lengthOf(1);
129
+ expect(result.patches[0].value).to.equal('New Heading');
130
+ expect(result.updateCount).to.equal(1);
131
+ expect(result.addCount).to.equal(0);
132
+ });
133
+
134
+ it('should merge heading patches with same opportunityId', () => {
135
+ const existingPatches = [
136
+ {
137
+ opportunityId: 'opp-faq',
138
+ op: 'appendChild',
139
+ value: 'Heading',
140
+ },
141
+ {
142
+ opportunityId: 'opp-faq',
143
+ suggestionId: 'sugg-1',
144
+ op: 'appendChild',
145
+ value: 'FAQ 1',
146
+ },
147
+ ];
148
+
149
+ const newPatches = [
150
+ {
151
+ opportunityId: 'opp-faq',
152
+ op: 'appendChild',
153
+ value: 'Updated Heading',
154
+ },
155
+ {
156
+ opportunityId: 'opp-faq',
157
+ suggestionId: 'sugg-2',
158
+ op: 'appendChild',
159
+ value: 'FAQ 2',
160
+ },
161
+ ];
162
+
163
+ const result = mergePatches(existingPatches, newPatches);
164
+
165
+ expect(result.patches).to.have.lengthOf(3);
166
+ expect(result.patches[0].value).to.equal('Updated Heading');
167
+ expect(result.updateCount).to.equal(1);
168
+ expect(result.addCount).to.equal(1);
169
+ });
170
+ });
171
+
172
+ describe('removePatchesBySuggestionIds', () => {
173
+ it('should remove patches with matching suggestion IDs', () => {
174
+ const config = {
175
+ url: 'https://example.com/page1',
176
+ version: '1.0',
177
+ forceFail: false,
178
+ prerender: true,
179
+ patches: [
180
+ {
181
+ opportunityId: 'opp-1',
182
+ suggestionId: 'sugg-1',
183
+ op: 'replace',
184
+ value: 'value-1',
185
+ },
186
+ {
187
+ opportunityId: 'opp-1',
188
+ suggestionId: 'sugg-2',
189
+ op: 'replace',
190
+ value: 'value-2',
191
+ },
192
+ ],
193
+ };
194
+
195
+ const result = removePatchesBySuggestionIds(config, ['sugg-1']);
196
+
197
+ expect(result.patches).to.have.lengthOf(1);
198
+ expect(result.patches[0].suggestionId).to.equal('sugg-2');
199
+ expect(result.removedCount).to.equal(1);
200
+ });
201
+
202
+ it('should remove URL paths with no remaining patches', () => {
203
+ const config = {
204
+ url: 'https://example.com/page1',
205
+ version: '1.0',
206
+ forceFail: false,
207
+ prerender: true,
208
+ patches: [
209
+ {
210
+ opportunityId: 'opp-1',
211
+ suggestionId: 'sugg-1',
212
+ op: 'replace',
213
+ value: 'value-1',
214
+ },
215
+ ],
216
+ };
217
+
218
+ const result = removePatchesBySuggestionIds(config, ['sugg-1']);
219
+
220
+ expect(result.patches).to.have.lengthOf(0);
221
+ expect(result.removedCount).to.equal(1);
222
+ });
223
+
224
+ it('should handle empty suggestion IDs array', () => {
225
+ const config = {
226
+ url: 'https://example.com/page1',
227
+ version: '1.0',
228
+ forceFail: false,
229
+ prerender: true,
230
+ patches: [
231
+ {
232
+ opportunityId: 'opp-1',
233
+ suggestionId: 'sugg-1',
234
+ op: 'replace',
235
+ value: 'value-1',
236
+ },
237
+ ],
238
+ };
239
+
240
+ const result = removePatchesBySuggestionIds(config, []);
241
+
242
+ expect(result.patches).to.have.lengthOf(1);
243
+ expect(result.removedCount).to.equal(0);
244
+ });
245
+
246
+ it('should handle non-matching suggestion IDs', () => {
247
+ const config = {
248
+ url: 'https://example.com/page1',
249
+ version: '1.0',
250
+ forceFail: false,
251
+ prerender: true,
252
+ patches: [
253
+ {
254
+ opportunityId: 'opp-1',
255
+ suggestionId: 'sugg-1',
256
+ op: 'replace',
257
+ value: 'value-1',
258
+ },
259
+ ],
260
+ };
261
+
262
+ const result = removePatchesBySuggestionIds(config, ['sugg-999']);
263
+
264
+ expect(result.patches).to.have.lengthOf(1);
265
+ expect(result.removedCount).to.equal(0);
266
+ });
267
+
268
+ it('should handle null/undefined config gracefully', () => {
269
+ const result1 = removePatchesBySuggestionIds(null, ['sugg-1']);
270
+ const result2 = removePatchesBySuggestionIds(undefined, ['sugg-1']);
271
+
272
+ expect(result1).to.be.null;
273
+ expect(result2).to.be.undefined;
274
+ });
275
+
276
+ it('should preserve patches without suggestionId (heading patches)', () => {
277
+ const config = {
278
+ url: 'https://example.com/page1',
279
+ version: '1.0',
280
+ forceFail: false,
281
+ prerender: true,
282
+ patches: [
283
+ {
284
+ opportunityId: 'opp-faq',
285
+ op: 'appendChild',
286
+ value: 'FAQs',
287
+ },
288
+ {
289
+ opportunityId: 'opp-faq',
290
+ suggestionId: 'sugg-1',
291
+ op: 'appendChild',
292
+ value: 'FAQ item 1',
293
+ },
294
+ ],
295
+ };
296
+
297
+ const result = removePatchesBySuggestionIds(config, ['sugg-1']);
298
+
299
+ expect(result.patches).to.have.lengthOf(1);
300
+ expect(result.patches[0].value).to.equal('FAQs');
301
+ expect(result.removedCount).to.equal(1);
302
+ });
303
+
304
+ it('should remove patches by additional patch keys', () => {
305
+ const config = {
306
+ url: 'https://example.com/page1',
307
+ version: '1.0',
308
+ forceFail: false,
309
+ prerender: true,
310
+ patches: [
311
+ {
312
+ opportunityId: 'opp-faq',
313
+ op: 'appendChild',
314
+ value: 'FAQs',
315
+ },
316
+ {
317
+ opportunityId: 'opp-faq',
318
+ suggestionId: 'sugg-1',
319
+ op: 'appendChild',
320
+ value: 'FAQ item 1',
321
+ },
322
+ {
323
+ opportunityId: 'opp-faq',
324
+ suggestionId: 'sugg-2',
325
+ op: 'appendChild',
326
+ value: 'FAQ item 2',
327
+ },
328
+ ],
329
+ };
330
+
331
+ // Remove all FAQs by passing heading patch key and suggestion IDs
332
+ const result = removePatchesBySuggestionIds(config, ['sugg-1', 'sugg-2'], ['opp-faq']);
333
+
334
+ expect(result.patches).to.have.lengthOf(0);
335
+ expect(result.removedCount).to.equal(3);
336
+ });
337
+
338
+ it('should remove patches by additional patch keys while keeping other suggestions', () => {
339
+ const config = {
340
+ url: 'https://example.com/page1',
341
+ version: '1.0',
342
+ forceFail: false,
343
+ prerender: true,
344
+ patches: [
345
+ {
346
+ opportunityId: 'opp-faq',
347
+ op: 'appendChild',
348
+ value: 'FAQs',
349
+ },
350
+ {
351
+ opportunityId: 'opp-faq',
352
+ suggestionId: 'sugg-1',
353
+ op: 'appendChild',
354
+ value: 'FAQ item 1',
355
+ },
356
+ {
357
+ opportunityId: 'opp-faq',
358
+ suggestionId: 'sugg-2',
359
+ op: 'appendChild',
360
+ value: 'FAQ item 2',
361
+ },
362
+ ],
363
+ };
364
+
365
+ // Remove only sugg-1, heading patch should remain
366
+ const result = removePatchesBySuggestionIds(config, ['sugg-1']);
367
+
368
+ expect(result.patches).to.have.lengthOf(2);
369
+ expect(result.patches[0].value).to.equal('FAQs');
370
+ expect(result.patches[1].suggestionId).to.equal('sugg-2');
371
+ expect(result.removedCount).to.equal(1);
372
+ });
373
+
374
+ it('should handle both suggestionIds and additional patch keys together', () => {
375
+ const config = {
376
+ url: 'https://example.com/page1',
377
+ version: '1.0',
378
+ forceFail: false,
379
+ prerender: true,
380
+ patches: [
381
+ {
382
+ opportunityId: 'opp-faq',
383
+ op: 'appendChild',
384
+ value: 'FAQs',
385
+ },
386
+ {
387
+ opportunityId: 'opp-faq',
388
+ suggestionId: 'sugg-1',
389
+ op: 'appendChild',
390
+ value: 'FAQ item 1',
391
+ },
392
+ {
393
+ opportunityId: 'opp-other',
394
+ suggestionId: 'sugg-2',
395
+ op: 'replace',
396
+ value: 'Other suggestion',
397
+ },
398
+ ],
399
+ };
400
+
401
+ // Remove sugg-1 and the heading patch
402
+ const result = removePatchesBySuggestionIds(config, ['sugg-1'], ['opp-faq']);
403
+
404
+ expect(result.patches).to.have.lengthOf(1);
405
+ expect(result.patches[0].suggestionId).to.equal('sugg-2');
406
+ expect(result.removedCount).to.equal(2);
407
+ });
408
+ });
409
+ });
@@ -0,0 +1,140 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import {
17
+ normalizePath,
18
+ getHostName,
19
+ base64UrlEncode,
20
+ getTokowakaConfigS3Path,
21
+ getTokowakaMetaconfigS3Path,
22
+ } from '../../src/utils/s3-utils.js';
23
+
24
+ describe('S3 Utils', () => {
25
+ describe('normalizePath', () => {
26
+ it('should add leading slash if missing', () => {
27
+ const result = normalizePath('page1');
28
+ expect(result).to.equal('/page1');
29
+ });
30
+
31
+ it('should keep single slash', () => {
32
+ const result = normalizePath('/');
33
+ expect(result).to.equal('/');
34
+ });
35
+
36
+ it('should remove trailing slash except for root', () => {
37
+ const result = normalizePath('/page1/');
38
+ expect(result).to.equal('/page1');
39
+ });
40
+
41
+ it('should handle path with leading slash', () => {
42
+ const result = normalizePath('/page1');
43
+ expect(result).to.equal('/page1');
44
+ });
45
+ });
46
+
47
+ describe('getHostName', () => {
48
+ it('should extract hostname and remove www', () => {
49
+ const url = new URL('https://www.example.com/page');
50
+ const logger = { error: () => {} };
51
+ const result = getHostName(url, logger);
52
+ expect(result).to.equal('example.com');
53
+ });
54
+
55
+ it('should handle hostname without www', () => {
56
+ const url = new URL('https://example.com/page');
57
+ const logger = { error: () => {} };
58
+ const result = getHostName(url, logger);
59
+ expect(result).to.equal('example.com');
60
+ });
61
+
62
+ it('should throw error on invalid URL', () => {
63
+ const logger = { error: () => {} };
64
+ const invalidUrl = { hostname: null };
65
+
66
+ try {
67
+ getHostName(invalidUrl, logger);
68
+ expect.fail('Should have thrown error');
69
+ } catch (error) {
70
+ expect(error.message).to.include('Error extracting host name');
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('getTokowakaConfigS3Path', () => {
76
+ const logger = { error: () => {} };
77
+
78
+ it('should generate correct S3 path for deploy', () => {
79
+ const url = 'https://example.com/page1';
80
+ const result = getTokowakaConfigS3Path(url, logger, false);
81
+ expect(result).to.equal('opportunities/example.com/L3BhZ2Ux');
82
+ });
83
+
84
+ it('should generate correct S3 path for preview', () => {
85
+ const url = 'https://example.com/page1';
86
+ const result = getTokowakaConfigS3Path(url, logger, true);
87
+ expect(result).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
88
+ });
89
+
90
+ it('should throw error on invalid URL', () => {
91
+ try {
92
+ getTokowakaConfigS3Path('not-a-valid-url', logger, false);
93
+ expect.fail('Should have thrown error');
94
+ } catch (error) {
95
+ expect(error.message).to.include('Failed to generate S3 path');
96
+ }
97
+ });
98
+ });
99
+
100
+ describe('getTokowakaMetaconfigS3Path', () => {
101
+ it('should generate correct metaconfig S3 path for deploy', () => {
102
+ const url = 'https://example.com/page1';
103
+ const logger = { error: () => {} };
104
+ const result = getTokowakaMetaconfigS3Path(url, logger, false);
105
+ expect(result).to.equal('opportunities/example.com/config');
106
+ });
107
+
108
+ it('should generate correct metaconfig S3 path for preview', () => {
109
+ const url = 'https://example.com/page1';
110
+ const logger = { error: () => {} };
111
+ const result = getTokowakaMetaconfigS3Path(url, logger, true);
112
+ expect(result).to.equal('preview/opportunities/example.com/config');
113
+ });
114
+
115
+ it('should throw error on invalid URL', () => {
116
+ const logger = { error: () => {} };
117
+
118
+ try {
119
+ getTokowakaMetaconfigS3Path('not-a-valid-url', logger, false);
120
+ expect.fail('Should have thrown error');
121
+ } catch (error) {
122
+ expect(error.message).to.include('Failed to generate metaconfig S3 path');
123
+ }
124
+ });
125
+ });
126
+
127
+ describe('base64UrlEncode', () => {
128
+ it('should encode string to base64url', () => {
129
+ const result = base64UrlEncode('/page1');
130
+ expect(result).to.equal('L3BhZ2Ux');
131
+ });
132
+
133
+ it('should handle special characters', () => {
134
+ const result = base64UrlEncode('/page?query=1');
135
+ // Should replace + with - and / with _
136
+ expect(result).to.not.include('+');
137
+ expect(result).to.not.include('=');
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import { getEffectiveBaseURL } from '../../src/utils/site-utils.js';
17
+
18
+ describe('Site Utils', () => {
19
+ describe('getEffectiveBaseURL', () => {
20
+ it('should return site baseURL when no override', () => {
21
+ const site = {
22
+ getBaseURL: () => 'https://example.com',
23
+ getConfig: () => ({
24
+ getFetchConfig: () => ({}),
25
+ }),
26
+ };
27
+
28
+ const result = getEffectiveBaseURL(site);
29
+ expect(result).to.equal('https://example.com');
30
+ });
31
+
32
+ it('should return override baseURL when valid', () => {
33
+ const site = {
34
+ getBaseURL: () => 'https://example.com',
35
+ getConfig: () => ({
36
+ getFetchConfig: () => ({
37
+ overrideBaseURL: 'https://override.com',
38
+ }),
39
+ }),
40
+ };
41
+
42
+ const result = getEffectiveBaseURL(site);
43
+ expect(result).to.equal('https://override.com');
44
+ });
45
+
46
+ it('should return site baseURL when override is invalid', () => {
47
+ const site = {
48
+ getBaseURL: () => 'https://example.com',
49
+ getConfig: () => ({
50
+ getFetchConfig: () => ({
51
+ overrideBaseURL: 'not-a-valid-url',
52
+ }),
53
+ }),
54
+ };
55
+
56
+ const result = getEffectiveBaseURL(site);
57
+ expect(result).to.equal('https://example.com');
58
+ });
59
+
60
+ it('should handle missing getFetchConfig', () => {
61
+ const site = {
62
+ getBaseURL: () => 'https://example.com',
63
+ getConfig: () => ({}),
64
+ };
65
+
66
+ const result = getEffectiveBaseURL(site);
67
+ expect(result).to.equal('https://example.com');
68
+ });
69
+
70
+ it('should handle missing config', () => {
71
+ const site = {
72
+ getBaseURL: () => 'https://example.com',
73
+ getConfig: () => null,
74
+ };
75
+
76
+ const result = getEffectiveBaseURL(site);
77
+ expect(result).to.equal('https://example.com');
78
+ });
79
+ });
80
+ });