@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.
- package/CHANGELOG.md +14 -0
- package/README.md +107 -7
- package/package.json +1 -1
- package/src/cdn/cdn-client-registry.js +2 -0
- package/src/cdn/fastly-cdn-client.js +156 -0
- package/src/index.d.ts +47 -11
- package/src/index.js +152 -86
- package/src/mappers/generic-mapper.js +5 -1
- package/src/utils/custom-html-utils.js +5 -5
- package/test/cdn/fastly-cdn-client.test.js +484 -0
- package/test/index.test.js +331 -118
- package/test/mappers/generic-mapper.test.js +82 -0
- package/test/utils/html-utils.test.js +12 -12
package/test/index.test.js
CHANGED
|
@@ -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
|
|
822
|
-
sinon.stub(client, 'fetchMetaconfig').resolves(
|
|
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
|
|
845
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
'
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2052
|
-
expect(
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2400
|
+
// Restore
|
|
2401
|
+
client.env.TOKOWAKA_CDN_PROVIDER = originalProvider;
|
|
2189
2402
|
});
|
|
2190
2403
|
});
|
|
2191
2404
|
});
|