@adobe/spacecat-shared-tokowaka-client 1.4.5 → 1.4.7
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 +14 -0
- package/README.md +14 -2
- package/package.json +3 -2
- package/src/index.js +99 -1
- package/src/utils/s3-utils.js +10 -3
- package/test/index.test.js +241 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.4.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.6...@adobe/spacecat-shared-tokowaka-client-v1.4.7) (2026-01-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* update tokowaka s3 config with patches ([#1263](https://github.com/adobe/spacecat-shared/issues/1263)) ([59605e9](https://github.com/adobe/spacecat-shared/commit/59605e9b75b7289029bd14e1832e594ea565296d))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.4.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.5...@adobe/spacecat-shared-tokowaka-client-v1.4.6) (2026-01-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add api to create/update s3 edge config ([#1260](https://github.com/adobe/spacecat-shared/issues/1260)) ([b5cfff2](https://github.com/adobe/spacecat-shared/commit/b5cfff29cdb6fa29e1b440f86eb86cf8dd471e43))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-tokowaka-client-v1.4.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.4...@adobe/spacecat-shared-tokowaka-client-v1.4.5) (2026-01-07)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -64,10 +64,12 @@ Creates a client instance from a context object.
|
|
|
64
64
|
|
|
65
65
|
#### `deploySuggestions(site, opportunity, suggestions)`
|
|
66
66
|
|
|
67
|
-
Generates configuration and uploads to S3 **per URL**. **Automatically fetches existing configuration for each URL and merges** new suggestions with it. Invalidates CDN cache after upload.
|
|
67
|
+
Generates configuration and uploads to S3 **per URL**. **Automatically fetches existing configuration for each URL and merges** new suggestions with it. Invalidates CDN cache after upload. **Updates the metaconfig's `patches` field** to track deployed endpoints.
|
|
68
68
|
|
|
69
69
|
**Architecture Change:** Creates one S3 file per URL instead of a single file with all URLs. This prevents files from growing too large over time.
|
|
70
70
|
|
|
71
|
+
**Metaconfig Tracking:** After successful deployment, the method updates the domain-level metaconfig's `patches` object with the normalized paths of all deployed endpoints (e.g., `{ "/products/item": true }`). This provides a centralized registry of all deployed endpoints for the domain.
|
|
72
|
+
|
|
71
73
|
**Returns:** `Promise<DeploymentResult>` with:
|
|
72
74
|
- `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
|
|
73
75
|
- `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
|
|
@@ -212,7 +214,17 @@ Domain-level metaconfig (created once per domain, shared by all URLs):
|
|
|
212
214
|
{
|
|
213
215
|
"siteId": "abc-123",
|
|
214
216
|
"apiKeys": ["tokowaka-api-key-1"],
|
|
215
|
-
"
|
|
217
|
+
"tokowakaEnabled": true,
|
|
218
|
+
"enhancements": true,
|
|
219
|
+
"prerender": {
|
|
220
|
+
"allowList": [],
|
|
221
|
+
"denyList": ["/*"]
|
|
222
|
+
},
|
|
223
|
+
"patches": {
|
|
224
|
+
"/products/item": true,
|
|
225
|
+
"/about": true,
|
|
226
|
+
"/contact": true
|
|
227
|
+
}
|
|
216
228
|
}
|
|
217
229
|
```
|
|
218
230
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-tokowaka-client",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Tokowaka Client for SpaceCat - Edge optimization config management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"@aws-sdk/client-cloudfront": "3.940.0",
|
|
39
39
|
"@aws-sdk/client-s3": "3.940.0",
|
|
40
40
|
"mdast-util-from-markdown": "2.0.2",
|
|
41
|
-
"mdast-util-to-hast": "12.3.0"
|
|
41
|
+
"mdast-util-to-hast": "12.3.0",
|
|
42
|
+
"uuid": "11.0.5"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"aws-sdk-client-mock": "4.1.0",
|
package/src/index.js
CHANGED
|
@@ -10,12 +10,19 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import crypto from 'crypto';
|
|
13
14
|
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
14
15
|
import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
|
|
16
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
17
|
import MapperRegistry from './mappers/mapper-registry.js';
|
|
16
18
|
import CdnClientRegistry from './cdn/cdn-client-registry.js';
|
|
17
19
|
import { mergePatches } from './utils/patch-utils.js';
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getTokowakaConfigS3Path,
|
|
22
|
+
getTokowakaMetaconfigS3Path,
|
|
23
|
+
getHostName,
|
|
24
|
+
normalizePath,
|
|
25
|
+
} from './utils/s3-utils.js';
|
|
19
26
|
import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from './utils/suggestion-utils.js';
|
|
20
27
|
import { getEffectiveBaseURL } from './utils/site-utils.js';
|
|
21
28
|
import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
@@ -92,6 +99,44 @@ class TokowakaClient {
|
|
|
92
99
|
return error;
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Updates the metaconfig with deployed endpoint paths
|
|
104
|
+
* @param {Object} metaconfig - Existing metaconfig object
|
|
105
|
+
* @param {Array<string>} deployedUrls - Array of successfully deployed URLs
|
|
106
|
+
* @param {string} baseUrl - Base URL for uploading metaconfig
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
async #updateMetaconfigWithDeployedPaths(metaconfig, deployedUrls, baseUrl) {
|
|
111
|
+
if (!Array.isArray(deployedUrls) || deployedUrls.length === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Initialize patches field if it doesn't exist
|
|
117
|
+
const updatedMetaconfig = {
|
|
118
|
+
...metaconfig,
|
|
119
|
+
patches: { ...(metaconfig.patches || {}) },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Extract normalized paths from deployed URLs and add to patches object
|
|
123
|
+
deployedUrls.forEach((url) => {
|
|
124
|
+
const urlObj = new URL(url);
|
|
125
|
+
const normalizedPath = normalizePath(urlObj.pathname);
|
|
126
|
+
updatedMetaconfig.patches[normalizedPath] = true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await this.uploadMetaconfig(baseUrl, updatedMetaconfig);
|
|
130
|
+
this.log.info(`Updated metaconfig with ${deployedUrls.length} deployed endpoint(s)`);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.log.error(`Failed to update metaconfig with deployed paths: ${error.message}`, error);
|
|
133
|
+
throw this.#createError(
|
|
134
|
+
`Failed to update metaconfig with deployed paths: ${error.message}`,
|
|
135
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
/**
|
|
96
141
|
* Gets the list of CDN providers from environment configuration
|
|
97
142
|
* Supports both single provider (string) and multiple providers (comma-separated string or array)
|
|
@@ -219,6 +264,56 @@ class TokowakaClient {
|
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Generates an API key for Tokowaka based on domain
|
|
269
|
+
* @param {string} domain - Domain name (e.g., 'example.com')
|
|
270
|
+
* @returns {string} - Base64 URL-encoded API key
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
/* eslint-disable class-methods-use-this */
|
|
274
|
+
#generateApiKey(normalizedHostName) {
|
|
275
|
+
const uuid = uuidv4();
|
|
276
|
+
return crypto
|
|
277
|
+
.createHash('sha256')
|
|
278
|
+
.update(`${uuid}${normalizedHostName}`)
|
|
279
|
+
.digest('base64url');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Creates and uploads domain-level metaconfig to S3
|
|
284
|
+
* Generates a new API key and creates the metaconfig structure
|
|
285
|
+
* @param {string} url - Full URL (used to extract domain)
|
|
286
|
+
* @param {string} siteId - Site ID
|
|
287
|
+
* @param {Object} options - Optional configuration
|
|
288
|
+
* @param {boolean} options.tokowakaEnabled - Whether to enable Tokowaka (default: true)
|
|
289
|
+
* @returns {Promise<Object>} - Object with s3Path and metaconfig
|
|
290
|
+
*/
|
|
291
|
+
async createMetaconfig(url, siteId, options = {}) {
|
|
292
|
+
if (!hasText(url)) {
|
|
293
|
+
throw this.#createError('URL is required', HTTP_BAD_REQUEST);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!hasText(siteId)) {
|
|
297
|
+
throw this.#createError('Site ID is required', HTTP_BAD_REQUEST);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const normalizedHostName = getHostName(url, this.log);
|
|
301
|
+
const apiKey = this.#generateApiKey(normalizedHostName);
|
|
302
|
+
|
|
303
|
+
const metaconfig = {
|
|
304
|
+
siteId,
|
|
305
|
+
apiKeys: [apiKey],
|
|
306
|
+
tokowakaEnabled: options.tokowakaEnabled ?? true,
|
|
307
|
+
enhancements: false,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const s3Path = await this.uploadMetaconfig(url, metaconfig);
|
|
311
|
+
|
|
312
|
+
this.log.info(`Created new Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
|
|
313
|
+
|
|
314
|
+
return metaconfig;
|
|
315
|
+
}
|
|
316
|
+
|
|
222
317
|
/**
|
|
223
318
|
* Uploads domain-level metaconfig to S3
|
|
224
319
|
* @param {string} url - Full URL (used to extract domain)
|
|
@@ -545,6 +640,9 @@ class TokowakaClient {
|
|
|
545
640
|
|
|
546
641
|
this.log.info(`Uploaded Tokowaka configs for ${s3Paths.length} URLs`);
|
|
547
642
|
|
|
643
|
+
// Update metaconfig with deployed paths
|
|
644
|
+
await this.#updateMetaconfigWithDeployedPaths(metaconfig, deployedUrls, baseURL);
|
|
645
|
+
|
|
548
646
|
// Invalidate CDN cache for all deployed URLs at once
|
|
549
647
|
const cdnInvalidations = await this.invalidateCdnCache({ urls: deployedUrls });
|
|
550
648
|
|
package/src/utils/s3-utils.js
CHANGED
|
@@ -35,10 +35,17 @@ export function normalizePath(pathname) {
|
|
|
35
35
|
* @returns {string} - Normalized hostname
|
|
36
36
|
* @throws {Error} - If hostname extraction fails
|
|
37
37
|
*/
|
|
38
|
-
export function getHostName(url, logger) {
|
|
38
|
+
export function getHostName(url, logger = console) {
|
|
39
39
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
let urlObj;
|
|
41
|
+
if (url instanceof URL) {
|
|
42
|
+
urlObj = url;
|
|
43
|
+
} else if (typeof url === 'string') {
|
|
44
|
+
urlObj = new URL(url);
|
|
45
|
+
} else {
|
|
46
|
+
throw new TypeError('Input must be a URL or a string');
|
|
47
|
+
}
|
|
48
|
+
return urlObj.hostname.replace(/^www\./, '');
|
|
42
49
|
} catch (error) {
|
|
43
50
|
logger.error(`Error extracting host name: ${error.message}`);
|
|
44
51
|
throw new Error(`Error extracting host name: ${url.toString()}`);
|
package/test/index.test.js
CHANGED
|
@@ -456,6 +456,92 @@ describe('TokowakaClient', () => {
|
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
458
|
|
|
459
|
+
describe('createMetaconfig', () => {
|
|
460
|
+
it('should create metaconfig with generated API key and default options', async () => {
|
|
461
|
+
const siteId = 'site-123';
|
|
462
|
+
const url = 'https://www.example.com/page1';
|
|
463
|
+
|
|
464
|
+
const result = await client.createMetaconfig(url, siteId);
|
|
465
|
+
|
|
466
|
+
expect(result).to.have.property('siteId', siteId);
|
|
467
|
+
expect(result).to.have.property('apiKeys');
|
|
468
|
+
expect(result.apiKeys).to.be.an('array').with.lengthOf(1);
|
|
469
|
+
expect(result.apiKeys[0]).to.be.a('string');
|
|
470
|
+
expect(result).to.have.property('tokowakaEnabled', true);
|
|
471
|
+
expect(result).to.have.property('enhancements', false);
|
|
472
|
+
|
|
473
|
+
// Verify uploadMetaconfig was called with correct metaconfig
|
|
474
|
+
expect(s3Client.send).to.have.been.calledOnce;
|
|
475
|
+
const command = s3Client.send.firstCall.args[0];
|
|
476
|
+
expect(command.input.Bucket).to.equal('test-bucket');
|
|
477
|
+
expect(command.input.Key).to.equal('opportunities/example.com/config');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should create metaconfig with tokowakaEnabled set to false', async () => {
|
|
481
|
+
const siteId = 'site-123';
|
|
482
|
+
const url = 'https://example.com';
|
|
483
|
+
|
|
484
|
+
const result = await client.createMetaconfig(url, siteId, { tokowakaEnabled: false });
|
|
485
|
+
|
|
486
|
+
expect(result).to.have.property('tokowakaEnabled', false);
|
|
487
|
+
expect(result).to.have.property('enhancements', false);
|
|
488
|
+
expect(result.apiKeys).to.have.lengthOf(1);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should create metaconfig with tokowakaEnabled set to true explicitly', async () => {
|
|
492
|
+
const siteId = 'site-123';
|
|
493
|
+
const url = 'https://example.com';
|
|
494
|
+
|
|
495
|
+
const result = await client.createMetaconfig(url, siteId, { tokowakaEnabled: true });
|
|
496
|
+
|
|
497
|
+
expect(result).to.have.property('tokowakaEnabled', true);
|
|
498
|
+
expect(result).to.have.property('enhancements', false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should throw error if URL is missing', async () => {
|
|
502
|
+
try {
|
|
503
|
+
await client.createMetaconfig('', 'site-123');
|
|
504
|
+
expect.fail('Should have thrown error');
|
|
505
|
+
} catch (error) {
|
|
506
|
+
expect(error.message).to.include('URL is required');
|
|
507
|
+
expect(error.status).to.equal(400);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should throw error if siteId is missing', async () => {
|
|
512
|
+
try {
|
|
513
|
+
await client.createMetaconfig('https://example.com', '');
|
|
514
|
+
expect.fail('Should have thrown error');
|
|
515
|
+
} catch (error) {
|
|
516
|
+
expect(error.message).to.include('Site ID is required');
|
|
517
|
+
expect(error.status).to.equal(400);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should handle S3 upload failure', async () => {
|
|
522
|
+
const s3Error = new Error('S3 network error');
|
|
523
|
+
s3Client.send.rejects(s3Error);
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
await client.createMetaconfig('https://example.com', 'site-123');
|
|
527
|
+
expect.fail('Should have thrown error');
|
|
528
|
+
} catch (error) {
|
|
529
|
+
expect(error.message).to.include('S3 upload failed');
|
|
530
|
+
expect(error.status).to.equal(500);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should strip www. from domain in metaconfig path', async () => {
|
|
535
|
+
const siteId = 'site-123';
|
|
536
|
+
const url = 'https://www.example.com/some/path';
|
|
537
|
+
|
|
538
|
+
await client.createMetaconfig(url, siteId);
|
|
539
|
+
|
|
540
|
+
const command = s3Client.send.firstCall.args[0];
|
|
541
|
+
expect(command.input.Key).to.equal('opportunities/example.com/config');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
459
545
|
describe('uploadConfig', () => {
|
|
460
546
|
it('should upload config to S3', async () => {
|
|
461
547
|
const config = {
|
|
@@ -944,6 +1030,161 @@ describe('TokowakaClient', () => {
|
|
|
944
1030
|
expect(result.failedSuggestions[0].suggestion.getId()).to.equal('sugg-2');
|
|
945
1031
|
});
|
|
946
1032
|
|
|
1033
|
+
it('should update metaconfig patches field with deployed endpoints', async () => {
|
|
1034
|
+
mockSuggestions = [
|
|
1035
|
+
{
|
|
1036
|
+
getId: () => 'sugg-1',
|
|
1037
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1038
|
+
getData: () => ({
|
|
1039
|
+
url: 'https://example.com/page1',
|
|
1040
|
+
recommendedAction: 'Page 1 Heading',
|
|
1041
|
+
checkType: 'heading-empty',
|
|
1042
|
+
transformRules: {
|
|
1043
|
+
action: 'replace',
|
|
1044
|
+
selector: 'h1',
|
|
1045
|
+
},
|
|
1046
|
+
}),
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
getId: () => 'sugg-2',
|
|
1050
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1051
|
+
getData: () => ({
|
|
1052
|
+
url: 'https://example.com/page2',
|
|
1053
|
+
recommendedAction: 'Page 2 Heading',
|
|
1054
|
+
checkType: 'heading-empty',
|
|
1055
|
+
transformRules: {
|
|
1056
|
+
action: 'replace',
|
|
1057
|
+
selector: 'h1',
|
|
1058
|
+
},
|
|
1059
|
+
}),
|
|
1060
|
+
},
|
|
1061
|
+
];
|
|
1062
|
+
|
|
1063
|
+
const result = await client.deploySuggestions(
|
|
1064
|
+
mockSite,
|
|
1065
|
+
mockOpportunity,
|
|
1066
|
+
mockSuggestions,
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1070
|
+
// Verify uploadMetaconfig was called to update the metaconfig
|
|
1071
|
+
expect(client.uploadMetaconfig).to.have.been.called;
|
|
1072
|
+
// Check that the last call included the patches field
|
|
1073
|
+
const { lastCall } = client.uploadMetaconfig;
|
|
1074
|
+
expect(lastCall.args[1]).to.have.property('patches');
|
|
1075
|
+
expect(lastCall.args[1].patches).to.deep.equal({
|
|
1076
|
+
'/page1': true,
|
|
1077
|
+
'/page2': true,
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('should add to existing patches in metaconfig when deploying new endpoints', async () => {
|
|
1082
|
+
// Set up metaconfig with existing patches
|
|
1083
|
+
// Reset the stub to provide consistent behavior
|
|
1084
|
+
client.fetchMetaconfig.reset();
|
|
1085
|
+
client.fetchMetaconfig.resolves({
|
|
1086
|
+
siteId: 'site-123',
|
|
1087
|
+
prerender: true,
|
|
1088
|
+
patches: {
|
|
1089
|
+
'/existing-page': true,
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
mockSuggestions = [
|
|
1094
|
+
{
|
|
1095
|
+
getId: () => 'sugg-1',
|
|
1096
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
1097
|
+
getData: () => ({
|
|
1098
|
+
url: 'https://example.com/new-page',
|
|
1099
|
+
recommendedAction: 'New Heading',
|
|
1100
|
+
checkType: 'heading-empty',
|
|
1101
|
+
transformRules: {
|
|
1102
|
+
action: 'replace',
|
|
1103
|
+
selector: 'h1',
|
|
1104
|
+
},
|
|
1105
|
+
}),
|
|
1106
|
+
},
|
|
1107
|
+
];
|
|
1108
|
+
|
|
1109
|
+
await client.deploySuggestions(
|
|
1110
|
+
mockSite,
|
|
1111
|
+
mockOpportunity,
|
|
1112
|
+
mockSuggestions,
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
// Verify the updated metaconfig includes both existing and new patches
|
|
1116
|
+
const { lastCall } = client.uploadMetaconfig;
|
|
1117
|
+
expect(lastCall.args[1].patches).to.deep.equal({
|
|
1118
|
+
'/existing-page': true,
|
|
1119
|
+
'/new-page': true,
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should throw error when metaconfig update fails', async () => {
|
|
1124
|
+
// Make uploadMetaconfig fail during the update
|
|
1125
|
+
client.uploadMetaconfig.rejects(new Error('S3 upload error'));
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
await client.deploySuggestions(
|
|
1129
|
+
mockSite,
|
|
1130
|
+
mockOpportunity,
|
|
1131
|
+
mockSuggestions,
|
|
1132
|
+
);
|
|
1133
|
+
expect.fail('Should have thrown error');
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
expect(error.message).to.include('Failed to update metaconfig with deployed paths');
|
|
1136
|
+
expect(error.status).to.equal(500);
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('should return early when no eligible suggestions to deploy', async () => {
|
|
1141
|
+
// All suggestions are ineligible
|
|
1142
|
+
mockSuggestions = [
|
|
1143
|
+
{
|
|
1144
|
+
getId: () => 'sugg-1',
|
|
1145
|
+
getData: () => ({
|
|
1146
|
+
url: 'https://example.com/page1',
|
|
1147
|
+
recommendedAction: 'New Heading',
|
|
1148
|
+
checkType: 'heading-missing', // Not eligible
|
|
1149
|
+
}),
|
|
1150
|
+
},
|
|
1151
|
+
];
|
|
1152
|
+
|
|
1153
|
+
const result = await client.deploySuggestions(
|
|
1154
|
+
mockSite,
|
|
1155
|
+
mockOpportunity,
|
|
1156
|
+
mockSuggestions,
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
// No suggestions deployed - returns early before metaconfig check
|
|
1160
|
+
expect(result.succeededSuggestions).to.have.length(0);
|
|
1161
|
+
expect(result.failedSuggestions).to.have.length(1);
|
|
1162
|
+
// fetchMetaconfig should not be called at all (returns before that point)
|
|
1163
|
+
expect(client.fetchMetaconfig).to.not.have.been.called;
|
|
1164
|
+
// uploadMetaconfig should not be called at all
|
|
1165
|
+
expect(client.uploadMetaconfig).to.not.have.been.called;
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('should not update metaconfig when all URLs fail to generate configs', async () => {
|
|
1169
|
+
// Stub generateConfig to return null (no config generated)
|
|
1170
|
+
sinon.stub(client, 'generateConfig').returns(null);
|
|
1171
|
+
|
|
1172
|
+
const result = await client.deploySuggestions(
|
|
1173
|
+
mockSite,
|
|
1174
|
+
mockOpportunity,
|
|
1175
|
+
mockSuggestions,
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Suggestions are marked as succeeded (eligible) but no configs uploaded
|
|
1179
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
1180
|
+
expect(result.s3Paths).to.have.length(0); // No configs uploaded
|
|
1181
|
+
// fetchMetaconfig called once for initial check, but not for update
|
|
1182
|
+
// since no URLs were actually deployed (deployedUrls is empty)
|
|
1183
|
+
expect(client.fetchMetaconfig).to.have.been.calledOnce;
|
|
1184
|
+
// uploadMetaconfig should not be called at all
|
|
1185
|
+
expect(client.uploadMetaconfig).to.not.have.been.called;
|
|
1186
|
+
});
|
|
1187
|
+
|
|
947
1188
|
it('should skip URL when generateConfig returns no patches', async () => {
|
|
948
1189
|
// Stub mapper to return empty patches for the first call, normal for subsequent calls
|
|
949
1190
|
const mapper = client.mapperRegistry.getMapper('headings');
|