@adobe/spacecat-shared-tokowaka-client 1.0.6 → 1.1.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 +16 -1
- package/package.json +1 -1
- package/src/index.js +106 -0
- package/src/mappers/base-mapper.js +22 -0
- package/src/mappers/faq-mapper.js +48 -0
- package/src/utils/patch-utils.js +51 -1
- package/test/index.test.js +488 -0
- package/test/mappers/base-mapper.test.js +159 -0
- package/test/mappers/faq-mapper.test.js +140 -0
- package/test/utils/patch-utils.test.js +293 -1
package/test/index.test.js
CHANGED
|
@@ -1150,6 +1150,494 @@ describe('TokowakaClient', () => {
|
|
|
1150
1150
|
});
|
|
1151
1151
|
});
|
|
1152
1152
|
|
|
1153
|
+
describe('rollbackSuggestions', () => {
|
|
1154
|
+
beforeEach(() => {
|
|
1155
|
+
// Stub CDN invalidation for rollback tests
|
|
1156
|
+
sinon.stub(client, 'invalidateCdnCache').resolves({
|
|
1157
|
+
status: 'success',
|
|
1158
|
+
provider: 'cloudfront',
|
|
1159
|
+
invalidationId: 'I123',
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
it('should rollback suggestions successfully', async () => {
|
|
1164
|
+
const existingConfig = {
|
|
1165
|
+
siteId: 'site-123',
|
|
1166
|
+
baseURL: 'https://example.com',
|
|
1167
|
+
version: '1.0',
|
|
1168
|
+
tokowakaForceFail: false,
|
|
1169
|
+
tokowakaOptimizations: {
|
|
1170
|
+
'/page1': {
|
|
1171
|
+
prerender: true,
|
|
1172
|
+
patches: [
|
|
1173
|
+
{
|
|
1174
|
+
op: 'replace',
|
|
1175
|
+
selector: 'h1',
|
|
1176
|
+
value: 'Heading 1',
|
|
1177
|
+
opportunityId: 'opp-123',
|
|
1178
|
+
suggestionId: 'sugg-1',
|
|
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
|
+
],
|
|
1201
|
+
},
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1206
|
+
|
|
1207
|
+
const result = await client.rollbackSuggestions(
|
|
1208
|
+
mockSite,
|
|
1209
|
+
mockOpportunity,
|
|
1210
|
+
mockSuggestions, // Only sugg-1 and sugg-2
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
expect(result).to.have.property('s3Path', 'opportunities/test-api-key-123');
|
|
1214
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1215
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
1216
|
+
expect(result.removedPatchesCount).to.equal(2);
|
|
1217
|
+
|
|
1218
|
+
// Verify uploaded config has sugg-3 but not sugg-1 and sugg-2
|
|
1219
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1220
|
+
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches).to.have.length(1);
|
|
1221
|
+
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches[0].suggestionId).to.equal('sugg-3');
|
|
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
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('should handle no existing config gracefully', async () => {
|
|
1251
|
+
sinon.stub(client, 'fetchConfig').resolves(null);
|
|
1252
|
+
|
|
1253
|
+
const result = await client.rollbackSuggestions(
|
|
1254
|
+
mockSite,
|
|
1255
|
+
mockOpportunity,
|
|
1256
|
+
mockSuggestions,
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
expect(result.succeededSuggestions).to.have.length(0);
|
|
1260
|
+
expect(result.failedSuggestions).to.have.length(2);
|
|
1261
|
+
expect(result.failedSuggestions[0].reason).to.include('No existing configuration found');
|
|
1262
|
+
expect(s3Client.send).to.not.have.been.called;
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it('should handle empty existing config optimizations', async () => {
|
|
1266
|
+
const existingConfig = {
|
|
1267
|
+
siteId: 'site-123',
|
|
1268
|
+
baseURL: 'https://example.com',
|
|
1269
|
+
version: '1.0',
|
|
1270
|
+
tokowakaForceFail: false,
|
|
1271
|
+
tokowakaOptimizations: {},
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1275
|
+
|
|
1276
|
+
const result = await client.rollbackSuggestions(
|
|
1277
|
+
mockSite,
|
|
1278
|
+
mockOpportunity,
|
|
1279
|
+
mockSuggestions,
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
expect(result.succeededSuggestions).to.have.length(0);
|
|
1283
|
+
expect(result.failedSuggestions).to.have.length(2);
|
|
1284
|
+
expect(result.failedSuggestions[0].reason).to.include('No patches found');
|
|
1285
|
+
expect(s3Client.send).to.not.have.been.called;
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('should handle suggestions not found in config', async () => {
|
|
1289
|
+
const existingConfig = {
|
|
1290
|
+
siteId: 'site-123',
|
|
1291
|
+
baseURL: 'https://example.com',
|
|
1292
|
+
version: '1.0',
|
|
1293
|
+
tokowakaForceFail: false,
|
|
1294
|
+
tokowakaOptimizations: {
|
|
1295
|
+
'/page1': {
|
|
1296
|
+
prerender: true,
|
|
1297
|
+
patches: [
|
|
1298
|
+
{
|
|
1299
|
+
op: 'replace',
|
|
1300
|
+
selector: 'h1',
|
|
1301
|
+
value: 'Heading',
|
|
1302
|
+
opportunityId: 'opp-123',
|
|
1303
|
+
suggestionId: 'sugg-999', // Different suggestion ID
|
|
1304
|
+
prerenderRequired: true,
|
|
1305
|
+
lastUpdated: 1234567890,
|
|
1306
|
+
},
|
|
1307
|
+
],
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1313
|
+
|
|
1314
|
+
const result = await client.rollbackSuggestions(
|
|
1315
|
+
mockSite,
|
|
1316
|
+
mockOpportunity,
|
|
1317
|
+
mockSuggestions,
|
|
1318
|
+
);
|
|
1319
|
+
|
|
1320
|
+
expect(result.succeededSuggestions).to.have.length(0);
|
|
1321
|
+
expect(result.failedSuggestions).to.have.length(2);
|
|
1322
|
+
expect(result.failedSuggestions[0].reason).to.include('No patches found');
|
|
1323
|
+
expect(s3Client.send).to.not.have.been.called;
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it('should return early when all suggestions are ineligible', async () => {
|
|
1327
|
+
// Create suggestions where ALL are ineligible
|
|
1328
|
+
const allIneligibleSuggestions = [
|
|
1329
|
+
{
|
|
1330
|
+
getId: () => 'sugg-1',
|
|
1331
|
+
getData: () => ({
|
|
1332
|
+
url: 'https://example.com/page1',
|
|
1333
|
+
recommendedAction: 'New Heading',
|
|
1334
|
+
checkType: 'heading-missing', // Not eligible
|
|
1335
|
+
}),
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
getId: () => 'sugg-2',
|
|
1339
|
+
getData: () => ({
|
|
1340
|
+
url: 'https://example.com/page1',
|
|
1341
|
+
recommendedAction: 'New Heading',
|
|
1342
|
+
checkType: 'heading-invalid', // Not eligible
|
|
1343
|
+
}),
|
|
1344
|
+
},
|
|
1345
|
+
];
|
|
1346
|
+
|
|
1347
|
+
const result = await client.rollbackSuggestions(
|
|
1348
|
+
mockSite,
|
|
1349
|
+
mockOpportunity,
|
|
1350
|
+
allIneligibleSuggestions,
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
expect(result.succeededSuggestions).to.have.length(0);
|
|
1354
|
+
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
|
+
expect(s3Client.send).to.not.have.been.called;
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
it('should handle ineligible suggestions during rollback', async () => {
|
|
1361
|
+
// Create suggestions where one is ineligible
|
|
1362
|
+
const mixedSuggestions = [
|
|
1363
|
+
{
|
|
1364
|
+
getId: () => 'sugg-1',
|
|
1365
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1366
|
+
getData: () => ({
|
|
1367
|
+
url: 'https://example.com/page1',
|
|
1368
|
+
recommendedAction: 'New Heading',
|
|
1369
|
+
checkType: 'heading-empty',
|
|
1370
|
+
transformRules: {
|
|
1371
|
+
action: 'replace',
|
|
1372
|
+
selector: 'h1',
|
|
1373
|
+
},
|
|
1374
|
+
}),
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
getId: () => 'sugg-2',
|
|
1378
|
+
getData: () => ({
|
|
1379
|
+
url: 'https://example.com/page1',
|
|
1380
|
+
recommendedAction: 'New Heading',
|
|
1381
|
+
checkType: 'heading-missing', // Not eligible
|
|
1382
|
+
}),
|
|
1383
|
+
},
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
const existingConfig = {
|
|
1387
|
+
siteId: 'site-123',
|
|
1388
|
+
baseURL: 'https://example.com',
|
|
1389
|
+
version: '1.0',
|
|
1390
|
+
tokowakaForceFail: false,
|
|
1391
|
+
tokowakaOptimizations: {
|
|
1392
|
+
'/page1': {
|
|
1393
|
+
prerender: true,
|
|
1394
|
+
patches: [
|
|
1395
|
+
{
|
|
1396
|
+
op: 'replace',
|
|
1397
|
+
selector: 'h1',
|
|
1398
|
+
value: 'Heading 1',
|
|
1399
|
+
opportunityId: 'opp-123',
|
|
1400
|
+
suggestionId: 'sugg-1',
|
|
1401
|
+
prerenderRequired: true,
|
|
1402
|
+
lastUpdated: 1234567890,
|
|
1403
|
+
},
|
|
1404
|
+
],
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
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',
|
|
1426
|
+
version: '1.0',
|
|
1427
|
+
tokowakaForceFail: false,
|
|
1428
|
+
tokowakaOptimizations: {
|
|
1429
|
+
'/page1': {
|
|
1430
|
+
prerender: true,
|
|
1431
|
+
patches: [
|
|
1432
|
+
{
|
|
1433
|
+
op: 'replace',
|
|
1434
|
+
selector: 'h1',
|
|
1435
|
+
value: 'Heading 1',
|
|
1436
|
+
opportunityId: 'opp-123',
|
|
1437
|
+
suggestionId: 'sugg-1',
|
|
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
|
+
],
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1461
|
+
|
|
1462
|
+
const result = await client.rollbackSuggestions(
|
|
1463
|
+
mockSite,
|
|
1464
|
+
mockOpportunity,
|
|
1465
|
+
[mockSuggestions[0]], // Only roll back sugg-1
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1469
|
+
expect(result.removedPatchesCount).to.equal(1);
|
|
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');
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('should throw error for unsupported opportunity type', async () => {
|
|
1478
|
+
mockOpportunity.getType = () => 'unsupported-type';
|
|
1479
|
+
|
|
1480
|
+
try {
|
|
1481
|
+
await client.rollbackSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1482
|
+
expect.fail('Should have thrown error');
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
expect(error.message).to.include('No mapper found for opportunity type: unsupported-type');
|
|
1485
|
+
expect(error.status).to.equal(501);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it('should remove FAQ heading patch when rolling back last FAQ suggestion', async () => {
|
|
1490
|
+
// Change opportunity to FAQ type
|
|
1491
|
+
mockOpportunity.getType = () => 'faq';
|
|
1492
|
+
|
|
1493
|
+
// Create FAQ suggestion
|
|
1494
|
+
const faqSuggestion = {
|
|
1495
|
+
getId: () => 'faq-sugg-1',
|
|
1496
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1497
|
+
getData: () => ({
|
|
1498
|
+
url: 'https://example.com/page1',
|
|
1499
|
+
shouldOptimize: true,
|
|
1500
|
+
item: {
|
|
1501
|
+
question: 'What is this?',
|
|
1502
|
+
answer: 'This is a FAQ',
|
|
1503
|
+
},
|
|
1504
|
+
transformRules: {
|
|
1505
|
+
action: 'appendChild',
|
|
1506
|
+
selector: 'body',
|
|
1507
|
+
},
|
|
1508
|
+
}),
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
const existingConfig = {
|
|
1512
|
+
siteId: 'site-123',
|
|
1513
|
+
baseURL: 'https://example.com',
|
|
1514
|
+
version: '1.0',
|
|
1515
|
+
tokowakaForceFail: false,
|
|
1516
|
+
tokowakaOptimizations: {
|
|
1517
|
+
'/page1': {
|
|
1518
|
+
prerender: true,
|
|
1519
|
+
patches: [
|
|
1520
|
+
{
|
|
1521
|
+
opportunityId: 'opp-123',
|
|
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',
|
|
1576
|
+
selector: 'body',
|
|
1577
|
+
},
|
|
1578
|
+
}),
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
const existingConfig = {
|
|
1582
|
+
siteId: 'site-123',
|
|
1583
|
+
baseURL: 'https://example.com',
|
|
1584
|
+
version: '1.0',
|
|
1585
|
+
tokowakaForceFail: false,
|
|
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
|
+
],
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
sinon.stub(client, 'fetchConfig').resolves(existingConfig);
|
|
1623
|
+
|
|
1624
|
+
const result = await client.rollbackSuggestions(
|
|
1625
|
+
mockSite,
|
|
1626
|
+
mockOpportunity,
|
|
1627
|
+
[faqSuggestion1], // Only rolling back one of two FAQs
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
expect(result.succeededSuggestions).to.have.length(1);
|
|
1631
|
+
expect(result.removedPatchesCount).to.equal(1); // Only FAQ item removed
|
|
1632
|
+
|
|
1633
|
+
// Verify uploaded config still has heading and faq-sugg-2
|
|
1634
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
1635
|
+
expect(uploadedConfig.tokowakaOptimizations['/page1'].patches).to.have.length(2);
|
|
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');
|
|
1638
|
+
});
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1153
1641
|
describe('previewSuggestions', () => {
|
|
1154
1642
|
let fetchStub;
|
|
1155
1643
|
|
|
@@ -207,4 +207,163 @@ describe('BaseOpportunityMapper', () => {
|
|
|
207
207
|
expect(patch.lastUpdated).to.equal(new Date('2025-01-15T10:00:00.000Z').getTime());
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
describe('rollbackPatches', () => {
|
|
212
|
+
let testMapper;
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
216
|
+
getOpportunityType() { return 'test'; }
|
|
217
|
+
|
|
218
|
+
requiresPrerender() { return true; }
|
|
219
|
+
|
|
220
|
+
suggestionsToPatches() { return []; }
|
|
221
|
+
|
|
222
|
+
canDeploy() { return { eligible: true }; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
testMapper = new TestMapper(log);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should remove patches by suggestion IDs using default implementation', () => {
|
|
229
|
+
const config = {
|
|
230
|
+
siteId: 'site-123',
|
|
231
|
+
baseURL: 'https://example.com',
|
|
232
|
+
version: '1.0',
|
|
233
|
+
tokowakaOptimizations: {
|
|
234
|
+
'/page1': {
|
|
235
|
+
prerender: true,
|
|
236
|
+
patches: [
|
|
237
|
+
{
|
|
238
|
+
opportunityId: 'opp-test',
|
|
239
|
+
suggestionId: 'sugg-1',
|
|
240
|
+
op: 'replace',
|
|
241
|
+
value: 'value-1',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
opportunityId: 'opp-test',
|
|
245
|
+
suggestionId: 'sugg-2',
|
|
246
|
+
op: 'replace',
|
|
247
|
+
value: 'value-2',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
255
|
+
|
|
256
|
+
expect(result.tokowakaOptimizations['/page1'].patches).to.have.lengthOf(1);
|
|
257
|
+
expect(result.tokowakaOptimizations['/page1'].patches[0].suggestionId).to.equal('sugg-2');
|
|
258
|
+
expect(result.removedCount).to.equal(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should handle null/undefined config gracefully', () => {
|
|
262
|
+
const result1 = testMapper.rollbackPatches(null, ['sugg-1'], 'opp-test');
|
|
263
|
+
expect(result1).to.be.null;
|
|
264
|
+
|
|
265
|
+
const result2 = testMapper.rollbackPatches(undefined, ['sugg-1'], 'opp-test');
|
|
266
|
+
expect(result2).to.be.undefined;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should remove patches for multiple suggestion IDs', () => {
|
|
270
|
+
const config = {
|
|
271
|
+
siteId: 'site-123',
|
|
272
|
+
baseURL: 'https://example.com',
|
|
273
|
+
version: '1.0',
|
|
274
|
+
tokowakaOptimizations: {
|
|
275
|
+
'/page1': {
|
|
276
|
+
prerender: true,
|
|
277
|
+
patches: [
|
|
278
|
+
{
|
|
279
|
+
opportunityId: 'opp-test',
|
|
280
|
+
suggestionId: 'sugg-1',
|
|
281
|
+
op: 'replace',
|
|
282
|
+
value: 'value-1',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
opportunityId: 'opp-test',
|
|
286
|
+
suggestionId: 'sugg-2',
|
|
287
|
+
op: 'replace',
|
|
288
|
+
value: 'value-2',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
opportunityId: 'opp-test',
|
|
292
|
+
suggestionId: 'sugg-3',
|
|
293
|
+
op: 'replace',
|
|
294
|
+
value: 'value-3',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1', 'sugg-3'], 'opp-test');
|
|
302
|
+
|
|
303
|
+
expect(result.tokowakaOptimizations['/page1'].patches).to.have.lengthOf(1);
|
|
304
|
+
expect(result.tokowakaOptimizations['/page1'].patches[0].suggestionId).to.equal('sugg-2');
|
|
305
|
+
expect(result.removedCount).to.equal(2);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should remove URL path when all patches are removed', () => {
|
|
309
|
+
const config = {
|
|
310
|
+
siteId: 'site-123',
|
|
311
|
+
baseURL: 'https://example.com',
|
|
312
|
+
version: '1.0',
|
|
313
|
+
tokowakaOptimizations: {
|
|
314
|
+
'/page1': {
|
|
315
|
+
prerender: true,
|
|
316
|
+
patches: [
|
|
317
|
+
{
|
|
318
|
+
opportunityId: 'opp-test',
|
|
319
|
+
suggestionId: 'sugg-1',
|
|
320
|
+
op: 'replace',
|
|
321
|
+
value: 'value-1',
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
329
|
+
|
|
330
|
+
// URL path should be removed when no patches remain
|
|
331
|
+
expect(result.tokowakaOptimizations).to.not.have.property('/page1');
|
|
332
|
+
expect(result.removedCount).to.equal(1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should preserve patches from other opportunities', () => {
|
|
336
|
+
const config = {
|
|
337
|
+
siteId: 'site-123',
|
|
338
|
+
baseURL: 'https://example.com',
|
|
339
|
+
version: '1.0',
|
|
340
|
+
tokowakaOptimizations: {
|
|
341
|
+
'/page1': {
|
|
342
|
+
prerender: true,
|
|
343
|
+
patches: [
|
|
344
|
+
{
|
|
345
|
+
opportunityId: 'opp-test',
|
|
346
|
+
suggestionId: 'sugg-1',
|
|
347
|
+
op: 'replace',
|
|
348
|
+
value: 'test-value',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
opportunityId: 'opp-other',
|
|
352
|
+
suggestionId: 'sugg-2',
|
|
353
|
+
op: 'replace',
|
|
354
|
+
value: 'other-value',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Default implementation removes by suggestionId regardless of opportunity
|
|
362
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
363
|
+
|
|
364
|
+
expect(result.tokowakaOptimizations['/page1'].patches).to.have.lengthOf(1);
|
|
365
|
+
expect(result.tokowakaOptimizations['/page1'].patches[0].suggestionId).to.equal('sugg-2');
|
|
366
|
+
expect(result.removedCount).to.equal(1);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
210
369
|
});
|