@adobe/spacecat-shared-tokowaka-client 1.1.0 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +144 -19
- package/package.json +1 -1
- package/src/index.d.ts +81 -15
- package/src/index.js +314 -260
- package/src/mappers/base-mapper.js +1 -1
- package/src/mappers/faq-mapper.js +18 -23
- package/src/utils/custom-html-utils.js +1 -0
- package/src/utils/patch-utils.js +12 -25
- package/src/utils/s3-utils.js +100 -7
- package/test/index.test.js +808 -990
- package/test/mappers/base-mapper.test.js +72 -88
- package/test/mappers/faq-mapper.test.js +61 -97
- package/test/utils/html-utils.test.js +10 -12
- package/test/utils/patch-utils.test.js +204 -235
- package/test/utils/s3-utils.test.js +140 -0
- package/test/utils/site-utils.test.js +80 -0
- package/test/utils/suggestion-utils.test.js +187 -0
package/test/index.test.js
CHANGED
|
@@ -50,7 +50,12 @@ describe('TokowakaClient', () => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
client = new TokowakaClient(
|
|
53
|
-
{
|
|
53
|
+
{
|
|
54
|
+
bucketName: 'test-bucket',
|
|
55
|
+
previewBucketName: 'test-preview-bucket',
|
|
56
|
+
s3Client,
|
|
57
|
+
env,
|
|
58
|
+
},
|
|
54
59
|
log,
|
|
55
60
|
);
|
|
56
61
|
|
|
@@ -59,8 +64,8 @@ describe('TokowakaClient', () => {
|
|
|
59
64
|
getBaseURL: () => 'https://example.com',
|
|
60
65
|
getConfig: () => ({
|
|
61
66
|
getTokowakaConfig: () => ({
|
|
62
|
-
apiKey: 'test-api-key-123',
|
|
63
67
|
forwardedHost: 'example.com',
|
|
68
|
+
apiKey: 'test-api-key',
|
|
64
69
|
}),
|
|
65
70
|
}),
|
|
66
71
|
};
|
|
@@ -108,6 +113,7 @@ describe('TokowakaClient', () => {
|
|
|
108
113
|
it('should create an instance with valid config', () => {
|
|
109
114
|
expect(client).to.be.instanceOf(TokowakaClient);
|
|
110
115
|
expect(client.deployBucketName).to.equal('test-bucket');
|
|
116
|
+
expect(client.previewBucketName).to.equal('test-preview-bucket');
|
|
111
117
|
expect(client.s3Client).to.equal(s3Client);
|
|
112
118
|
});
|
|
113
119
|
|
|
@@ -120,12 +126,24 @@ describe('TokowakaClient', () => {
|
|
|
120
126
|
expect(() => new TokowakaClient({ bucketName: 'test-bucket' }, log))
|
|
121
127
|
.to.throw('S3 client is required');
|
|
122
128
|
});
|
|
129
|
+
|
|
130
|
+
it('should use deployBucketName for preview if previewBucketName not provided', () => {
|
|
131
|
+
const clientWithoutPreview = new TokowakaClient(
|
|
132
|
+
{ bucketName: 'test-bucket', s3Client },
|
|
133
|
+
log,
|
|
134
|
+
);
|
|
135
|
+
// previewBucketName is undefined if not explicitly provided
|
|
136
|
+
expect(clientWithoutPreview.previewBucketName).to.be.undefined;
|
|
137
|
+
});
|
|
123
138
|
});
|
|
124
139
|
|
|
125
140
|
describe('createFrom', () => {
|
|
126
141
|
it('should create client from context', () => {
|
|
127
142
|
const context = {
|
|
128
|
-
env: {
|
|
143
|
+
env: {
|
|
144
|
+
TOKOWAKA_SITE_CONFIG_BUCKET: 'test-bucket',
|
|
145
|
+
TOKOWAKA_PREVIEW_BUCKET: 'test-preview-bucket',
|
|
146
|
+
},
|
|
129
147
|
s3: { s3Client },
|
|
130
148
|
log,
|
|
131
149
|
};
|
|
@@ -134,6 +152,7 @@ describe('TokowakaClient', () => {
|
|
|
134
152
|
|
|
135
153
|
expect(createdClient).to.be.instanceOf(TokowakaClient);
|
|
136
154
|
expect(context.tokowakaClient).to.equal(createdClient);
|
|
155
|
+
expect(createdClient.previewBucketName).to.equal('test-preview-bucket');
|
|
137
156
|
});
|
|
138
157
|
|
|
139
158
|
it('should reuse existing client from context', () => {
|
|
@@ -177,13 +196,8 @@ describe('TokowakaClient', () => {
|
|
|
177
196
|
}
|
|
178
197
|
|
|
179
198
|
// eslint-disable-next-line class-methods-use-this
|
|
180
|
-
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// eslint-disable-next-line class-methods-use-this
|
|
185
|
-
validateSuggestionData() {
|
|
186
|
-
return true;
|
|
199
|
+
suggestionsToPatches() {
|
|
200
|
+
return [];
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
// eslint-disable-next-line class-methods-use-this
|
|
@@ -201,21 +215,20 @@ describe('TokowakaClient', () => {
|
|
|
201
215
|
});
|
|
202
216
|
|
|
203
217
|
describe('generateConfig', () => {
|
|
204
|
-
it('should generate config for headings opportunity', () => {
|
|
205
|
-
const
|
|
218
|
+
it('should generate config for headings opportunity with single URL', () => {
|
|
219
|
+
const url = 'https://example.com/page1';
|
|
220
|
+
const config = client.generateConfig(url, mockOpportunity, mockSuggestions);
|
|
206
221
|
|
|
207
222
|
expect(config).to.deep.include({
|
|
208
|
-
|
|
209
|
-
baseURL: 'https://example.com',
|
|
223
|
+
url: 'https://example.com/page1',
|
|
210
224
|
version: '1.0',
|
|
211
|
-
|
|
225
|
+
forceFail: false,
|
|
226
|
+
prerender: true,
|
|
212
227
|
});
|
|
213
228
|
|
|
214
|
-
expect(config.
|
|
215
|
-
expect(config.tokowakaOptimizations['/page1'].prerender).to.be.true;
|
|
216
|
-
expect(config.tokowakaOptimizations['/page1'].patches).to.have.length(2);
|
|
229
|
+
expect(config.patches).to.have.length(2);
|
|
217
230
|
|
|
218
|
-
const patch = config.
|
|
231
|
+
const patch = config.patches[0];
|
|
219
232
|
expect(patch).to.include({
|
|
220
233
|
op: 'replace',
|
|
221
234
|
selector: 'h1',
|
|
@@ -270,21 +283,20 @@ describe('TokowakaClient', () => {
|
|
|
270
283
|
},
|
|
271
284
|
];
|
|
272
285
|
|
|
273
|
-
const
|
|
286
|
+
const url = 'https://example.com/page1';
|
|
287
|
+
const config = client.generateConfig(url, mockOpportunity, mockSuggestions);
|
|
274
288
|
|
|
275
289
|
expect(config).to.deep.include({
|
|
276
|
-
|
|
277
|
-
baseURL: 'https://example.com',
|
|
290
|
+
url: 'https://example.com/page1',
|
|
278
291
|
version: '1.0',
|
|
279
|
-
|
|
292
|
+
forceFail: false,
|
|
293
|
+
prerender: true,
|
|
280
294
|
});
|
|
281
295
|
|
|
282
|
-
expect(config.
|
|
283
|
-
expect(config.tokowakaOptimizations['/page1'].prerender).to.be.true;
|
|
284
|
-
expect(config.tokowakaOptimizations['/page1'].patches).to.have.length(3); // heading + 2 FAQs
|
|
296
|
+
expect(config.patches).to.have.length(3); // heading + 2 FAQs
|
|
285
297
|
|
|
286
298
|
// First patch: heading (no suggestionId)
|
|
287
|
-
const headingPatch = config.
|
|
299
|
+
const headingPatch = config.patches[0];
|
|
288
300
|
expect(headingPatch).to.include({
|
|
289
301
|
op: 'appendChild',
|
|
290
302
|
selector: 'main',
|
|
@@ -296,7 +308,7 @@ describe('TokowakaClient', () => {
|
|
|
296
308
|
expect(headingPatch.value.tagName).to.equal('h2');
|
|
297
309
|
|
|
298
310
|
// Second patch: first FAQ
|
|
299
|
-
const firstFaqPatch = config.
|
|
311
|
+
const firstFaqPatch = config.patches[1];
|
|
300
312
|
expect(firstFaqPatch).to.include({
|
|
301
313
|
op: 'appendChild',
|
|
302
314
|
selector: 'main',
|
|
@@ -304,217 +316,234 @@ describe('TokowakaClient', () => {
|
|
|
304
316
|
prerenderRequired: true,
|
|
305
317
|
});
|
|
306
318
|
expect(firstFaqPatch.suggestionId).to.equal('sugg-faq-1');
|
|
307
|
-
expect(firstFaqPatch).to.have.property('lastUpdated');
|
|
308
319
|
expect(firstFaqPatch.value.tagName).to.equal('div');
|
|
309
|
-
|
|
310
|
-
// Third patch: second FAQ
|
|
311
|
-
const secondFaqPatch = config.tokowakaOptimizations['/page1'].patches[2];
|
|
312
|
-
expect(secondFaqPatch).to.include({
|
|
313
|
-
op: 'appendChild',
|
|
314
|
-
selector: 'main',
|
|
315
|
-
opportunityId: 'opp-faq-123',
|
|
316
|
-
prerenderRequired: true,
|
|
317
|
-
});
|
|
318
|
-
expect(secondFaqPatch.suggestionId).to.equal('sugg-faq-2');
|
|
319
|
-
expect(secondFaqPatch).to.have.property('lastUpdated');
|
|
320
|
-
expect(secondFaqPatch.value.tagName).to.equal('div');
|
|
321
320
|
});
|
|
322
321
|
|
|
323
|
-
it('should
|
|
322
|
+
it('should return null if no eligible suggestions', () => {
|
|
324
323
|
mockSuggestions = [
|
|
325
324
|
{
|
|
326
325
|
getId: () => 'sugg-1',
|
|
327
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
328
326
|
getData: () => ({
|
|
329
327
|
url: 'https://example.com/page1',
|
|
330
|
-
|
|
331
|
-
checkType: 'heading-empty',
|
|
332
|
-
transformRules: {
|
|
333
|
-
action: 'replace',
|
|
334
|
-
selector: 'h1',
|
|
335
|
-
},
|
|
336
|
-
}),
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
getId: () => 'sugg-2',
|
|
340
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
341
|
-
getData: () => ({
|
|
342
|
-
url: 'https://example.com/page2',
|
|
343
|
-
recommendedAction: 'Page 2 Heading',
|
|
344
|
-
checkType: 'heading-empty',
|
|
345
|
-
transformRules: {
|
|
346
|
-
action: 'replace',
|
|
347
|
-
selector: 'h1',
|
|
348
|
-
},
|
|
328
|
+
// Missing required fields
|
|
349
329
|
}),
|
|
350
330
|
},
|
|
351
331
|
];
|
|
352
332
|
|
|
353
|
-
const
|
|
333
|
+
const url = 'https://example.com/page1';
|
|
334
|
+
const config = client.generateConfig(url, mockOpportunity, mockSuggestions);
|
|
354
335
|
|
|
355
|
-
expect(
|
|
356
|
-
expect(config.tokowakaOptimizations).to.have.property('/page1');
|
|
357
|
-
expect(config.tokowakaOptimizations).to.have.property('/page2');
|
|
336
|
+
expect(config).to.be.null;
|
|
358
337
|
});
|
|
359
338
|
|
|
360
|
-
it('should
|
|
361
|
-
|
|
362
|
-
mockSite.getConfig = () => ({
|
|
363
|
-
getTokowakaConfig: () => ({
|
|
364
|
-
apiKey: 'test-api-key-123',
|
|
365
|
-
cdnProvider: 'cloudfront',
|
|
366
|
-
}),
|
|
367
|
-
getFetchConfig: () => ({
|
|
368
|
-
overrideBaseURL: 'https://override.example.com',
|
|
369
|
-
}),
|
|
370
|
-
});
|
|
339
|
+
it('should handle unsupported opportunity types', () => {
|
|
340
|
+
mockOpportunity.getType = () => 'unsupported-type';
|
|
371
341
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
342
|
+
expect(() => client.generateConfig('https://example.com/page1', mockOpportunity, mockSuggestions))
|
|
343
|
+
.to.throw(/No mapper found for opportunity type: unsupported-type/)
|
|
344
|
+
.with.property('status', 501);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('fetchMetaconfig', () => {
|
|
349
|
+
it('should fetch metaconfig from S3', async () => {
|
|
350
|
+
const metaconfig = {
|
|
351
|
+
siteId: 'site-123',
|
|
352
|
+
prerender: true,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
s3Client.send.resolves({
|
|
356
|
+
Body: {
|
|
357
|
+
transformToString: async () => JSON.stringify(metaconfig),
|
|
385
358
|
},
|
|
386
|
-
|
|
359
|
+
});
|
|
387
360
|
|
|
388
|
-
const
|
|
361
|
+
const result = await client.fetchMetaconfig('https://example.com/page1');
|
|
362
|
+
|
|
363
|
+
expect(result).to.deep.equal(metaconfig);
|
|
364
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
389
365
|
|
|
390
|
-
|
|
391
|
-
expect(
|
|
366
|
+
const command = s3Client.send.firstCall.args[0];
|
|
367
|
+
expect(command.input.Bucket).to.equal('test-bucket');
|
|
368
|
+
expect(command.input.Key).to.equal('opportunities/example.com/config');
|
|
392
369
|
});
|
|
393
370
|
|
|
394
|
-
it('should
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
371
|
+
it('should fetch metaconfig from preview bucket', async () => {
|
|
372
|
+
const metaconfig = {
|
|
373
|
+
siteId: 'site-123',
|
|
374
|
+
prerender: true,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
s3Client.send.resolves({
|
|
378
|
+
Body: {
|
|
379
|
+
transformToString: async () => JSON.stringify(metaconfig),
|
|
402
380
|
},
|
|
403
|
-
|
|
381
|
+
});
|
|
404
382
|
|
|
405
|
-
|
|
383
|
+
await client.fetchMetaconfig('https://example.com/page1', true);
|
|
406
384
|
|
|
407
|
-
|
|
408
|
-
expect(
|
|
385
|
+
const command = s3Client.send.firstCall.args[0];
|
|
386
|
+
expect(command.input.Bucket).to.equal('test-preview-bucket');
|
|
387
|
+
expect(command.input.Key).to.equal('preview/opportunities/example.com/config');
|
|
409
388
|
});
|
|
410
389
|
|
|
411
|
-
it('should
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
416
|
-
getData: () => ({
|
|
417
|
-
url: 'http://invalid domain with spaces.com',
|
|
418
|
-
checkType: 'heading-empty',
|
|
419
|
-
recommendedAction: 'Heading',
|
|
420
|
-
transformRules: {
|
|
421
|
-
action: 'replace',
|
|
422
|
-
selector: 'h1',
|
|
423
|
-
},
|
|
424
|
-
}),
|
|
425
|
-
},
|
|
426
|
-
];
|
|
390
|
+
it('should return null if metaconfig does not exist', async () => {
|
|
391
|
+
const noSuchKeyError = new Error('NoSuchKey');
|
|
392
|
+
noSuchKeyError.name = 'NoSuchKey';
|
|
393
|
+
s3Client.send.rejects(noSuchKeyError);
|
|
427
394
|
|
|
428
|
-
const
|
|
395
|
+
const result = await client.fetchMetaconfig('https://example.com/page1');
|
|
429
396
|
|
|
430
|
-
expect(
|
|
431
|
-
expect(log.warn).to.have.been.calledWith(sinon.match(/Failed to extract pathname from URL/));
|
|
397
|
+
expect(result).to.be.null;
|
|
432
398
|
});
|
|
433
399
|
|
|
434
|
-
it('should
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
400
|
+
it('should throw error on S3 fetch failure', async () => {
|
|
401
|
+
const s3Error = new Error('Access Denied');
|
|
402
|
+
s3Error.name = 'AccessDenied';
|
|
403
|
+
s3Client.send.rejects(s3Error);
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await client.fetchMetaconfig('https://example.com/page1');
|
|
407
|
+
expect.fail('Should have thrown error');
|
|
408
|
+
} catch (error) {
|
|
409
|
+
expect(error.message).to.include('S3 fetch failed');
|
|
410
|
+
expect(error.status).to.equal(500);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should throw error if URL is missing', async () => {
|
|
415
|
+
try {
|
|
416
|
+
await client.fetchMetaconfig('');
|
|
417
|
+
expect.fail('Should have thrown error');
|
|
418
|
+
} catch (error) {
|
|
419
|
+
expect(error.message).to.include('URL is required');
|
|
420
|
+
expect(error.status).to.equal(400);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('uploadMetaconfig', () => {
|
|
426
|
+
it('should upload metaconfig to S3', async () => {
|
|
427
|
+
const metaconfig = {
|
|
428
|
+
siteId: 'site-123',
|
|
429
|
+
prerender: true,
|
|
430
|
+
};
|
|
444
431
|
|
|
445
|
-
const
|
|
432
|
+
const s3Path = await client.uploadMetaconfig('https://example.com/page1', metaconfig);
|
|
433
|
+
|
|
434
|
+
expect(s3Path).to.equal('opportunities/example.com/config');
|
|
435
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
446
436
|
|
|
447
|
-
|
|
448
|
-
expect(
|
|
437
|
+
const command = s3Client.send.firstCall.args[0];
|
|
438
|
+
expect(command.input.Bucket).to.equal('test-bucket');
|
|
439
|
+
expect(command.input.Key).to.equal('opportunities/example.com/config');
|
|
440
|
+
expect(command.input.ContentType).to.equal('application/json');
|
|
441
|
+
expect(JSON.parse(command.input.Body)).to.deep.equal(metaconfig);
|
|
449
442
|
});
|
|
450
443
|
|
|
451
|
-
it('should
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
getData: () => ({
|
|
457
|
-
url: 'https://example.com/page1',
|
|
458
|
-
}),
|
|
459
|
-
},
|
|
460
|
-
];
|
|
444
|
+
it('should upload metaconfig to preview bucket', async () => {
|
|
445
|
+
const metaconfig = {
|
|
446
|
+
siteId: 'site-123',
|
|
447
|
+
prerender: true,
|
|
448
|
+
};
|
|
461
449
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
450
|
+
const s3Path = await client.uploadMetaconfig('https://example.com/page1', metaconfig, true);
|
|
451
|
+
|
|
452
|
+
expect(s3Path).to.equal('preview/opportunities/example.com/config');
|
|
453
|
+
|
|
454
|
+
const command = s3Client.send.firstCall.args[0];
|
|
455
|
+
expect(command.input.Bucket).to.equal('test-preview-bucket');
|
|
465
456
|
});
|
|
466
457
|
|
|
467
|
-
it('should
|
|
468
|
-
|
|
458
|
+
it('should throw error if URL is missing', async () => {
|
|
459
|
+
try {
|
|
460
|
+
await client.uploadMetaconfig('', { siteId: 'site-123', prerender: true });
|
|
461
|
+
expect.fail('Should have thrown error');
|
|
462
|
+
} catch (error) {
|
|
463
|
+
expect(error.message).to.include('URL is required');
|
|
464
|
+
expect(error.status).to.equal(400);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
469
467
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
468
|
+
it('should throw error if metaconfig is empty', async () => {
|
|
469
|
+
try {
|
|
470
|
+
await client.uploadMetaconfig('https://example.com/page1', {});
|
|
471
|
+
expect.fail('Should have thrown error');
|
|
472
|
+
} catch (error) {
|
|
473
|
+
expect(error.message).to.include('Metaconfig object is required');
|
|
474
|
+
expect(error.status).to.equal(400);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should throw error on S3 upload failure', async () => {
|
|
479
|
+
const s3Error = new Error('Access Denied');
|
|
480
|
+
s3Client.send.rejects(s3Error);
|
|
476
481
|
|
|
477
|
-
|
|
482
|
+
try {
|
|
483
|
+
await client.uploadMetaconfig('https://example.com/page1', { siteId: 'site-123', prerender: true });
|
|
484
|
+
expect.fail('Should have thrown error');
|
|
485
|
+
} catch (error) {
|
|
486
|
+
expect(error.message).to.include('S3 upload failed');
|
|
487
|
+
expect(error.status).to.equal(500);
|
|
488
|
+
}
|
|
478
489
|
});
|
|
479
490
|
});
|
|
480
491
|
|
|
481
492
|
describe('uploadConfig', () => {
|
|
482
493
|
it('should upload config to S3', async () => {
|
|
483
494
|
const config = {
|
|
484
|
-
|
|
485
|
-
baseURL: 'https://example.com',
|
|
495
|
+
url: 'https://example.com/page1',
|
|
486
496
|
version: '1.0',
|
|
487
|
-
|
|
488
|
-
|
|
497
|
+
forceFail: false,
|
|
498
|
+
prerender: true,
|
|
499
|
+
patches: [],
|
|
489
500
|
};
|
|
490
501
|
|
|
491
|
-
const s3Key = await client.uploadConfig('
|
|
502
|
+
const s3Key = await client.uploadConfig('https://example.com/page1', config);
|
|
492
503
|
|
|
493
|
-
expect(s3Key).to.equal('opportunities/
|
|
504
|
+
expect(s3Key).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
494
505
|
expect(s3Client.send).to.have.been.calledOnce;
|
|
495
506
|
|
|
496
507
|
const command = s3Client.send.firstCall.args[0];
|
|
497
508
|
expect(command.input.Bucket).to.equal('test-bucket');
|
|
498
|
-
expect(command.input.Key).to.equal('opportunities/
|
|
509
|
+
expect(command.input.Key).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
499
510
|
expect(command.input.ContentType).to.equal('application/json');
|
|
500
511
|
expect(JSON.parse(command.input.Body)).to.deep.equal(config);
|
|
501
512
|
});
|
|
502
513
|
|
|
503
|
-
it('should
|
|
504
|
-
const config = {
|
|
514
|
+
it('should upload config to preview bucket', async () => {
|
|
515
|
+
const config = {
|
|
516
|
+
url: 'https://example.com/page1',
|
|
517
|
+
version: '1.0',
|
|
518
|
+
forceFail: false,
|
|
519
|
+
prerender: true,
|
|
520
|
+
patches: [],
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const s3Key = await client.uploadConfig('https://example.com/page1', config, true);
|
|
524
|
+
|
|
525
|
+
expect(s3Key).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
|
|
526
|
+
|
|
527
|
+
const command = s3Client.send.firstCall.args[0];
|
|
528
|
+
expect(command.input.Bucket).to.equal('test-preview-bucket');
|
|
529
|
+
expect(command.input.Key).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should throw error if URL is missing', async () => {
|
|
533
|
+
const config = { url: 'https://example.com/page1', patches: [] };
|
|
505
534
|
|
|
506
535
|
try {
|
|
507
536
|
await client.uploadConfig('', config);
|
|
508
537
|
expect.fail('Should have thrown error');
|
|
509
538
|
} catch (error) {
|
|
510
|
-
expect(error.message).to.include('
|
|
539
|
+
expect(error.message).to.include('URL is required');
|
|
511
540
|
expect(error.status).to.equal(400);
|
|
512
541
|
}
|
|
513
542
|
});
|
|
514
543
|
|
|
515
544
|
it('should throw error if config is empty', async () => {
|
|
516
545
|
try {
|
|
517
|
-
await client.uploadConfig('
|
|
546
|
+
await client.uploadConfig('https://example.com/page1', {});
|
|
518
547
|
expect.fail('Should have thrown error');
|
|
519
548
|
} catch (error) {
|
|
520
549
|
expect(error.message).to.include('Config object is required');
|
|
@@ -524,10 +553,10 @@ describe('TokowakaClient', () => {
|
|
|
524
553
|
|
|
525
554
|
it('should handle S3 upload failure', async () => {
|
|
526
555
|
s3Client.send.rejects(new Error('Network error'));
|
|
527
|
-
const config = {
|
|
556
|
+
const config = { url: 'https://example.com/page1', patches: [] };
|
|
528
557
|
|
|
529
558
|
try {
|
|
530
|
-
await client.uploadConfig('
|
|
559
|
+
await client.uploadConfig('https://example.com/page1', config);
|
|
531
560
|
expect.fail('Should have thrown error');
|
|
532
561
|
} catch (error) {
|
|
533
562
|
expect(error.message).to.include('S3 upload failed');
|
|
@@ -539,26 +568,21 @@ describe('TokowakaClient', () => {
|
|
|
539
568
|
describe('fetchConfig', () => {
|
|
540
569
|
it('should fetch existing config from S3', async () => {
|
|
541
570
|
const existingConfig = {
|
|
542
|
-
|
|
543
|
-
baseURL: 'https://example.com',
|
|
571
|
+
url: 'https://example.com/page1',
|
|
544
572
|
version: '1.0',
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
prerenderRequired: true,
|
|
557
|
-
lastUpdated: 1234567890,
|
|
558
|
-
},
|
|
559
|
-
],
|
|
573
|
+
forceFail: false,
|
|
574
|
+
prerender: true,
|
|
575
|
+
patches: [
|
|
576
|
+
{
|
|
577
|
+
op: 'replace',
|
|
578
|
+
selector: 'h1',
|
|
579
|
+
value: 'Old Heading',
|
|
580
|
+
opportunityId: 'opp-123',
|
|
581
|
+
suggestionId: 'sugg-1',
|
|
582
|
+
prerenderRequired: true,
|
|
583
|
+
lastUpdated: 1234567890,
|
|
560
584
|
},
|
|
561
|
-
|
|
585
|
+
],
|
|
562
586
|
};
|
|
563
587
|
|
|
564
588
|
s3Client.send.resolves({
|
|
@@ -567,14 +591,36 @@ describe('TokowakaClient', () => {
|
|
|
567
591
|
},
|
|
568
592
|
});
|
|
569
593
|
|
|
570
|
-
const config = await client.fetchConfig('
|
|
594
|
+
const config = await client.fetchConfig('https://example.com/page1');
|
|
571
595
|
|
|
572
596
|
expect(config).to.deep.equal(existingConfig);
|
|
573
597
|
expect(s3Client.send).to.have.been.calledOnce;
|
|
574
598
|
|
|
575
599
|
const command = s3Client.send.firstCall.args[0];
|
|
576
600
|
expect(command.input.Bucket).to.equal('test-bucket');
|
|
577
|
-
expect(command.input.Key).to.equal('opportunities/
|
|
601
|
+
expect(command.input.Key).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should fetch config from preview bucket', async () => {
|
|
605
|
+
const existingConfig = {
|
|
606
|
+
url: 'https://example.com/page1',
|
|
607
|
+
version: '1.0',
|
|
608
|
+
forceFail: false,
|
|
609
|
+
prerender: true,
|
|
610
|
+
patches: [],
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
s3Client.send.resolves({
|
|
614
|
+
Body: {
|
|
615
|
+
transformToString: async () => JSON.stringify(existingConfig),
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await client.fetchConfig('https://example.com/page1', true);
|
|
620
|
+
|
|
621
|
+
const command = s3Client.send.firstCall.args[0];
|
|
622
|
+
expect(command.input.Bucket).to.equal('test-preview-bucket');
|
|
623
|
+
expect(command.input.Key).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
|
|
578
624
|
});
|
|
579
625
|
|
|
580
626
|
it('should return null if config does not exist', async () => {
|
|
@@ -582,7 +628,7 @@ describe('TokowakaClient', () => {
|
|
|
582
628
|
noSuchKeyError.name = 'NoSuchKey';
|
|
583
629
|
s3Client.send.rejects(noSuchKeyError);
|
|
584
630
|
|
|
585
|
-
const config = await client.fetchConfig('
|
|
631
|
+
const config = await client.fetchConfig('https://example.com/page1');
|
|
586
632
|
|
|
587
633
|
expect(config).to.be.null;
|
|
588
634
|
});
|
|
@@ -592,17 +638,17 @@ describe('TokowakaClient', () => {
|
|
|
592
638
|
noSuchKeyError.Code = 'NoSuchKey';
|
|
593
639
|
s3Client.send.rejects(noSuchKeyError);
|
|
594
640
|
|
|
595
|
-
const config = await client.fetchConfig('
|
|
641
|
+
const config = await client.fetchConfig('https://example.com/page1');
|
|
596
642
|
|
|
597
643
|
expect(config).to.be.null;
|
|
598
644
|
});
|
|
599
645
|
|
|
600
|
-
it('should throw error if
|
|
646
|
+
it('should throw error if URL is missing', async () => {
|
|
601
647
|
try {
|
|
602
648
|
await client.fetchConfig('');
|
|
603
649
|
expect.fail('Should have thrown error');
|
|
604
650
|
} catch (error) {
|
|
605
|
-
expect(error.message).to.include('
|
|
651
|
+
expect(error.message).to.include('URL is required');
|
|
606
652
|
expect(error.status).to.equal(400);
|
|
607
653
|
}
|
|
608
654
|
});
|
|
@@ -611,7 +657,7 @@ describe('TokowakaClient', () => {
|
|
|
611
657
|
s3Client.send.rejects(new Error('Network error'));
|
|
612
658
|
|
|
613
659
|
try {
|
|
614
|
-
await client.fetchConfig('
|
|
660
|
+
await client.fetchConfig('https://example.com/page1');
|
|
615
661
|
expect.fail('Should have thrown error');
|
|
616
662
|
} catch (error) {
|
|
617
663
|
expect(error.message).to.include('S3 fetch failed');
|
|
@@ -626,58 +672,48 @@ describe('TokowakaClient', () => {
|
|
|
626
672
|
|
|
627
673
|
beforeEach(() => {
|
|
628
674
|
existingConfig = {
|
|
629
|
-
|
|
630
|
-
baseURL: 'https://example.com',
|
|
675
|
+
url: 'https://example.com/page1',
|
|
631
676
|
version: '1.0',
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
prerenderRequired: true,
|
|
644
|
-
lastUpdated: 1234567890,
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
op: 'replace',
|
|
648
|
-
selector: 'h2',
|
|
649
|
-
value: 'Old Subtitle',
|
|
650
|
-
opportunityId: 'opp-456',
|
|
651
|
-
suggestionId: 'sugg-2',
|
|
652
|
-
prerenderRequired: true,
|
|
653
|
-
lastUpdated: 1234567890,
|
|
654
|
-
},
|
|
655
|
-
],
|
|
677
|
+
forceFail: false,
|
|
678
|
+
prerender: true,
|
|
679
|
+
patches: [
|
|
680
|
+
{
|
|
681
|
+
op: 'replace',
|
|
682
|
+
selector: 'h1',
|
|
683
|
+
value: 'Old Heading',
|
|
684
|
+
opportunityId: 'opp-123',
|
|
685
|
+
suggestionId: 'sugg-1',
|
|
686
|
+
prerenderRequired: true,
|
|
687
|
+
lastUpdated: 1234567890,
|
|
656
688
|
},
|
|
657
|
-
|
|
689
|
+
{
|
|
690
|
+
op: 'replace',
|
|
691
|
+
selector: 'h2',
|
|
692
|
+
value: 'Old Subtitle',
|
|
693
|
+
opportunityId: 'opp-456',
|
|
694
|
+
suggestionId: 'sugg-2',
|
|
695
|
+
prerenderRequired: true,
|
|
696
|
+
lastUpdated: 1234567890,
|
|
697
|
+
},
|
|
698
|
+
],
|
|
658
699
|
};
|
|
659
700
|
|
|
660
701
|
newConfig = {
|
|
661
|
-
|
|
662
|
-
baseURL: 'https://example.com',
|
|
702
|
+
url: 'https://example.com/page1',
|
|
663
703
|
version: '1.0',
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
prerenderRequired: true,
|
|
676
|
-
lastUpdated: 1234567900,
|
|
677
|
-
},
|
|
678
|
-
],
|
|
704
|
+
forceFail: false,
|
|
705
|
+
prerender: true,
|
|
706
|
+
patches: [
|
|
707
|
+
{
|
|
708
|
+
op: 'replace',
|
|
709
|
+
selector: 'h1',
|
|
710
|
+
value: 'Updated Heading',
|
|
711
|
+
opportunityId: 'opp-123',
|
|
712
|
+
suggestionId: 'sugg-1',
|
|
713
|
+
prerenderRequired: true,
|
|
714
|
+
lastUpdated: 1234567900,
|
|
679
715
|
},
|
|
680
|
-
|
|
716
|
+
],
|
|
681
717
|
};
|
|
682
718
|
});
|
|
683
719
|
|
|
@@ -690,21 +726,21 @@ describe('TokowakaClient', () => {
|
|
|
690
726
|
it('should update existing patch with same opportunityId and suggestionId', () => {
|
|
691
727
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
692
728
|
|
|
693
|
-
expect(merged.
|
|
729
|
+
expect(merged.patches).to.have.length(2);
|
|
694
730
|
|
|
695
731
|
// First patch should be updated
|
|
696
|
-
const updatedPatch = merged.
|
|
732
|
+
const updatedPatch = merged.patches[0];
|
|
697
733
|
expect(updatedPatch.value).to.equal('Updated Heading');
|
|
698
734
|
expect(updatedPatch.lastUpdated).to.equal(1234567900);
|
|
699
735
|
|
|
700
736
|
// Second patch should remain unchanged
|
|
701
|
-
const unchangedPatch = merged.
|
|
737
|
+
const unchangedPatch = merged.patches[1];
|
|
702
738
|
expect(unchangedPatch.value).to.equal('Old Subtitle');
|
|
703
739
|
expect(unchangedPatch.opportunityId).to.equal('opp-456');
|
|
704
740
|
});
|
|
705
741
|
|
|
706
742
|
it('should add new patch if opportunityId and suggestionId do not exist', () => {
|
|
707
|
-
newConfig.
|
|
743
|
+
newConfig.patches.push({
|
|
708
744
|
op: 'replace',
|
|
709
745
|
selector: 'h3',
|
|
710
746
|
value: 'New Section Title',
|
|
@@ -716,106 +752,59 @@ describe('TokowakaClient', () => {
|
|
|
716
752
|
|
|
717
753
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
718
754
|
|
|
719
|
-
expect(merged.
|
|
755
|
+
expect(merged.patches).to.have.length(3);
|
|
720
756
|
|
|
721
757
|
// New patch should be added at the end
|
|
722
|
-
const newPatch = merged.
|
|
758
|
+
const newPatch = merged.patches[2];
|
|
723
759
|
expect(newPatch.value).to.equal('New Section Title');
|
|
724
760
|
expect(newPatch.opportunityId).to.equal('opp-789');
|
|
725
761
|
expect(newPatch.suggestionId).to.equal('sugg-3');
|
|
726
762
|
});
|
|
727
763
|
|
|
728
|
-
it('should add new URL path if it does not exist in existing config', () => {
|
|
729
|
-
newConfig.tokowakaOptimizations['/page2'] = {
|
|
730
|
-
prerender: true,
|
|
731
|
-
patches: [
|
|
732
|
-
{
|
|
733
|
-
op: 'replace',
|
|
734
|
-
selector: 'h1',
|
|
735
|
-
value: 'Page 2 Heading',
|
|
736
|
-
opportunityId: 'opp-999',
|
|
737
|
-
suggestionId: 'sugg-4',
|
|
738
|
-
prerenderRequired: true,
|
|
739
|
-
lastUpdated: 1234567900,
|
|
740
|
-
},
|
|
741
|
-
],
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
745
|
-
|
|
746
|
-
expect(merged.tokowakaOptimizations).to.have.property('/page1');
|
|
747
|
-
expect(merged.tokowakaOptimizations).to.have.property('/page2');
|
|
748
|
-
expect(merged.tokowakaOptimizations['/page2'].patches).to.have.length(1);
|
|
749
|
-
expect(merged.tokowakaOptimizations['/page2'].patches[0].value).to.equal('Page 2 Heading');
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
it('should preserve existing URL paths not present in new config', () => {
|
|
753
|
-
existingConfig.tokowakaOptimizations['/page3'] = {
|
|
754
|
-
prerender: false,
|
|
755
|
-
patches: [
|
|
756
|
-
{
|
|
757
|
-
op: 'replace',
|
|
758
|
-
selector: 'h1',
|
|
759
|
-
value: 'Page 3 Heading',
|
|
760
|
-
opportunityId: 'opp-333',
|
|
761
|
-
suggestionId: 'sugg-5',
|
|
762
|
-
prerenderRequired: false,
|
|
763
|
-
lastUpdated: 1234567890,
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
769
|
-
|
|
770
|
-
expect(merged.tokowakaOptimizations).to.have.property('/page1');
|
|
771
|
-
expect(merged.tokowakaOptimizations).to.have.property('/page3');
|
|
772
|
-
expect(merged.tokowakaOptimizations['/page3'].patches[0].value).to.equal('Page 3 Heading');
|
|
773
|
-
});
|
|
774
|
-
|
|
775
764
|
it('should update config metadata from new config', () => {
|
|
776
765
|
newConfig.version = '2.0';
|
|
777
|
-
newConfig.
|
|
766
|
+
newConfig.forceFail = true;
|
|
778
767
|
|
|
779
768
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
780
769
|
|
|
781
770
|
expect(merged.version).to.equal('2.0');
|
|
782
|
-
expect(merged.
|
|
771
|
+
expect(merged.forceFail).to.equal(true);
|
|
783
772
|
});
|
|
784
773
|
|
|
785
774
|
it('should handle empty patches array in existing config', () => {
|
|
786
|
-
existingConfig.
|
|
775
|
+
existingConfig.patches = [];
|
|
787
776
|
|
|
788
777
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
789
778
|
|
|
790
|
-
expect(merged.
|
|
791
|
-
expect(merged.
|
|
779
|
+
expect(merged.patches).to.have.length(1);
|
|
780
|
+
expect(merged.patches[0].value).to.equal('Updated Heading');
|
|
792
781
|
});
|
|
793
782
|
|
|
794
783
|
it('should handle empty patches array in new config', () => {
|
|
795
|
-
newConfig.
|
|
784
|
+
newConfig.patches = [];
|
|
796
785
|
|
|
797
786
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
798
787
|
|
|
799
|
-
expect(merged.
|
|
800
|
-
expect(merged.
|
|
788
|
+
expect(merged.patches).to.have.length(2);
|
|
789
|
+
expect(merged.patches[0].value).to.equal('Old Heading');
|
|
801
790
|
});
|
|
802
791
|
|
|
803
|
-
it('should handle
|
|
804
|
-
|
|
792
|
+
it('should handle undefined patches in existing config', () => {
|
|
793
|
+
existingConfig.patches = undefined;
|
|
805
794
|
|
|
806
795
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
807
796
|
|
|
808
|
-
expect(merged.
|
|
809
|
-
expect(merged.
|
|
797
|
+
expect(merged.patches).to.have.length(1);
|
|
798
|
+
expect(merged.patches[0].value).to.equal('Updated Heading');
|
|
810
799
|
});
|
|
811
800
|
|
|
812
|
-
it('should handle
|
|
813
|
-
|
|
801
|
+
it('should handle undefined patches in new config', () => {
|
|
802
|
+
newConfig.patches = undefined;
|
|
814
803
|
|
|
815
804
|
const merged = client.mergeConfigs(existingConfig, newConfig);
|
|
816
805
|
|
|
817
|
-
expect(merged.
|
|
818
|
-
expect(merged.
|
|
806
|
+
expect(merged.patches).to.have.length(2);
|
|
807
|
+
expect(merged.patches[0].value).to.equal('Old Heading');
|
|
819
808
|
});
|
|
820
809
|
});
|
|
821
810
|
|
|
@@ -829,6 +818,10 @@ describe('TokowakaClient', () => {
|
|
|
829
818
|
});
|
|
830
819
|
// Stub fetchConfig to return null by default (no existing config)
|
|
831
820
|
sinon.stub(client, 'fetchConfig').resolves(null);
|
|
821
|
+
// Stub fetchMetaconfig to return null by default (will create new)
|
|
822
|
+
sinon.stub(client, 'fetchMetaconfig').resolves(null);
|
|
823
|
+
// Stub uploadMetaconfig
|
|
824
|
+
sinon.stub(client, 'uploadMetaconfig').resolves('opportunities/example.com/config');
|
|
832
825
|
});
|
|
833
826
|
|
|
834
827
|
it('should deploy suggestions successfully', async () => {
|
|
@@ -838,33 +831,98 @@ describe('TokowakaClient', () => {
|
|
|
838
831
|
mockSuggestions,
|
|
839
832
|
);
|
|
840
833
|
|
|
841
|
-
expect(result).to.have.property('
|
|
842
|
-
expect(
|
|
834
|
+
expect(result).to.have.property('s3Paths');
|
|
835
|
+
expect(result.s3Paths).to.be.an('array').with.length(1);
|
|
836
|
+
expect(result.s3Paths[0]).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
837
|
+
expect(result).to.have.property('cdnInvalidations');
|
|
838
|
+
expect(result.cdnInvalidations).to.be.an('array').with.length(1);
|
|
839
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
840
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
841
|
+
expect(s3Client.send).to.have.been.called;
|
|
843
842
|
});
|
|
844
843
|
|
|
845
|
-
it('should
|
|
846
|
-
|
|
847
|
-
|
|
844
|
+
it('should create metaconfig on first deployment', async () => {
|
|
845
|
+
await client.deploySuggestions(
|
|
846
|
+
mockSite,
|
|
847
|
+
mockOpportunity,
|
|
848
|
+
mockSuggestions,
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
expect(client.fetchMetaconfig).to.have.been.calledOnce;
|
|
852
|
+
expect(client.uploadMetaconfig).to.have.been.calledOnce;
|
|
853
|
+
|
|
854
|
+
const metaconfigArg = client.uploadMetaconfig.firstCall.args[1];
|
|
855
|
+
expect(metaconfigArg).to.deep.include({
|
|
856
|
+
siteId: 'site-123',
|
|
857
|
+
prerender: true,
|
|
848
858
|
});
|
|
859
|
+
});
|
|
849
860
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
861
|
+
it('should reuse existing metaconfig', async () => {
|
|
862
|
+
client.fetchMetaconfig.resolves({
|
|
863
|
+
siteId: 'site-123',
|
|
864
|
+
prerender: true,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
await client.deploySuggestions(
|
|
868
|
+
mockSite,
|
|
869
|
+
mockOpportunity,
|
|
870
|
+
mockSuggestions,
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
expect(client.fetchMetaconfig).to.have.been.calledOnce;
|
|
874
|
+
expect(client.uploadMetaconfig).to.not.have.been.called;
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('should handle suggestions for multiple URLs', async () => {
|
|
878
|
+
mockSuggestions = [
|
|
879
|
+
{
|
|
880
|
+
getId: () => 'sugg-1',
|
|
881
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
882
|
+
getData: () => ({
|
|
883
|
+
url: 'https://example.com/page1',
|
|
884
|
+
recommendedAction: 'Page 1 Heading',
|
|
885
|
+
checkType: 'heading-empty',
|
|
886
|
+
transformRules: {
|
|
887
|
+
action: 'replace',
|
|
888
|
+
selector: 'h1',
|
|
889
|
+
},
|
|
890
|
+
}),
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
getId: () => 'sugg-2',
|
|
894
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
895
|
+
getData: () => ({
|
|
896
|
+
url: 'https://example.com/page2',
|
|
897
|
+
recommendedAction: 'Page 2 Heading',
|
|
898
|
+
checkType: 'heading-empty',
|
|
899
|
+
transformRules: {
|
|
900
|
+
action: 'replace',
|
|
901
|
+
selector: 'h1',
|
|
902
|
+
},
|
|
903
|
+
}),
|
|
904
|
+
},
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
const result = await client.deploySuggestions(
|
|
908
|
+
mockSite,
|
|
909
|
+
mockOpportunity,
|
|
910
|
+
mockSuggestions,
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
expect(result.s3Paths).to.have.length(2);
|
|
914
|
+
expect(result.cdnInvalidations).to.have.length(2);
|
|
915
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
857
916
|
});
|
|
858
917
|
|
|
859
918
|
it('should handle suggestions that are not eligible for deployment', async () => {
|
|
860
|
-
// Create suggestions with different checkTypes
|
|
861
919
|
mockSuggestions = [
|
|
862
920
|
{
|
|
863
921
|
getId: () => 'sugg-1',
|
|
864
922
|
getData: () => ({
|
|
865
923
|
url: 'https://example.com/page1',
|
|
866
924
|
recommendedAction: 'New Heading',
|
|
867
|
-
checkType: 'heading-missing', // Not eligible
|
|
925
|
+
checkType: 'heading-missing', // Not eligible
|
|
868
926
|
}),
|
|
869
927
|
},
|
|
870
928
|
{
|
|
@@ -893,15 +951,27 @@ describe('TokowakaClient', () => {
|
|
|
893
951
|
expect(result.failedSuggestions[0].reason).to.include('can be deployed');
|
|
894
952
|
});
|
|
895
953
|
|
|
896
|
-
it('should
|
|
897
|
-
// All suggestions are ineligible
|
|
954
|
+
it('should handle multi-URL deploy where one URL has no eligible suggestions', async () => {
|
|
898
955
|
mockSuggestions = [
|
|
899
956
|
{
|
|
900
957
|
getId: () => 'sugg-1',
|
|
958
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
901
959
|
getData: () => ({
|
|
902
960
|
url: 'https://example.com/page1',
|
|
903
961
|
recommendedAction: 'New Heading',
|
|
904
|
-
checkType: 'heading-
|
|
962
|
+
checkType: 'heading-empty', // Eligible
|
|
963
|
+
transformRules: {
|
|
964
|
+
action: 'replace',
|
|
965
|
+
selector: 'h1',
|
|
966
|
+
},
|
|
967
|
+
}),
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
getId: () => 'sugg-2',
|
|
971
|
+
getData: () => ({
|
|
972
|
+
url: 'https://example.com/page2',
|
|
973
|
+
recommendedAction: 'New Heading',
|
|
974
|
+
checkType: 'heading-missing', // Not eligible
|
|
905
975
|
}),
|
|
906
976
|
},
|
|
907
977
|
];
|
|
@@ -912,20 +982,32 @@ describe('TokowakaClient', () => {
|
|
|
912
982
|
mockSuggestions,
|
|
913
983
|
);
|
|
914
984
|
|
|
915
|
-
expect(result.succeededSuggestions).to.have.length(
|
|
985
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
916
986
|
expect(result.failedSuggestions).to.have.length(1);
|
|
917
|
-
expect(
|
|
918
|
-
|
|
919
|
-
|
|
987
|
+
expect(result.failedSuggestions[0].suggestion.getId()).to.equal('sugg-2');
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should skip URL when generateConfig returns no patches', async () => {
|
|
991
|
+
// Stub mapper to return empty patches for the first call, normal for subsequent calls
|
|
992
|
+
const mapper = client.mapperRegistry.getMapper('headings');
|
|
993
|
+
const originalSuggestionsToPatches = mapper.suggestionsToPatches.bind(mapper);
|
|
994
|
+
let callCount = 0;
|
|
995
|
+
sinon.stub(mapper, 'suggestionsToPatches').callsFake((...args) => {
|
|
996
|
+
callCount += 1;
|
|
997
|
+
if (callCount === 1) {
|
|
998
|
+
// First call (for page1) returns no patches
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
// Subsequent calls work normally
|
|
1002
|
+
return originalSuggestionsToPatches(...args);
|
|
1003
|
+
});
|
|
920
1004
|
|
|
921
|
-
it('should return early when suggestions pass eligibility but fail during config generation', async () => {
|
|
922
|
-
// Suggestions pass canDeploy but have no URL (caught in generateConfig)
|
|
923
1005
|
mockSuggestions = [
|
|
924
1006
|
{
|
|
925
1007
|
getId: () => 'sugg-1',
|
|
926
1008
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
927
1009
|
getData: () => ({
|
|
928
|
-
|
|
1010
|
+
url: 'https://example.com/page1',
|
|
929
1011
|
recommendedAction: 'New Heading',
|
|
930
1012
|
checkType: 'heading-empty',
|
|
931
1013
|
transformRules: {
|
|
@@ -934,6 +1016,42 @@ describe('TokowakaClient', () => {
|
|
|
934
1016
|
},
|
|
935
1017
|
}),
|
|
936
1018
|
},
|
|
1019
|
+
{
|
|
1020
|
+
getId: () => 'sugg-2',
|
|
1021
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1022
|
+
getData: () => ({
|
|
1023
|
+
url: 'https://example.com/page2',
|
|
1024
|
+
recommendedAction: 'New Subtitle',
|
|
1025
|
+
checkType: 'heading-empty',
|
|
1026
|
+
transformRules: {
|
|
1027
|
+
action: 'replace',
|
|
1028
|
+
selector: 'h2',
|
|
1029
|
+
},
|
|
1030
|
+
}),
|
|
1031
|
+
},
|
|
1032
|
+
];
|
|
1033
|
+
|
|
1034
|
+
const result = await client.deploySuggestions(
|
|
1035
|
+
mockSite,
|
|
1036
|
+
mockOpportunity,
|
|
1037
|
+
mockSuggestions,
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
// Both suggestions are in result but sugg-1 skipped deployment due to no patches
|
|
1041
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1042
|
+
expect(result.s3Paths).to.have.length(1); // Only one URL actually deployed
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('should return early when no eligible suggestions', async () => {
|
|
1046
|
+
mockSuggestions = [
|
|
1047
|
+
{
|
|
1048
|
+
getId: () => 'sugg-1',
|
|
1049
|
+
getData: () => ({
|
|
1050
|
+
url: 'https://example.com/page1',
|
|
1051
|
+
recommendedAction: 'New Heading',
|
|
1052
|
+
checkType: 'heading-missing', // Wrong checkType name, not eligible
|
|
1053
|
+
}),
|
|
1054
|
+
},
|
|
937
1055
|
];
|
|
938
1056
|
|
|
939
1057
|
const result = await client.deploySuggestions(
|
|
@@ -944,6 +1062,7 @@ describe('TokowakaClient', () => {
|
|
|
944
1062
|
|
|
945
1063
|
expect(result.succeededSuggestions).to.have.length(0);
|
|
946
1064
|
expect(result.failedSuggestions).to.have.length(1);
|
|
1065
|
+
expect(log.warn).to.have.been.calledWith('No eligible suggestions to deploy');
|
|
947
1066
|
expect(s3Client.send).to.not.have.been.called;
|
|
948
1067
|
});
|
|
949
1068
|
|
|
@@ -955,63 +1074,27 @@ describe('TokowakaClient', () => {
|
|
|
955
1074
|
expect.fail('Should have thrown error');
|
|
956
1075
|
} catch (error) {
|
|
957
1076
|
expect(error.message).to.include('No mapper found for opportunity type: unsupported-type');
|
|
958
|
-
expect(error.message).to.include('Supported types:');
|
|
959
1077
|
expect(error.status).to.equal(501);
|
|
960
1078
|
}
|
|
961
1079
|
});
|
|
962
1080
|
|
|
963
|
-
it('should handle null tokowakaConfig gracefully', async () => {
|
|
964
|
-
mockSite.getConfig = () => ({
|
|
965
|
-
getTokowakaConfig: () => null,
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
try {
|
|
969
|
-
await client.deploySuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
970
|
-
expect.fail('Should have thrown error');
|
|
971
|
-
} catch (error) {
|
|
972
|
-
expect(error.message).to.include('Tokowaka API key configured');
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
it('should use default reason when eligibility has no reason', async () => {
|
|
977
|
-
// Create a mock mapper that returns eligible=false without reason
|
|
978
|
-
const mockMapper = {
|
|
979
|
-
canDeploy: sinon.stub().returns({ eligible: false }), // No reason provided
|
|
980
|
-
};
|
|
981
|
-
sinon.stub(client.mapperRegistry, 'getMapper').returns(mockMapper);
|
|
982
|
-
|
|
983
|
-
const result = await client.deploySuggestions(
|
|
984
|
-
mockSite,
|
|
985
|
-
mockOpportunity,
|
|
986
|
-
mockSuggestions,
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
expect(result.failedSuggestions).to.have.length(2);
|
|
990
|
-
expect(result.failedSuggestions[0].reason).to.equal('Suggestion cannot be deployed');
|
|
991
|
-
});
|
|
992
|
-
|
|
993
1081
|
it('should fetch existing config and merge when deploying', async () => {
|
|
994
1082
|
const existingConfig = {
|
|
995
|
-
|
|
996
|
-
baseURL: 'https://example.com',
|
|
1083
|
+
url: 'https://example.com/page1',
|
|
997
1084
|
version: '1.0',
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
prerenderRequired: true,
|
|
1010
|
-
lastUpdated: 1234567890,
|
|
1011
|
-
},
|
|
1012
|
-
],
|
|
1085
|
+
forceFail: false,
|
|
1086
|
+
prerender: true,
|
|
1087
|
+
patches: [
|
|
1088
|
+
{
|
|
1089
|
+
op: 'replace',
|
|
1090
|
+
selector: 'h3',
|
|
1091
|
+
value: 'Existing Heading',
|
|
1092
|
+
opportunityId: 'opp-999',
|
|
1093
|
+
suggestionId: 'sugg-999',
|
|
1094
|
+
prerenderRequired: true,
|
|
1095
|
+
lastUpdated: 1234567890,
|
|
1013
1096
|
},
|
|
1014
|
-
|
|
1097
|
+
],
|
|
1015
1098
|
};
|
|
1016
1099
|
|
|
1017
1100
|
client.fetchConfig.resolves(existingConfig);
|
|
@@ -1022,53 +1105,31 @@ describe('TokowakaClient', () => {
|
|
|
1022
1105
|
mockSuggestions,
|
|
1023
1106
|
);
|
|
1024
1107
|
|
|
1025
|
-
expect(client.fetchConfig).to.have.been.
|
|
1026
|
-
expect(result).to.have.
|
|
1108
|
+
expect(client.fetchConfig).to.have.been.called;
|
|
1109
|
+
expect(result.s3Paths).to.have.length(1);
|
|
1027
1110
|
|
|
1028
1111
|
// Verify the uploaded config contains both existing and new patches
|
|
1029
1112
|
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1030
|
-
expect(uploadedConfig.
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
it('should use new config when no existing config found', async () => {
|
|
1034
|
-
client.fetchConfig.resolves(null);
|
|
1035
|
-
|
|
1036
|
-
const result = await client.deploySuggestions(
|
|
1037
|
-
mockSite,
|
|
1038
|
-
mockOpportunity,
|
|
1039
|
-
mockSuggestions,
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
expect(client.fetchConfig).to.have.been.calledWith('test-api-key-123');
|
|
1043
|
-
expect(result).to.have.property('s3Path', 'opportunities/test-api-key-123');
|
|
1044
|
-
|
|
1045
|
-
// Verify only new patches are in the config
|
|
1046
|
-
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1047
|
-
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches).to.have.length(2);
|
|
1113
|
+
expect(uploadedConfig.patches).to.have.length(3);
|
|
1048
1114
|
});
|
|
1049
1115
|
|
|
1050
1116
|
it('should update existing patch when deploying same opportunityId and suggestionId', async () => {
|
|
1051
1117
|
const existingConfig = {
|
|
1052
|
-
|
|
1053
|
-
baseURL: 'https://example.com',
|
|
1118
|
+
url: 'https://example.com/page1',
|
|
1054
1119
|
version: '1.0',
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
prerenderRequired: true,
|
|
1067
|
-
lastUpdated: 1234567890,
|
|
1068
|
-
},
|
|
1069
|
-
],
|
|
1120
|
+
forceFail: false,
|
|
1121
|
+
prerender: true,
|
|
1122
|
+
patches: [
|
|
1123
|
+
{
|
|
1124
|
+
op: 'replace',
|
|
1125
|
+
selector: 'h1',
|
|
1126
|
+
value: 'Old Heading Value',
|
|
1127
|
+
opportunityId: 'opp-123',
|
|
1128
|
+
suggestionId: 'sugg-1',
|
|
1129
|
+
prerenderRequired: true,
|
|
1130
|
+
lastUpdated: 1234567890,
|
|
1070
1131
|
},
|
|
1071
|
-
|
|
1132
|
+
],
|
|
1072
1133
|
};
|
|
1073
1134
|
|
|
1074
1135
|
client.fetchConfig.resolves(existingConfig);
|
|
@@ -1079,75 +1140,19 @@ describe('TokowakaClient', () => {
|
|
|
1079
1140
|
mockSuggestions,
|
|
1080
1141
|
);
|
|
1081
1142
|
|
|
1082
|
-
expect(result).to.have.
|
|
1143
|
+
expect(result.s3Paths).to.have.length(1);
|
|
1083
1144
|
|
|
1084
1145
|
// Verify the patch was updated, not duplicated
|
|
1085
1146
|
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1086
|
-
expect(uploadedConfig.
|
|
1147
|
+
expect(uploadedConfig.patches).to.have.length(2);
|
|
1087
1148
|
|
|
1088
1149
|
// First patch should be updated with new value
|
|
1089
|
-
const updatedPatch = uploadedConfig.
|
|
1150
|
+
const updatedPatch = uploadedConfig.patches[0];
|
|
1090
1151
|
expect(updatedPatch.value).to.equal('New Heading');
|
|
1091
1152
|
expect(updatedPatch.opportunityId).to.equal('opp-123');
|
|
1092
1153
|
expect(updatedPatch.suggestionId).to.equal('sugg-1');
|
|
1093
1154
|
expect(updatedPatch.lastUpdated).to.be.greaterThan(1234567890);
|
|
1094
1155
|
});
|
|
1095
|
-
|
|
1096
|
-
it('should preserve existing URL paths when merging', async () => {
|
|
1097
|
-
const existingConfig = {
|
|
1098
|
-
siteId: 'site-123',
|
|
1099
|
-
baseURL: 'https://example.com',
|
|
1100
|
-
version: '1.0',
|
|
1101
|
-
tokowakaForceFail: false,
|
|
1102
|
-
tokowakaOptimizations: {
|
|
1103
|
-
'/page1': {
|
|
1104
|
-
prerender: true,
|
|
1105
|
-
patches: [
|
|
1106
|
-
{
|
|
1107
|
-
op: 'replace',
|
|
1108
|
-
selector: 'h1',
|
|
1109
|
-
value: 'Page 1 Heading',
|
|
1110
|
-
opportunityId: 'opp-123',
|
|
1111
|
-
suggestionId: 'sugg-1',
|
|
1112
|
-
prerenderRequired: true,
|
|
1113
|
-
lastUpdated: 1234567890,
|
|
1114
|
-
},
|
|
1115
|
-
],
|
|
1116
|
-
},
|
|
1117
|
-
'/other-page': {
|
|
1118
|
-
prerender: false,
|
|
1119
|
-
patches: [
|
|
1120
|
-
{
|
|
1121
|
-
op: 'replace',
|
|
1122
|
-
selector: 'h1',
|
|
1123
|
-
value: 'Other Page Heading',
|
|
1124
|
-
opportunityId: 'opp-888',
|
|
1125
|
-
suggestionId: 'sugg-888',
|
|
1126
|
-
prerenderRequired: false,
|
|
1127
|
-
lastUpdated: 1234567890,
|
|
1128
|
-
},
|
|
1129
|
-
],
|
|
1130
|
-
},
|
|
1131
|
-
},
|
|
1132
|
-
};
|
|
1133
|
-
|
|
1134
|
-
client.fetchConfig.resolves(existingConfig);
|
|
1135
|
-
|
|
1136
|
-
const result = await client.deploySuggestions(
|
|
1137
|
-
mockSite,
|
|
1138
|
-
mockOpportunity,
|
|
1139
|
-
mockSuggestions,
|
|
1140
|
-
);
|
|
1141
|
-
|
|
1142
|
-
expect(result).to.have.property('s3Path', 'opportunities/test-api-key-123');
|
|
1143
|
-
|
|
1144
|
-
// Verify existing URL paths are preserved
|
|
1145
|
-
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1146
|
-
expect(uploadedConfig.tokowakaOptimizations).to.have.property('/page1');
|
|
1147
|
-
expect(uploadedConfig.tokowakaOptimizations).to.have.property('/other-page');
|
|
1148
|
-
expect(uploadedConfig.tokowakaOptimizations['/other-page'].patches[0].value)
|
|
1149
|
-
.to.equal('Other Page Heading');
|
|
1150
|
-
});
|
|
1151
1156
|
});
|
|
1152
1157
|
|
|
1153
1158
|
describe('rollbackSuggestions', () => {
|
|
@@ -1162,44 +1167,39 @@ describe('TokowakaClient', () => {
|
|
|
1162
1167
|
|
|
1163
1168
|
it('should rollback suggestions successfully', async () => {
|
|
1164
1169
|
const existingConfig = {
|
|
1165
|
-
|
|
1166
|
-
baseURL: 'https://example.com',
|
|
1170
|
+
url: 'https://example.com/page1',
|
|
1167
1171
|
version: '1.0',
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
prerenderRequired: true,
|
|
1180
|
-
lastUpdated: 1234567890,
|
|
1181
|
-
},
|
|
1182
|
-
{
|
|
1183
|
-
op: 'replace',
|
|
1184
|
-
selector: 'h2',
|
|
1185
|
-
value: 'Heading 2',
|
|
1186
|
-
opportunityId: 'opp-123',
|
|
1187
|
-
suggestionId: 'sugg-2',
|
|
1188
|
-
prerenderRequired: true,
|
|
1189
|
-
lastUpdated: 1234567890,
|
|
1190
|
-
},
|
|
1191
|
-
{
|
|
1192
|
-
op: 'replace',
|
|
1193
|
-
selector: 'h3',
|
|
1194
|
-
value: 'Heading 3',
|
|
1195
|
-
opportunityId: 'opp-123',
|
|
1196
|
-
suggestionId: 'sugg-3',
|
|
1197
|
-
prerenderRequired: true,
|
|
1198
|
-
lastUpdated: 1234567890,
|
|
1199
|
-
},
|
|
1200
|
-
],
|
|
1172
|
+
forceFail: false,
|
|
1173
|
+
prerender: true,
|
|
1174
|
+
patches: [
|
|
1175
|
+
{
|
|
1176
|
+
op: 'replace',
|
|
1177
|
+
selector: 'h1',
|
|
1178
|
+
value: 'Heading 1',
|
|
1179
|
+
opportunityId: 'opp-123',
|
|
1180
|
+
suggestionId: 'sugg-1',
|
|
1181
|
+
prerenderRequired: true,
|
|
1182
|
+
lastUpdated: 1234567890,
|
|
1201
1183
|
},
|
|
1202
|
-
|
|
1184
|
+
{
|
|
1185
|
+
op: 'replace',
|
|
1186
|
+
selector: 'h2',
|
|
1187
|
+
value: 'Heading 2',
|
|
1188
|
+
opportunityId: 'opp-123',
|
|
1189
|
+
suggestionId: 'sugg-2',
|
|
1190
|
+
prerenderRequired: true,
|
|
1191
|
+
lastUpdated: 1234567890,
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
op: 'replace',
|
|
1195
|
+
selector: 'h3',
|
|
1196
|
+
value: 'Heading 3',
|
|
1197
|
+
opportunityId: 'opp-123',
|
|
1198
|
+
suggestionId: 'sugg-3',
|
|
1199
|
+
prerenderRequired: true,
|
|
1200
|
+
lastUpdated: 1234567890,
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
1203
|
};
|
|
1204
1204
|
|
|
1205
1205
|
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
@@ -1210,41 +1210,16 @@ describe('TokowakaClient', () => {
|
|
|
1210
1210
|
mockSuggestions, // Only sugg-1 and sugg-2
|
|
1211
1211
|
);
|
|
1212
1212
|
|
|
1213
|
-
expect(result).to.have.
|
|
1213
|
+
expect(result.s3Paths).to.have.length(1);
|
|
1214
|
+
expect(result.s3Paths[0]).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
1214
1215
|
expect(result.succeededSuggestions).to.have.length(2);
|
|
1215
1216
|
expect(result.failedSuggestions).to.have.length(0);
|
|
1216
1217
|
expect(result.removedPatchesCount).to.equal(2);
|
|
1217
1218
|
|
|
1218
1219
|
// Verify uploaded config has sugg-3 but not sugg-1 and sugg-2
|
|
1219
1220
|
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1220
|
-
expect(uploadedConfig.
|
|
1221
|
-
expect(uploadedConfig.
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
it('should throw error if site does not have Tokowaka API key', async () => {
|
|
1225
|
-
mockSite.getConfig = () => ({
|
|
1226
|
-
getTokowakaConfig: () => ({}),
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
try {
|
|
1230
|
-
await client.rollbackSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1231
|
-
expect.fail('Should have thrown error');
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
expect(error.message).to.include('Tokowaka API key configured');
|
|
1234
|
-
expect(error.status).to.equal(400);
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
it('should throw error if site getConfig returns null', async () => {
|
|
1239
|
-
mockSite.getConfig = () => null;
|
|
1240
|
-
|
|
1241
|
-
try {
|
|
1242
|
-
await client.rollbackSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1243
|
-
expect.fail('Should have thrown error');
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
expect(error.message).to.include('Tokowaka API key configured');
|
|
1246
|
-
expect(error.status).to.equal(400);
|
|
1247
|
-
}
|
|
1221
|
+
expect(uploadedConfig.patches).to.have.length(1);
|
|
1222
|
+
expect(uploadedConfig.patches[0].suggestionId).to.equal('sugg-3');
|
|
1248
1223
|
});
|
|
1249
1224
|
|
|
1250
1225
|
it('should handle no existing config gracefully', async () => {
|
|
@@ -1256,19 +1231,20 @@ describe('TokowakaClient', () => {
|
|
|
1256
1231
|
mockSuggestions,
|
|
1257
1232
|
);
|
|
1258
1233
|
|
|
1259
|
-
|
|
1260
|
-
expect(result.
|
|
1261
|
-
expect(result.failedSuggestions
|
|
1234
|
+
// Code continues and marks eligible suggestions as succeeded even if no config found
|
|
1235
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1236
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1237
|
+
expect(result.s3Paths).to.have.length(0);
|
|
1262
1238
|
expect(s3Client.send).to.not.have.been.called;
|
|
1263
1239
|
});
|
|
1264
1240
|
|
|
1265
|
-
it('should handle empty existing config
|
|
1241
|
+
it('should handle empty existing config patches', async () => {
|
|
1266
1242
|
const existingConfig = {
|
|
1267
|
-
|
|
1268
|
-
baseURL: 'https://example.com',
|
|
1243
|
+
url: 'https://example.com/page1',
|
|
1269
1244
|
version: '1.0',
|
|
1270
|
-
|
|
1271
|
-
|
|
1245
|
+
forceFail: false,
|
|
1246
|
+
prerender: true,
|
|
1247
|
+
patches: [],
|
|
1272
1248
|
};
|
|
1273
1249
|
|
|
1274
1250
|
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
@@ -1279,34 +1255,30 @@ describe('TokowakaClient', () => {
|
|
|
1279
1255
|
mockSuggestions,
|
|
1280
1256
|
);
|
|
1281
1257
|
|
|
1282
|
-
|
|
1283
|
-
expect(result.
|
|
1284
|
-
expect(result.failedSuggestions
|
|
1258
|
+
// Code marks eligible suggestions as succeeded even if no patches to remove
|
|
1259
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1260
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1261
|
+
expect(result.s3Paths).to.have.length(0);
|
|
1285
1262
|
expect(s3Client.send).to.not.have.been.called;
|
|
1286
1263
|
});
|
|
1287
1264
|
|
|
1288
1265
|
it('should handle suggestions not found in config', async () => {
|
|
1289
1266
|
const existingConfig = {
|
|
1290
|
-
|
|
1291
|
-
baseURL: 'https://example.com',
|
|
1267
|
+
url: 'https://example.com/page1',
|
|
1292
1268
|
version: '1.0',
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
prerenderRequired: true,
|
|
1305
|
-
lastUpdated: 1234567890,
|
|
1306
|
-
},
|
|
1307
|
-
],
|
|
1269
|
+
forceFail: false,
|
|
1270
|
+
prerender: true,
|
|
1271
|
+
patches: [
|
|
1272
|
+
{
|
|
1273
|
+
op: 'replace',
|
|
1274
|
+
selector: 'h1',
|
|
1275
|
+
value: 'Heading',
|
|
1276
|
+
opportunityId: 'opp-123',
|
|
1277
|
+
suggestionId: 'sugg-999', // Different suggestion ID
|
|
1278
|
+
prerenderRequired: true,
|
|
1279
|
+
lastUpdated: 1234567890,
|
|
1308
1280
|
},
|
|
1309
|
-
|
|
1281
|
+
],
|
|
1310
1282
|
};
|
|
1311
1283
|
|
|
1312
1284
|
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
@@ -1317,15 +1289,15 @@ describe('TokowakaClient', () => {
|
|
|
1317
1289
|
mockSuggestions,
|
|
1318
1290
|
);
|
|
1319
1291
|
|
|
1320
|
-
|
|
1321
|
-
expect(result.
|
|
1322
|
-
expect(result.failedSuggestions
|
|
1292
|
+
// Code marks eligible suggestions as succeeded even if patches not found
|
|
1293
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1294
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1295
|
+
expect(result.s3Paths).to.have.length(0);
|
|
1323
1296
|
expect(s3Client.send).to.not.have.been.called;
|
|
1324
1297
|
});
|
|
1325
1298
|
|
|
1326
|
-
it('should return early when all suggestions are ineligible', async () => {
|
|
1327
|
-
|
|
1328
|
-
const allIneligibleSuggestions = [
|
|
1299
|
+
it('should return early when all suggestions are ineligible for rollback', async () => {
|
|
1300
|
+
const ineligibleSuggestions = [
|
|
1329
1301
|
{
|
|
1330
1302
|
getId: () => 'sugg-1',
|
|
1331
1303
|
getData: () => ({
|
|
@@ -1338,8 +1310,8 @@ describe('TokowakaClient', () => {
|
|
|
1338
1310
|
getId: () => 'sugg-2',
|
|
1339
1311
|
getData: () => ({
|
|
1340
1312
|
url: 'https://example.com/page1',
|
|
1341
|
-
recommendedAction: 'New
|
|
1342
|
-
checkType: 'heading-
|
|
1313
|
+
recommendedAction: 'New Subtitle',
|
|
1314
|
+
checkType: 'heading-wrong', // Not eligible
|
|
1343
1315
|
}),
|
|
1344
1316
|
},
|
|
1345
1317
|
];
|
|
@@ -1347,25 +1319,60 @@ describe('TokowakaClient', () => {
|
|
|
1347
1319
|
const result = await client.rollbackSuggestions(
|
|
1348
1320
|
mockSite,
|
|
1349
1321
|
mockOpportunity,
|
|
1350
|
-
|
|
1322
|
+
ineligibleSuggestions,
|
|
1351
1323
|
);
|
|
1352
1324
|
|
|
1353
1325
|
expect(result.succeededSuggestions).to.have.length(0);
|
|
1354
1326
|
expect(result.failedSuggestions).to.have.length(2);
|
|
1355
|
-
expect(result.failedSuggestions[0].reason).to.include('can be deployed');
|
|
1356
|
-
expect(result.failedSuggestions[1].reason).to.include('can be deployed');
|
|
1357
1327
|
expect(s3Client.send).to.not.have.been.called;
|
|
1358
1328
|
});
|
|
1359
1329
|
|
|
1360
|
-
it('should
|
|
1361
|
-
//
|
|
1362
|
-
const
|
|
1330
|
+
it('should delete config file when all patches are rolled back', async () => {
|
|
1331
|
+
// Code uploads empty config instead of deleting
|
|
1332
|
+
const existingConfig = {
|
|
1333
|
+
url: 'https://example.com/page1',
|
|
1334
|
+
version: '1.0',
|
|
1335
|
+
forceFail: false,
|
|
1336
|
+
prerender: true,
|
|
1337
|
+
patches: [
|
|
1338
|
+
{
|
|
1339
|
+
op: 'replace',
|
|
1340
|
+
selector: 'h1',
|
|
1341
|
+
value: 'Heading 1',
|
|
1342
|
+
opportunityId: 'opp-123',
|
|
1343
|
+
suggestionId: 'sugg-1',
|
|
1344
|
+
prerenderRequired: true,
|
|
1345
|
+
lastUpdated: 1234567890,
|
|
1346
|
+
},
|
|
1347
|
+
],
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1351
|
+
|
|
1352
|
+
const result = await client.rollbackSuggestions(
|
|
1353
|
+
mockSite,
|
|
1354
|
+
mockOpportunity,
|
|
1355
|
+
[mockSuggestions[0]], // Only roll back sugg-1 (all patches for this URL)
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1359
|
+
expect(result.removedPatchesCount).to.equal(1);
|
|
1360
|
+
|
|
1361
|
+
// Code uploads empty patches array instead of deleting
|
|
1362
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
1363
|
+
const command = s3Client.send.firstCall.args[0];
|
|
1364
|
+
expect(command.constructor.name).to.equal('PutObjectCommand');
|
|
1365
|
+
expect(command.input.Key).to.equal('opportunities/example.com/L3BhZ2Ux');
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('should handle rollback for multiple URLs', async () => {
|
|
1369
|
+
mockSuggestions = [
|
|
1363
1370
|
{
|
|
1364
1371
|
getId: () => 'sugg-1',
|
|
1365
1372
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1366
1373
|
getData: () => ({
|
|
1367
1374
|
url: 'https://example.com/page1',
|
|
1368
|
-
recommendedAction: '
|
|
1375
|
+
recommendedAction: 'Page 1 Heading',
|
|
1369
1376
|
checkType: 'heading-empty',
|
|
1370
1377
|
transformRules: {
|
|
1371
1378
|
action: 'replace',
|
|
@@ -1375,103 +1382,70 @@ describe('TokowakaClient', () => {
|
|
|
1375
1382
|
},
|
|
1376
1383
|
{
|
|
1377
1384
|
getId: () => 'sugg-2',
|
|
1385
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1378
1386
|
getData: () => ({
|
|
1379
|
-
url: 'https://example.com/
|
|
1380
|
-
recommendedAction: '
|
|
1381
|
-
checkType: 'heading-
|
|
1387
|
+
url: 'https://example.com/page2',
|
|
1388
|
+
recommendedAction: 'Page 2 Heading',
|
|
1389
|
+
checkType: 'heading-empty',
|
|
1390
|
+
transformRules: {
|
|
1391
|
+
action: 'replace',
|
|
1392
|
+
selector: 'h1',
|
|
1393
|
+
},
|
|
1382
1394
|
}),
|
|
1383
1395
|
},
|
|
1384
1396
|
];
|
|
1385
1397
|
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
baseURL: 'https://example.com',
|
|
1398
|
+
const config1 = {
|
|
1399
|
+
url: 'https://example.com/page1',
|
|
1389
1400
|
version: '1.0',
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
prerenderRequired: true,
|
|
1402
|
-
lastUpdated: 1234567890,
|
|
1403
|
-
},
|
|
1404
|
-
],
|
|
1401
|
+
forceFail: false,
|
|
1402
|
+
prerender: true,
|
|
1403
|
+
patches: [
|
|
1404
|
+
{
|
|
1405
|
+
op: 'replace',
|
|
1406
|
+
selector: 'h1',
|
|
1407
|
+
value: 'Heading 1',
|
|
1408
|
+
opportunityId: 'opp-123',
|
|
1409
|
+
suggestionId: 'sugg-1',
|
|
1410
|
+
prerenderRequired: true,
|
|
1411
|
+
lastUpdated: 1234567890,
|
|
1405
1412
|
},
|
|
1406
|
-
|
|
1413
|
+
],
|
|
1407
1414
|
};
|
|
1408
1415
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const result = await client.rollbackSuggestions(
|
|
1412
|
-
mockSite,
|
|
1413
|
-
mockOpportunity,
|
|
1414
|
-
mixedSuggestions,
|
|
1415
|
-
);
|
|
1416
|
-
|
|
1417
|
-
expect(result.succeededSuggestions).to.have.length(1);
|
|
1418
|
-
expect(result.failedSuggestions).to.have.length(1);
|
|
1419
|
-
expect(result.failedSuggestions[0].reason).to.include('can be deployed');
|
|
1420
|
-
});
|
|
1421
|
-
|
|
1422
|
-
it('should remove URL path when all patches are rolled back', async () => {
|
|
1423
|
-
const existingConfig = {
|
|
1424
|
-
siteId: 'site-123',
|
|
1425
|
-
baseURL: 'https://example.com',
|
|
1416
|
+
const config2 = {
|
|
1417
|
+
url: 'https://example.com/page2',
|
|
1426
1418
|
version: '1.0',
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
prerenderRequired: true,
|
|
1439
|
-
lastUpdated: 1234567890,
|
|
1440
|
-
},
|
|
1441
|
-
],
|
|
1442
|
-
},
|
|
1443
|
-
'/page2': {
|
|
1444
|
-
prerender: true,
|
|
1445
|
-
patches: [
|
|
1446
|
-
{
|
|
1447
|
-
op: 'replace',
|
|
1448
|
-
selector: 'h1',
|
|
1449
|
-
value: 'Heading 2',
|
|
1450
|
-
opportunityId: 'opp-123',
|
|
1451
|
-
suggestionId: 'sugg-999',
|
|
1452
|
-
prerenderRequired: true,
|
|
1453
|
-
lastUpdated: 1234567890,
|
|
1454
|
-
},
|
|
1455
|
-
],
|
|
1419
|
+
forceFail: false,
|
|
1420
|
+
prerender: true,
|
|
1421
|
+
patches: [
|
|
1422
|
+
{
|
|
1423
|
+
op: 'replace',
|
|
1424
|
+
selector: 'h1',
|
|
1425
|
+
value: 'Heading 2',
|
|
1426
|
+
opportunityId: 'opp-123',
|
|
1427
|
+
suggestionId: 'sugg-2',
|
|
1428
|
+
prerenderRequired: true,
|
|
1429
|
+
lastUpdated: 1234567890,
|
|
1456
1430
|
},
|
|
1457
|
-
|
|
1431
|
+
],
|
|
1458
1432
|
};
|
|
1459
1433
|
|
|
1460
|
-
sinon.stub(client, 'fetchConfig')
|
|
1434
|
+
sinon.stub(client, 'fetchConfig')
|
|
1435
|
+
.onFirstCall()
|
|
1436
|
+
.resolves(config1)
|
|
1437
|
+
.onSecondCall()
|
|
1438
|
+
.resolves(config2);
|
|
1461
1439
|
|
|
1462
1440
|
const result = await client.rollbackSuggestions(
|
|
1463
1441
|
mockSite,
|
|
1464
1442
|
mockOpportunity,
|
|
1465
|
-
|
|
1443
|
+
mockSuggestions,
|
|
1466
1444
|
);
|
|
1467
1445
|
|
|
1468
|
-
expect(result.
|
|
1469
|
-
expect(result.
|
|
1470
|
-
|
|
1471
|
-
// Verify uploaded config doesn't have /page1 anymore
|
|
1472
|
-
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1473
|
-
expect(uploadedConfig.tokowakaOptimizations).to.not.have.property('/page1');
|
|
1474
|
-
expect(uploadedConfig.tokowakaOptimizations).to.have.property('/page2');
|
|
1446
|
+
expect(result.s3Paths).to.have.length(2);
|
|
1447
|
+
expect(result.cdnInvalidations).to.have.length(2);
|
|
1448
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1475
1449
|
});
|
|
1476
1450
|
|
|
1477
1451
|
it('should throw error for unsupported opportunity type', async () => {
|
|
@@ -1509,114 +1483,30 @@ describe('TokowakaClient', () => {
|
|
|
1509
1483
|
};
|
|
1510
1484
|
|
|
1511
1485
|
const existingConfig = {
|
|
1512
|
-
|
|
1513
|
-
baseURL: 'https://example.com',
|
|
1486
|
+
url: 'https://example.com/page1',
|
|
1514
1487
|
version: '1.0',
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
// FAQ heading patch (no suggestionId)
|
|
1523
|
-
op: 'appendChild',
|
|
1524
|
-
selector: 'body',
|
|
1525
|
-
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1526
|
-
prerenderRequired: true,
|
|
1527
|
-
lastUpdated: 1234567890,
|
|
1528
|
-
},
|
|
1529
|
-
{
|
|
1530
|
-
opportunityId: 'opp-123',
|
|
1531
|
-
suggestionId: 'faq-sugg-1',
|
|
1532
|
-
op: 'appendChild',
|
|
1533
|
-
selector: 'body',
|
|
1534
|
-
value: { type: 'element', tagName: 'div' },
|
|
1535
|
-
prerenderRequired: true,
|
|
1536
|
-
lastUpdated: 1234567890,
|
|
1537
|
-
},
|
|
1538
|
-
],
|
|
1539
|
-
},
|
|
1540
|
-
},
|
|
1541
|
-
};
|
|
1542
|
-
|
|
1543
|
-
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1544
|
-
|
|
1545
|
-
const result = await client.rollbackSuggestions(
|
|
1546
|
-
mockSite,
|
|
1547
|
-
mockOpportunity,
|
|
1548
|
-
[faqSuggestion],
|
|
1549
|
-
);
|
|
1550
|
-
|
|
1551
|
-
expect(result.succeededSuggestions).to.have.length(1);
|
|
1552
|
-
expect(result.removedPatchesCount).to.equal(2); // FAQ item + heading
|
|
1553
|
-
|
|
1554
|
-
// Verify uploaded config has no patches for /page1 (both FAQ and heading removed)
|
|
1555
|
-
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1556
|
-
expect(uploadedConfig.tokowakaOptimizations).to.not.have.property('/page1');
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
it('should keep FAQ heading patch when rolling back some but not all FAQ suggestions', async () => {
|
|
1560
|
-
// Change opportunity to FAQ type
|
|
1561
|
-
mockOpportunity.getType = () => 'faq';
|
|
1562
|
-
|
|
1563
|
-
// Create FAQ suggestions
|
|
1564
|
-
const faqSuggestion1 = {
|
|
1565
|
-
getId: () => 'faq-sugg-1',
|
|
1566
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1567
|
-
getData: () => ({
|
|
1568
|
-
url: 'https://example.com/page1',
|
|
1569
|
-
shouldOptimize: true,
|
|
1570
|
-
item: {
|
|
1571
|
-
question: 'What is this?',
|
|
1572
|
-
answer: 'This is FAQ 1',
|
|
1573
|
-
},
|
|
1574
|
-
transformRules: {
|
|
1575
|
-
action: 'appendChild',
|
|
1488
|
+
forceFail: false,
|
|
1489
|
+
prerender: true,
|
|
1490
|
+
patches: [
|
|
1491
|
+
{
|
|
1492
|
+
opportunityId: 'opp-123',
|
|
1493
|
+
// FAQ heading patch (no suggestionId)
|
|
1494
|
+
op: 'appendChild',
|
|
1576
1495
|
selector: 'body',
|
|
1496
|
+
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1497
|
+
prerenderRequired: true,
|
|
1498
|
+
lastUpdated: 1234567890,
|
|
1577
1499
|
},
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
tokowakaOptimizations: {
|
|
1587
|
-
'/page1': {
|
|
1588
|
-
prerender: true,
|
|
1589
|
-
patches: [
|
|
1590
|
-
{
|
|
1591
|
-
opportunityId: 'opp-123',
|
|
1592
|
-
// FAQ heading patch (no suggestionId)
|
|
1593
|
-
op: 'appendChild',
|
|
1594
|
-
selector: 'body',
|
|
1595
|
-
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1596
|
-
prerenderRequired: true,
|
|
1597
|
-
lastUpdated: 1234567890,
|
|
1598
|
-
},
|
|
1599
|
-
{
|
|
1600
|
-
opportunityId: 'opp-123',
|
|
1601
|
-
suggestionId: 'faq-sugg-1',
|
|
1602
|
-
op: 'appendChild',
|
|
1603
|
-
selector: 'body',
|
|
1604
|
-
value: { type: 'element', tagName: 'div' },
|
|
1605
|
-
prerenderRequired: true,
|
|
1606
|
-
lastUpdated: 1234567890,
|
|
1607
|
-
},
|
|
1608
|
-
{
|
|
1609
|
-
opportunityId: 'opp-123',
|
|
1610
|
-
suggestionId: 'faq-sugg-2',
|
|
1611
|
-
op: 'appendChild',
|
|
1612
|
-
selector: 'body',
|
|
1613
|
-
value: { type: 'element', tagName: 'div' },
|
|
1614
|
-
prerenderRequired: true,
|
|
1615
|
-
lastUpdated: 1234567890,
|
|
1616
|
-
},
|
|
1617
|
-
],
|
|
1500
|
+
{
|
|
1501
|
+
opportunityId: 'opp-123',
|
|
1502
|
+
suggestionId: 'faq-sugg-1',
|
|
1503
|
+
op: 'appendChild',
|
|
1504
|
+
selector: 'body',
|
|
1505
|
+
value: { type: 'element', tagName: 'div' },
|
|
1506
|
+
prerenderRequired: true,
|
|
1507
|
+
lastUpdated: 1234567890,
|
|
1618
1508
|
},
|
|
1619
|
-
|
|
1509
|
+
],
|
|
1620
1510
|
};
|
|
1621
1511
|
|
|
1622
1512
|
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
@@ -1624,17 +1514,15 @@ describe('TokowakaClient', () => {
|
|
|
1624
1514
|
const result = await client.rollbackSuggestions(
|
|
1625
1515
|
mockSite,
|
|
1626
1516
|
mockOpportunity,
|
|
1627
|
-
[
|
|
1517
|
+
[faqSuggestion],
|
|
1628
1518
|
);
|
|
1629
1519
|
|
|
1630
1520
|
expect(result.succeededSuggestions).to.have.length(1);
|
|
1631
|
-
expect(result.removedPatchesCount).to.equal(
|
|
1521
|
+
expect(result.removedPatchesCount).to.equal(2); // FAQ item + heading
|
|
1632
1522
|
|
|
1633
|
-
//
|
|
1634
|
-
const
|
|
1635
|
-
expect(
|
|
1636
|
-
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches[0]).to.not.have.property('suggestionId'); // Heading
|
|
1637
|
-
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches[1].suggestionId).to.equal('faq-sugg-2');
|
|
1523
|
+
// Code uploads empty config instead of deleting
|
|
1524
|
+
const command = s3Client.send.firstCall.args[0];
|
|
1525
|
+
expect(command.constructor.name).to.equal('PutObjectCommand');
|
|
1638
1526
|
});
|
|
1639
1527
|
});
|
|
1640
1528
|
|
|
@@ -1667,7 +1555,6 @@ describe('TokowakaClient', () => {
|
|
|
1667
1555
|
|
|
1668
1556
|
// Add TOKOWAKA_EDGE_URL to env
|
|
1669
1557
|
client.env.TOKOWAKA_EDGE_URL = 'https://edge-dev.tokowaka.now';
|
|
1670
|
-
client.previewBucketName = 'test-preview-bucket';
|
|
1671
1558
|
});
|
|
1672
1559
|
|
|
1673
1560
|
afterEach(() => {
|
|
@@ -1684,7 +1571,7 @@ describe('TokowakaClient', () => {
|
|
|
1684
1571
|
{ warmupDelayMs: 0 },
|
|
1685
1572
|
);
|
|
1686
1573
|
|
|
1687
|
-
expect(result).to.have.property('s3Path', 'preview/opportunities/
|
|
1574
|
+
expect(result).to.have.property('s3Path', 'preview/opportunities/example.com/L3BhZ2Ux');
|
|
1688
1575
|
expect(result).to.have.property('succeededSuggestions');
|
|
1689
1576
|
expect(result.succeededSuggestions).to.have.length(2);
|
|
1690
1577
|
expect(result).to.have.property('failedSuggestions');
|
|
@@ -1714,28 +1601,30 @@ describe('TokowakaClient', () => {
|
|
|
1714
1601
|
}
|
|
1715
1602
|
});
|
|
1716
1603
|
|
|
1717
|
-
it('should throw error if site does not have
|
|
1604
|
+
it('should throw error if site does not have forwardedHost', async () => {
|
|
1718
1605
|
mockSite.getConfig = () => ({
|
|
1719
|
-
getTokowakaConfig: () => ({
|
|
1606
|
+
getTokowakaConfig: () => ({}),
|
|
1720
1607
|
});
|
|
1721
1608
|
|
|
1722
1609
|
try {
|
|
1723
1610
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1724
1611
|
expect.fail('Should have thrown error');
|
|
1725
1612
|
} catch (error) {
|
|
1726
|
-
expect(error.message).to.include('Tokowaka API key or forwarded host configured');
|
|
1613
|
+
expect(error.message).to.include('Site does not have a Tokowaka API key or forwarded host configured');
|
|
1727
1614
|
expect(error.status).to.equal(400);
|
|
1728
1615
|
}
|
|
1729
1616
|
});
|
|
1730
1617
|
|
|
1731
|
-
it('should throw error if
|
|
1732
|
-
mockSite.getConfig = () =>
|
|
1618
|
+
it('should throw error if getTokowakaConfig returns null', async () => {
|
|
1619
|
+
mockSite.getConfig = () => ({
|
|
1620
|
+
getTokowakaConfig: () => null,
|
|
1621
|
+
});
|
|
1733
1622
|
|
|
1734
1623
|
try {
|
|
1735
1624
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1736
1625
|
expect.fail('Should have thrown error');
|
|
1737
1626
|
} catch (error) {
|
|
1738
|
-
expect(error.message).to.include('Tokowaka API key or forwarded host configured');
|
|
1627
|
+
expect(error.message).to.include('Site does not have a Tokowaka API key or forwarded host configured');
|
|
1739
1628
|
expect(error.status).to.equal(400);
|
|
1740
1629
|
}
|
|
1741
1630
|
});
|
|
@@ -1775,16 +1664,19 @@ describe('TokowakaClient', () => {
|
|
|
1775
1664
|
expect(result.config).to.be.null;
|
|
1776
1665
|
});
|
|
1777
1666
|
|
|
1778
|
-
it('should return early when generateConfig
|
|
1779
|
-
//
|
|
1667
|
+
it('should return early when generateConfig returns no patches', async () => {
|
|
1668
|
+
// Stub mapper to return eligible but no patches
|
|
1669
|
+
const mapper = client.mapperRegistry.getMapper('headings');
|
|
1670
|
+
sinon.stub(mapper, 'suggestionsToPatches').returns([]);
|
|
1671
|
+
|
|
1780
1672
|
mockSuggestions = [
|
|
1781
1673
|
{
|
|
1782
|
-
getId: () => 'sugg-
|
|
1674
|
+
getId: () => 'sugg-1',
|
|
1783
1675
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1784
1676
|
getData: () => ({
|
|
1785
|
-
|
|
1677
|
+
url: 'https://example.com/page1',
|
|
1786
1678
|
recommendedAction: 'New Heading',
|
|
1787
|
-
checkType: 'heading-empty',
|
|
1679
|
+
checkType: 'heading-empty', // Eligible
|
|
1788
1680
|
transformRules: {
|
|
1789
1681
|
action: 'replace',
|
|
1790
1682
|
selector: 'h1',
|
|
@@ -1797,38 +1689,74 @@ describe('TokowakaClient', () => {
|
|
|
1797
1689
|
mockSite,
|
|
1798
1690
|
mockOpportunity,
|
|
1799
1691
|
mockSuggestions,
|
|
1800
|
-
{ warmupDelayMs: 0 },
|
|
1801
1692
|
);
|
|
1802
1693
|
|
|
1803
1694
|
expect(result.succeededSuggestions).to.have.length(0);
|
|
1804
1695
|
expect(result.failedSuggestions).to.have.length(1);
|
|
1805
1696
|
expect(result.config).to.be.null;
|
|
1806
|
-
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
it('should throw error when preview URL not found in suggestion data', async () => {
|
|
1700
|
+
mockSuggestions = [
|
|
1701
|
+
{
|
|
1702
|
+
getId: () => 'sugg-1',
|
|
1703
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1704
|
+
getData: () => ({
|
|
1705
|
+
// URL missing
|
|
1706
|
+
recommendedAction: 'New Heading',
|
|
1707
|
+
checkType: 'heading-empty',
|
|
1708
|
+
transformRules: {
|
|
1709
|
+
action: 'replace',
|
|
1710
|
+
selector: 'h1',
|
|
1711
|
+
},
|
|
1712
|
+
}),
|
|
1713
|
+
},
|
|
1714
|
+
];
|
|
1715
|
+
|
|
1716
|
+
try {
|
|
1717
|
+
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1718
|
+
expect.fail('Should have thrown error');
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
expect(error.message).to.include('Preview URL not found in suggestion data');
|
|
1721
|
+
expect(error.status).to.equal(400);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
it('should throw error when HTML fetch fails', async () => {
|
|
1726
|
+
fetchStub.rejects(new Error('Network timeout'));
|
|
1727
|
+
|
|
1728
|
+
try {
|
|
1729
|
+
await client.previewSuggestions(
|
|
1730
|
+
mockSite,
|
|
1731
|
+
mockOpportunity,
|
|
1732
|
+
mockSuggestions,
|
|
1733
|
+
{ warmupDelayMs: 0 },
|
|
1734
|
+
);
|
|
1735
|
+
expect.fail('Should have thrown error');
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
expect(error.message).to.include('Preview failed: Unable to fetch HTML');
|
|
1738
|
+
expect(error.status).to.equal(500);
|
|
1739
|
+
}
|
|
1807
1740
|
});
|
|
1808
1741
|
|
|
1809
1742
|
it('should merge with existing deployed patches for the same URL', async () => {
|
|
1810
1743
|
// Setup existing config with deployed patches
|
|
1811
1744
|
const existingConfig = {
|
|
1812
|
-
|
|
1813
|
-
baseURL: 'https://example.com',
|
|
1745
|
+
url: 'https://example.com/page1',
|
|
1814
1746
|
version: '1.0',
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
prerenderRequired: true,
|
|
1827
|
-
lastUpdated: 1234567890,
|
|
1828
|
-
},
|
|
1829
|
-
],
|
|
1747
|
+
forceFail: false,
|
|
1748
|
+
prerender: true,
|
|
1749
|
+
patches: [
|
|
1750
|
+
{
|
|
1751
|
+
op: 'replace',
|
|
1752
|
+
selector: 'title',
|
|
1753
|
+
value: 'Deployed Title',
|
|
1754
|
+
opportunityId: 'opp-456',
|
|
1755
|
+
suggestionId: 'sugg-deployed',
|
|
1756
|
+
prerenderRequired: true,
|
|
1757
|
+
lastUpdated: 1234567890,
|
|
1830
1758
|
},
|
|
1831
|
-
|
|
1759
|
+
],
|
|
1832
1760
|
};
|
|
1833
1761
|
|
|
1834
1762
|
client.fetchConfig.resolves(existingConfig);
|
|
@@ -1844,127 +1772,31 @@ describe('TokowakaClient', () => {
|
|
|
1844
1772
|
|
|
1845
1773
|
// Verify config was uploaded with merged patches
|
|
1846
1774
|
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1847
|
-
expect(uploadedConfig.
|
|
1775
|
+
expect(uploadedConfig.patches).to.have.length(3);
|
|
1848
1776
|
|
|
1849
1777
|
// Should have existing deployed patch + 2 new preview patches
|
|
1850
|
-
const deployedPatch = uploadedConfig.
|
|
1778
|
+
const deployedPatch = uploadedConfig.patches
|
|
1851
1779
|
.find((p) => p.suggestionId === 'sugg-deployed');
|
|
1852
1780
|
expect(deployedPatch).to.exist;
|
|
1853
1781
|
expect(deployedPatch.value).to.equal('Deployed Title');
|
|
1854
1782
|
});
|
|
1855
1783
|
|
|
1856
|
-
it('should
|
|
1857
|
-
|
|
1858
|
-
const existingConfig = {
|
|
1859
|
-
siteId: 'site-123',
|
|
1860
|
-
baseURL: 'https://example.com',
|
|
1861
|
-
version: '1.0',
|
|
1862
|
-
tokowakaForceFail: false,
|
|
1863
|
-
tokowakaOptimizations: {
|
|
1864
|
-
'/other-page': {
|
|
1865
|
-
prerender: true,
|
|
1866
|
-
patches: [
|
|
1867
|
-
{
|
|
1868
|
-
op: 'replace',
|
|
1869
|
-
selector: 'title',
|
|
1870
|
-
value: 'Other Page Title',
|
|
1871
|
-
opportunityId: 'opp-999',
|
|
1872
|
-
suggestionId: 'sugg-other',
|
|
1873
|
-
prerenderRequired: true,
|
|
1874
|
-
lastUpdated: 1234567890,
|
|
1875
|
-
},
|
|
1876
|
-
],
|
|
1877
|
-
},
|
|
1878
|
-
},
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
client.fetchConfig.resolves(existingConfig);
|
|
1882
|
-
|
|
1883
|
-
const result = await client.previewSuggestions(
|
|
1784
|
+
it('should upload config to preview S3 path', async () => {
|
|
1785
|
+
await client.previewSuggestions(
|
|
1884
1786
|
mockSite,
|
|
1885
1787
|
mockOpportunity,
|
|
1886
1788
|
mockSuggestions,
|
|
1887
1789
|
{ warmupDelayMs: 0 },
|
|
1888
1790
|
);
|
|
1889
1791
|
|
|
1890
|
-
expect(
|
|
1891
|
-
expect(log.info).to.have.been.calledWith(sinon.match(/No deployed patches found/));
|
|
1892
|
-
});
|
|
1893
|
-
|
|
1894
|
-
it('should throw error when HTML fetch fails', async () => {
|
|
1895
|
-
// Mock fetch to fail
|
|
1896
|
-
fetchStub.rejects(new Error('Network error'));
|
|
1897
|
-
|
|
1898
|
-
try {
|
|
1899
|
-
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1900
|
-
expect.fail('Should have thrown error');
|
|
1901
|
-
} catch (error) {
|
|
1902
|
-
expect(error.message).to.include('Preview failed');
|
|
1903
|
-
expect(error.status).to.equal(500);
|
|
1904
|
-
}
|
|
1905
|
-
});
|
|
1906
|
-
|
|
1907
|
-
it('should retry HTML fetch on failure', async () => {
|
|
1908
|
-
// First 2 calls fail (warmup succeeds, first actual call fails)
|
|
1909
|
-
// Then succeed on retry
|
|
1910
|
-
fetchStub.onCall(0).resolves({ // warmup original
|
|
1911
|
-
ok: true,
|
|
1912
|
-
status: 200,
|
|
1913
|
-
statusText: 'OK',
|
|
1914
|
-
headers: {
|
|
1915
|
-
get: () => null,
|
|
1916
|
-
},
|
|
1917
|
-
text: async () => 'warmup',
|
|
1918
|
-
});
|
|
1919
|
-
fetchStub.onCall(1).rejects(new Error('Temporary failure')); // actual original - fail
|
|
1920
|
-
fetchStub.onCall(2).resolves({ // retry original - success
|
|
1921
|
-
ok: true,
|
|
1922
|
-
status: 200,
|
|
1923
|
-
statusText: 'OK',
|
|
1924
|
-
headers: {
|
|
1925
|
-
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
1926
|
-
},
|
|
1927
|
-
text: async () => '<html>Original</html>',
|
|
1928
|
-
});
|
|
1929
|
-
fetchStub.onCall(3).resolves({ // warmup optimized
|
|
1930
|
-
ok: true,
|
|
1931
|
-
status: 200,
|
|
1932
|
-
statusText: 'OK',
|
|
1933
|
-
headers: {
|
|
1934
|
-
get: () => null,
|
|
1935
|
-
},
|
|
1936
|
-
text: async () => 'warmup',
|
|
1937
|
-
});
|
|
1938
|
-
fetchStub.onCall(4).resolves({ // actual optimized
|
|
1939
|
-
ok: true,
|
|
1940
|
-
status: 200,
|
|
1941
|
-
statusText: 'OK',
|
|
1942
|
-
headers: {
|
|
1943
|
-
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
1944
|
-
},
|
|
1945
|
-
text: async () => '<html>Optimized</html>',
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
|
-
const result = await client.previewSuggestions(
|
|
1949
|
-
mockSite,
|
|
1950
|
-
mockOpportunity,
|
|
1951
|
-
mockSuggestions,
|
|
1952
|
-
{ warmupDelayMs: 0, retryDelayMs: 0 },
|
|
1953
|
-
);
|
|
1792
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
1954
1793
|
|
|
1955
|
-
|
|
1956
|
-
expect(
|
|
1957
|
-
expect(
|
|
1794
|
+
const putCommand = s3Client.send.firstCall.args[0];
|
|
1795
|
+
expect(putCommand.input.Bucket).to.equal('test-preview-bucket');
|
|
1796
|
+
expect(putCommand.input.Key).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
|
|
1958
1797
|
});
|
|
1959
1798
|
|
|
1960
|
-
it('should
|
|
1961
|
-
mockSite.getConfig = () => ({
|
|
1962
|
-
getTokowakaConfig: () => ({
|
|
1963
|
-
apiKey: 'test-api-key-123',
|
|
1964
|
-
forwardedHost: 'custom.example.com',
|
|
1965
|
-
}),
|
|
1966
|
-
});
|
|
1967
|
-
|
|
1799
|
+
it('should invalidate CDN cache for preview path', async () => {
|
|
1968
1800
|
await client.previewSuggestions(
|
|
1969
1801
|
mockSite,
|
|
1970
1802
|
mockOpportunity,
|
|
@@ -1972,60 +1804,58 @@ describe('TokowakaClient', () => {
|
|
|
1972
1804
|
{ warmupDelayMs: 0 },
|
|
1973
1805
|
);
|
|
1974
1806
|
|
|
1975
|
-
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
expect(actualCall).to.exist;
|
|
1982
|
-
});
|
|
1983
|
-
|
|
1984
|
-
it('should throw error when forwardedHost is not configured', async () => {
|
|
1985
|
-
mockSite.getConfig = () => ({
|
|
1986
|
-
getTokowakaConfig: () => ({
|
|
1987
|
-
apiKey: 'test-api-key-123',
|
|
1988
|
-
// forwardedHost is missing
|
|
1989
|
-
}),
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
try {
|
|
1993
|
-
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1994
|
-
expect.fail('Should have thrown error');
|
|
1995
|
-
} catch (error) {
|
|
1996
|
-
expect(error.message).to.include('Tokowaka API key or forwarded host configured');
|
|
1997
|
-
expect(error.status).to.equal(400);
|
|
1998
|
-
}
|
|
1807
|
+
expect(client.invalidateCdnCache).to.have.been.calledOnce;
|
|
1808
|
+
const { firstCall } = client.invalidateCdnCache;
|
|
1809
|
+
expect(firstCall.args[0]).to.equal('https://example.com/page1');
|
|
1810
|
+
expect(firstCall.args[1]).to.equal('cloudfront');
|
|
1811
|
+
expect(firstCall.args[2]).to.be.true; // isPreview
|
|
1999
1812
|
});
|
|
2000
1813
|
|
|
2001
|
-
it('should
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1814
|
+
it('should throw error if suggestions span multiple URLs', async () => {
|
|
1815
|
+
mockSuggestions = [
|
|
1816
|
+
{
|
|
1817
|
+
getId: () => 'sugg-1',
|
|
1818
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1819
|
+
getData: () => ({
|
|
1820
|
+
url: 'https://example.com/page1',
|
|
1821
|
+
recommendedAction: 'Page 1 Heading',
|
|
1822
|
+
checkType: 'heading-empty',
|
|
1823
|
+
transformRules: {
|
|
1824
|
+
action: 'replace',
|
|
1825
|
+
selector: 'h1',
|
|
1826
|
+
},
|
|
1827
|
+
}),
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
getId: () => 'sugg-2',
|
|
1831
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1832
|
+
getData: () => ({
|
|
1833
|
+
url: 'https://example.com/page2', // Different URL
|
|
1834
|
+
recommendedAction: 'Page 2 Heading',
|
|
1835
|
+
checkType: 'heading-empty',
|
|
1836
|
+
transformRules: {
|
|
1837
|
+
action: 'replace',
|
|
1838
|
+
selector: 'h1',
|
|
1839
|
+
},
|
|
1840
|
+
}),
|
|
1841
|
+
},
|
|
1842
|
+
];
|
|
2010
1843
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
1844
|
+
// Code doesn't validate multi-URL, silently uses first URL
|
|
1845
|
+
// fetchConfig and invalidateCdnCache already stubbed in beforeEach
|
|
1846
|
+
// Only need to stub uploadConfig
|
|
1847
|
+
sinon.stub(client, 'uploadConfig').resolves('preview/opportunities/example.com/L3BhZ2Ux');
|
|
2015
1848
|
|
|
2016
|
-
|
|
2017
|
-
await client.previewSuggestions(
|
|
1849
|
+
const result = await client.previewSuggestions(
|
|
2018
1850
|
mockSite,
|
|
2019
1851
|
mockOpportunity,
|
|
2020
1852
|
mockSuggestions,
|
|
2021
1853
|
{ warmupDelayMs: 0 },
|
|
2022
1854
|
);
|
|
2023
1855
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
true,
|
|
2028
|
-
);
|
|
1856
|
+
// Preview succeeds, using first URL only
|
|
1857
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1858
|
+
expect(result.config.url).to.equal('https://example.com/page1');
|
|
2029
1859
|
});
|
|
2030
1860
|
});
|
|
2031
1861
|
|
|
@@ -2045,7 +1875,7 @@ describe('TokowakaClient', () => {
|
|
|
2045
1875
|
});
|
|
2046
1876
|
|
|
2047
1877
|
it('should invalidate CDN cache successfully', async () => {
|
|
2048
|
-
const result = await client.invalidateCdnCache('
|
|
1878
|
+
const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
|
|
2049
1879
|
|
|
2050
1880
|
expect(result).to.deep.equal({
|
|
2051
1881
|
status: 'success',
|
|
@@ -2054,56 +1884,44 @@ describe('TokowakaClient', () => {
|
|
|
2054
1884
|
});
|
|
2055
1885
|
|
|
2056
1886
|
expect(mockCdnClient.invalidateCache).to.have.been.calledWith([
|
|
2057
|
-
'/opportunities/
|
|
1887
|
+
'/opportunities/example.com/L3BhZ2Ux',
|
|
2058
1888
|
]);
|
|
2059
1889
|
expect(log.debug).to.have.been.calledWith(sinon.match(/Invalidating CDN cache/));
|
|
2060
1890
|
expect(log.info).to.have.been.calledWith(sinon.match(/CDN cache invalidation completed/));
|
|
2061
1891
|
});
|
|
2062
1892
|
|
|
2063
|
-
it('should
|
|
2064
|
-
|
|
2065
|
-
await client.invalidateCdnCache('', 'cloudfront');
|
|
2066
|
-
expect.fail('Should have thrown error');
|
|
2067
|
-
} catch (error) {
|
|
2068
|
-
expect(error.message).to.equal('Tokowaka API key and provider are required');
|
|
2069
|
-
expect(error.status).to.equal(400);
|
|
2070
|
-
}
|
|
2071
|
-
});
|
|
1893
|
+
it('should invalidate CDN cache for preview path', async () => {
|
|
1894
|
+
await client.invalidateCdnCache('https://example.com/page1', 'cloudfront', true);
|
|
2072
1895
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
expect.fail('Should have thrown error');
|
|
2077
|
-
} catch (error) {
|
|
2078
|
-
expect(error.message).to.equal('Tokowaka API key and provider are required');
|
|
2079
|
-
expect(error.status).to.equal(400);
|
|
2080
|
-
}
|
|
1896
|
+
expect(mockCdnClient.invalidateCache).to.have.been.calledWith([
|
|
1897
|
+
'/preview/opportunities/example.com/L3BhZ2Ux',
|
|
1898
|
+
]);
|
|
2081
1899
|
});
|
|
2082
1900
|
|
|
2083
|
-
it('should
|
|
1901
|
+
it('should throw error if URL is missing', async () => {
|
|
2084
1902
|
try {
|
|
2085
|
-
await client.invalidateCdnCache('
|
|
1903
|
+
await client.invalidateCdnCache('', 'cloudfront');
|
|
2086
1904
|
expect.fail('Should have thrown error');
|
|
2087
1905
|
} catch (error) {
|
|
2088
|
-
expect(error.message).to.equal('
|
|
1906
|
+
expect(error.message).to.equal('URL and provider are required');
|
|
2089
1907
|
expect(error.status).to.equal(400);
|
|
2090
1908
|
}
|
|
2091
1909
|
});
|
|
2092
1910
|
|
|
2093
|
-
it('should
|
|
1911
|
+
it('should throw error if provider is missing', async () => {
|
|
2094
1912
|
try {
|
|
2095
|
-
await client.invalidateCdnCache(
|
|
1913
|
+
await client.invalidateCdnCache('https://example.com/page1', '');
|
|
2096
1914
|
expect.fail('Should have thrown error');
|
|
2097
1915
|
} catch (error) {
|
|
2098
|
-
expect(error.message).to.equal('
|
|
1916
|
+
expect(error.message).to.equal('URL and provider are required');
|
|
2099
1917
|
expect(error.status).to.equal(400);
|
|
2100
1918
|
}
|
|
2101
1919
|
});
|
|
2102
1920
|
|
|
2103
|
-
it('should return
|
|
1921
|
+
it('should return error object if no CDN client available', async () => {
|
|
2104
1922
|
client.cdnClientRegistry.getClient.returns(null);
|
|
2105
1923
|
|
|
2106
|
-
const result = await client.invalidateCdnCache('
|
|
1924
|
+
const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
|
|
2107
1925
|
|
|
2108
1926
|
expect(result).to.deep.equal({
|
|
2109
1927
|
status: 'error',
|
|
@@ -2116,7 +1934,7 @@ describe('TokowakaClient', () => {
|
|
|
2116
1934
|
it('should return error object if CDN invalidation fails', async () => {
|
|
2117
1935
|
mockCdnClient.invalidateCache.rejects(new Error('CDN API error'));
|
|
2118
1936
|
|
|
2119
|
-
const result = await client.invalidateCdnCache('
|
|
1937
|
+
const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
|
|
2120
1938
|
|
|
2121
1939
|
expect(result).to.deep.equal({
|
|
2122
1940
|
status: 'error',
|