@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
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
- if (!metaconfig) {
919
- throw this.#createError(
920
- 'No domain-level metaconfig found. '
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
  };
@@ -2941,61 +2941,70 @@ describe('TokowakaClient', () => {
2941
2941
  }
2942
2942
  });
2943
2943
 
2944
- it('should throw error if metaconfig does not exist', async () => {
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
- try {
2948
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2949
- expect.fail('Should have thrown error');
2950
- } catch (error) {
2951
- expect(error.message).to.include('No domain-level metaconfig found');
2952
- expect(error.status).to.equal(500);
2953
- }
2954
- });
2948
+ const result = await client.previewSuggestions(
2949
+ mockSite,
2950
+ mockOpportunity,
2951
+ mockSuggestions,
2952
+ { warmupDelayMs: 0 },
2953
+ );
2955
2954
 
2956
- it('should throw error if metaconfig does not have apiKeys', async () => {
2957
- client.fetchMetaconfig.resolves({
2958
- siteId: 'site-123',
2959
- // apiKeys missing
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
- try {
2963
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
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 throw error if metaconfig has empty apiKeys array', async () => {
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
- try {
2978
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2979
- expect.fail('Should have thrown error');
2980
- } catch (error) {
2981
- expect(error.message).to.include('Metaconfig does not have valid API keys configured');
2982
- expect(error.status).to.equal(500);
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 throw error if metaconfig apiKeys first value is empty', async () => {
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: ['', 'test-api-key-2'],
2993
+ apiKeys: [],
2990
2994
  });
2991
2995
 
2992
- try {
2993
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2994
- expect.fail('Should have thrown error');
2995
- } catch (error) {
2996
- expect(error.message).to.include('Metaconfig does not have valid API keys configured');
2997
- expect(error.status).to.equal(500);
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 throw error when apiKey is missing', async () => {
86
- try {
87
- await fetchHtmlWithWarmup(
88
- 'https://example.com/page',
89
- '',
90
- 'host',
91
- 'edge-url',
92
- log,
93
- false,
94
- );
95
- expect.fail('Should have thrown error');
96
- } catch (error) {
97
- expect(error.message).to.equal('Edge Optimize API key is required for fetching HTML');
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 () => {