@adobe/spacecat-shared-tokowaka-client 1.3.2 → 1.4.1
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 +138 -81
- package/src/mappers/generic-mapper.js +5 -1
- package/test/cdn/fastly-cdn-client.test.js +484 -0
- package/test/index.test.js +313 -113
- package/test/mappers/generic-mapper.test.js +137 -2
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* eslint-env mocha */
|
|
14
|
+
|
|
15
|
+
import { expect } from 'chai';
|
|
16
|
+
import sinon from 'sinon';
|
|
17
|
+
import FastlyCdnClient from '../../src/cdn/fastly-cdn-client.js';
|
|
18
|
+
|
|
19
|
+
describe('FastlyCdnClient', () => {
|
|
20
|
+
let log;
|
|
21
|
+
let fetchStub;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
log = {
|
|
25
|
+
info: sinon.stub(),
|
|
26
|
+
warn: sinon.stub(),
|
|
27
|
+
error: sinon.stub(),
|
|
28
|
+
debug: sinon.stub(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Stub global fetch
|
|
32
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
sinon.restore();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('constructor', () => {
|
|
40
|
+
it('should throw error for invalid JSON in TOKOWAKA_CDN_CONFIG', () => {
|
|
41
|
+
const env = {
|
|
42
|
+
TOKOWAKA_CDN_CONFIG: 'invalid-json{',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(() => new FastlyCdnClient(env, log))
|
|
46
|
+
.to.throw('Invalid TOKOWAKA_CDN_CONFIG: must be valid JSON');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should throw error if fastly config is missing', () => {
|
|
50
|
+
const env = {
|
|
51
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
52
|
+
cloudfront: { distributionId: 'test' },
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(() => new FastlyCdnClient(env, log))
|
|
57
|
+
.to.throw("Missing 'fastly' config in TOKOWAKA_CDN_CONFIG");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create client with valid config', () => {
|
|
61
|
+
const env = {
|
|
62
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
63
|
+
fastly: {
|
|
64
|
+
serviceId: 'test-service-id',
|
|
65
|
+
apiToken: 'test-api-token',
|
|
66
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const client = new FastlyCdnClient(env, log);
|
|
72
|
+
expect(client).to.be.instanceOf(FastlyCdnClient);
|
|
73
|
+
expect(client.getProviderName()).to.equal('fastly');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('validateConfig', () => {
|
|
78
|
+
it('should return false if serviceId is missing', () => {
|
|
79
|
+
const env = {
|
|
80
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
81
|
+
fastly: {
|
|
82
|
+
apiToken: 'test-api-token',
|
|
83
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const client = new FastlyCdnClient(env, log);
|
|
89
|
+
expect(client.validateConfig()).to.be.false;
|
|
90
|
+
expect(log.error.calledOnce).to.be.true;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return false if apiToken is missing', () => {
|
|
94
|
+
const env = {
|
|
95
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
96
|
+
fastly: {
|
|
97
|
+
serviceId: 'test-service-id',
|
|
98
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const client = new FastlyCdnClient(env, log);
|
|
104
|
+
expect(client.validateConfig()).to.be.false;
|
|
105
|
+
expect(log.error.calledOnce).to.be.true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return false if distributionUrl is missing', () => {
|
|
109
|
+
const env = {
|
|
110
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
111
|
+
fastly: {
|
|
112
|
+
serviceId: 'test-service-id',
|
|
113
|
+
apiToken: 'test-api-token',
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const client = new FastlyCdnClient(env, log);
|
|
119
|
+
expect(client.validateConfig()).to.be.false;
|
|
120
|
+
expect(log.error).to.have.been.calledWith(sinon.match(/distributionUrl/));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return true with valid config', () => {
|
|
124
|
+
const env = {
|
|
125
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
126
|
+
fastly: {
|
|
127
|
+
serviceId: 'test-service-id',
|
|
128
|
+
apiToken: 'test-api-token',
|
|
129
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const client = new FastlyCdnClient(env, log);
|
|
135
|
+
expect(client.validateConfig()).to.be.true;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('invalidateCache', () => {
|
|
140
|
+
let client;
|
|
141
|
+
let env;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
env = {
|
|
145
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
146
|
+
fastly: {
|
|
147
|
+
serviceId: 'test-service-id',
|
|
148
|
+
apiToken: 'test-api-token',
|
|
149
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
client = new FastlyCdnClient(env, log);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should throw error for invalid config', async () => {
|
|
157
|
+
const invalidEnv = {
|
|
158
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
159
|
+
fastly: {
|
|
160
|
+
serviceId: 'test-service-id',
|
|
161
|
+
// missing apiToken
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
const invalidClient = new FastlyCdnClient(invalidEnv, log);
|
|
166
|
+
|
|
167
|
+
// Should throw error when invalidating cache with invalid config
|
|
168
|
+
try {
|
|
169
|
+
await invalidClient.invalidateCache(['/path1']);
|
|
170
|
+
expect.fail('Should have thrown an error');
|
|
171
|
+
} catch (error) {
|
|
172
|
+
expect(error.message).to.equal('Invalid Fastly CDN configuration');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should return skipped status for empty paths', async () => {
|
|
177
|
+
const result = await client.invalidateCache([]);
|
|
178
|
+
expect(result.status).to.equal('skipped');
|
|
179
|
+
expect(result.message).to.equal('No paths to invalidate');
|
|
180
|
+
expect(log.warn.calledOnce).to.be.true;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should successfully purge cache for valid single path', async () => {
|
|
184
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
185
|
+
|
|
186
|
+
fetchStub.resolves({
|
|
187
|
+
ok: true,
|
|
188
|
+
json: async () => ({ status: 'ok', id: 'purge-123' }),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await client.invalidateCache(paths);
|
|
192
|
+
|
|
193
|
+
expect(result.status).to.equal('success');
|
|
194
|
+
expect(result.provider).to.equal('fastly');
|
|
195
|
+
expect(result.serviceId).to.equal('test-service-id');
|
|
196
|
+
expect(result.successCount).to.equal(1);
|
|
197
|
+
expect(result.failedCount).to.equal(0);
|
|
198
|
+
expect(result.totalPaths).to.equal(1);
|
|
199
|
+
expect(result.purgeId).to.equal('purge-123');
|
|
200
|
+
|
|
201
|
+
expect(fetchStub.calledOnce).to.be.true;
|
|
202
|
+
const fetchCall = fetchStub.getCall(0);
|
|
203
|
+
expect(fetchCall.args[0]).to.include('test-service-id');
|
|
204
|
+
expect(fetchCall.args[1].method).to.equal('POST');
|
|
205
|
+
expect(fetchCall.args[1].headers['Fastly-Key']).to.equal('test-api-token');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should use batch purge for multiple paths (legacy test)', async () => {
|
|
209
|
+
const paths = [
|
|
210
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
211
|
+
'/opportunities/example.com/L2Fib3V0',
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
fetchStub.resolves({
|
|
215
|
+
ok: true,
|
|
216
|
+
json: async () => ({ status: 'ok', id: 'batch-purge-123' }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const result = await client.invalidateCache(paths);
|
|
220
|
+
|
|
221
|
+
expect(result.status).to.equal('success');
|
|
222
|
+
expect(result.totalKeys).to.equal(2);
|
|
223
|
+
expect(result.successCount).to.equal(2);
|
|
224
|
+
expect(result.failedCount).to.equal(0);
|
|
225
|
+
// Now uses batch, so only 1 call
|
|
226
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should handle batch failures for multiple paths', async () => {
|
|
230
|
+
const paths = [
|
|
231
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
232
|
+
'/opportunities/example.com/L2Fib3V0',
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// With batch purge, single failure fails all
|
|
236
|
+
fetchStub.resolves({
|
|
237
|
+
ok: false,
|
|
238
|
+
status: 403,
|
|
239
|
+
text: async () => 'Forbidden',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const result = await client.invalidateCache(paths);
|
|
243
|
+
|
|
244
|
+
expect(result.status).to.equal('failed');
|
|
245
|
+
expect(result.totalKeys).to.equal(2);
|
|
246
|
+
expect(result.successCount).to.equal(0);
|
|
247
|
+
expect(result.failedCount).to.equal(2);
|
|
248
|
+
expect(result.error).to.equal('Forbidden');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle network errors for single path', async () => {
|
|
252
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
253
|
+
|
|
254
|
+
fetchStub.rejects(new Error('Network error'));
|
|
255
|
+
|
|
256
|
+
const result = await client.invalidateCache(paths);
|
|
257
|
+
|
|
258
|
+
expect(result.status).to.equal('error');
|
|
259
|
+
expect(result.successCount).to.equal(0);
|
|
260
|
+
expect(result.failedCount).to.equal(1);
|
|
261
|
+
expect(result.error).to.equal('Network error');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should convert paths to surrogate keys correctly in batch', async () => {
|
|
265
|
+
const paths = [
|
|
266
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
267
|
+
'/preview/opportunities/test.com/abc123',
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
fetchStub.resolves({
|
|
271
|
+
ok: true,
|
|
272
|
+
json: async () => ({ status: 'ok' }),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await client.invalidateCache(paths);
|
|
276
|
+
|
|
277
|
+
// Now uses batch, so only 1 call
|
|
278
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
279
|
+
|
|
280
|
+
// Check that surrogate keys are full CloudFront URLs (space-separated)
|
|
281
|
+
const fetchCall = fetchStub.getCall(0);
|
|
282
|
+
const surrogateKey = fetchCall.args[1].headers['Surrogate-Key'];
|
|
283
|
+
expect(surrogateKey).to.include('https://test.cloudfront.net/opportunities/example.com/L3Byb2R1Y3Rz');
|
|
284
|
+
expect(surrogateKey).to.include('https://test.cloudfront.net/preview/opportunities/test.com/abc123');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should normalize paths without leading slash (line 78)', async () => {
|
|
288
|
+
const paths = [
|
|
289
|
+
'opportunities/example.com/config', // No leading slash - tests line 78
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
fetchStub.resolves({
|
|
293
|
+
ok: true,
|
|
294
|
+
json: async () => ({ status: 'ok', id: 'normalize-test' }),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const result = await client.invalidateCache(paths);
|
|
298
|
+
|
|
299
|
+
expect(result.status).to.equal('success');
|
|
300
|
+
|
|
301
|
+
// Check that path was normalized with leading slash
|
|
302
|
+
const fetchCall = fetchStub.getCall(0);
|
|
303
|
+
const surrogateKey = fetchCall.args[1].headers['Surrogate-Key'];
|
|
304
|
+
expect(surrogateKey).to.equal('https://test.cloudfront.net/opportunities/example.com/config');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should include duration in result for single path', async () => {
|
|
308
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
309
|
+
|
|
310
|
+
fetchStub.resolves({
|
|
311
|
+
ok: true,
|
|
312
|
+
json: async () => ({ status: 'ok' }),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = await client.invalidateCache(paths);
|
|
316
|
+
|
|
317
|
+
expect(result.duration).to.be.a('number');
|
|
318
|
+
expect(result.duration).to.be.greaterThanOrEqual(0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should handle errors during result processing', async () => {
|
|
322
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
323
|
+
|
|
324
|
+
// Create a response that will throw during json() call
|
|
325
|
+
fetchStub.resolves({
|
|
326
|
+
ok: true,
|
|
327
|
+
json: async () => {
|
|
328
|
+
throw new Error('JSON parsing error');
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const result = await client.invalidateCache(paths);
|
|
333
|
+
|
|
334
|
+
// Should catch the error and add it to results
|
|
335
|
+
expect(result.status).to.equal('error');
|
|
336
|
+
expect(result.failedCount).to.equal(1);
|
|
337
|
+
expect(result.error).to.equal('JSON parsing error');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('unified purging (single endpoint for all cases)', () => {
|
|
342
|
+
let client;
|
|
343
|
+
let env;
|
|
344
|
+
|
|
345
|
+
beforeEach(() => {
|
|
346
|
+
env = {
|
|
347
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
348
|
+
fastly: {
|
|
349
|
+
serviceId: 'test-service-id',
|
|
350
|
+
apiToken: 'test-api-token',
|
|
351
|
+
distributionUrl: 'https://test.cloudfront.net',
|
|
352
|
+
},
|
|
353
|
+
}),
|
|
354
|
+
};
|
|
355
|
+
client = new FastlyCdnClient(env, log);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should use batch endpoint for multiple paths', async () => {
|
|
359
|
+
const paths = [
|
|
360
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
361
|
+
'/opportunities/example.com/L2Fib3V0',
|
|
362
|
+
'/opportunities/example.com/L2NvbnRhY3Q',
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
fetchStub.resolves({
|
|
366
|
+
ok: true,
|
|
367
|
+
json: async () => ({ status: 'ok', id: 'batch-purge-123' }),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = await client.invalidateCache(paths);
|
|
371
|
+
|
|
372
|
+
expect(result.status).to.equal('success');
|
|
373
|
+
expect(result.provider).to.equal('fastly');
|
|
374
|
+
expect(result.totalKeys).to.equal(3);
|
|
375
|
+
expect(result.successCount).to.equal(3);
|
|
376
|
+
expect(result.failedCount).to.equal(0);
|
|
377
|
+
expect(result.purgeId).to.equal('batch-purge-123');
|
|
378
|
+
|
|
379
|
+
// Should make only ONE API call for batch
|
|
380
|
+
expect(fetchStub.calledOnce).to.be.true;
|
|
381
|
+
|
|
382
|
+
// Check that it used the batch endpoint
|
|
383
|
+
const fetchCall = fetchStub.getCall(0);
|
|
384
|
+
expect(fetchCall.args[0]).to.equal('https://api.fastly.com/service/test-service-id/purge');
|
|
385
|
+
|
|
386
|
+
// Check that surrogate keys are full CloudFront URLs (space-separated)
|
|
387
|
+
const { headers } = fetchCall.args[1];
|
|
388
|
+
expect(headers['Surrogate-Key']).to.be.a('string');
|
|
389
|
+
const keys = headers['Surrogate-Key'].split(' ');
|
|
390
|
+
expect(keys).to.have.lengthOf(3);
|
|
391
|
+
expect(headers['Surrogate-Key']).to.include('https://test.cloudfront.net/opportunities/example.com/');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should handle batch purge failures', async () => {
|
|
395
|
+
const paths = [
|
|
396
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
397
|
+
'/opportunities/example.com/L2Fib3V0',
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
fetchStub.resolves({
|
|
401
|
+
ok: false,
|
|
402
|
+
status: 403,
|
|
403
|
+
text: async () => 'Forbidden',
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const result = await client.invalidateCache(paths);
|
|
407
|
+
|
|
408
|
+
expect(result.status).to.equal('failed');
|
|
409
|
+
expect(result.totalKeys).to.equal(2);
|
|
410
|
+
expect(result.successCount).to.equal(0);
|
|
411
|
+
expect(result.failedCount).to.equal(2);
|
|
412
|
+
expect(result.error).to.equal('Forbidden');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should handle batch purge network errors', async () => {
|
|
416
|
+
const paths = [
|
|
417
|
+
'/opportunities/example.com/L3Byb2R1Y3Rz',
|
|
418
|
+
'/opportunities/example.com/L2Fib3V0',
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
fetchStub.rejects(new Error('Network timeout'));
|
|
422
|
+
|
|
423
|
+
const result = await client.invalidateCache(paths);
|
|
424
|
+
|
|
425
|
+
expect(result.status).to.equal('error');
|
|
426
|
+
expect(result.totalKeys).to.equal(2);
|
|
427
|
+
expect(result.successCount).to.equal(0);
|
|
428
|
+
expect(result.failedCount).to.equal(2);
|
|
429
|
+
expect(result.error).to.equal('Network timeout');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should use batch endpoint for single path too', async () => {
|
|
433
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
434
|
+
|
|
435
|
+
fetchStub.resolves({
|
|
436
|
+
ok: true,
|
|
437
|
+
json: async () => ({ status: 'ok', id: 'purge-123' }),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = await client.invalidateCache(paths);
|
|
441
|
+
|
|
442
|
+
expect(result.status).to.equal('success');
|
|
443
|
+
expect(result.totalKeys).to.equal(1);
|
|
444
|
+
expect(result.successCount).to.equal(1);
|
|
445
|
+
expect(result.failedCount).to.equal(0);
|
|
446
|
+
expect(result.purgeId).to.equal('purge-123');
|
|
447
|
+
|
|
448
|
+
// Should use batch endpoint even for single path (consistent behavior)
|
|
449
|
+
const fetchCall = fetchStub.getCall(0);
|
|
450
|
+
expect(fetchCall.args[0]).to.equal('https://api.fastly.com/service/test-service-id/purge');
|
|
451
|
+
expect(fetchCall.args[1].headers['Surrogate-Key']).to.equal('https://test.cloudfront.net/opportunities/example.com/L3Byb2R1Y3Rz');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should handle single path purge failure', async () => {
|
|
455
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
456
|
+
|
|
457
|
+
fetchStub.resolves({
|
|
458
|
+
ok: false,
|
|
459
|
+
status: 403,
|
|
460
|
+
text: async () => 'Forbidden',
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const result = await client.invalidateCache(paths);
|
|
464
|
+
|
|
465
|
+
expect(result.status).to.equal('failed');
|
|
466
|
+
expect(result.successCount).to.equal(0);
|
|
467
|
+
expect(result.failedCount).to.equal(1);
|
|
468
|
+
expect(result.error).to.equal('Forbidden');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should handle single path network error', async () => {
|
|
472
|
+
const paths = ['/opportunities/example.com/L3Byb2R1Y3Rz'];
|
|
473
|
+
|
|
474
|
+
fetchStub.rejects(new Error('Network timeout'));
|
|
475
|
+
|
|
476
|
+
const result = await client.invalidateCache(paths);
|
|
477
|
+
|
|
478
|
+
expect(result.status).to.equal('error');
|
|
479
|
+
expect(result.successCount).to.equal(0);
|
|
480
|
+
expect(result.failedCount).to.equal(1);
|
|
481
|
+
expect(result.error).to.equal('Network timeout');
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|