@adobe/spacecat-shared-tokowaka-client 1.6.0 → 1.7.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 +14 -0
- package/package.json +1 -1
- package/src/index.js +83 -17
- package/src/utils/custom-html-utils.js +2 -6
- package/test/index.test.js +478 -39
- package/test/utils/html-utils.test.js +27 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.7.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.6.1...@adobe/spacecat-shared-tokowaka-client-v1.7.0) (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* edge optimize status api addition ([#1292](https://github.com/adobe/spacecat-shared/issues/1292)) ([f2c89be](https://github.com/adobe/spacecat-shared/commit/f2c89be3fb4fb2580a13a3a3f7517ce92cf1ce69))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.6.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.6.0...@adobe/spacecat-shared-tokowaka-client-v1.6.1) (2026-01-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* api key now optional for edge-preview ([#1299](https://github.com/adobe/spacecat-shared/issues/1299)) ([76eb046](https://github.com/adobe/spacecat-shared/commit/76eb04640dab752a83909a51827abde1e41fc57e))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-tokowaka-client-v1.6.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.8...@adobe/spacecat-shared-tokowaka-client-v1.6.0) (2026-01-29)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -913,26 +913,13 @@ class TokowakaClient {
|
|
|
913
913
|
}
|
|
914
914
|
|
|
915
915
|
// Fetch metaconfig to get API key
|
|
916
|
-
const metaconfig = await this.fetchMetaconfig(previewUrl);
|
|
916
|
+
const metaconfig = await this.fetchMetaconfig(previewUrl) || {};
|
|
917
917
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
+ 'A domain-level metaconfig needs to be created first before previewing suggestions.',
|
|
922
|
-
HTTP_INTERNAL_SERVER_ERROR,
|
|
923
|
-
);
|
|
918
|
+
let apiKey;
|
|
919
|
+
if (Array.isArray(metaconfig.apiKeys) && metaconfig.apiKeys.length > 0) {
|
|
920
|
+
[apiKey] = metaconfig.apiKeys;
|
|
924
921
|
}
|
|
925
922
|
|
|
926
|
-
const { apiKeys } = metaconfig;
|
|
927
|
-
if (!Array.isArray(apiKeys) || apiKeys.length === 0 || !hasText(apiKeys[0])) {
|
|
928
|
-
throw this.#createError(
|
|
929
|
-
'Metaconfig does not have valid API keys configured. '
|
|
930
|
-
+ 'Please ensure the metaconfig has at least one API key.',
|
|
931
|
-
HTTP_INTERNAL_SERVER_ERROR,
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const apiKey = apiKeys[0];
|
|
936
923
|
const forwardedHost = calculateForwardedHost(previewUrl, this.log);
|
|
937
924
|
|
|
938
925
|
// Fetch existing deployed configuration for this URL from production S3
|
|
@@ -1036,6 +1023,85 @@ class TokowakaClient {
|
|
|
1036
1023
|
},
|
|
1037
1024
|
};
|
|
1038
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Checks if Edge Optimize is enabled for a specific page path
|
|
1029
|
+
* Follows one level of redirect and retries on failures
|
|
1030
|
+
* @param {Object} site - Site entity
|
|
1031
|
+
* @param {string} path - Path to check (e.g., '/products/chair')
|
|
1032
|
+
* @returns {Promise<Object>} - Status result with edgeOptimizeEnabled flag
|
|
1033
|
+
*/
|
|
1034
|
+
async checkEdgeOptimizeStatus(site, path) {
|
|
1035
|
+
if (!isNonEmptyObject(site)) {
|
|
1036
|
+
throw this.#createError('Site is required', HTTP_BAD_REQUEST);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!hasText(path)) {
|
|
1040
|
+
throw this.#createError('Path is required', HTTP_BAD_REQUEST);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const baseURL = getEffectiveBaseURL(site);
|
|
1044
|
+
const targetUrl = new URL(path, baseURL).toString();
|
|
1045
|
+
|
|
1046
|
+
this.log.info(`Checking edge optimize status for ${targetUrl}`);
|
|
1047
|
+
|
|
1048
|
+
const maxRetries = 3;
|
|
1049
|
+
let attempt = 0;
|
|
1050
|
+
|
|
1051
|
+
while (attempt <= maxRetries) {
|
|
1052
|
+
try {
|
|
1053
|
+
this.log.debug(`Attempt ${attempt + 1}/${maxRetries + 1}: Checking edge optimize status for ${targetUrl}`);
|
|
1054
|
+
|
|
1055
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1056
|
+
const response = await fetch(targetUrl, {
|
|
1057
|
+
method: 'GET',
|
|
1058
|
+
headers: {
|
|
1059
|
+
'User-Agent': 'chatgpt-user',
|
|
1060
|
+
'fastly-debug': '1',
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
this.log.debug(`Response status: ${response.status}`);
|
|
1065
|
+
|
|
1066
|
+
const edgeOptimizeEnabled = response.headers.get('x-tokowaka-request-id') !== null
|
|
1067
|
+
|| response.headers.get('x-edgeoptimize-request-id') !== null;
|
|
1068
|
+
|
|
1069
|
+
this.log.debug(`Edge optimize headers found: ${edgeOptimizeEnabled}`);
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
edgeOptimizeEnabled,
|
|
1073
|
+
};
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
attempt += 1;
|
|
1076
|
+
|
|
1077
|
+
if (attempt > maxRetries) {
|
|
1078
|
+
// All retries exhausted
|
|
1079
|
+
this.log.error(`Failed after ${maxRetries + 1} attempts: ${error.message}`);
|
|
1080
|
+
throw this.#createError(
|
|
1081
|
+
`Failed to check edge optimize status: ${error.message}`,
|
|
1082
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Exponential backoff: 200ms, 400ms, 800ms
|
|
1087
|
+
const delay = 100 * (2 ** attempt);
|
|
1088
|
+
this.log.warn(
|
|
1089
|
+
`Attempt ${attempt} to fetch failed: ${error.message}. Retrying in ${delay}ms...`,
|
|
1090
|
+
);
|
|
1091
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1092
|
+
await new Promise((res) => {
|
|
1093
|
+
setTimeout(res, delay);
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/* c8 ignore start */
|
|
1098
|
+
// This should never be reached, but needed for consistent-return
|
|
1099
|
+
throw this.#createError(
|
|
1100
|
+
'Failed to check edge optimize status after all retries',
|
|
1101
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
1102
|
+
);
|
|
1103
|
+
/* c8 ignore stop */
|
|
1104
|
+
}
|
|
1039
1105
|
}
|
|
1040
1106
|
|
|
1041
1107
|
// Export the client as default and base classes for custom implementations
|
|
@@ -104,7 +104,7 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
|
|
|
104
104
|
* Fetches HTML content from edge with warmup call and retry logic
|
|
105
105
|
* Makes an initial warmup call, waits, then makes the actual call with retries
|
|
106
106
|
* @param {string} url - Full URL to fetch
|
|
107
|
-
* @param {string} apiKey - Edge Optimize API key
|
|
107
|
+
* @param {string} apiKey - Edge Optimize API key (optional)
|
|
108
108
|
* @param {string} forwardedHost - Host to forward in x-forwarded-host header
|
|
109
109
|
* @param {string} edgeUrl - Edge URL
|
|
110
110
|
* @param {boolean} isOptimized - Whether to fetch optimized HTML (with preview param)
|
|
@@ -130,10 +130,6 @@ export async function fetchHtmlWithWarmup(
|
|
|
130
130
|
throw new Error('URL is required for fetching HTML');
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (!hasText(apiKey)) {
|
|
134
|
-
throw new Error('Edge Optimize API key is required for fetching HTML');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
133
|
if (!hasText(forwardedHost)) {
|
|
138
134
|
throw new Error('Forwarded host is required for fetching HTML');
|
|
139
135
|
}
|
|
@@ -158,7 +154,7 @@ export async function fetchHtmlWithWarmup(
|
|
|
158
154
|
|
|
159
155
|
const headers = {
|
|
160
156
|
'x-forwarded-host': forwardedHost,
|
|
161
|
-
'x-edgeoptimize-api-key': apiKey,
|
|
157
|
+
...(apiKey && { 'x-edgeoptimize-api-key': apiKey }),
|
|
162
158
|
'x-edgeoptimize-url': urlPath,
|
|
163
159
|
'Accept-Encoding': 'identity', // Disable compression to avoid content-length: 0 issue
|
|
164
160
|
};
|
package/test/index.test.js
CHANGED
|
@@ -2941,61 +2941,70 @@ describe('TokowakaClient', () => {
|
|
|
2941
2941
|
}
|
|
2942
2942
|
});
|
|
2943
2943
|
|
|
2944
|
-
it('should
|
|
2944
|
+
it('should preview suggestions successfully without metaconfig (optional)', async () => {
|
|
2945
|
+
// Override the stub from beforeEach to return null
|
|
2945
2946
|
client.fetchMetaconfig.resolves(null);
|
|
2946
2947
|
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
}
|
|
2954
|
-
});
|
|
2948
|
+
const result = await client.previewSuggestions(
|
|
2949
|
+
mockSite,
|
|
2950
|
+
mockOpportunity,
|
|
2951
|
+
mockSuggestions,
|
|
2952
|
+
{ warmupDelayMs: 0 },
|
|
2953
|
+
);
|
|
2955
2954
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2955
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
2956
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
2957
|
+
expect(result.config).to.exist;
|
|
2958
|
+
expect(result.html).to.exist;
|
|
2959
|
+
expect(result.html.originalHtml).to.equal('<html><body>Test HTML</body></html>');
|
|
2960
|
+
expect(result.html.optimizedHtml).to.equal('<html><body>Test HTML</body></html>');
|
|
2961
2961
|
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
expect.fail('Should have thrown error');
|
|
2965
|
-
} catch (error) {
|
|
2966
|
-
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
2967
|
-
expect(error.status).to.equal(500);
|
|
2968
|
-
}
|
|
2962
|
+
// Verify fetch was called without API key (undefined)
|
|
2963
|
+
expect(fetchStub.callCount).to.equal(4); // 2 warmup + 2 actual (original + optimized)
|
|
2969
2964
|
});
|
|
2970
2965
|
|
|
2971
|
-
it('should
|
|
2966
|
+
it('should preview suggestions successfully without apiKeys in metaconfig (optional)', async () => {
|
|
2967
|
+
// Override the stub from beforeEach
|
|
2972
2968
|
client.fetchMetaconfig.resolves({
|
|
2973
2969
|
siteId: 'site-123',
|
|
2974
|
-
apiKeys
|
|
2970
|
+
// apiKeys missing - should work without it
|
|
2975
2971
|
});
|
|
2976
2972
|
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2973
|
+
const result = await client.previewSuggestions(
|
|
2974
|
+
mockSite,
|
|
2975
|
+
mockOpportunity,
|
|
2976
|
+
mockSuggestions,
|
|
2977
|
+
{ warmupDelayMs: 0 },
|
|
2978
|
+
);
|
|
2979
|
+
|
|
2980
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
2981
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
2982
|
+
expect(result.config).to.exist;
|
|
2983
|
+
expect(result.html).to.exist;
|
|
2984
|
+
expect(result.html.originalHtml).to.equal('<html><body>Test HTML</body></html>');
|
|
2985
|
+
expect(result.html.optimizedHtml).to.equal('<html><body>Test HTML</body></html>');
|
|
2986
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
2984
2987
|
});
|
|
2985
2988
|
|
|
2986
|
-
it('should
|
|
2989
|
+
it('should preview suggestions successfully with empty apiKeys array (optional)', async () => {
|
|
2990
|
+
// Override the stub from beforeEach
|
|
2987
2991
|
client.fetchMetaconfig.resolves({
|
|
2988
2992
|
siteId: 'site-123',
|
|
2989
|
-
apiKeys: [
|
|
2993
|
+
apiKeys: [],
|
|
2990
2994
|
});
|
|
2991
2995
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2996
|
+
const result = await client.previewSuggestions(
|
|
2997
|
+
mockSite,
|
|
2998
|
+
mockOpportunity,
|
|
2999
|
+
mockSuggestions,
|
|
3000
|
+
{ warmupDelayMs: 0 },
|
|
3001
|
+
);
|
|
3002
|
+
|
|
3003
|
+
expect(result.succeededSuggestions).to.have.length(2);
|
|
3004
|
+
expect(result.failedSuggestions).to.have.length(0);
|
|
3005
|
+
expect(result.config).to.exist;
|
|
3006
|
+
expect(result.html).to.exist;
|
|
3007
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
2999
3008
|
});
|
|
3000
3009
|
|
|
3001
3010
|
it('should throw error for unsupported opportunity type', async () => {
|
|
@@ -3554,4 +3563,434 @@ describe('TokowakaClient', () => {
|
|
|
3554
3563
|
client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
|
|
3555
3564
|
});
|
|
3556
3565
|
});
|
|
3566
|
+
|
|
3567
|
+
describe('checkEdgeOptimizeStatus', () => {
|
|
3568
|
+
let fetchStub;
|
|
3569
|
+
|
|
3570
|
+
beforeEach(() => {
|
|
3571
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
3572
|
+
});
|
|
3573
|
+
|
|
3574
|
+
afterEach(() => {
|
|
3575
|
+
fetchStub.restore();
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
describe('Input Validation', () => {
|
|
3579
|
+
it('should throw error when site is not provided', async () => {
|
|
3580
|
+
try {
|
|
3581
|
+
await client.checkEdgeOptimizeStatus(null, '/');
|
|
3582
|
+
expect.fail('Should have thrown error');
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
expect(error.message).to.include('Site is required');
|
|
3585
|
+
expect(error.status).to.equal(400);
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
|
|
3589
|
+
it('should throw error when site is empty object', async () => {
|
|
3590
|
+
try {
|
|
3591
|
+
await client.checkEdgeOptimizeStatus({}, '/');
|
|
3592
|
+
expect.fail('Should have thrown error');
|
|
3593
|
+
} catch (error) {
|
|
3594
|
+
expect(error.message).to.include('Site is required');
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
|
|
3598
|
+
it('should throw error when path is not provided', async () => {
|
|
3599
|
+
const site = {
|
|
3600
|
+
getId: () => 'site-id',
|
|
3601
|
+
getBaseURL: () => 'https://example.com',
|
|
3602
|
+
getConfig: () => ({}),
|
|
3603
|
+
getDeliveryType: () => 'aem_edge',
|
|
3604
|
+
};
|
|
3605
|
+
|
|
3606
|
+
try {
|
|
3607
|
+
await client.checkEdgeOptimizeStatus(site, '');
|
|
3608
|
+
expect.fail('Should have thrown error');
|
|
3609
|
+
} catch (error) {
|
|
3610
|
+
expect(error.message).to.include('Path is required');
|
|
3611
|
+
expect(error.status).to.equal(400);
|
|
3612
|
+
}
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
it('should throw error when path is null', async () => {
|
|
3616
|
+
const site = {
|
|
3617
|
+
getId: () => 'site-id',
|
|
3618
|
+
getBaseURL: () => 'https://example.com',
|
|
3619
|
+
getConfig: () => ({}),
|
|
3620
|
+
getDeliveryType: () => 'aem_edge',
|
|
3621
|
+
};
|
|
3622
|
+
|
|
3623
|
+
try {
|
|
3624
|
+
await client.checkEdgeOptimizeStatus(site, null);
|
|
3625
|
+
expect.fail('Should have thrown error');
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
expect(error.message).to.include('Path is required');
|
|
3628
|
+
}
|
|
3629
|
+
});
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
describe('Direct Response (No Redirect)', () => {
|
|
3633
|
+
let site;
|
|
3634
|
+
|
|
3635
|
+
beforeEach(() => {
|
|
3636
|
+
site = {
|
|
3637
|
+
getId: () => 'site-id',
|
|
3638
|
+
getBaseURL: () => 'https://example.com',
|
|
3639
|
+
getConfig: () => ({}),
|
|
3640
|
+
getDeliveryType: () => 'aem_edge',
|
|
3641
|
+
};
|
|
3642
|
+
});
|
|
3643
|
+
|
|
3644
|
+
it('should return edgeOptimizeEnabled: true when x-tokowaka-request-id header is present', async () => {
|
|
3645
|
+
const headersMap = new Map([
|
|
3646
|
+
['x-tokowaka-request-id', 'abc123'],
|
|
3647
|
+
]);
|
|
3648
|
+
const mockResponse = {
|
|
3649
|
+
status: 200,
|
|
3650
|
+
headers: {
|
|
3651
|
+
get: (key) => headersMap.get(key) || null,
|
|
3652
|
+
},
|
|
3653
|
+
};
|
|
3654
|
+
|
|
3655
|
+
fetchStub.resolves(mockResponse);
|
|
3656
|
+
|
|
3657
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/');
|
|
3658
|
+
|
|
3659
|
+
expect(result).to.deep.equal({
|
|
3660
|
+
edgeOptimizeEnabled: true,
|
|
3661
|
+
});
|
|
3662
|
+
expect(fetchStub).to.have.been.calledOnce;
|
|
3663
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/');
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
it('should return edgeOptimizeEnabled: true when x-edgeoptimize-request-id header is present', async () => {
|
|
3667
|
+
const headersMap = new Map([
|
|
3668
|
+
['x-edgeoptimize-request-id', 'xyz789'],
|
|
3669
|
+
]);
|
|
3670
|
+
const mockResponse = {
|
|
3671
|
+
status: 200,
|
|
3672
|
+
headers: {
|
|
3673
|
+
get: (key) => headersMap.get(key) || null,
|
|
3674
|
+
},
|
|
3675
|
+
};
|
|
3676
|
+
|
|
3677
|
+
fetchStub.resolves(mockResponse);
|
|
3678
|
+
|
|
3679
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/products');
|
|
3680
|
+
|
|
3681
|
+
expect(result).to.deep.equal({
|
|
3682
|
+
edgeOptimizeEnabled: true,
|
|
3683
|
+
});
|
|
3684
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/products');
|
|
3685
|
+
});
|
|
3686
|
+
|
|
3687
|
+
it('should return edgeOptimizeEnabled: false when headers are not present', async () => {
|
|
3688
|
+
const mockResponse = {
|
|
3689
|
+
status: 200,
|
|
3690
|
+
headers: new Map(),
|
|
3691
|
+
};
|
|
3692
|
+
mockResponse.headers.get = () => null;
|
|
3693
|
+
|
|
3694
|
+
fetchStub.resolves(mockResponse);
|
|
3695
|
+
|
|
3696
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/');
|
|
3697
|
+
|
|
3698
|
+
expect(result).to.deep.equal({
|
|
3699
|
+
edgeOptimizeEnabled: false,
|
|
3700
|
+
});
|
|
3701
|
+
});
|
|
3702
|
+
|
|
3703
|
+
it('should work with 404 status and edge optimize enabled', async () => {
|
|
3704
|
+
const headersMap = new Map([
|
|
3705
|
+
['x-tokowaka-request-id', 'abc123'],
|
|
3706
|
+
]);
|
|
3707
|
+
const mockResponse = {
|
|
3708
|
+
status: 404,
|
|
3709
|
+
headers: {
|
|
3710
|
+
get: (key) => headersMap.get(key) || null,
|
|
3711
|
+
},
|
|
3712
|
+
};
|
|
3713
|
+
|
|
3714
|
+
fetchStub.resolves(mockResponse);
|
|
3715
|
+
|
|
3716
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/not-found');
|
|
3717
|
+
|
|
3718
|
+
expect(result).to.deep.equal({
|
|
3719
|
+
edgeOptimizeEnabled: true,
|
|
3720
|
+
});
|
|
3721
|
+
});
|
|
3722
|
+
|
|
3723
|
+
it('should send correct User-Agent header', async () => {
|
|
3724
|
+
const mockResponse = {
|
|
3725
|
+
status: 200,
|
|
3726
|
+
headers: new Map(),
|
|
3727
|
+
};
|
|
3728
|
+
mockResponse.headers.get = () => null;
|
|
3729
|
+
|
|
3730
|
+
fetchStub.resolves(mockResponse);
|
|
3731
|
+
|
|
3732
|
+
await client.checkEdgeOptimizeStatus(site, '/');
|
|
3733
|
+
|
|
3734
|
+
const fetchOptions = fetchStub.firstCall.args[1];
|
|
3735
|
+
expect(fetchOptions.headers['User-Agent']).to.equal('chatgpt-user');
|
|
3736
|
+
});
|
|
3737
|
+
});
|
|
3738
|
+
|
|
3739
|
+
describe('Retry Logic', () => {
|
|
3740
|
+
let site;
|
|
3741
|
+
let clock;
|
|
3742
|
+
|
|
3743
|
+
beforeEach(() => {
|
|
3744
|
+
site = {
|
|
3745
|
+
getId: () => 'site-id',
|
|
3746
|
+
getBaseURL: () => 'https://example.com',
|
|
3747
|
+
getConfig: () => ({}),
|
|
3748
|
+
getDeliveryType: () => 'aem_edge',
|
|
3749
|
+
};
|
|
3750
|
+
clock = sinon.useFakeTimers();
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
afterEach(() => {
|
|
3754
|
+
clock.restore();
|
|
3755
|
+
});
|
|
3756
|
+
|
|
3757
|
+
it('should retry 3 times on network error with exponential backoff', async () => {
|
|
3758
|
+
const networkError = new Error('Network timeout');
|
|
3759
|
+
fetchStub.rejects(networkError);
|
|
3760
|
+
|
|
3761
|
+
const promise = client.checkEdgeOptimizeStatus(site, '/');
|
|
3762
|
+
|
|
3763
|
+
// Wait for first attempt
|
|
3764
|
+
await clock.tickAsync(0);
|
|
3765
|
+
|
|
3766
|
+
// Wait for 200ms delay after first failure
|
|
3767
|
+
await clock.tickAsync(200);
|
|
3768
|
+
|
|
3769
|
+
// Wait for 400ms delay after second failure
|
|
3770
|
+
await clock.tickAsync(400);
|
|
3771
|
+
|
|
3772
|
+
// Wait for 800ms delay after third failure
|
|
3773
|
+
await clock.tickAsync(800);
|
|
3774
|
+
|
|
3775
|
+
try {
|
|
3776
|
+
await promise;
|
|
3777
|
+
expect.fail('Should have thrown error');
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
expect(error.message).to.include('Failed to check edge optimize status');
|
|
3780
|
+
expect(error.message).to.include('Network timeout');
|
|
3781
|
+
expect(error.status).to.equal(500);
|
|
3782
|
+
expect(fetchStub.callCount).to.equal(4); // Initial + 3 retries
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
|
|
3786
|
+
it('should succeed on second attempt after first failure', async () => {
|
|
3787
|
+
const headersMap = new Map([
|
|
3788
|
+
['x-tokowaka-request-id', 'abc123'],
|
|
3789
|
+
]);
|
|
3790
|
+
const mockResponse = {
|
|
3791
|
+
status: 200,
|
|
3792
|
+
headers: {
|
|
3793
|
+
get: (key) => headersMap.get(key) || null,
|
|
3794
|
+
},
|
|
3795
|
+
};
|
|
3796
|
+
|
|
3797
|
+
fetchStub.onFirstCall().rejects(new Error('Temporary failure'));
|
|
3798
|
+
fetchStub.onSecondCall().resolves(mockResponse);
|
|
3799
|
+
|
|
3800
|
+
const promise = client.checkEdgeOptimizeStatus(site, '/');
|
|
3801
|
+
|
|
3802
|
+
await clock.tickAsync(200);
|
|
3803
|
+
|
|
3804
|
+
const result = await promise;
|
|
3805
|
+
|
|
3806
|
+
expect(result).to.deep.equal({
|
|
3807
|
+
edgeOptimizeEnabled: true,
|
|
3808
|
+
});
|
|
3809
|
+
expect(fetchStub).to.have.been.calledTwice;
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3812
|
+
it('should succeed on third attempt after two failures', async () => {
|
|
3813
|
+
const headersMap = new Map([
|
|
3814
|
+
['x-edgeoptimize-request-id', 'xyz789'],
|
|
3815
|
+
]);
|
|
3816
|
+
const mockResponse = {
|
|
3817
|
+
status: 200,
|
|
3818
|
+
headers: {
|
|
3819
|
+
get: (key) => headersMap.get(key) || null,
|
|
3820
|
+
},
|
|
3821
|
+
};
|
|
3822
|
+
|
|
3823
|
+
fetchStub.onFirstCall().rejects(new Error('Failure 1'));
|
|
3824
|
+
fetchStub.onSecondCall().rejects(new Error('Failure 2'));
|
|
3825
|
+
fetchStub.onThirdCall().resolves(mockResponse);
|
|
3826
|
+
|
|
3827
|
+
const promise = client.checkEdgeOptimizeStatus(site, '/');
|
|
3828
|
+
|
|
3829
|
+
await clock.tickAsync(200); // First retry delay
|
|
3830
|
+
await clock.tickAsync(400); // Second retry delay
|
|
3831
|
+
|
|
3832
|
+
const result = await promise;
|
|
3833
|
+
|
|
3834
|
+
expect(result).to.deep.equal({
|
|
3835
|
+
edgeOptimizeEnabled: true,
|
|
3836
|
+
});
|
|
3837
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
3838
|
+
});
|
|
3839
|
+
|
|
3840
|
+
it('should log warnings on retries', async () => {
|
|
3841
|
+
const networkError = new Error('Connection refused');
|
|
3842
|
+
const mockResponse = {
|
|
3843
|
+
status: 200,
|
|
3844
|
+
headers: new Map(),
|
|
3845
|
+
};
|
|
3846
|
+
mockResponse.headers.get = () => null;
|
|
3847
|
+
|
|
3848
|
+
fetchStub.onFirstCall().rejects(networkError);
|
|
3849
|
+
fetchStub.onSecondCall().resolves(mockResponse);
|
|
3850
|
+
|
|
3851
|
+
const promise = client.checkEdgeOptimizeStatus(site, '/');
|
|
3852
|
+
await clock.tickAsync(200);
|
|
3853
|
+
await promise;
|
|
3854
|
+
|
|
3855
|
+
expect(log.warn).to.have.been.calledWith(
|
|
3856
|
+
sinon.match(/Attempt 1 to fetch failed.*Connection refused.*Retrying in 200ms/),
|
|
3857
|
+
);
|
|
3858
|
+
});
|
|
3859
|
+
});
|
|
3860
|
+
|
|
3861
|
+
describe('URL Construction', () => {
|
|
3862
|
+
let site;
|
|
3863
|
+
|
|
3864
|
+
beforeEach(() => {
|
|
3865
|
+
site = {
|
|
3866
|
+
getId: () => 'site-id',
|
|
3867
|
+
getBaseURL: () => 'https://example.com',
|
|
3868
|
+
getConfig: () => ({}),
|
|
3869
|
+
getDeliveryType: () => 'aem_edge',
|
|
3870
|
+
};
|
|
3871
|
+
});
|
|
3872
|
+
|
|
3873
|
+
it('should construct URL correctly with simple path', async () => {
|
|
3874
|
+
const mockResponse = {
|
|
3875
|
+
status: 200,
|
|
3876
|
+
headers: new Map(),
|
|
3877
|
+
};
|
|
3878
|
+
mockResponse.headers.get = () => null;
|
|
3879
|
+
|
|
3880
|
+
fetchStub.resolves(mockResponse);
|
|
3881
|
+
|
|
3882
|
+
await client.checkEdgeOptimizeStatus(site, '/products/chairs');
|
|
3883
|
+
|
|
3884
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/products/chairs');
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3887
|
+
it('should construct URL correctly with multi-level path', async () => {
|
|
3888
|
+
const mockResponse = {
|
|
3889
|
+
status: 200,
|
|
3890
|
+
headers: new Map(),
|
|
3891
|
+
};
|
|
3892
|
+
mockResponse.headers.get = () => null;
|
|
3893
|
+
|
|
3894
|
+
fetchStub.resolves(mockResponse);
|
|
3895
|
+
|
|
3896
|
+
await client.checkEdgeOptimizeStatus(site, '/a/b/c/d');
|
|
3897
|
+
|
|
3898
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/a/b/c/d');
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
it('should handle baseURL with trailing slash', async () => {
|
|
3902
|
+
site = {
|
|
3903
|
+
getId: () => 'site-id',
|
|
3904
|
+
getBaseURL: () => 'https://example.com/',
|
|
3905
|
+
getConfig: () => ({}),
|
|
3906
|
+
getDeliveryType: () => 'aem_edge',
|
|
3907
|
+
};
|
|
3908
|
+
|
|
3909
|
+
const mockResponse = {
|
|
3910
|
+
status: 200,
|
|
3911
|
+
headers: new Map(),
|
|
3912
|
+
};
|
|
3913
|
+
mockResponse.headers.get = () => null;
|
|
3914
|
+
|
|
3915
|
+
fetchStub.resolves(mockResponse);
|
|
3916
|
+
|
|
3917
|
+
await client.checkEdgeOptimizeStatus(site, '/about');
|
|
3918
|
+
|
|
3919
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/about');
|
|
3920
|
+
});
|
|
3921
|
+
|
|
3922
|
+
it('should handle baseURL without trailing slash', async () => {
|
|
3923
|
+
site = {
|
|
3924
|
+
getId: () => 'site-id',
|
|
3925
|
+
getBaseURL: () => 'https://example.com',
|
|
3926
|
+
getConfig: () => ({}),
|
|
3927
|
+
getDeliveryType: () => 'aem_edge',
|
|
3928
|
+
};
|
|
3929
|
+
|
|
3930
|
+
const mockResponse = {
|
|
3931
|
+
status: 200,
|
|
3932
|
+
headers: new Map(),
|
|
3933
|
+
};
|
|
3934
|
+
mockResponse.headers.get = () => null;
|
|
3935
|
+
|
|
3936
|
+
fetchStub.resolves(mockResponse);
|
|
3937
|
+
|
|
3938
|
+
await client.checkEdgeOptimizeStatus(site, '/about');
|
|
3939
|
+
|
|
3940
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://example.com/about');
|
|
3941
|
+
});
|
|
3942
|
+
});
|
|
3943
|
+
|
|
3944
|
+
describe('Edge Cases', () => {
|
|
3945
|
+
let site;
|
|
3946
|
+
|
|
3947
|
+
beforeEach(() => {
|
|
3948
|
+
site = {
|
|
3949
|
+
getId: () => 'site-id',
|
|
3950
|
+
getBaseURL: () => 'https://example.com',
|
|
3951
|
+
getConfig: () => ({}),
|
|
3952
|
+
getDeliveryType: () => 'aem_edge',
|
|
3953
|
+
};
|
|
3954
|
+
});
|
|
3955
|
+
|
|
3956
|
+
it('should handle both headers present', async () => {
|
|
3957
|
+
const headersMap = new Map([
|
|
3958
|
+
['x-tokowaka-request-id', 'abc123'],
|
|
3959
|
+
['x-edgeoptimize-request-id', 'xyz789'],
|
|
3960
|
+
]);
|
|
3961
|
+
const mockResponse = {
|
|
3962
|
+
status: 200,
|
|
3963
|
+
headers: {
|
|
3964
|
+
get: (key) => headersMap.get(key) || null,
|
|
3965
|
+
},
|
|
3966
|
+
};
|
|
3967
|
+
|
|
3968
|
+
fetchStub.resolves(mockResponse);
|
|
3969
|
+
|
|
3970
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/');
|
|
3971
|
+
|
|
3972
|
+
expect(result.edgeOptimizeEnabled).to.be.true;
|
|
3973
|
+
});
|
|
3974
|
+
|
|
3975
|
+
it('should handle 500 error with edge optimize header', async () => {
|
|
3976
|
+
const headersMap = new Map([
|
|
3977
|
+
['x-tokowaka-request-id', 'abc123'],
|
|
3978
|
+
]);
|
|
3979
|
+
const mockResponse = {
|
|
3980
|
+
status: 500,
|
|
3981
|
+
headers: {
|
|
3982
|
+
get: (key) => headersMap.get(key) || null,
|
|
3983
|
+
},
|
|
3984
|
+
};
|
|
3985
|
+
|
|
3986
|
+
fetchStub.resolves(mockResponse);
|
|
3987
|
+
|
|
3988
|
+
const result = await client.checkEdgeOptimizeStatus(site, '/error');
|
|
3989
|
+
|
|
3990
|
+
expect(result).to.deep.equal({
|
|
3991
|
+
edgeOptimizeEnabled: true,
|
|
3992
|
+
});
|
|
3993
|
+
});
|
|
3994
|
+
});
|
|
3995
|
+
});
|
|
3557
3996
|
});
|
|
@@ -82,20 +82,33 @@ describe('HTML Utils', () => {
|
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it('should
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
'
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
it('should successfully fetch HTML without apiKey (optional parameter)', async () => {
|
|
86
|
+
fetchStub.resolves({
|
|
87
|
+
ok: true,
|
|
88
|
+
status: 200,
|
|
89
|
+
statusText: 'OK',
|
|
90
|
+
headers: {
|
|
91
|
+
get: (name) => (name === 'x-edgeoptimize-cache' ? 'HIT' : null),
|
|
92
|
+
},
|
|
93
|
+
text: async () => '<html>Test HTML without API key</html>',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const html = await fetchHtmlWithWarmup(
|
|
97
|
+
'https://example.com/page',
|
|
98
|
+
null, // apiKey is optional
|
|
99
|
+
'host',
|
|
100
|
+
'https://edge.example.com',
|
|
101
|
+
log,
|
|
102
|
+
false,
|
|
103
|
+
{ warmupDelayMs: 0 },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(html).to.equal('<html>Test HTML without API key</html>');
|
|
107
|
+
expect(fetchStub.callCount).to.equal(2); // warmup + actual
|
|
108
|
+
|
|
109
|
+
// Verify that x-edgeoptimize-api-key header is not present
|
|
110
|
+
const fetchOptions = fetchStub.firstCall.args[1];
|
|
111
|
+
expect(fetchOptions.headers).to.not.have.property('x-edgeoptimize-api-key');
|
|
99
112
|
});
|
|
100
113
|
|
|
101
114
|
it('should successfully fetch HTML with all required parameters', async () => {
|