@adobe/spacecat-shared-tokowaka-client 1.0.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/.mocha-multi.json +7 -0
- package/.nycrc.json +15 -0
- package/.releaserc.cjs +17 -0
- package/CHANGELOG.md +24 -0
- package/CODE_OF_CONDUCT.md +75 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE.txt +203 -0
- package/README.md +101 -0
- package/package.json +53 -0
- package/src/cdn/base-cdn-client.js +50 -0
- package/src/cdn/cdn-client-registry.js +87 -0
- package/src/cdn/cloudfront-cdn-client.js +128 -0
- package/src/constants.js +17 -0
- package/src/index.d.ts +289 -0
- package/src/index.js +447 -0
- package/src/mappers/base-mapper.js +86 -0
- package/src/mappers/content-summarization-mapper.js +106 -0
- package/src/mappers/headings-mapper.js +118 -0
- package/src/mappers/mapper-registry.js +87 -0
- package/test/cdn/base-cdn-client.test.js +52 -0
- package/test/cdn/cdn-client-registry.test.js +179 -0
- package/test/cdn/cloudfront-cdn-client.test.js +330 -0
- package/test/index.test.js +1142 -0
- package/test/mappers/base-mapper.test.js +110 -0
- package/test/mappers/content-mapper.test.js +355 -0
- package/test/mappers/headings-mapper.test.js +428 -0
- package/test/mappers/mapper-registry.test.js +197 -0
- package/test/setup-env.js +18 -0
|
@@ -0,0 +1,330 @@
|
|
|
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, use } from 'chai';
|
|
16
|
+
import sinon from 'sinon';
|
|
17
|
+
import sinonChai from 'sinon-chai';
|
|
18
|
+
import { mockClient } from 'aws-sdk-client-mock';
|
|
19
|
+
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
|
20
|
+
import CloudFrontCdnClient from '../../src/cdn/cloudfront-cdn-client.js';
|
|
21
|
+
|
|
22
|
+
use(sinonChai);
|
|
23
|
+
|
|
24
|
+
describe('CloudFrontCdnClient', () => {
|
|
25
|
+
let client;
|
|
26
|
+
let log;
|
|
27
|
+
let cloudFrontMock;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
log = {
|
|
31
|
+
info: sinon.stub(),
|
|
32
|
+
warn: sinon.stub(),
|
|
33
|
+
error: sinon.stub(),
|
|
34
|
+
debug: sinon.stub(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Mock the CloudFront SDK client
|
|
38
|
+
cloudFrontMock = mockClient(CloudFrontClient);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
// Reset all mocks
|
|
43
|
+
cloudFrontMock.reset();
|
|
44
|
+
sinon.restore();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('constructor', () => {
|
|
48
|
+
it('should throw error for invalid JSON in TOKOWAKA_CDN_CONFIG', () => {
|
|
49
|
+
const env = {
|
|
50
|
+
TOKOWAKA_CDN_CONFIG: 'invalid-json{',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(() => new CloudFrontCdnClient(env, log))
|
|
54
|
+
.to.throw('Invalid TOKOWAKA_CDN_CONFIG: must be valid JSON');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should throw error when cloudfront config is missing', () => {
|
|
58
|
+
const env = {
|
|
59
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
60
|
+
someOtherProvider: {},
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(() => new CloudFrontCdnClient(env, log))
|
|
65
|
+
.to.throw("Missing 'cloudfront' config in TOKOWAKA_CDN_CONFIG");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getProviderName', () => {
|
|
70
|
+
it('should return cloudfront', () => {
|
|
71
|
+
const env = {
|
|
72
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
73
|
+
cloudfront: {
|
|
74
|
+
distributionId: 'E123456',
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
client = new CloudFrontCdnClient(env, log);
|
|
80
|
+
|
|
81
|
+
expect(client.getProviderName()).to.equal('cloudfront');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('validateConfig', () => {
|
|
86
|
+
it('should return true for valid config with only distributionId', () => {
|
|
87
|
+
const env = {
|
|
88
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
89
|
+
cloudfront: {
|
|
90
|
+
distributionId: 'E123456',
|
|
91
|
+
region: 'us-east-1',
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
client = new CloudFrontCdnClient(env, log);
|
|
96
|
+
|
|
97
|
+
expect(client.validateConfig()).to.be.true;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return true for valid config with credentials', () => {
|
|
101
|
+
const env = {
|
|
102
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
103
|
+
cloudfront: {
|
|
104
|
+
distributionId: 'E123456',
|
|
105
|
+
region: 'us-east-1',
|
|
106
|
+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
107
|
+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
client = new CloudFrontCdnClient(env, log);
|
|
112
|
+
|
|
113
|
+
expect(client.validateConfig()).to.be.true;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return false if distributionId is missing', () => {
|
|
117
|
+
const env = {
|
|
118
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
119
|
+
cloudfront: {
|
|
120
|
+
region: 'us-east-1',
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
client = new CloudFrontCdnClient(env, log);
|
|
125
|
+
|
|
126
|
+
const result = client.validateConfig();
|
|
127
|
+
|
|
128
|
+
expect(result).to.be.false;
|
|
129
|
+
expect(log.error).to.have.been.calledWith(
|
|
130
|
+
'CloudFront CDN config missing required fields: distributionId and region',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('invalidateCache', () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
const env = {
|
|
138
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
139
|
+
cloudfront: {
|
|
140
|
+
distributionId: 'E123456',
|
|
141
|
+
region: 'us-east-1',
|
|
142
|
+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
143
|
+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
client = new CloudFrontCdnClient(env, log);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should invalidate cache successfully', async () => {
|
|
151
|
+
const mockResponse = {
|
|
152
|
+
Invalidation: {
|
|
153
|
+
Id: 'I2J4EXAMPLE',
|
|
154
|
+
Status: 'InProgress',
|
|
155
|
+
CreateTime: new Date('2025-01-15T10:30:00.000Z'),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
cloudFrontMock.on(CreateInvalidationCommand).resolves(mockResponse);
|
|
159
|
+
|
|
160
|
+
const paths = ['/path1', '/path2'];
|
|
161
|
+
const result = await client.invalidateCache(paths);
|
|
162
|
+
|
|
163
|
+
expect(result).to.deep.include({
|
|
164
|
+
status: 'success',
|
|
165
|
+
provider: 'cloudfront',
|
|
166
|
+
invalidationId: 'I2J4EXAMPLE',
|
|
167
|
+
invalidationStatus: 'InProgress',
|
|
168
|
+
createTime: mockResponse.Invalidation.CreateTime,
|
|
169
|
+
paths: 2,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(log.debug).to.have.been.calledWith(sinon.match(/Initiating CloudFront cache invalidation/));
|
|
173
|
+
expect(log.info).to.have.been.calledWith(sinon.match(/CloudFront cache invalidation initiated/));
|
|
174
|
+
expect(log.info).to.have.been.calledWith(sinon.match(/took \d+ms/));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should format paths to start with /', async () => {
|
|
178
|
+
const mockResponse = {
|
|
179
|
+
Invalidation: {
|
|
180
|
+
Id: 'I2J4EXAMPLE',
|
|
181
|
+
Status: 'InProgress',
|
|
182
|
+
CreateTime: new Date(),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
cloudFrontMock.on(CreateInvalidationCommand).resolves(mockResponse);
|
|
186
|
+
|
|
187
|
+
const paths = ['path1', '/path2', 'path3'];
|
|
188
|
+
await client.invalidateCache(paths);
|
|
189
|
+
|
|
190
|
+
const calls = cloudFrontMock.commandCalls(CreateInvalidationCommand);
|
|
191
|
+
expect(calls).to.have.length(1);
|
|
192
|
+
expect(calls[0].args[0].input.InvalidationBatch.Paths.Items).to.deep.equal([
|
|
193
|
+
'/path1',
|
|
194
|
+
'/path2',
|
|
195
|
+
'/path3',
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should throw error if config is invalid', async () => {
|
|
200
|
+
client.cdnConfig = {};
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await client.invalidateCache(['/path1']);
|
|
204
|
+
expect.fail('Should have thrown error');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
expect(error.message).to.equal('Invalid CloudFront CDN configuration');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return skipped result if paths array is empty', async () => {
|
|
211
|
+
const result = await client.invalidateCache([]);
|
|
212
|
+
|
|
213
|
+
expect(result).to.deep.equal({
|
|
214
|
+
status: 'skipped',
|
|
215
|
+
message: 'No paths to invalidate',
|
|
216
|
+
});
|
|
217
|
+
expect(log.warn).to.have.been.calledWith('No paths provided for cache invalidation');
|
|
218
|
+
|
|
219
|
+
// Verify no CloudFront commands were sent
|
|
220
|
+
const calls = cloudFrontMock.commandCalls(CreateInvalidationCommand);
|
|
221
|
+
expect(calls).to.have.length(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return skipped result if paths is not an array', async () => {
|
|
225
|
+
const result = await client.invalidateCache(null);
|
|
226
|
+
|
|
227
|
+
expect(result).to.deep.equal({
|
|
228
|
+
status: 'skipped',
|
|
229
|
+
message: 'No paths to invalidate',
|
|
230
|
+
});
|
|
231
|
+
expect(log.warn).to.have.been.calledWith('No paths provided for cache invalidation');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw error on CloudFront API failure', async () => {
|
|
235
|
+
cloudFrontMock.on(CreateInvalidationCommand).rejects(new Error('CloudFront API error'));
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await client.invalidateCache(['/path1']);
|
|
239
|
+
expect.fail('Should have thrown error');
|
|
240
|
+
} catch (error) {
|
|
241
|
+
expect(error.message).to.include('CloudFront API error');
|
|
242
|
+
expect(log.error).to.have.been.calledWith(sinon.match(/Failed to invalidate CloudFront cache after \d+ms/));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('client initialization', () => {
|
|
248
|
+
it('should initialize client with explicit credentials', async () => {
|
|
249
|
+
const env = {
|
|
250
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
251
|
+
cloudfront: {
|
|
252
|
+
distributionId: 'E123456',
|
|
253
|
+
region: 'us-west-2',
|
|
254
|
+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
255
|
+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
256
|
+
sessionToken: 'SESSION_TOKEN',
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
client = new CloudFrontCdnClient(env, log);
|
|
261
|
+
|
|
262
|
+
// Client should be null initially
|
|
263
|
+
expect(client.client).to.be.null;
|
|
264
|
+
|
|
265
|
+
// Mock successful invalidation
|
|
266
|
+
cloudFrontMock.on(CreateInvalidationCommand).resolves({
|
|
267
|
+
Invalidation: {
|
|
268
|
+
Id: 'I123',
|
|
269
|
+
Status: 'InProgress',
|
|
270
|
+
CreateTime: new Date(),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await client.invalidateCache(['/test']);
|
|
275
|
+
|
|
276
|
+
// Verify the command was called
|
|
277
|
+
const calls = cloudFrontMock.commandCalls(CreateInvalidationCommand);
|
|
278
|
+
expect(calls).to.have.length(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should initialize client without credentials (Lambda role)', async () => {
|
|
282
|
+
const env = {
|
|
283
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
284
|
+
cloudfront: {
|
|
285
|
+
distributionId: 'E123456',
|
|
286
|
+
region: 'us-east-1',
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
client = new CloudFrontCdnClient(env, log);
|
|
291
|
+
|
|
292
|
+
// Client should be null initially
|
|
293
|
+
expect(client.client).to.be.null;
|
|
294
|
+
|
|
295
|
+
// Mock successful invalidation
|
|
296
|
+
cloudFrontMock.on(CreateInvalidationCommand).resolves({
|
|
297
|
+
Invalidation: {
|
|
298
|
+
Id: 'I123',
|
|
299
|
+
Status: 'InProgress',
|
|
300
|
+
CreateTime: new Date(),
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await client.invalidateCache(['/test']);
|
|
305
|
+
|
|
306
|
+
// Verify the command was called
|
|
307
|
+
const calls = cloudFrontMock.commandCalls(CreateInvalidationCommand);
|
|
308
|
+
expect(calls).to.have.length(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should lazy-initialize CloudFront client on first use', () => {
|
|
312
|
+
const env = {
|
|
313
|
+
TOKOWAKA_CDN_CONFIG: JSON.stringify({
|
|
314
|
+
cloudfront: {
|
|
315
|
+
distributionId: 'E123456',
|
|
316
|
+
region: 'us-east-1',
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
client = new CloudFrontCdnClient(env, log);
|
|
321
|
+
|
|
322
|
+
// Client should be null initially - it's lazy-initialized on first use
|
|
323
|
+
expect(client.client).to.be.null;
|
|
324
|
+
|
|
325
|
+
// The #initializeClient() method is called internally by invalidateCache()
|
|
326
|
+
// and getInvalidationStatus(), which we test in other test cases.
|
|
327
|
+
// Those tests verify the client gets created when needed.
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|