@adobe/spacecat-shared-tokowaka-client 1.4.4 → 1.4.6
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 +5 -8
- package/package.json +3 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +90 -26
- package/src/utils/custom-html-utils.js +26 -0
- package/src/utils/s3-utils.js +10 -3
- package/test/index.test.js +131 -10
- package/test/utils/html-utils.test.js +101 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@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)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 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))
|
|
7
|
+
|
|
8
|
+
# [@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)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* fetch api key from s3 and fix invalidation sequence ([#1251](https://github.com/adobe/spacecat-shared/issues/1251)) ([fe66bd6](https://github.com/adobe/spacecat-shared/commit/fe66bd6732dc200ab4df27babc1e0831f6187271))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-tokowaka-client-v1.4.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.3...@adobe/spacecat-shared-tokowaka-client-v1.4.4) (2026-01-05)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -146,21 +146,17 @@ The client invalidates CDN cache after uploading configurations. Failures are lo
|
|
|
146
146
|
|
|
147
147
|
## Site Configuration
|
|
148
148
|
|
|
149
|
-
Sites must have the
|
|
149
|
+
Sites must have the `tokowakaConfig` field in their site-config to confirm that tokowaka is enabled for that particular site.
|
|
150
150
|
|
|
151
151
|
```javascript
|
|
152
152
|
{
|
|
153
|
+
...
|
|
153
154
|
"tokowakaConfig": {
|
|
154
|
-
"
|
|
155
|
-
"forwardedHost": "www.example.com" // Required for preview functionality
|
|
155
|
+
"enabled": true
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
**Note:**
|
|
161
|
-
- `apiKey` is optional and **not used** for S3 paths or HTTP headers (kept in schema for potential future use)
|
|
162
|
-
- `forwardedHost` is **required** for preview functionality to fetch HTML from Tokowaka edge
|
|
163
|
-
|
|
164
160
|
## Supported Opportunity Types
|
|
165
161
|
|
|
166
162
|
### Headings
|
|
@@ -215,6 +211,7 @@ Domain-level metaconfig (created once per domain, shared by all URLs):
|
|
|
215
211
|
```json
|
|
216
212
|
{
|
|
217
213
|
"siteId": "abc-123",
|
|
214
|
+
"apiKeys": ["tokowaka-api-key-1"],
|
|
218
215
|
"prerender": true
|
|
219
216
|
}
|
|
220
217
|
```
|
|
@@ -308,7 +305,7 @@ TOKOWAKA_CDN_CONFIG='{"cloudfront":{"distributionId":"...","region":"us-east-1"}
|
|
|
308
305
|
|
|
309
306
|
**Behavior:**
|
|
310
307
|
- All URLs are batched into a single invalidation request per provider
|
|
311
|
-
- All CDN providers are invalidated
|
|
308
|
+
- All CDN providers are invalidated sequentially
|
|
312
309
|
- Failures in one CDN don't block others
|
|
313
310
|
- Each CDN returns its own result in the `cdnInvalidations` array
|
|
314
311
|
- Results include status, provider name, and provider-specific details
|
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.6",
|
|
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.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -10,15 +10,17 @@
|
|
|
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 { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path } from './utils/s3-utils.js';
|
|
20
|
+
import { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path, getHostName } from './utils/s3-utils.js';
|
|
19
21
|
import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from './utils/suggestion-utils.js';
|
|
20
22
|
import { getEffectiveBaseURL } from './utils/site-utils.js';
|
|
21
|
-
import { fetchHtmlWithWarmup } from './utils/custom-html-utils.js';
|
|
23
|
+
import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
22
24
|
|
|
23
25
|
const HTTP_BAD_REQUEST = 400;
|
|
24
26
|
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
@@ -219,10 +221,60 @@ class TokowakaClient {
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Generates an API key for Tokowaka based on domain
|
|
226
|
+
* @param {string} domain - Domain name (e.g., 'example.com')
|
|
227
|
+
* @returns {string} - Base64 URL-encoded API key
|
|
228
|
+
* @private
|
|
229
|
+
*/
|
|
230
|
+
/* eslint-disable class-methods-use-this */
|
|
231
|
+
#generateApiKey(normalizedHostName) {
|
|
232
|
+
const uuid = uuidv4();
|
|
233
|
+
return crypto
|
|
234
|
+
.createHash('sha256')
|
|
235
|
+
.update(`${uuid}${normalizedHostName}`)
|
|
236
|
+
.digest('base64url');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Creates and uploads domain-level metaconfig to S3
|
|
241
|
+
* Generates a new API key and creates the metaconfig structure
|
|
242
|
+
* @param {string} url - Full URL (used to extract domain)
|
|
243
|
+
* @param {string} siteId - Site ID
|
|
244
|
+
* @param {Object} options - Optional configuration
|
|
245
|
+
* @param {boolean} options.tokowakaEnabled - Whether to enable Tokowaka (default: true)
|
|
246
|
+
* @returns {Promise<Object>} - Object with s3Path and metaconfig
|
|
247
|
+
*/
|
|
248
|
+
async createMetaconfig(url, siteId, options = {}) {
|
|
249
|
+
if (!hasText(url)) {
|
|
250
|
+
throw this.#createError('URL is required', HTTP_BAD_REQUEST);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!hasText(siteId)) {
|
|
254
|
+
throw this.#createError('Site ID is required', HTTP_BAD_REQUEST);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const normalizedHostName = getHostName(url, this.log);
|
|
258
|
+
const apiKey = this.#generateApiKey(normalizedHostName);
|
|
259
|
+
|
|
260
|
+
const metaconfig = {
|
|
261
|
+
siteId,
|
|
262
|
+
apiKeys: [apiKey],
|
|
263
|
+
tokowakaEnabled: options.tokowakaEnabled ?? true,
|
|
264
|
+
enhancements: false,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const s3Path = await this.uploadMetaconfig(url, metaconfig);
|
|
268
|
+
|
|
269
|
+
this.log.info(`Created new Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
|
|
270
|
+
|
|
271
|
+
return metaconfig;
|
|
272
|
+
}
|
|
273
|
+
|
|
222
274
|
/**
|
|
223
275
|
* Uploads domain-level metaconfig to S3
|
|
224
276
|
* @param {string} url - Full URL (used to extract domain)
|
|
225
|
-
* @param {Object} metaconfig - Metaconfig object (siteId, prerender)
|
|
277
|
+
* @param {Object} metaconfig - Metaconfig object (siteId, apiKeys, prerender)
|
|
226
278
|
* @returns {Promise<string>} - S3 key of uploaded metaconfig
|
|
227
279
|
*/
|
|
228
280
|
async uploadMetaconfig(url, metaconfig) {
|
|
@@ -373,7 +425,7 @@ class TokowakaClient {
|
|
|
373
425
|
|
|
374
426
|
/**
|
|
375
427
|
* CDN cache invalidation method that supports invalidating URL configs
|
|
376
|
-
* or custom S3 paths across provided or default CDN providers
|
|
428
|
+
* or custom S3 paths across provided or default CDN providers.
|
|
377
429
|
* @param {Object} options - Invalidation options
|
|
378
430
|
* @param {Array<string>} options.urls - Array of full URLs to invalidate (for URL configs)
|
|
379
431
|
* @param {Array<string>} options.paths - Custom S3 paths to invalidate directly
|
|
@@ -416,37 +468,37 @@ class TokowakaClient {
|
|
|
416
468
|
+ `via providers: ${providerList.join(', ')}`,
|
|
417
469
|
);
|
|
418
470
|
|
|
419
|
-
|
|
420
|
-
const
|
|
471
|
+
const results = [];
|
|
472
|
+
for (const provider of providerList) {
|
|
421
473
|
try {
|
|
422
474
|
const cdnClient = this.cdnClientRegistry.getClient(provider);
|
|
423
475
|
if (!cdnClient) {
|
|
424
476
|
this.log.warn(`No CDN client available for provider: ${provider}`);
|
|
425
|
-
|
|
477
|
+
results.push({
|
|
426
478
|
status: 'error',
|
|
427
479
|
provider,
|
|
428
480
|
message: `No CDN client available for provider: ${provider}`,
|
|
429
|
-
};
|
|
481
|
+
});
|
|
482
|
+
// eslint-disable-next-line no-continue
|
|
483
|
+
continue;
|
|
430
484
|
}
|
|
431
485
|
|
|
486
|
+
// eslint-disable-next-line no-await-in-loop
|
|
432
487
|
const result = await cdnClient.invalidateCache(pathsToInvalidate);
|
|
433
488
|
this.log.info(
|
|
434
489
|
`CDN cache invalidation completed for ${provider}: `
|
|
435
490
|
+ `${pathsToInvalidate.length} path(s)`,
|
|
436
491
|
);
|
|
437
|
-
|
|
492
|
+
results.push(result);
|
|
438
493
|
} catch (error) {
|
|
439
494
|
this.log.warn(`Failed to invalidate ${provider} CDN cache: ${error.message}`, error);
|
|
440
|
-
|
|
495
|
+
results.push({
|
|
441
496
|
status: 'error',
|
|
442
497
|
provider,
|
|
443
498
|
message: error.message,
|
|
444
|
-
};
|
|
499
|
+
});
|
|
445
500
|
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Wait for all provider invalidations to complete
|
|
449
|
-
const results = await Promise.all(invalidationPromises);
|
|
501
|
+
}
|
|
450
502
|
return results;
|
|
451
503
|
}
|
|
452
504
|
|
|
@@ -699,17 +751,6 @@ class TokowakaClient {
|
|
|
699
751
|
* @returns {Promise<Object>} - Preview result with config and succeeded/failed suggestions
|
|
700
752
|
*/
|
|
701
753
|
async previewSuggestions(site, opportunity, suggestions, options = {}) {
|
|
702
|
-
// Get site's forwarded host for preview
|
|
703
|
-
const { forwardedHost, apiKey } = site.getConfig()?.getTokowakaConfig() || {};
|
|
704
|
-
|
|
705
|
-
if (!hasText(forwardedHost) || !hasText(apiKey)) {
|
|
706
|
-
throw this.#createError(
|
|
707
|
-
'Site does not have a Tokowaka API key or forwarded host configured. '
|
|
708
|
-
+ 'Please onboard the site to Tokowaka first.',
|
|
709
|
-
HTTP_BAD_REQUEST,
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
754
|
const opportunityType = opportunity.getType();
|
|
714
755
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
715
756
|
if (!mapper) {
|
|
@@ -755,6 +796,29 @@ class TokowakaClient {
|
|
|
755
796
|
throw this.#createError('Preview URL not found in suggestion data', HTTP_BAD_REQUEST);
|
|
756
797
|
}
|
|
757
798
|
|
|
799
|
+
// Fetch metaconfig to get API key
|
|
800
|
+
const metaconfig = await this.fetchMetaconfig(previewUrl);
|
|
801
|
+
|
|
802
|
+
if (!metaconfig) {
|
|
803
|
+
throw this.#createError(
|
|
804
|
+
'No domain-level metaconfig found. '
|
|
805
|
+
+ 'A domain-level metaconfig needs to be created first before previewing suggestions.',
|
|
806
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const { apiKeys } = metaconfig;
|
|
811
|
+
if (!Array.isArray(apiKeys) || apiKeys.length === 0 || !hasText(apiKeys[0])) {
|
|
812
|
+
throw this.#createError(
|
|
813
|
+
'Metaconfig does not have valid API keys configured. '
|
|
814
|
+
+ 'Please ensure the metaconfig has at least one API key.',
|
|
815
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const apiKey = apiKeys[0];
|
|
820
|
+
const forwardedHost = calculateForwardedHost(previewUrl, this.log);
|
|
821
|
+
|
|
758
822
|
// Fetch existing deployed configuration for this URL from production S3
|
|
759
823
|
this.log.debug(`Fetching existing deployed Tokowaka config for URL: ${previewUrl}`);
|
|
760
824
|
const existingConfig = await this.fetchConfig(previewUrl, false);
|
|
@@ -193,3 +193,29 @@ export async function fetchHtmlWithWarmup(
|
|
|
193
193
|
throw new Error(errorMsg);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
export function calculateForwardedHost(url, logger = console) {
|
|
198
|
+
try {
|
|
199
|
+
const urlObj = new URL(url);
|
|
200
|
+
const { hostname } = urlObj;
|
|
201
|
+
|
|
202
|
+
// If hostname already starts with www., keep it as is
|
|
203
|
+
if (hostname.startsWith('www.')) {
|
|
204
|
+
logger.debug(`Forwarded host: ${hostname}`);
|
|
205
|
+
return hostname;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Count dots to determine if it's a bare domain or has a subdomain
|
|
209
|
+
const dotCount = (hostname.match(/\./g) || []).length;
|
|
210
|
+
|
|
211
|
+
// If only 1 dot (bare domain like example.com), prepend www.
|
|
212
|
+
// If 2+ dots (subdomain like subdomain.example.com), keep as is
|
|
213
|
+
const forwardedHost = dotCount === 1 ? `www.${hostname}` : hostname;
|
|
214
|
+
|
|
215
|
+
logger.debug(`Forwarded host: ${forwardedHost}`);
|
|
216
|
+
return forwardedHost;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.error(`Error calculating forwarded host from URL ${url}: ${error.message}`);
|
|
219
|
+
throw new Error(`Error calculating forwarded host from URL ${url}: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
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 = {
|
|
@@ -1698,6 +1784,12 @@ describe('TokowakaClient', () => {
|
|
|
1698
1784
|
// Stub fetchConfig to return null by default (no existing config)
|
|
1699
1785
|
sinon.stub(client, 'fetchConfig').resolves(null);
|
|
1700
1786
|
|
|
1787
|
+
// Stub fetchMetaconfig to return metaconfig with apiKeys array
|
|
1788
|
+
sinon.stub(client, 'fetchMetaconfig').resolves({
|
|
1789
|
+
siteId: 'site-123',
|
|
1790
|
+
apiKeys: ['test-api-key-1', 'test-api-key-2'],
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1701
1793
|
// Add TOKOWAKA_EDGE_URL to env
|
|
1702
1794
|
client.env.TOKOWAKA_EDGE_URL = 'https://edge-dev.tokowaka.now';
|
|
1703
1795
|
});
|
|
@@ -1804,31 +1896,60 @@ describe('TokowakaClient', () => {
|
|
|
1804
1896
|
}
|
|
1805
1897
|
});
|
|
1806
1898
|
|
|
1807
|
-
it('should throw error if
|
|
1808
|
-
|
|
1809
|
-
|
|
1899
|
+
it('should throw error if metaconfig does not exist', async () => {
|
|
1900
|
+
client.fetchMetaconfig.resolves(null);
|
|
1901
|
+
|
|
1902
|
+
try {
|
|
1903
|
+
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1904
|
+
expect.fail('Should have thrown error');
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
expect(error.message).to.include('No domain-level metaconfig found');
|
|
1907
|
+
expect(error.status).to.equal(500);
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
it('should throw error if metaconfig does not have apiKeys', async () => {
|
|
1912
|
+
client.fetchMetaconfig.resolves({
|
|
1913
|
+
siteId: 'site-123',
|
|
1914
|
+
// apiKeys missing
|
|
1810
1915
|
});
|
|
1811
1916
|
|
|
1812
1917
|
try {
|
|
1813
1918
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1814
1919
|
expect.fail('Should have thrown error');
|
|
1815
1920
|
} catch (error) {
|
|
1816
|
-
expect(error.message).to.include('
|
|
1817
|
-
expect(error.status).to.equal(
|
|
1921
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1922
|
+
expect(error.status).to.equal(500);
|
|
1818
1923
|
}
|
|
1819
1924
|
});
|
|
1820
1925
|
|
|
1821
|
-
it('should throw error if
|
|
1822
|
-
|
|
1823
|
-
|
|
1926
|
+
it('should throw error if metaconfig has empty apiKeys array', async () => {
|
|
1927
|
+
client.fetchMetaconfig.resolves({
|
|
1928
|
+
siteId: 'site-123',
|
|
1929
|
+
apiKeys: [],
|
|
1824
1930
|
});
|
|
1825
1931
|
|
|
1826
1932
|
try {
|
|
1827
1933
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1828
1934
|
expect.fail('Should have thrown error');
|
|
1829
1935
|
} catch (error) {
|
|
1830
|
-
expect(error.message).to.include('
|
|
1831
|
-
expect(error.status).to.equal(
|
|
1936
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1937
|
+
expect(error.status).to.equal(500);
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('should throw error if metaconfig apiKeys first value is empty', async () => {
|
|
1942
|
+
client.fetchMetaconfig.resolves({
|
|
1943
|
+
siteId: 'site-123',
|
|
1944
|
+
apiKeys: ['', 'test-api-key-2'],
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
try {
|
|
1948
|
+
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1949
|
+
expect.fail('Should have thrown error');
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1952
|
+
expect(error.status).to.equal(500);
|
|
1832
1953
|
}
|
|
1833
1954
|
});
|
|
1834
1955
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { expect } from 'chai';
|
|
16
16
|
import sinon from 'sinon';
|
|
17
|
-
import { fetchHtmlWithWarmup } from '../../src/utils/custom-html-utils.js';
|
|
17
|
+
import { fetchHtmlWithWarmup, calculateForwardedHost } from '../../src/utils/custom-html-utils.js';
|
|
18
18
|
|
|
19
19
|
describe('HTML Utils', () => {
|
|
20
20
|
describe('fetchHtmlWithWarmup', () => {
|
|
@@ -429,4 +429,104 @@ describe('HTML Utils', () => {
|
|
|
429
429
|
expect(fetchStub.callCount).to.equal(2); // warmup + 1 actual
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
|
+
|
|
433
|
+
describe('calculateForwardedHost', () => {
|
|
434
|
+
let log;
|
|
435
|
+
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
log = {
|
|
438
|
+
debug: sinon.stub(),
|
|
439
|
+
warn: sinon.stub(),
|
|
440
|
+
error: sinon.stub(),
|
|
441
|
+
info: sinon.stub(),
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
afterEach(() => {
|
|
446
|
+
sinon.restore();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should add www prefix to bare domain', async () => {
|
|
450
|
+
const result = calculateForwardedHost('https://example.com/page', log);
|
|
451
|
+
expect(result).to.equal('www.example.com');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should keep www prefix when already present', async () => {
|
|
455
|
+
const result = calculateForwardedHost('https://www.example.com/page', log);
|
|
456
|
+
expect(result).to.equal('www.example.com');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should keep subdomain without adding www', async () => {
|
|
460
|
+
const result = calculateForwardedHost('https://subdomain.example.com/page', log);
|
|
461
|
+
expect(result).to.equal('subdomain.example.com');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should keep www.subdomain as is', async () => {
|
|
465
|
+
const result = calculateForwardedHost('https://www.subdomain.example.com/page', log);
|
|
466
|
+
expect(result).to.equal('www.subdomain.example.com');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should add www to bare domain with port', async () => {
|
|
470
|
+
const result = calculateForwardedHost('https://example.com:8080/page', log);
|
|
471
|
+
expect(result).to.equal('www.example.com');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should keep www with port', async () => {
|
|
475
|
+
const result = calculateForwardedHost('https://www.example.com:8080/page', log);
|
|
476
|
+
expect(result).to.equal('www.example.com');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should handle URL with query parameters', async () => {
|
|
480
|
+
const result = calculateForwardedHost('https://example.com/page?param=value', log);
|
|
481
|
+
expect(result).to.equal('www.example.com');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should handle URL with hash fragment', async () => {
|
|
485
|
+
const result = calculateForwardedHost('https://example.com/page#section', log);
|
|
486
|
+
expect(result).to.equal('www.example.com');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should handle http protocol', async () => {
|
|
490
|
+
const result = calculateForwardedHost('http://example.com/page', log);
|
|
491
|
+
expect(result).to.equal('www.example.com');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should handle deep subdomains', async () => {
|
|
495
|
+
const result = calculateForwardedHost('https://level1.level2.example.com/page', log);
|
|
496
|
+
expect(result).to.equal('level1.level2.example.com');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should throw error for invalid URL', async () => {
|
|
500
|
+
try {
|
|
501
|
+
calculateForwardedHost('not-a-valid-url', log);
|
|
502
|
+
expect.fail('Should have thrown error');
|
|
503
|
+
} catch (error) {
|
|
504
|
+
expect(error.message).to.include('Error calculating forwarded host from URL');
|
|
505
|
+
expect(error.message).to.include('not-a-valid-url');
|
|
506
|
+
expect(log.error).to.have.been.calledOnce;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should throw error for empty URL', async () => {
|
|
511
|
+
try {
|
|
512
|
+
calculateForwardedHost('', log);
|
|
513
|
+
expect.fail('Should have thrown error');
|
|
514
|
+
} catch (error) {
|
|
515
|
+
expect(error.message).to.include('Error calculating forwarded host from URL');
|
|
516
|
+
expect(log.error).to.have.been.calledOnce;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should use console as default logger when not provided', async () => {
|
|
521
|
+
const result = calculateForwardedHost('https://example.com/page');
|
|
522
|
+
expect(result).to.equal('www.example.com');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should handle hostname with no dots (edge case)', async () => {
|
|
526
|
+
// This is an edge case for localhost or single-word hostnames
|
|
527
|
+
const result = calculateForwardedHost('http://localhost/page', log);
|
|
528
|
+
// No dots means dotCount = 0, so it won't match dotCount === 1, keeps as is
|
|
529
|
+
expect(result).to.equal('localhost');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
432
532
|
});
|