@adobe/spacecat-shared-tokowaka-client 1.4.0 → 1.4.2

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.
@@ -64,8 +64,7 @@ describe('TokowakaClient', () => {
64
64
  getBaseURL: () => 'https://example.com',
65
65
  getConfig: () => ({
66
66
  getTokowakaConfig: () => ({
67
- forwardedHost: 'example.com',
68
- apiKey: 'test-api-key',
67
+ forwardedHost: 'www.example.com',
69
68
  }),
70
69
  }),
71
70
  };
@@ -368,25 +367,6 @@ describe('TokowakaClient', () => {
368
367
  expect(command.input.Key).to.equal('opportunities/example.com/config');
369
368
  });
370
369
 
371
- it('should fetch metaconfig from preview bucket', async () => {
372
- const metaconfig = {
373
- siteId: 'site-123',
374
- prerender: true,
375
- };
376
-
377
- s3Client.send.resolves({
378
- Body: {
379
- transformToString: async () => JSON.stringify(metaconfig),
380
- },
381
- });
382
-
383
- await client.fetchMetaconfig('https://example.com/page1', true);
384
-
385
- const command = s3Client.send.firstCall.args[0];
386
- expect(command.input.Bucket).to.equal('test-preview-bucket');
387
- expect(command.input.Key).to.equal('preview/opportunities/example.com/config');
388
- });
389
-
390
370
  it('should return null if metaconfig does not exist', async () => {
391
371
  const noSuchKeyError = new Error('NoSuchKey');
392
372
  noSuchKeyError.name = 'NoSuchKey';
@@ -441,20 +421,6 @@ describe('TokowakaClient', () => {
441
421
  expect(JSON.parse(command.input.Body)).to.deep.equal(metaconfig);
442
422
  });
443
423
 
444
- it('should upload metaconfig to preview bucket', async () => {
445
- const metaconfig = {
446
- siteId: 'site-123',
447
- prerender: true,
448
- };
449
-
450
- const s3Path = await client.uploadMetaconfig('https://example.com/page1', metaconfig, true);
451
-
452
- expect(s3Path).to.equal('preview/opportunities/example.com/config');
453
-
454
- const command = s3Client.send.firstCall.args[0];
455
- expect(command.input.Bucket).to.equal('test-preview-bucket');
456
- });
457
-
458
424
  it('should throw error if URL is missing', async () => {
459
425
  try {
460
426
  await client.uploadMetaconfig('', { siteId: 'site-123', prerender: true });
@@ -810,16 +776,19 @@ describe('TokowakaClient', () => {
810
776
 
811
777
  describe('deploySuggestions', () => {
812
778
  beforeEach(() => {
813
- // Stub CDN invalidation for deploy tests
814
- sinon.stub(client, 'invalidateCdnCache').resolves({
779
+ // Stub CDN invalidation for deploy tests (now handles both single and batch)
780
+ sinon.stub(client, 'invalidateCdnCache').resolves([{
815
781
  status: 'success',
816
782
  provider: 'cloudfront',
817
783
  invalidationId: 'I123',
818
- });
784
+ }]);
819
785
  // Stub fetchConfig to return null by default (no existing config)
820
786
  sinon.stub(client, 'fetchConfig').resolves(null);
821
- // Stub fetchMetaconfig to return null by default (will create new)
822
- sinon.stub(client, 'fetchMetaconfig').resolves(null);
787
+ // Stub fetchMetaconfig to return existing metaconfig (required for deployment)
788
+ sinon.stub(client, 'fetchMetaconfig').resolves({
789
+ siteId: 'site-123',
790
+ prerender: true,
791
+ });
823
792
  // Stub uploadMetaconfig
824
793
  sinon.stub(client, 'uploadMetaconfig').resolves('opportunities/example.com/config');
825
794
  });
@@ -835,43 +804,28 @@ describe('TokowakaClient', () => {
835
804
  expect(result.s3Paths).to.be.an('array').with.length(1);
836
805
  expect(result.s3Paths[0]).to.equal('opportunities/example.com/L3BhZ2Ux');
837
806
  expect(result).to.have.property('cdnInvalidations');
807
+ // Only 1 invalidation result returned (for batch URLs)
808
+ // Metaconfig invalidation happens inside uploadMetaconfig() automatically
838
809
  expect(result.cdnInvalidations).to.be.an('array').with.length(1);
839
810
  expect(result.succeededSuggestions).to.have.length(2);
840
811
  expect(result.failedSuggestions).to.have.length(0);
841
812
  expect(s3Client.send).to.have.been.called;
842
813
  });
843
814
 
844
- it('should create metaconfig on first deployment', async () => {
845
- await client.deploySuggestions(
846
- mockSite,
847
- mockOpportunity,
848
- mockSuggestions,
849
- );
850
-
851
- expect(client.fetchMetaconfig).to.have.been.calledOnce;
852
- expect(client.uploadMetaconfig).to.have.been.calledOnce;
853
-
854
- const metaconfigArg = client.uploadMetaconfig.firstCall.args[1];
855
- expect(metaconfigArg).to.deep.include({
856
- siteId: 'site-123',
857
- prerender: true,
858
- });
859
- });
860
-
861
- it('should reuse existing metaconfig', async () => {
862
- client.fetchMetaconfig.resolves({
863
- siteId: 'site-123',
864
- prerender: true,
865
- });
815
+ it('should throw error if metaconfig does not exist', async () => {
816
+ client.fetchMetaconfig.resolves(null);
866
817
 
867
- await client.deploySuggestions(
868
- mockSite,
869
- mockOpportunity,
870
- mockSuggestions,
871
- );
872
-
873
- expect(client.fetchMetaconfig).to.have.been.calledOnce;
874
- expect(client.uploadMetaconfig).to.not.have.been.called;
818
+ try {
819
+ await client.deploySuggestions(
820
+ mockSite,
821
+ mockOpportunity,
822
+ mockSuggestions,
823
+ );
824
+ expect.fail('Should have thrown error');
825
+ } catch (error) {
826
+ expect(error.message).to.include('No domain-level metaconfig found');
827
+ expect(error.status).to.equal(400);
828
+ }
875
829
  });
876
830
 
877
831
  it('should handle suggestions for multiple URLs', async () => {
@@ -911,7 +865,9 @@ describe('TokowakaClient', () => {
911
865
  );
912
866
 
913
867
  expect(result.s3Paths).to.have.length(2);
914
- expect(result.cdnInvalidations).to.have.length(2);
868
+ // Only 1 invalidation result returned (for batch URLs)
869
+ // Metaconfig invalidation happens inside uploadMetaconfig() automatically
870
+ expect(result.cdnInvalidations).to.have.length(1);
915
871
  expect(result.succeededSuggestions).to.have.length(2);
916
872
  });
917
873
 
@@ -1147,6 +1103,8 @@ describe('TokowakaClient', () => {
1147
1103
  expect(result.succeededSuggestions).to.have.length(1);
1148
1104
  expect(result.failedSuggestions).to.have.length(0);
1149
1105
  expect(result.s3Paths).to.have.length(1);
1106
+ // Only 1 invalidation result returned (for batch URLs)
1107
+ // Metaconfig invalidation happens inside uploadMetaconfig() automatically
1150
1108
  expect(result.cdnInvalidations).to.have.length(1);
1151
1109
 
1152
1110
  // Verify uploaded config has no patches but prerender is enabled
@@ -1155,12 +1113,12 @@ describe('TokowakaClient', () => {
1155
1113
  expect(uploadedConfig.prerender).to.equal(true);
1156
1114
  expect(uploadedConfig.url).to.equal('https://example.com/page1');
1157
1115
 
1158
- // Verify CDN was invalidated
1116
+ // Verify CDN was invalidated using batch method with new options signature
1159
1117
  expect(client.invalidateCdnCache).to.have.been.calledOnce;
1160
- expect(client.invalidateCdnCache).to.have.been.calledWith(
1161
- 'https://example.com/page1',
1162
- 'cloudfront',
1163
- );
1118
+ const invalidateCall = client.invalidateCdnCache.firstCall.args[0];
1119
+ expect(invalidateCall).to.deep.include({
1120
+ urls: ['https://example.com/page1'],
1121
+ });
1164
1122
  });
1165
1123
 
1166
1124
  it('should throw error for unsupported opportunity type', async () => {
@@ -1254,12 +1212,12 @@ describe('TokowakaClient', () => {
1254
1212
 
1255
1213
  describe('rollbackSuggestions', () => {
1256
1214
  beforeEach(() => {
1257
- // Stub CDN invalidation for rollback tests
1258
- sinon.stub(client, 'invalidateCdnCache').resolves({
1215
+ // Stub CDN invalidation for rollback tests (now handles both single and batch)
1216
+ sinon.stub(client, 'invalidateCdnCache').resolves([{
1259
1217
  status: 'success',
1260
1218
  provider: 'cloudfront',
1261
1219
  invalidationId: 'I123',
1262
- });
1220
+ }]);
1263
1221
  });
1264
1222
 
1265
1223
  it('should rollback suggestions successfully', async () => {
@@ -1375,12 +1333,12 @@ describe('TokowakaClient', () => {
1375
1333
  expect(uploadedConfig.patches).to.have.length(1);
1376
1334
  expect(uploadedConfig.patches[0].suggestionId).to.equal('other-sugg-1');
1377
1335
 
1378
- // Verify CDN was invalidated
1336
+ // Verify CDN was invalidated using batch method with new options signature
1379
1337
  expect(client.invalidateCdnCache).to.have.been.calledOnce;
1380
- expect(client.invalidateCdnCache).to.have.been.calledWith(
1381
- 'https://example.com/page1',
1382
- 'cloudfront',
1383
- );
1338
+ const invalidateCall = client.invalidateCdnCache.firstCall.args[0];
1339
+ expect(invalidateCall).to.deep.include({
1340
+ urls: ['https://example.com/page1'],
1341
+ });
1384
1342
  });
1385
1343
 
1386
1344
  it('should handle no existing config gracefully', async () => {
@@ -1629,7 +1587,8 @@ describe('TokowakaClient', () => {
1629
1587
  );
1630
1588
 
1631
1589
  expect(result.s3Paths).to.have.length(2);
1632
- expect(result.cdnInvalidations).to.have.length(2);
1590
+ // Batch invalidation returns 1 result per CDN provider, not per URL
1591
+ expect(result.cdnInvalidations).to.have.length(1);
1633
1592
  expect(result.succeededSuggestions).to.have.length(2);
1634
1593
  });
1635
1594
 
@@ -1738,14 +1697,16 @@ describe('TokowakaClient', () => {
1738
1697
  // Stub fetchConfig to return null by default (no existing config)
1739
1698
  sinon.stub(client, 'fetchConfig').resolves(null);
1740
1699
 
1741
- // Add TOKOWAKA_EDGE_URL to env
1700
+ // Add TOKOWAKA_EDGE_URL and TOKOWAKA_PREVIEW_API_KEY to env
1742
1701
  client.env.TOKOWAKA_EDGE_URL = 'https://edge-dev.tokowaka.now';
1702
+ client.env.TOKOWAKA_PREVIEW_API_KEY = 'internal-preview-key-123';
1743
1703
  });
1744
1704
 
1745
1705
  afterEach(() => {
1746
1706
  // fetchStub will be restored by global afterEach sinon.restore()
1747
1707
  // Just clean up env changes
1748
1708
  delete client.env.TOKOWAKA_EDGE_URL;
1709
+ delete client.env.TOKOWAKA_PREVIEW_API_KEY;
1749
1710
  });
1750
1711
 
1751
1712
  it('should preview suggestions successfully with HTML', async () => {
@@ -1774,6 +1735,18 @@ describe('TokowakaClient', () => {
1774
1735
  expect(s3Client.send).to.have.been.calledOnce;
1775
1736
  });
1776
1737
 
1738
+ it('should throw error if TOKOWAKA_PREVIEW_API_KEY is not configured', async () => {
1739
+ delete client.env.TOKOWAKA_PREVIEW_API_KEY;
1740
+
1741
+ try {
1742
+ await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
1743
+ expect.fail('Should have thrown error');
1744
+ } catch (error) {
1745
+ expect(error.message).to.include('TOKOWAKA_PREVIEW_API_KEY is required for preview');
1746
+ expect(error.status).to.equal(500);
1747
+ }
1748
+ });
1749
+
1777
1750
  it('should preview prerender-only suggestions with no patches', async () => {
1778
1751
  // Update fetchConfig to return existing config with deployed patches
1779
1752
  client.fetchConfig.resolves({
@@ -1853,7 +1826,7 @@ describe('TokowakaClient', () => {
1853
1826
  await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
1854
1827
  expect.fail('Should have thrown error');
1855
1828
  } catch (error) {
1856
- expect(error.message).to.include('Site does not have a Tokowaka API key or forwarded host configured');
1829
+ expect(error.message).to.include('Site does not have a Tokowaka forwarded host configured');
1857
1830
  expect(error.status).to.equal(400);
1858
1831
  }
1859
1832
  });
@@ -1867,7 +1840,7 @@ describe('TokowakaClient', () => {
1867
1840
  await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
1868
1841
  expect.fail('Should have thrown error');
1869
1842
  } catch (error) {
1870
- expect(error.message).to.include('Site does not have a Tokowaka API key or forwarded host configured');
1843
+ expect(error.message).to.include('Site does not have a Tokowaka forwarded host configured');
1871
1844
  expect(error.status).to.equal(400);
1872
1845
  }
1873
1846
  });
@@ -2048,10 +2021,11 @@ describe('TokowakaClient', () => {
2048
2021
  );
2049
2022
 
2050
2023
  expect(client.invalidateCdnCache).to.have.been.calledOnce;
2051
- const { firstCall } = client.invalidateCdnCache;
2052
- expect(firstCall.args[0]).to.equal('https://example.com/page1');
2053
- expect(firstCall.args[1]).to.equal('cloudfront');
2054
- expect(firstCall.args[2]).to.be.true; // isPreview
2024
+ const invalidateCall = client.invalidateCdnCache.firstCall.args[0];
2025
+ expect(invalidateCall).to.deep.include({
2026
+ urls: ['https://example.com/page1'],
2027
+ isPreview: true,
2028
+ });
2055
2029
  });
2056
2030
 
2057
2031
  it('should throw error if suggestions span multiple URLs', async () => {
@@ -2118,9 +2092,12 @@ describe('TokowakaClient', () => {
2118
2092
  });
2119
2093
 
2120
2094
  it('should invalidate CDN cache successfully', async () => {
2121
- const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
2095
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2122
2096
 
2123
- expect(result).to.deep.equal({
2097
+ // Now returns array with one result per provider
2098
+ expect(result).to.be.an('array');
2099
+ expect(result).to.have.lengthOf(1);
2100
+ expect(result[0]).to.deep.equal({
2124
2101
  status: 'success',
2125
2102
  provider: 'cloudfront',
2126
2103
  invalidationId: 'I123',
@@ -2129,63 +2106,299 @@ describe('TokowakaClient', () => {
2129
2106
  expect(mockCdnClient.invalidateCache).to.have.been.calledWith([
2130
2107
  '/opportunities/example.com/L3BhZ2Ux',
2131
2108
  ]);
2132
- expect(log.debug).to.have.been.calledWith(sinon.match(/Invalidating CDN cache/));
2109
+ expect(log.info).to.have.been.calledWith(sinon.match(/Invalidating CDN cache/));
2133
2110
  expect(log.info).to.have.been.calledWith(sinon.match(/CDN cache invalidation completed/));
2134
2111
  });
2135
2112
 
2136
2113
  it('should invalidate CDN cache for preview path', async () => {
2137
- await client.invalidateCdnCache('https://example.com/page1', 'cloudfront', true);
2114
+ await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront', isPreview: true });
2138
2115
 
2139
2116
  expect(mockCdnClient.invalidateCache).to.have.been.calledWith([
2140
2117
  '/preview/opportunities/example.com/L3BhZ2Ux',
2141
2118
  ]);
2142
2119
  });
2143
2120
 
2144
- it('should throw error if URL is missing', async () => {
2145
- try {
2146
- await client.invalidateCdnCache('', 'cloudfront');
2147
- expect.fail('Should have thrown error');
2148
- } catch (error) {
2149
- expect(error.message).to.equal('URL and provider are required');
2150
- expect(error.status).to.equal(400);
2151
- }
2121
+ it('should return empty array if URL array is empty', async () => {
2122
+ const result = await client.invalidateCdnCache({ urls: [], providers: 'cloudfront' });
2123
+ expect(result).to.deep.equal([]);
2152
2124
  });
2153
2125
 
2154
- it('should throw error if provider is missing', async () => {
2155
- try {
2156
- await client.invalidateCdnCache('https://example.com/page1', '');
2157
- expect.fail('Should have thrown error');
2158
- } catch (error) {
2159
- expect(error.message).to.equal('URL and provider are required');
2160
- expect(error.status).to.equal(400);
2161
- }
2126
+ it('should return empty array if provider is missing', async () => {
2127
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: '' });
2128
+ expect(result).to.deep.equal([]);
2129
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2130
+ });
2131
+
2132
+ it('should return error object if no CDN client available', async () => {
2133
+ client.cdnClientRegistry.getClient.returns(null);
2134
+
2135
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2136
+
2137
+ // Now returns array with one result per provider
2138
+ expect(result).to.be.an('array');
2139
+ expect(result).to.have.lengthOf(1);
2140
+ expect(result[0]).to.deep.equal({
2141
+ status: 'error',
2142
+ provider: 'cloudfront',
2143
+ message: 'No CDN client available for provider: cloudfront',
2144
+ });
2145
+ });
2146
+
2147
+ it('should return error object if CDN invalidation fails', async () => {
2148
+ mockCdnClient.invalidateCache.rejects(new Error('CDN API error'));
2149
+
2150
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2151
+
2152
+ // Now returns array with one result per provider
2153
+ expect(result).to.be.an('array');
2154
+ expect(result).to.have.lengthOf(1);
2155
+ expect(result[0]).to.deep.equal({
2156
+ status: 'error',
2157
+ provider: 'cloudfront',
2158
+ message: 'CDN API error',
2159
+ });
2160
+
2161
+ expect(log.warn).to.have.been.calledWith(sinon.match(/Failed to invalidate cloudfront CDN cache/));
2162
+ });
2163
+ });
2164
+
2165
+ describe('invalidateCdnCache (batch/multiple URLs)', () => {
2166
+ let mockCdnClient;
2167
+
2168
+ beforeEach(() => {
2169
+ mockCdnClient = {
2170
+ invalidateCache: sinon.stub().resolves({
2171
+ status: 'success',
2172
+ provider: 'cloudfront',
2173
+ invalidationId: 'I123',
2174
+ }),
2175
+ };
2176
+
2177
+ sinon.stub(client.cdnClientRegistry, 'getClient').returns(mockCdnClient);
2178
+ });
2179
+
2180
+ it('should invalidate CDN cache for multiple URLs (batch)', async () => {
2181
+ const urls = [
2182
+ 'https://example.com/page1',
2183
+ 'https://example.com/page2',
2184
+ 'https://example.com/page3',
2185
+ ];
2186
+
2187
+ // Pass array of URLs for batch invalidation
2188
+ const result = await client.invalidateCdnCache({ urls, providers: 'cloudfront' });
2189
+
2190
+ expect(result).to.be.an('array');
2191
+ expect(result).to.have.lengthOf(1);
2192
+ expect(result[0]).to.deep.equal({
2193
+ status: 'success',
2194
+ provider: 'cloudfront',
2195
+ invalidationId: 'I123',
2196
+ });
2197
+
2198
+ expect(mockCdnClient.invalidateCache).to.have.been.calledWith([
2199
+ '/opportunities/example.com/L3BhZ2Ux',
2200
+ '/opportunities/example.com/L3BhZ2Uy',
2201
+ '/opportunities/example.com/L3BhZ2Uz',
2202
+ ]);
2203
+ expect(log.info).to.have.been.calledWith(sinon.match(/Invalidating CDN cache for 3 path\(s\)/));
2204
+ });
2205
+
2206
+ it('should return empty array for empty URLs array', async () => {
2207
+ const result = await client.invalidateCdnCache({ urls: [], providers: 'cloudfront' });
2208
+ expect(result).to.deep.equal([]);
2209
+ });
2210
+
2211
+ it('should return empty array if providers is empty', async () => {
2212
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: '' });
2213
+ expect(result).to.deep.equal([]);
2214
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2162
2215
  });
2163
2216
 
2164
2217
  it('should return error object if no CDN client available', async () => {
2165
2218
  client.cdnClientRegistry.getClient.returns(null);
2166
2219
 
2167
- const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
2220
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2168
2221
 
2169
- expect(result).to.deep.equal({
2222
+ expect(result).to.be.an('array');
2223
+ expect(result).to.have.lengthOf(1);
2224
+ expect(result[0]).to.deep.equal({
2170
2225
  status: 'error',
2171
2226
  provider: 'cloudfront',
2172
2227
  message: 'No CDN client available for provider: cloudfront',
2173
2228
  });
2174
- expect(log.error).to.have.been.calledWith(sinon.match(/Failed to invalidate Tokowaka CDN cache/));
2175
2229
  });
2176
2230
 
2177
2231
  it('should return error object if CDN invalidation fails', async () => {
2178
2232
  mockCdnClient.invalidateCache.rejects(new Error('CDN API error'));
2179
2233
 
2180
- const result = await client.invalidateCdnCache('https://example.com/page1', 'cloudfront');
2234
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2181
2235
 
2182
- expect(result).to.deep.equal({
2236
+ expect(result).to.be.an('array');
2237
+ expect(result).to.have.lengthOf(1);
2238
+ expect(result[0]).to.deep.equal({
2183
2239
  status: 'error',
2184
2240
  provider: 'cloudfront',
2185
2241
  message: 'CDN API error',
2186
2242
  });
2243
+ });
2244
+
2245
+ it('should handle multiple providers in parallel', async () => {
2246
+ const mockFastlyClient = {
2247
+ invalidateCache: sinon.stub().resolves({
2248
+ status: 'success',
2249
+ provider: 'fastly',
2250
+ purgeId: 'F456',
2251
+ }),
2252
+ };
2253
+
2254
+ client.cdnClientRegistry.getClient.withArgs('cloudfront').returns(mockCdnClient);
2255
+ client.cdnClientRegistry.getClient.withArgs('fastly').returns(mockFastlyClient);
2256
+
2257
+ const result = await client.invalidateCdnCache({
2258
+ urls: ['https://example.com/page1'],
2259
+ providers: ['cloudfront', 'fastly'],
2260
+ });
2261
+
2262
+ expect(result).to.be.an('array');
2263
+ expect(result).to.have.lengthOf(2);
2264
+ expect(result[0].provider).to.equal('cloudfront');
2265
+ expect(result[1].provider).to.equal('fastly');
2266
+
2267
+ expect(mockCdnClient.invalidateCache).to.have.been.calledOnce;
2268
+ expect(mockFastlyClient.invalidateCache).to.have.been.calledOnce;
2269
+ });
2270
+
2271
+ it('should handle errors from getClient', async () => {
2272
+ // Simulate an error when getting the CDN client
2273
+ client.cdnClientRegistry.getClient.restore();
2274
+ sinon.stub(client.cdnClientRegistry, 'getClient').throws(new Error('Unexpected error'));
2275
+
2276
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2277
+
2278
+ expect(result).to.be.an('array');
2279
+ expect(result).to.have.lengthOf(1);
2280
+ expect(result[0]).to.deep.equal({
2281
+ status: 'error',
2282
+ provider: 'cloudfront',
2283
+ message: 'Unexpected error',
2284
+ });
2285
+
2286
+ // Error is caught in provider-specific error handler
2287
+ expect(log.warn).to.have.been.calledWith(sinon.match(/Failed to invalidate cloudfront CDN cache/));
2288
+ });
2289
+
2290
+ it('should handle empty CDN provider config', async () => {
2291
+ // Test with existing client but passing no providers
2292
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: [] });
2293
+
2294
+ expect(result).to.be.an('array');
2295
+ expect(result).to.have.lengthOf(0);
2296
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2297
+ });
2298
+
2299
+ it('should handle multiple providers passed as array', async () => {
2300
+ const mockFastlyClient = {
2301
+ invalidateCache: sinon.stub().resolves({
2302
+ status: 'success',
2303
+ provider: 'fastly',
2304
+ }),
2305
+ };
2306
+
2307
+ client.cdnClientRegistry.getClient.restore();
2308
+ sinon.stub(client.cdnClientRegistry, 'getClient')
2309
+ .withArgs('cloudfront')
2310
+ .returns(mockCdnClient)
2311
+ .withArgs('fastly')
2312
+ .returns(mockFastlyClient);
2313
+
2314
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: ['cloudfront', 'fastly'] });
2315
+
2316
+ expect(result).to.be.an('array');
2317
+ expect(result).to.have.lengthOf(2);
2318
+ expect(mockCdnClient.invalidateCache).to.have.been.calledOnce;
2319
+ expect(mockFastlyClient.invalidateCache).to.have.been.calledOnce;
2320
+ });
2321
+
2322
+ it('should handle empty paths after filtering', async () => {
2323
+ // Call the method with URLs to test path generation
2324
+ const result = await client.invalidateCdnCache({ urls: ['https://example.com/page1'], providers: 'cloudfront' });
2325
+
2326
+ // Should successfully invalidate (paths are generated from URL)
2327
+ expect(result).to.be.an('array');
2328
+ expect(result).to.have.lengthOf(1);
2329
+ });
2330
+ });
2331
+
2332
+ describe('#getCdnProviders (edge cases)', () => {
2333
+ it('should handle missing CDN provider config (lines 105-106)', async () => {
2334
+ // Temporarily remove TOKOWAKA_CDN_PROVIDER to test early return
2335
+ const originalProvider = client.env.TOKOWAKA_CDN_PROVIDER;
2336
+ delete client.env.TOKOWAKA_CDN_PROVIDER;
2337
+
2338
+ // Call uploadMetaconfig which uses #getCdnProviders internally
2339
+ await client.uploadMetaconfig('https://example.com/page1', { siteId: 'test', prerender: true });
2340
+
2341
+ // No CDN invalidation should happen (no providers configured)
2342
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2343
+
2344
+ // Restore
2345
+ client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
2346
+ });
2347
+
2348
+ it('should handle array provider config with falsy values (line 111)', async () => {
2349
+ // Temporarily modify env to test array filtering
2350
+ const originalProvider = client.env.TOKOWAKA_CDN_PROVIDER;
2351
+ client.env.TOKOWAKA_CDN_PROVIDER = ['cloudfront', '', null, undefined]; // Array with falsy values
2352
+
2353
+ const mockCloudFrontClient = {
2354
+ invalidateCache: sinon.stub().resolves({
2355
+ status: 'success',
2356
+ provider: 'cloudfront',
2357
+ }),
2358
+ };
2359
+
2360
+ sinon.stub(client.cdnClientRegistry, 'getClient')
2361
+ .withArgs('cloudfront')
2362
+ .returns(mockCloudFrontClient);
2363
+
2364
+ // Call uploadMetaconfig which uses #getCdnProviders internally
2365
+ await client.uploadMetaconfig('https://example.com/page1', { siteId: 'test', prerender: true });
2366
+
2367
+ // Should only use 'cloudfront' (falsy values filtered out)
2368
+ expect(mockCloudFrontClient.invalidateCache).to.have.been.calledOnce;
2369
+
2370
+ // Restore
2371
+ client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
2372
+ });
2373
+
2374
+ it('should handle invalid CDN provider type - number (lines 120-121)', async () => {
2375
+ // Temporarily modify env to test invalid type
2376
+ const originalProvider = client.env.TOKOWAKA_CDN_PROVIDER;
2377
+ client.env.TOKOWAKA_CDN_PROVIDER = 12345; // Invalid type (number)
2378
+
2379
+ // Call uploadMetaconfig which uses #getCdnProviders internally
2380
+ await client.uploadMetaconfig('https://example.com/page1', { siteId: 'test', prerender: true });
2381
+
2382
+ // No CDN invalidation should happen (no providers)
2383
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2384
+
2385
+ // Restore
2386
+ client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
2387
+ });
2388
+
2389
+ it('should handle invalid CDN provider type - object (lines 120-121)', async () => {
2390
+ // Temporarily modify env to test invalid type
2391
+ const originalProvider = client.env.TOKOWAKA_CDN_PROVIDER;
2392
+ client.env.TOKOWAKA_CDN_PROVIDER = { key: 'value' }; // Invalid type (object)
2393
+
2394
+ // Call uploadMetaconfig which uses #getCdnProviders internally
2395
+ await client.uploadMetaconfig('https://example.com/page1', { siteId: 'test', prerender: true });
2396
+
2397
+ // No CDN invalidation should happen (no providers)
2398
+ expect(log.warn).to.have.been.calledWith('No CDN providers specified for cache invalidation');
2187
2399
 
2188
- expect(log.error).to.have.been.calledWith(sinon.match(/Failed to invalidate Tokowaka CDN cache/));
2400
+ // Restore
2401
+ client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
2189
2402
  });
2190
2403
  });
2191
2404
  });