@adobe/spacecat-shared-tokowaka-client 1.19.0 → 1.20.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 +12 -0
- package/package.json +4 -1
- package/src/cdn/cloudfront/edge-code.js +152 -0
- package/src/cdn/cloudfront/index.js +1863 -0
- package/src/index.d.ts +262 -1
- package/src/index.js +108 -6
- package/test/cdn/cloudfront/index.test.js +4185 -0
- package/test/index.test.js +212 -0
|
@@ -0,0 +1,4185 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
import { expect } from 'chai';
|
|
14
|
+
import sinon from 'sinon';
|
|
15
|
+
import esmock from 'esmock';
|
|
16
|
+
|
|
17
|
+
describe('edge-optimize support', () => {
|
|
18
|
+
let stsSendStub;
|
|
19
|
+
let cfSendStub;
|
|
20
|
+
let iamSendStub;
|
|
21
|
+
let lambdaSendStub;
|
|
22
|
+
let edgeOptimize;
|
|
23
|
+
|
|
24
|
+
// esmock ONCE for the whole file (not per-test) — esmock re-instantiates the mocked module
|
|
25
|
+
// graph on every call and accumulates memory, which contributes to the suite's heap pressure.
|
|
26
|
+
// The mocked clients call the `*SendStub` closures, which read the `let` bindings reassigned
|
|
27
|
+
// fresh in beforeEach, so a single esmock works for all tests.
|
|
28
|
+
before(async function setupEsmock() {
|
|
29
|
+
// One-time esmock of the AWS SDK module graph. This is memory-heavy, so under the full CI
|
|
30
|
+
// suite (12k+ tests + nyc coverage + heap pressure) it can take well over the default/30s
|
|
31
|
+
// even though it runs in ~1s locally. Give the hook generous headroom so it can't flake the
|
|
32
|
+
// whole build on suite growth (it still completes in seconds in practice).
|
|
33
|
+
this.timeout(120000);
|
|
34
|
+
// Each command in a mocked module is a constructor FUNCTION (not a class) — eslint forbids
|
|
35
|
+
// multiple class declarations in one file, so we capture the command name + input on `this`.
|
|
36
|
+
const cfCommand = (Name) => function CloudFrontCommand(input) {
|
|
37
|
+
this.input = input;
|
|
38
|
+
this.commandName = Name;
|
|
39
|
+
};
|
|
40
|
+
const iamCommand = (Name) => function IamCommand(input) {
|
|
41
|
+
this.input = input;
|
|
42
|
+
this.commandName = Name;
|
|
43
|
+
};
|
|
44
|
+
const lambdaCommand = (Name) => function LambdaCommand(input) {
|
|
45
|
+
this.input = input;
|
|
46
|
+
this.commandName = Name;
|
|
47
|
+
};
|
|
48
|
+
edgeOptimize = await esmock('../../../src/cdn/cloudfront/index.js', {
|
|
49
|
+
'@aws-sdk/client-sts': {
|
|
50
|
+
STSClient: function STSClient() {
|
|
51
|
+
this.send = (cmd) => stsSendStub(cmd);
|
|
52
|
+
},
|
|
53
|
+
AssumeRoleCommand: function AssumeRoleCommand(input) {
|
|
54
|
+
this.input = input;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'@aws-sdk/client-cloudfront': {
|
|
58
|
+
CloudFrontClient: function CloudFrontClient(config) {
|
|
59
|
+
this.config = config;
|
|
60
|
+
this.send = (cmd) => cfSendStub(cmd);
|
|
61
|
+
},
|
|
62
|
+
ListDistributionsCommand: cfCommand('ListDistributions'),
|
|
63
|
+
GetDistributionCommand: cfCommand('GetDistribution'),
|
|
64
|
+
GetDistributionConfigCommand: cfCommand('GetDistributionConfig'),
|
|
65
|
+
GetCachePolicyConfigCommand: cfCommand('GetCachePolicyConfig'),
|
|
66
|
+
GetCachePolicyCommand: cfCommand('GetCachePolicy'),
|
|
67
|
+
ListCachePoliciesCommand: cfCommand('ListCachePolicies'),
|
|
68
|
+
CreateCachePolicyCommand: cfCommand('CreateCachePolicy'),
|
|
69
|
+
UpdateCachePolicyCommand: cfCommand('UpdateCachePolicy'),
|
|
70
|
+
CreateFunctionCommand: cfCommand('CreateFunction'),
|
|
71
|
+
UpdateFunctionCommand: cfCommand('UpdateFunction'),
|
|
72
|
+
DescribeFunctionCommand: cfCommand('DescribeFunction'),
|
|
73
|
+
PublishFunctionCommand: cfCommand('PublishFunction'),
|
|
74
|
+
UpdateDistributionCommand: cfCommand('UpdateDistribution'),
|
|
75
|
+
},
|
|
76
|
+
'@aws-sdk/client-iam': {
|
|
77
|
+
IAMClient: function IAMClient(config) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
this.send = (cmd) => iamSendStub(cmd);
|
|
80
|
+
},
|
|
81
|
+
CreateRoleCommand: iamCommand('CreateRole'),
|
|
82
|
+
GetRoleCommand: iamCommand('GetRole'),
|
|
83
|
+
GetRolePolicyCommand: iamCommand('GetRolePolicy'),
|
|
84
|
+
PutRolePolicyCommand: iamCommand('PutRolePolicy'),
|
|
85
|
+
UpdateAssumeRolePolicyCommand: iamCommand('UpdateAssumeRolePolicy'),
|
|
86
|
+
},
|
|
87
|
+
'@aws-sdk/client-lambda': {
|
|
88
|
+
LambdaClient: function LambdaClient(config) {
|
|
89
|
+
this.config = config;
|
|
90
|
+
this.send = (cmd) => lambdaSendStub(cmd);
|
|
91
|
+
},
|
|
92
|
+
CreateFunctionCommand: lambdaCommand('CreateFunction'),
|
|
93
|
+
UpdateFunctionCodeCommand: lambdaCommand('UpdateFunctionCode'),
|
|
94
|
+
GetFunctionConfigurationCommand: lambdaCommand('GetFunctionConfiguration'),
|
|
95
|
+
ListVersionsByFunctionCommand: lambdaCommand('ListVersionsByFunction'),
|
|
96
|
+
PublishVersionCommand: lambdaCommand('PublishVersion'),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
// Fresh stubs per test; the esmocked clients read these `let` bindings at call time.
|
|
103
|
+
stsSendStub = sinon.stub();
|
|
104
|
+
cfSendStub = sinon.stub();
|
|
105
|
+
iamSendStub = sinon.stub();
|
|
106
|
+
lambdaSendStub = sinon.stub();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
sinon.restore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('assumeConnectorRole', () => {
|
|
114
|
+
it('assumes the role and returns mapped credentials', async () => {
|
|
115
|
+
stsSendStub.resolves({
|
|
116
|
+
Credentials: {
|
|
117
|
+
AccessKeyId: 'AKIA',
|
|
118
|
+
SecretAccessKey: 'secret',
|
|
119
|
+
SessionToken: 'token',
|
|
120
|
+
Expiration: new Date('2030-01-01T00:00:00Z'),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await edgeOptimize.assumeConnectorRole({
|
|
125
|
+
accountId: '120569600543',
|
|
126
|
+
externalId: 'ext-123',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.roleArn).to.equal('arn:aws:iam::120569600543:role/AdobeLLMOptimizerCloudFrontConnectorRole');
|
|
130
|
+
expect(result.accountId).to.equal('120569600543');
|
|
131
|
+
expect(result.credentials.accessKeyId).to.equal('AKIA');
|
|
132
|
+
expect(result.credentials.secretAccessKey).to.equal('secret');
|
|
133
|
+
expect(result.credentials.sessionToken).to.equal('token');
|
|
134
|
+
expect(stsSendStub.calledOnce).to.equal(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('uses a custom role name when provided', async () => {
|
|
138
|
+
stsSendStub.resolves({
|
|
139
|
+
Credentials: { AccessKeyId: 'A', SecretAccessKey: 'S', SessionToken: 'T' },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await edgeOptimize.assumeConnectorRole({
|
|
143
|
+
accountId: '120569600543',
|
|
144
|
+
externalId: 'ext',
|
|
145
|
+
roleName: 'CustomRole',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.roleArn).to.equal('arn:aws:iam::120569600543:role/CustomRole');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('throws for an invalid account id', async () => {
|
|
152
|
+
let error;
|
|
153
|
+
try {
|
|
154
|
+
await edgeOptimize.assumeConnectorRole({ accountId: '123', externalId: 'ext' });
|
|
155
|
+
} catch (e) {
|
|
156
|
+
error = e;
|
|
157
|
+
}
|
|
158
|
+
expect(error).to.be.an('error');
|
|
159
|
+
expect(error.message).to.include('12-digit');
|
|
160
|
+
expect(stsSendStub.called).to.equal(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('throws when the external id is missing', async () => {
|
|
164
|
+
let error;
|
|
165
|
+
try {
|
|
166
|
+
await edgeOptimize.assumeConnectorRole({ accountId: '120569600543', externalId: '' });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
error = e;
|
|
169
|
+
}
|
|
170
|
+
expect(error).to.be.an('error');
|
|
171
|
+
expect(error.message).to.include('externalId');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('throws when STS returns no credentials', async () => {
|
|
175
|
+
stsSendStub.resolves({});
|
|
176
|
+
let error;
|
|
177
|
+
try {
|
|
178
|
+
await edgeOptimize.assumeConnectorRole({ accountId: '120569600543', externalId: 'ext' });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
error = e;
|
|
181
|
+
}
|
|
182
|
+
expect(error).to.be.an('error');
|
|
183
|
+
expect(error.message).to.include('no credentials');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('listDistributions', () => {
|
|
188
|
+
it('maps the distribution list to the wizard projection', async () => {
|
|
189
|
+
cfSendStub.resolves({
|
|
190
|
+
DistributionList: {
|
|
191
|
+
Items: [
|
|
192
|
+
{
|
|
193
|
+
Id: 'E123',
|
|
194
|
+
DomainName: 'd.cloudfront.net',
|
|
195
|
+
Aliases: { Items: ['www.example.com'] },
|
|
196
|
+
Status: 'Deployed',
|
|
197
|
+
Enabled: true,
|
|
198
|
+
Comment: 'prod',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await edgeOptimize.listDistributions({
|
|
205
|
+
accessKeyId: 'A', secretAccessKey: 'S', sessionToken: 'T',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result).to.have.length(1);
|
|
209
|
+
expect(result[0]).to.deep.equal({
|
|
210
|
+
id: 'E123',
|
|
211
|
+
domainName: 'd.cloudfront.net',
|
|
212
|
+
aliases: ['www.example.com'],
|
|
213
|
+
status: 'Deployed',
|
|
214
|
+
enabled: true,
|
|
215
|
+
comment: 'prod',
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns an empty array when there are no distributions', async () => {
|
|
220
|
+
cfSendStub.resolves({ DistributionList: {} });
|
|
221
|
+
|
|
222
|
+
const result = await edgeOptimize.listDistributions({});
|
|
223
|
+
|
|
224
|
+
expect(result).to.deep.equal([]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('defaults aliases and comment when absent and reflects disabled state', async () => {
|
|
228
|
+
cfSendStub.resolves({
|
|
229
|
+
DistributionList: {
|
|
230
|
+
Items: [{
|
|
231
|
+
Id: 'E2', DomainName: 'd2.cloudfront.net', Status: 'InProgress', Enabled: false,
|
|
232
|
+
}],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const result = await edgeOptimize.listDistributions({});
|
|
237
|
+
|
|
238
|
+
expect(result[0].aliases).to.deep.equal([]);
|
|
239
|
+
expect(result[0].comment).to.equal('');
|
|
240
|
+
expect(result[0].enabled).to.equal(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('paginates through every page when the result is truncated', async () => {
|
|
244
|
+
// First page is truncated (IsTruncated + NextMarker) → a second ListDistributions call must
|
|
245
|
+
// follow the marker; the loop stops once IsTruncated is false. Both pages are aggregated.
|
|
246
|
+
cfSendStub.onFirstCall().resolves({
|
|
247
|
+
DistributionList: {
|
|
248
|
+
IsTruncated: true,
|
|
249
|
+
NextMarker: 'page-2',
|
|
250
|
+
Items: [{
|
|
251
|
+
Id: 'E1', DomainName: 'd1.cloudfront.net', Status: 'Deployed', Enabled: true,
|
|
252
|
+
}],
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
cfSendStub.onSecondCall().resolves({
|
|
256
|
+
DistributionList: {
|
|
257
|
+
IsTruncated: false,
|
|
258
|
+
Items: [{
|
|
259
|
+
Id: 'E2', DomainName: 'd2.cloudfront.net', Status: 'Deployed', Enabled: true,
|
|
260
|
+
}],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await edgeOptimize.listDistributions({});
|
|
265
|
+
|
|
266
|
+
expect(cfSendStub.callCount).to.equal(2);
|
|
267
|
+
// The first page is fetched with no Marker; the second follows the NextMarker.
|
|
268
|
+
expect(cfSendStub.firstCall.args[0].input).to.deep.equal({});
|
|
269
|
+
expect(cfSendStub.secondCall.args[0].input).to.deep.equal({ Marker: 'page-2' });
|
|
270
|
+
expect(result.map((d) => d.id)).to.deep.equal(['E1', 'E2']);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('getDistributionConfig', () => {
|
|
275
|
+
it('maps origins, default cache behavior, and ordered cache behaviors', async () => {
|
|
276
|
+
cfSendStub.resolves({
|
|
277
|
+
DistributionConfig: {
|
|
278
|
+
Origins: {
|
|
279
|
+
Items: [
|
|
280
|
+
{ Id: 'origin-aem', DomainName: 'origin.example.com', OriginPath: '/content' },
|
|
281
|
+
{ Id: 'EdgeOptimizeOrigin', DomainName: 'live.edgeoptimize.net' },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
DefaultCacheBehavior: { TargetOriginId: 'origin-aem' },
|
|
285
|
+
CacheBehaviors: {
|
|
286
|
+
Items: [
|
|
287
|
+
{ PathPattern: '/api/*', TargetOriginId: 'origin-aem' },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const result = await edgeOptimize.getDistributionConfig({}, 'E2EXAMPLE');
|
|
294
|
+
|
|
295
|
+
expect(cfSendStub.calledOnce).to.equal(true);
|
|
296
|
+
expect(cfSendStub.firstCall.args[0].input).to.deep.equal({ Id: 'E2EXAMPLE' });
|
|
297
|
+
expect(result.origins).to.deep.equal([
|
|
298
|
+
{ id: 'origin-aem', domainName: 'origin.example.com', originPath: '/content' },
|
|
299
|
+
{ id: 'EdgeOptimizeOrigin', domainName: 'live.edgeoptimize.net', originPath: '' },
|
|
300
|
+
]);
|
|
301
|
+
expect(result.defaultCacheBehavior).to.deep.equal({
|
|
302
|
+
pathPattern: 'Default (*)',
|
|
303
|
+
targetOriginId: 'origin-aem',
|
|
304
|
+
});
|
|
305
|
+
expect(result.cacheBehaviors).to.deep.equal([
|
|
306
|
+
{ pathPattern: '/api/*', targetOriginId: 'origin-aem' },
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('defaults to empty collections when the config is sparse', async () => {
|
|
311
|
+
cfSendStub.resolves({ DistributionConfig: {} });
|
|
312
|
+
|
|
313
|
+
const result = await edgeOptimize.getDistributionConfig({}, 'E2EXAMPLE');
|
|
314
|
+
|
|
315
|
+
expect(result.origins).to.deep.equal([]);
|
|
316
|
+
expect(result.defaultCacheBehavior).to.equal(null);
|
|
317
|
+
expect(result.cacheBehaviors).to.deep.equal([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('falls back to an empty config object when DistributionConfig is absent', async () => {
|
|
321
|
+
cfSendStub.resolves({});
|
|
322
|
+
|
|
323
|
+
const result = await edgeOptimize.getDistributionConfig({}, 'E2EXAMPLE');
|
|
324
|
+
|
|
325
|
+
expect(result.origins).to.deep.equal([]);
|
|
326
|
+
expect(result.defaultCacheBehavior).to.equal(null);
|
|
327
|
+
expect(result.cacheBehaviors).to.deep.equal([]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('falls back to an empty list when the SDK returns nothing', async () => {
|
|
331
|
+
cfSendStub.resolves(undefined);
|
|
332
|
+
|
|
333
|
+
const result = await edgeOptimize.getDistributionConfig({}, 'E2EXAMPLE');
|
|
334
|
+
|
|
335
|
+
expect(result.origins).to.deep.equal([]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('throws when the distribution id is missing', async () => {
|
|
339
|
+
let error;
|
|
340
|
+
try {
|
|
341
|
+
await edgeOptimize.getDistributionConfig({}, '');
|
|
342
|
+
} catch (e) {
|
|
343
|
+
error = e;
|
|
344
|
+
}
|
|
345
|
+
expect(error).to.be.an('error');
|
|
346
|
+
expect(error.message).to.include('distributionId');
|
|
347
|
+
expect(cfSendStub.called).to.equal(false);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('listDistributions edge cases', () => {
|
|
352
|
+
it('falls back to an empty list when the SDK returns nothing', async () => {
|
|
353
|
+
cfSendStub.resolves(undefined);
|
|
354
|
+
|
|
355
|
+
const result = await edgeOptimize.listDistributions({});
|
|
356
|
+
|
|
357
|
+
expect(result).to.deep.equal([]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('CloudFrontEdgeClient', () => {
|
|
362
|
+
it('requires credentials', () => {
|
|
363
|
+
expect(() => new edgeOptimize.CloudFrontEdgeClient()).to.throw('credentials are required');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('stores credentials once and delegates through generic methods', async () => {
|
|
367
|
+
cfSendStub.resolves({
|
|
368
|
+
DistributionList: {
|
|
369
|
+
Items: [{
|
|
370
|
+
Id: 'E123',
|
|
371
|
+
DomainName: 'd.cloudfront.net',
|
|
372
|
+
Status: 'Deployed',
|
|
373
|
+
Enabled: true,
|
|
374
|
+
}],
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
const credentials = { accessKeyId: 'A', secretAccessKey: 'S', sessionToken: 'T' };
|
|
378
|
+
const client = new edgeOptimize.CloudFrontEdgeClient({
|
|
379
|
+
credentials,
|
|
380
|
+
region: 'us-west-2',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const result = await client.listDistributions();
|
|
384
|
+
|
|
385
|
+
expect(client.credentials).to.equal(credentials);
|
|
386
|
+
expect(client.region).to.equal('us-west-2');
|
|
387
|
+
expect(result.map((d) => d.id)).to.deep.equal(['E123']);
|
|
388
|
+
expect(cfSendStub.calledOnce).to.equal(true);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('CloudFrontEdgeClient method delegation', () => {
|
|
393
|
+
const creds = { accessKeyId: 'A', secretAccessKey: 'S', sessionToken: 'T' };
|
|
394
|
+
const client = () => new edgeOptimize.CloudFrontEdgeClient({ credentials: creds, region: 'us-east-1' });
|
|
395
|
+
|
|
396
|
+
// Dispatch a *SendStub by command name (robust to call order / poll counts).
|
|
397
|
+
const dispatch = (stub, map) => stub.callsFake((cmd) => {
|
|
398
|
+
const r = map[cmd.commandName];
|
|
399
|
+
if (r === undefined) {
|
|
400
|
+
throw new Error(`unexpected command in test: ${cmd.commandName}`);
|
|
401
|
+
}
|
|
402
|
+
return Promise.resolve(typeof r === 'function' ? r(cmd) : r);
|
|
403
|
+
});
|
|
404
|
+
const throwNamed = (name) => () => {
|
|
405
|
+
const e = new Error(name);
|
|
406
|
+
e.name = name;
|
|
407
|
+
throw e;
|
|
408
|
+
};
|
|
409
|
+
const validTrust = encodeURIComponent(JSON.stringify({
|
|
410
|
+
Version: '2012-10-17',
|
|
411
|
+
Statement: [{
|
|
412
|
+
Effect: 'Allow',
|
|
413
|
+
Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] },
|
|
414
|
+
Action: 'sts:AssumeRole',
|
|
415
|
+
}],
|
|
416
|
+
}));
|
|
417
|
+
const okRoleIam = {
|
|
418
|
+
GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: validTrust } },
|
|
419
|
+
GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' },
|
|
420
|
+
UpdateAssumeRolePolicy: {},
|
|
421
|
+
PutRolePolicy: {},
|
|
422
|
+
};
|
|
423
|
+
const deployParams = {
|
|
424
|
+
distributionId: 'E2EXAMPLE123',
|
|
425
|
+
originId: 'origin-aem',
|
|
426
|
+
behavior: 'default',
|
|
427
|
+
originDomain: 'dev.edgeoptimize.net',
|
|
428
|
+
originHeaders: { apiKey: 'eo-key', forwardedHost: 'www.example.com' },
|
|
429
|
+
accountId: '120569600543',
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
it('getDistributionConfig delegates to the free function', async () => {
|
|
433
|
+
dispatch(cfSendStub, {
|
|
434
|
+
GetDistributionConfig: { DistributionConfig: { Origins: { Items: [{ Id: 'o1', DomainName: 'x' }] } } },
|
|
435
|
+
});
|
|
436
|
+
const res = await client().getDistributionConfig('E1');
|
|
437
|
+
expect(res.origins).to.deep.equal([{ id: 'o1', domainName: 'x', originPath: '' }]);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('createOrigin delegates to the free function', async () => {
|
|
441
|
+
dispatch(cfSendStub, {
|
|
442
|
+
GetDistributionConfig: { DistributionConfig: { Origins: { Quantity: 0, Items: [] } }, ETag: 'e1' },
|
|
443
|
+
UpdateDistribution: {},
|
|
444
|
+
});
|
|
445
|
+
const res = await client().createOrigin('E1', 'dev.edgeoptimize.net', {
|
|
446
|
+
apiKey: 'k', forwardedHost: 'h',
|
|
447
|
+
});
|
|
448
|
+
expect(res).to.deep.equal({
|
|
449
|
+
created: true, alreadyExisted: false, updated: false, originId: 'EdgeOptimize_Origin',
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('createCloudFrontFunction delegates to the free function', async () => {
|
|
454
|
+
dispatch(cfSendStub, {
|
|
455
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists'),
|
|
456
|
+
CreateFunction: { ETag: 'fn-etag' },
|
|
457
|
+
PublishFunction: {},
|
|
458
|
+
});
|
|
459
|
+
const res = await client().createCloudFrontFunction('origin-aem', 'E1');
|
|
460
|
+
expect(res.created).to.equal(true);
|
|
461
|
+
expect(res.stage).to.equal('LIVE');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('updateCacheSettings delegates to the free function', async () => {
|
|
465
|
+
dispatch(cfSendStub, {
|
|
466
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
467
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
468
|
+
GetCachePolicyConfig: {
|
|
469
|
+
CachePolicyConfig: {
|
|
470
|
+
Name: 'p',
|
|
471
|
+
MinTTL: 60,
|
|
472
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
473
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 0, Items: [] } },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
ETag: 'cp-etag',
|
|
477
|
+
},
|
|
478
|
+
UpdateCachePolicy: {},
|
|
479
|
+
});
|
|
480
|
+
const res = await client().updateCacheSettings('E1', 'default');
|
|
481
|
+
expect(res.scenario).to.equal('custom');
|
|
482
|
+
expect(res.updated).to.equal(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('createLambdaAtEdge delegates to the free function', async () => {
|
|
486
|
+
dispatch(iamSendStub, {
|
|
487
|
+
GetRole: throwNamed('NoSuchEntityException'),
|
|
488
|
+
CreateRole: { Role: { Arn: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role' } },
|
|
489
|
+
PutRolePolicy: {},
|
|
490
|
+
});
|
|
491
|
+
dispatch(lambdaSendStub, {
|
|
492
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException'),
|
|
493
|
+
CreateFunction: { FunctionArn: 'arn:fn' },
|
|
494
|
+
});
|
|
495
|
+
const res = await client().createLambdaAtEdge('120569600543', { distributionId: 'E1' });
|
|
496
|
+
expect(res.status).to.equal('provisioning');
|
|
497
|
+
expect(res.roleArn).to.include('edgeoptimize-origin-role');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('getLambdaAtEdgeStatus delegates to the free function', async () => {
|
|
501
|
+
dispatch(iamSendStub, { GetRole: throwNamed('NoSuchEntityException') });
|
|
502
|
+
dispatch(lambdaSendStub, { GetFunctionConfiguration: throwNamed('ResourceNotFoundException') });
|
|
503
|
+
const res = await client().getLambdaAtEdgeStatus('E1');
|
|
504
|
+
expect(res).to.deep.equal({
|
|
505
|
+
roleExists: false, roleOk: false, exists: false, versionArn: null, ready: false,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('applyAssociations delegates to the free function', async () => {
|
|
510
|
+
const lambdaArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:1';
|
|
511
|
+
dispatch(cfSendStub, {
|
|
512
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
513
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: {} }, ETag: 'dist-etag' },
|
|
514
|
+
UpdateDistribution: {},
|
|
515
|
+
});
|
|
516
|
+
const res = await client().applyAssociations('E1', 'default', lambdaArn);
|
|
517
|
+
expect(res).to.deep.equal({ cloudFrontFunctionArn: 'arn:cf-fn', lambdaArn });
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('runDeployStep delegates to the free function', async () => {
|
|
521
|
+
dispatch(cfSendStub, {
|
|
522
|
+
GetDistributionConfig: {
|
|
523
|
+
DistributionConfig: {
|
|
524
|
+
Origins: {
|
|
525
|
+
Items: [{
|
|
526
|
+
Id: 'EdgeOptimize_Origin',
|
|
527
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
528
|
+
CustomHeaders: {
|
|
529
|
+
Items: [
|
|
530
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
531
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
}],
|
|
535
|
+
},
|
|
536
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
537
|
+
},
|
|
538
|
+
ETag: 'etag',
|
|
539
|
+
},
|
|
540
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
541
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
542
|
+
GetCachePolicyConfig: {
|
|
543
|
+
CachePolicyConfig: {
|
|
544
|
+
Name: 'p',
|
|
545
|
+
MinTTL: 0,
|
|
546
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
547
|
+
HeadersConfig: {
|
|
548
|
+
HeaderBehavior: 'whitelist',
|
|
549
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
ETag: 'cp-etag',
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
dispatch(lambdaSendStub, {
|
|
557
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException'),
|
|
558
|
+
ListVersionsByFunction: { Versions: [] },
|
|
559
|
+
CreateFunction: { FunctionArn: 'arn:lambda', Version: '$LATEST' },
|
|
560
|
+
});
|
|
561
|
+
dispatch(iamSendStub, okRoleIam);
|
|
562
|
+
const res = await client().runDeployStep(deployParams);
|
|
563
|
+
expect(res.steps.find((s) => s.key === 'origin').status).to.equal('done');
|
|
564
|
+
expect(res.steps.find((s) => s.key === 'lambda').status).to.equal('in_progress');
|
|
565
|
+
expect(res.routingDeployed).to.equal(false);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('planDeploy delegates to the free function', async () => {
|
|
569
|
+
dispatch(cfSendStub, {
|
|
570
|
+
GetDistributionConfig: {
|
|
571
|
+
DistributionConfig: {
|
|
572
|
+
Origins: { Items: [] },
|
|
573
|
+
DefaultCacheBehavior: {
|
|
574
|
+
ForwardedValues: { Headers: { Quantity: 0, Items: [] } },
|
|
575
|
+
MinTTL: 60,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists'),
|
|
580
|
+
});
|
|
581
|
+
dispatch(lambdaSendStub, { GetFunctionConfiguration: throwNamed('ResourceNotFoundException') });
|
|
582
|
+
dispatch(iamSendStub, { GetRole: throwNamed('NoSuchEntityException') });
|
|
583
|
+
const res = await client().planDeploy(deployParams);
|
|
584
|
+
expect(res.canProceed).to.equal(true);
|
|
585
|
+
expect(res.steps.map((s) => s.key)).to.deep.equal(['origin', 'function', 'cache', 'lambda', 'associate']);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
describe('createOrigin', () => {
|
|
590
|
+
it('adds the Edge Optimize origin when it does not exist', async () => {
|
|
591
|
+
cfSendStub.onFirstCall().resolves({
|
|
592
|
+
DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'origin-aem', DomainName: 'origin.example.com' }] } },
|
|
593
|
+
ETag: 'etag-1',
|
|
594
|
+
});
|
|
595
|
+
cfSendStub.onSecondCall().resolves({});
|
|
596
|
+
|
|
597
|
+
const result = await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net');
|
|
598
|
+
|
|
599
|
+
expect(result).to.deep.equal({
|
|
600
|
+
created: true, alreadyExisted: false, updated: false, originId: 'EdgeOptimize_Origin',
|
|
601
|
+
});
|
|
602
|
+
expect(cfSendStub.secondCall.args[0].commandName).to.equal('UpdateDistribution');
|
|
603
|
+
const update = cfSendStub.secondCall.args[0].input;
|
|
604
|
+
expect(update.IfMatch).to.equal('etag-1');
|
|
605
|
+
const added = update.DistributionConfig.Origins.Items.find((o) => o.Id === 'EdgeOptimize_Origin');
|
|
606
|
+
expect(added.DomainName).to.equal('dev.edgeoptimize.net');
|
|
607
|
+
expect(added.CustomOriginConfig.OriginProtocolPolicy).to.equal('https-only');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('defaults the origin domain to the production EO domain', async () => {
|
|
611
|
+
cfSendStub.onFirstCall().resolves({
|
|
612
|
+
DistributionConfig: { Origins: { Items: [] } },
|
|
613
|
+
ETag: 'etag-1',
|
|
614
|
+
});
|
|
615
|
+
cfSendStub.onSecondCall().resolves({});
|
|
616
|
+
|
|
617
|
+
await edgeOptimize.createOrigin({}, 'E2EXAMPLE');
|
|
618
|
+
|
|
619
|
+
const added = cfSendStub.secondCall.args[0].input
|
|
620
|
+
.DistributionConfig.Origins.Items.find((o) => o.Id === 'EdgeOptimize_Origin');
|
|
621
|
+
expect(added.DomainName).to.equal('live.edgeoptimize.net');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('handles an absent Origins collection on the config', async () => {
|
|
625
|
+
cfSendStub.onFirstCall().resolves({ DistributionConfig: {}, ETag: 'etag-1' });
|
|
626
|
+
cfSendStub.onSecondCall().resolves({});
|
|
627
|
+
|
|
628
|
+
const result = await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net');
|
|
629
|
+
|
|
630
|
+
expect(result.created).to.equal(true);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('sets the EO custom headers on the new origin', async () => {
|
|
634
|
+
cfSendStub.onFirstCall().resolves({
|
|
635
|
+
DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'origin-aem', DomainName: 'origin.example.com' }] } },
|
|
636
|
+
ETag: 'etag-1',
|
|
637
|
+
});
|
|
638
|
+
cfSendStub.onSecondCall().resolves({});
|
|
639
|
+
|
|
640
|
+
await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net', {
|
|
641
|
+
apiKey: 'eo-key-123', forwardedHost: 'www.example.com', fetcherKey: 'fk-9',
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const update = cfSendStub.secondCall.args[0].input;
|
|
645
|
+
const added = update.DistributionConfig.Origins.Items.find((o) => o.Id === 'EdgeOptimize_Origin');
|
|
646
|
+
expect(added.CustomHeaders.Quantity).to.equal(3);
|
|
647
|
+
const headerMap = added.CustomHeaders.Items.reduce((acc, h) => {
|
|
648
|
+
acc[h.HeaderName] = h.HeaderValue;
|
|
649
|
+
return acc;
|
|
650
|
+
}, {});
|
|
651
|
+
expect(headerMap).to.deep.equal({
|
|
652
|
+
'x-edgeoptimize-api-key': 'eo-key-123',
|
|
653
|
+
'x-forwarded-host': 'www.example.com',
|
|
654
|
+
'x-edgeoptimize-fetcher-key': 'fk-9',
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('is idempotent when the origin already exists by id', async () => {
|
|
659
|
+
cfSendStub.resolves({
|
|
660
|
+
DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'x' }] } },
|
|
661
|
+
ETag: 'etag-1',
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const result = await edgeOptimize.createOrigin({}, 'E2EXAMPLE');
|
|
665
|
+
|
|
666
|
+
expect(result).to.deep.equal({
|
|
667
|
+
created: false, alreadyExisted: true, updated: false, originId: 'EdgeOptimize_Origin',
|
|
668
|
+
});
|
|
669
|
+
expect(cfSendStub.calledOnce).to.equal(true); // never updated
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('stops when a non-Edge Optimize origin already uses the EO domain', async () => {
|
|
673
|
+
cfSendStub.resolves({
|
|
674
|
+
DistributionConfig: { Origins: { Items: [{ Id: 'custom', DomainName: 'dev.edgeoptimize.net' }] } },
|
|
675
|
+
ETag: 'etag-1',
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
let error;
|
|
679
|
+
try {
|
|
680
|
+
await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net');
|
|
681
|
+
} catch (e) {
|
|
682
|
+
error = e;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
expect(error.message).to.include('refusing to reuse a non-Edge Optimize origin');
|
|
686
|
+
expect(cfSendStub.calledOnce).to.equal(true);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('patches the headers when the origin exists without them (self-heal)', async () => {
|
|
690
|
+
cfSendStub.onFirstCall().resolves({
|
|
691
|
+
DistributionConfig: {
|
|
692
|
+
Origins: {
|
|
693
|
+
Quantity: 1,
|
|
694
|
+
Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net', CustomHeaders: { Quantity: 0, Items: [] } }],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
ETag: 'etag-1',
|
|
698
|
+
});
|
|
699
|
+
cfSendStub.onSecondCall().resolves({});
|
|
700
|
+
|
|
701
|
+
const result = await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net', {
|
|
702
|
+
apiKey: 'eo-key-123', forwardedHost: 'www.example.com',
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(result).to.deep.equal({
|
|
706
|
+
created: false, alreadyExisted: true, updated: true, originId: 'EdgeOptimize_Origin',
|
|
707
|
+
});
|
|
708
|
+
expect(cfSendStub.secondCall.args[0].commandName).to.equal('UpdateDistribution');
|
|
709
|
+
const patched = cfSendStub.secondCall.args[0].input
|
|
710
|
+
.DistributionConfig.Origins.Items.find((o) => o.Id === 'EdgeOptimize_Origin');
|
|
711
|
+
expect(patched.CustomHeaders.Quantity).to.equal(2);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('does not patch when the existing headers already match', async () => {
|
|
715
|
+
cfSendStub.resolves({
|
|
716
|
+
DistributionConfig: {
|
|
717
|
+
Origins: {
|
|
718
|
+
Quantity: 1,
|
|
719
|
+
Items: [{
|
|
720
|
+
Id: 'EdgeOptimize_Origin',
|
|
721
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
722
|
+
CustomHeaders: {
|
|
723
|
+
Quantity: 2,
|
|
724
|
+
Items: [
|
|
725
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key-123' },
|
|
726
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
}],
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
ETag: 'etag-1',
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const result = await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net', {
|
|
736
|
+
apiKey: 'eo-key-123', forwardedHost: 'www.example.com',
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
expect(result.updated).to.equal(false);
|
|
740
|
+
expect(cfSendStub.calledOnce).to.equal(true); // no UpdateDistribution
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('throws when the distribution id is missing', async () => {
|
|
744
|
+
let error;
|
|
745
|
+
try {
|
|
746
|
+
await edgeOptimize.createOrigin({}, '');
|
|
747
|
+
} catch (e) {
|
|
748
|
+
error = e;
|
|
749
|
+
}
|
|
750
|
+
expect(error.message).to.include('distributionId');
|
|
751
|
+
expect(cfSendStub.called).to.equal(false);
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
describe('buildCloudfrontFunctionCode', () => {
|
|
756
|
+
it('embeds the default origin id and null targeted paths', () => {
|
|
757
|
+
const code = edgeOptimize.buildCloudfrontFunctionCode('origin-aem');
|
|
758
|
+
expect(code).to.include('{ "originId": "origin-aem" }');
|
|
759
|
+
expect(code).to.include('var TARGETED_PATHS = null;');
|
|
760
|
+
expect(code).to.include("import cf from 'cloudfront';");
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('embeds explicit targeted paths as JSON', () => {
|
|
764
|
+
const code = edgeOptimize.buildCloudfrontFunctionCode('origin-aem', ['/a', '/b']);
|
|
765
|
+
expect(code).to.include('var TARGETED_PATHS = ["/a","/b"];');
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('escapes the default origin id as a JavaScript string literal', () => {
|
|
769
|
+
const code = edgeOptimize.buildCloudfrontFunctionCode('origin-aem"; throw new Error("x")//');
|
|
770
|
+
|
|
771
|
+
expect(code).to.include('{ "originId": "origin-aem\\"; throw new Error(\\"x\\")//" }');
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
describe('buildEdgeOptimizeLambdaCode', () => {
|
|
776
|
+
it('bakes the EO origin domain into the routing check (per environment)', () => {
|
|
777
|
+
const dev = edgeOptimize.buildEdgeOptimizeLambdaCode('dev.edgeoptimize.net');
|
|
778
|
+
expect(dev).to.include('originDomain === "dev.edgeoptimize.net"');
|
|
779
|
+
expect(dev).to.not.include('originDomain === "live.edgeoptimize.net"');
|
|
780
|
+
|
|
781
|
+
const prod = edgeOptimize.buildEdgeOptimizeLambdaCode('live.edgeoptimize.net');
|
|
782
|
+
expect(prod).to.include('originDomain === "live.edgeoptimize.net"');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('escapes the EO origin domain as a JavaScript string literal', () => {
|
|
786
|
+
const code = edgeOptimize.buildEdgeOptimizeLambdaCode('live.edgeoptimize.net"; throw new Error("x")//');
|
|
787
|
+
|
|
788
|
+
expect(code).to.include('originDomain === "live.edgeoptimize.net\\"; throw new Error(\\"x\\")//"');
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
describe('createCloudFrontFunction', () => {
|
|
793
|
+
it('creates and publishes a new function when none exists', async () => {
|
|
794
|
+
cfSendStub.onFirstCall().rejects(Object.assign(new Error('not found'), { name: 'NoSuchFunctionExists' }));
|
|
795
|
+
cfSendStub.onSecondCall().resolves({ ETag: 'fn-etag' }); // CreateFunction
|
|
796
|
+
cfSendStub.onThirdCall().resolves({}); // PublishFunction
|
|
797
|
+
|
|
798
|
+
const result = await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE');
|
|
799
|
+
|
|
800
|
+
expect(result).to.deep.equal({ name: 'edgeoptimize-routing-adobe-E2EXAMPLE', created: true, stage: 'LIVE' });
|
|
801
|
+
expect(cfSendStub.secondCall.args[0].commandName).to.equal('CreateFunction');
|
|
802
|
+
expect(cfSendStub.thirdCall.args[0].commandName).to.equal('PublishFunction');
|
|
803
|
+
expect(cfSendStub.thirdCall.args[0].input.IfMatch).to.equal('fn-etag');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('updates and publishes when the function already exists', async () => {
|
|
807
|
+
cfSendStub.onFirstCall().resolves({ ETag: 'dev-etag' }); // DescribeFunction DEVELOPMENT
|
|
808
|
+
cfSendStub.onSecondCall().resolves({ ETag: 'updated-etag' }); // UpdateFunction
|
|
809
|
+
cfSendStub.onThirdCall().resolves({}); // PublishFunction
|
|
810
|
+
|
|
811
|
+
const result = await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE');
|
|
812
|
+
|
|
813
|
+
expect(result.created).to.equal(false);
|
|
814
|
+
expect(cfSendStub.secondCall.args[0].commandName).to.equal('UpdateFunction');
|
|
815
|
+
expect(cfSendStub.thirdCall.args[0].input.IfMatch).to.equal('updated-etag');
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('throws when defaultOriginId is missing', async () => {
|
|
819
|
+
let error;
|
|
820
|
+
try {
|
|
821
|
+
await edgeOptimize.createCloudFrontFunction({}, '');
|
|
822
|
+
} catch (e) {
|
|
823
|
+
error = e;
|
|
824
|
+
}
|
|
825
|
+
expect(error.message).to.include('defaultOriginId');
|
|
826
|
+
expect(cfSendStub.called).to.equal(false);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('throws when distributionId is missing', async () => {
|
|
830
|
+
let error;
|
|
831
|
+
try {
|
|
832
|
+
await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', '');
|
|
833
|
+
} catch (e) {
|
|
834
|
+
error = e;
|
|
835
|
+
}
|
|
836
|
+
expect(error.message).to.include('distributionId');
|
|
837
|
+
expect(cfSendStub.called).to.equal(false);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('rethrows unexpected describe errors', async () => {
|
|
841
|
+
cfSendStub.onFirstCall().rejects(new Error('boom'));
|
|
842
|
+
let error;
|
|
843
|
+
try {
|
|
844
|
+
await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE');
|
|
845
|
+
} catch (e) {
|
|
846
|
+
error = e;
|
|
847
|
+
}
|
|
848
|
+
expect(error.message).to.equal('boom');
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
describe('updateCacheSettings', () => {
|
|
853
|
+
// Dispatch cfSendStub by command name so tests are robust to call order.
|
|
854
|
+
const wireCloudFront = (responders) => {
|
|
855
|
+
cfSendStub.callsFake((cmd) => {
|
|
856
|
+
const fn = responders[cmd.commandName];
|
|
857
|
+
if (!fn) {
|
|
858
|
+
throw new Error(`unexpected command in test: ${cmd.commandName}`);
|
|
859
|
+
}
|
|
860
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
861
|
+
});
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const lastCommand = (name) => cfSendStub.getCalls()
|
|
865
|
+
.filter((c) => c.args[0].commandName === name).pop()?.args[0];
|
|
866
|
+
|
|
867
|
+
it('updates a CUSTOM policy to add the EO headers + MinTTL 0', async () => {
|
|
868
|
+
wireCloudFront({
|
|
869
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
870
|
+
ListCachePolicies: { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-x' } }] } },
|
|
871
|
+
GetCachePolicyConfig: {
|
|
872
|
+
CachePolicyConfig: {
|
|
873
|
+
Name: 'my-policy',
|
|
874
|
+
MinTTL: 60,
|
|
875
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
876
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 1, Items: ['accept'] } },
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
ETag: 'cp-etag',
|
|
880
|
+
},
|
|
881
|
+
UpdateCachePolicy: {},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
885
|
+
|
|
886
|
+
expect(result.scenario).to.equal('custom');
|
|
887
|
+
expect(result.policyId).to.equal('cp-1');
|
|
888
|
+
expect(result.updated).to.equal(true);
|
|
889
|
+
const updated = lastCommand('UpdateCachePolicy').input.CachePolicyConfig;
|
|
890
|
+
expect(updated.MinTTL).to.equal(0);
|
|
891
|
+
const items = updated.ParametersInCacheKeyAndForwardedToOrigin.HeadersConfig.Headers.Items;
|
|
892
|
+
expect(items).to.include('x-edgeoptimize-config');
|
|
893
|
+
expect(items).to.include('x-edgeoptimize-url');
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('updates a CUSTOM policy MinTTL only when the headers are already present', async () => {
|
|
897
|
+
wireCloudFront({
|
|
898
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
899
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
900
|
+
GetCachePolicyConfig: {
|
|
901
|
+
CachePolicyConfig: {
|
|
902
|
+
Name: 'my-policy',
|
|
903
|
+
MinTTL: 99,
|
|
904
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
905
|
+
HeadersConfig: {
|
|
906
|
+
HeaderBehavior: 'whitelist',
|
|
907
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
ETag: 'cp-etag',
|
|
912
|
+
},
|
|
913
|
+
UpdateCachePolicy: {},
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
917
|
+
|
|
918
|
+
expect(result.updated).to.equal(true);
|
|
919
|
+
const updated = lastCommand('UpdateCachePolicy').input.CachePolicyConfig;
|
|
920
|
+
expect(updated.MinTTL).to.equal(0);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('updates a sparse CUSTOM policy (no managed list, no params, no MinTTL)', async () => {
|
|
924
|
+
wireCloudFront({
|
|
925
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
926
|
+
// managed list response has no CachePolicyList → `?.Items || []` (managedIds empty).
|
|
927
|
+
ListCachePolicies: {},
|
|
928
|
+
// custom policy with NO ParametersInCacheKeyAndForwardedToOrigin and NO MinTTL.
|
|
929
|
+
GetCachePolicyConfig: { CachePolicyConfig: { Name: 'bare' }, ETag: 'cp-etag' },
|
|
930
|
+
UpdateCachePolicy: {},
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
934
|
+
|
|
935
|
+
expect(result.scenario).to.equal('custom');
|
|
936
|
+
expect(result.updated).to.equal(true);
|
|
937
|
+
const updated = lastCommand('UpdateCachePolicy').input.CachePolicyConfig;
|
|
938
|
+
const items = updated.ParametersInCacheKeyAndForwardedToOrigin.HeadersConfig.Headers.Items;
|
|
939
|
+
expect(items).to.include('x-edgeoptimize-config');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('does not add headers to a CUSTOM policy with HeaderBehavior allViewer', async () => {
|
|
943
|
+
wireCloudFront({
|
|
944
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
945
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
946
|
+
GetCachePolicyConfig: {
|
|
947
|
+
CachePolicyConfig: {
|
|
948
|
+
Name: 'my-policy',
|
|
949
|
+
MinTTL: 0,
|
|
950
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'allViewer' } },
|
|
951
|
+
},
|
|
952
|
+
ETag: 'cp-etag',
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
957
|
+
|
|
958
|
+
// allViewer already forwards all headers + MinTTL already 0 → nothing to do.
|
|
959
|
+
expect(result.updated).to.equal(false);
|
|
960
|
+
expect(result.alreadyForwarded).to.equal(true);
|
|
961
|
+
expect(lastCommand('UpdateCachePolicy')).to.equal(undefined);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('respects setMinTTLZero:false (keeps a long MinTTL on a custom policy)', async () => {
|
|
965
|
+
wireCloudFront({
|
|
966
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
967
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
968
|
+
GetCachePolicyConfig: {
|
|
969
|
+
CachePolicyConfig: {
|
|
970
|
+
Name: 'my-policy',
|
|
971
|
+
MinTTL: 9999,
|
|
972
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'none' } },
|
|
973
|
+
},
|
|
974
|
+
ETag: 'cp-etag',
|
|
975
|
+
},
|
|
976
|
+
UpdateCachePolicy: {},
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default', { setMinTTLZero: false });
|
|
980
|
+
|
|
981
|
+
const updated = lastCommand('UpdateCachePolicy').input.CachePolicyConfig;
|
|
982
|
+
expect(updated.MinTTL).to.equal(9999); // untouched
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('is a no-op when a custom policy already forwards the headers and MinTTL is 0', async () => {
|
|
986
|
+
wireCloudFront({
|
|
987
|
+
GetDistributionConfig: { DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp-1' } } },
|
|
988
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
989
|
+
GetCachePolicyConfig: {
|
|
990
|
+
CachePolicyConfig: {
|
|
991
|
+
Name: 'my-policy',
|
|
992
|
+
MinTTL: 0,
|
|
993
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
994
|
+
HeadersConfig: {
|
|
995
|
+
HeaderBehavior: 'whitelist',
|
|
996
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
ETag: 'cp-etag',
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1005
|
+
|
|
1006
|
+
expect(result).to.deep.equal({
|
|
1007
|
+
scenario: 'custom', policyId: 'cp-1', updated: false, alreadyForwarded: true,
|
|
1008
|
+
});
|
|
1009
|
+
expect(lastCommand('UpdateCachePolicy')).to.equal(undefined); // never updated
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('CLONES an AWS-managed policy into a per-distribution custom policy and repoints the behavior', async () => {
|
|
1013
|
+
wireCloudFront({
|
|
1014
|
+
GetDistributionConfig: {
|
|
1015
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1', ForwardedValues: { x: 1 } } },
|
|
1016
|
+
ETag: 'dist-etag',
|
|
1017
|
+
},
|
|
1018
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
1019
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
1020
|
+
: { CachePolicyList: { Items: [] } }), // no existing custom edgeoptimize-cache
|
|
1021
|
+
GetCachePolicy: {
|
|
1022
|
+
CachePolicy: {
|
|
1023
|
+
CachePolicyConfig: {
|
|
1024
|
+
Name: 'Managed-CachingOptimized',
|
|
1025
|
+
MinTTL: 86400,
|
|
1026
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'none' } },
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
CreateCachePolicy: { CachePolicy: { Id: 'new-eo-policy' } },
|
|
1031
|
+
UpdateDistribution: {},
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1035
|
+
|
|
1036
|
+
expect(result.scenario).to.equal('managed');
|
|
1037
|
+
expect(result.policyId).to.equal('new-eo-policy');
|
|
1038
|
+
expect(result.reused).to.equal(false);
|
|
1039
|
+
const created = lastCommand('CreateCachePolicy').input.CachePolicyConfig;
|
|
1040
|
+
expect(created.Name).to.equal('CachingOptimized-adobe-E2EXAMPLE');
|
|
1041
|
+
expect(created.MinTTL).to.equal(0);
|
|
1042
|
+
const items = created.ParametersInCacheKeyAndForwardedToOrigin.HeadersConfig.Headers.Items;
|
|
1043
|
+
expect(items).to.include('x-edgeoptimize-config');
|
|
1044
|
+
// behavior repointed to the new policy + ForwardedValues removed
|
|
1045
|
+
const cfg = lastCommand('UpdateDistribution').input.DistributionConfig;
|
|
1046
|
+
expect(cfg.DefaultCacheBehavior.CachePolicyId).to.equal('new-eo-policy');
|
|
1047
|
+
expect(cfg.DefaultCacheBehavior.ForwardedValues).to.equal(undefined);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('keeps a short MinTTL (<=5s) when cloning a managed policy instead of forcing it to 0', async () => {
|
|
1051
|
+
wireCloudFront({
|
|
1052
|
+
GetDistributionConfig: {
|
|
1053
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1' } },
|
|
1054
|
+
ETag: 'dist-etag',
|
|
1055
|
+
},
|
|
1056
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
1057
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
1058
|
+
: { CachePolicyList: { Items: [] } }),
|
|
1059
|
+
GetCachePolicy: {
|
|
1060
|
+
CachePolicy: {
|
|
1061
|
+
CachePolicyConfig: {
|
|
1062
|
+
Name: 'Managed-CachingOptimized',
|
|
1063
|
+
MinTTL: 3,
|
|
1064
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'none' } },
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
CreateCachePolicy: { CachePolicy: { Id: 'new-eo-policy' } },
|
|
1069
|
+
UpdateDistribution: {},
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1073
|
+
|
|
1074
|
+
const created = lastCommand('CreateCachePolicy').input.CachePolicyConfig;
|
|
1075
|
+
expect(created.MinTTL).to.equal(3); // <= 5s kept, not zeroed
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('clones a managed policy with sparse reads (no params, no custom list)', async () => {
|
|
1079
|
+
wireCloudFront({
|
|
1080
|
+
GetDistributionConfig: {
|
|
1081
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1' } },
|
|
1082
|
+
ETag: 'dist-etag',
|
|
1083
|
+
},
|
|
1084
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
1085
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
1086
|
+
: {}), // custom list response has no CachePolicyList → `?.Items || []` fallback
|
|
1087
|
+
GetCachePolicy: {
|
|
1088
|
+
// source has NO ParametersInCacheKeyAndForwardedToOrigin → `cloned... || {}` fallback
|
|
1089
|
+
CachePolicy: { CachePolicyConfig: { Name: 'Managed-Basic' } },
|
|
1090
|
+
},
|
|
1091
|
+
CreateCachePolicy: { CachePolicy: { Id: 'new-eo-policy' } },
|
|
1092
|
+
UpdateDistribution: {},
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1096
|
+
|
|
1097
|
+
expect(result.scenario).to.equal('managed');
|
|
1098
|
+
expect(result.reused).to.equal(false);
|
|
1099
|
+
const created = lastCommand('CreateCachePolicy').input.CachePolicyConfig;
|
|
1100
|
+
const items = created.ParametersInCacheKeyAndForwardedToOrigin.HeadersConfig.Headers.Items;
|
|
1101
|
+
expect(items).to.include('x-edgeoptimize-config');
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('reuses an existing edgeoptimize-cache custom policy (idempotent managed path)', async () => {
|
|
1105
|
+
wireCloudFront({
|
|
1106
|
+
GetDistributionConfig: {
|
|
1107
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1' } },
|
|
1108
|
+
ETag: 'dist-etag',
|
|
1109
|
+
},
|
|
1110
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
1111
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
1112
|
+
: { CachePolicyList: { Items: [{ CachePolicy: { Id: 'existing-eo', CachePolicyConfig: { Name: 'X-adobe-E2EXAMPLE' } } }] } }),
|
|
1113
|
+
GetCachePolicy: {
|
|
1114
|
+
CachePolicy: { CachePolicyConfig: { Name: 'Managed-X', ParametersInCacheKeyAndForwardedToOrigin: {} } },
|
|
1115
|
+
},
|
|
1116
|
+
UpdateDistribution: {},
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1120
|
+
|
|
1121
|
+
expect(result.scenario).to.equal('managed');
|
|
1122
|
+
expect(result.policyId).to.equal('existing-eo');
|
|
1123
|
+
expect(result.reused).to.equal(true);
|
|
1124
|
+
expect(lastCommand('CreateCachePolicy')).to.equal(undefined); // reused, not created
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('handles a LEGACY behavior (ForwardedValues, no CachePolicyId)', async () => {
|
|
1128
|
+
wireCloudFront({
|
|
1129
|
+
GetDistributionConfig: {
|
|
1130
|
+
DistributionConfig: {
|
|
1131
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Quantity: 1, Items: ['accept'] } }, MinTTL: 60 },
|
|
1132
|
+
},
|
|
1133
|
+
ETag: 'dist-etag',
|
|
1134
|
+
},
|
|
1135
|
+
UpdateDistribution: {},
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1139
|
+
|
|
1140
|
+
expect(result.scenario).to.equal('legacy');
|
|
1141
|
+
expect(result.updated).to.equal(true);
|
|
1142
|
+
const cfg = lastCommand('UpdateDistribution').input.DistributionConfig;
|
|
1143
|
+
const items = cfg.DefaultCacheBehavior.ForwardedValues.Headers.Items;
|
|
1144
|
+
expect(items).to.include('x-edgeoptimize-config');
|
|
1145
|
+
expect(cfg.DefaultCacheBehavior.MinTTL).to.equal(0);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it('handles a sparse LEGACY behavior (no ForwardedValues / Headers / MinTTL)', async () => {
|
|
1149
|
+
wireCloudFront({
|
|
1150
|
+
// legacy (no CachePolicyId), NO ForwardedValues / Headers / MinTTL → `|| {}`,
|
|
1151
|
+
// `?.Items || []`, and `behavior.MinTTL ?? 0` fallbacks.
|
|
1152
|
+
GetDistributionConfig: {
|
|
1153
|
+
DistributionConfig: { DefaultCacheBehavior: {} },
|
|
1154
|
+
ETag: 'dist-etag',
|
|
1155
|
+
},
|
|
1156
|
+
UpdateDistribution: {},
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1160
|
+
|
|
1161
|
+
expect(result.scenario).to.equal('legacy');
|
|
1162
|
+
expect(result.updated).to.equal(true);
|
|
1163
|
+
const cfg = lastCommand('UpdateDistribution').input.DistributionConfig;
|
|
1164
|
+
expect(cfg.DefaultCacheBehavior.ForwardedValues.Headers.Items).to.include('x-edgeoptimize-config');
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('is a no-op LEGACY behavior when headers already forwarded and MinTTL short', async () => {
|
|
1168
|
+
wireCloudFront({
|
|
1169
|
+
GetDistributionConfig: {
|
|
1170
|
+
DistributionConfig: {
|
|
1171
|
+
DefaultCacheBehavior: {
|
|
1172
|
+
ForwardedValues: { Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
1173
|
+
MinTTL: 0,
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
ETag: 'dist-etag',
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1181
|
+
|
|
1182
|
+
expect(result).to.deep.equal({
|
|
1183
|
+
scenario: 'legacy', policyId: null, updated: false, alreadyForwarded: true,
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it('does not add headers on a LEGACY behavior that already forwards "*"', async () => {
|
|
1188
|
+
wireCloudFront({
|
|
1189
|
+
GetDistributionConfig: {
|
|
1190
|
+
DistributionConfig: {
|
|
1191
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Quantity: 1, Items: ['*'] } }, MinTTL: 0 },
|
|
1192
|
+
},
|
|
1193
|
+
ETag: 'dist-etag',
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default');
|
|
1198
|
+
|
|
1199
|
+
expect(result.scenario).to.equal('legacy');
|
|
1200
|
+
expect(result.updated).to.equal(false);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('targets a named (non-default) custom-policy behavior', async () => {
|
|
1204
|
+
wireCloudFront({
|
|
1205
|
+
GetDistributionConfig: {
|
|
1206
|
+
DistributionConfig: {
|
|
1207
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-default' },
|
|
1208
|
+
CacheBehaviors: { Items: [{ PathPattern: '/api/*', CachePolicyId: 'cp-api' }] },
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
1212
|
+
GetCachePolicyConfig: {
|
|
1213
|
+
CachePolicyConfig: {
|
|
1214
|
+
Name: 'api',
|
|
1215
|
+
MinTTL: 0,
|
|
1216
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'none' } },
|
|
1217
|
+
},
|
|
1218
|
+
ETag: 'cp-etag',
|
|
1219
|
+
},
|
|
1220
|
+
UpdateCachePolicy: {},
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const result = await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', '/api/*');
|
|
1224
|
+
expect(result.policyId).to.equal('cp-api');
|
|
1225
|
+
expect(lastCommand('GetCachePolicyConfig').input.Id).to.equal('cp-api');
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it('throws when a named behavior is not found', async () => {
|
|
1229
|
+
wireCloudFront({
|
|
1230
|
+
GetDistributionConfig: {
|
|
1231
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp' }, CacheBehaviors: { Items: [] } },
|
|
1232
|
+
},
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
let error;
|
|
1236
|
+
try {
|
|
1237
|
+
await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', '/missing/*');
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
error = e;
|
|
1240
|
+
}
|
|
1241
|
+
expect(error.message).to.include('Behavior not found: /missing/*');
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('throws for a named behavior when the config has no CacheBehaviors at all', async () => {
|
|
1245
|
+
wireCloudFront({
|
|
1246
|
+
// no CacheBehaviors → `config.CacheBehaviors?.Items || []` fallback in getBehavior.
|
|
1247
|
+
GetDistributionConfig: {
|
|
1248
|
+
DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'cp' } },
|
|
1249
|
+
},
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
let error;
|
|
1253
|
+
try {
|
|
1254
|
+
await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', '/api/*');
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
error = e;
|
|
1257
|
+
}
|
|
1258
|
+
expect(error.message).to.include('Behavior not found: /api/*');
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('throws when distributionId is missing', async () => {
|
|
1262
|
+
let error;
|
|
1263
|
+
try {
|
|
1264
|
+
await edgeOptimize.updateCacheSettings({}, '', 'default');
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
error = e;
|
|
1267
|
+
}
|
|
1268
|
+
expect(error.message).to.include('distributionId');
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it('throws when pathPattern is missing', async () => {
|
|
1272
|
+
let error;
|
|
1273
|
+
try {
|
|
1274
|
+
await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', '');
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
error = e;
|
|
1277
|
+
}
|
|
1278
|
+
expect(error.message).to.include('pathPattern');
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
describe('buildEoClonedCachePolicyName', () => {
|
|
1283
|
+
it('strips the Managed- prefix and appends the per-dist suffix', () => {
|
|
1284
|
+
expect(edgeOptimize.buildEoClonedCachePolicyName('Managed-CachingOptimized', 'E1'))
|
|
1285
|
+
.to.equal('CachingOptimized-adobe-E1');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('defaults the base name when the source is empty', () => {
|
|
1289
|
+
expect(edgeOptimize.buildEoClonedCachePolicyName('', 'E1')).to.equal('cache-adobe-E1');
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
describe('buildLambdaZip', () => {
|
|
1294
|
+
it('produces a zip buffer with the local-file-header signature', () => {
|
|
1295
|
+
const zip = edgeOptimize.buildLambdaZip('index.mjs', 'console.log(1)');
|
|
1296
|
+
expect(Buffer.isBuffer(zip)).to.equal(true);
|
|
1297
|
+
expect(zip.readUInt32LE(0)).to.equal(0x04034b50);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it('accepts a Buffer payload directly', () => {
|
|
1301
|
+
const zip = edgeOptimize.buildLambdaZip('index.mjs', Buffer.from('console.log(2)', 'utf-8'));
|
|
1302
|
+
expect(Buffer.isBuffer(zip)).to.equal(true);
|
|
1303
|
+
expect(zip.readUInt32LE(0)).to.equal(0x04034b50);
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
describe('createLambdaAtEdge', () => {
|
|
1308
|
+
const creds = { accessKeyId: 'A', secretAccessKey: 'S', sessionToken: 'T' };
|
|
1309
|
+
|
|
1310
|
+
// IAM + Lambda stubs dispatch by command name (robust to call order/poll counts).
|
|
1311
|
+
const wireIam = (responders) => {
|
|
1312
|
+
iamSendStub.callsFake((cmd) => {
|
|
1313
|
+
const r = responders[cmd.commandName];
|
|
1314
|
+
return Promise.resolve(typeof r === 'function' ? r(cmd) : (r || {}));
|
|
1315
|
+
});
|
|
1316
|
+
};
|
|
1317
|
+
const wireLambda = (responders) => {
|
|
1318
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1319
|
+
const r = responders[cmd.commandName];
|
|
1320
|
+
if (r === undefined) {
|
|
1321
|
+
throw new Error(`unexpected lambda command: ${cmd.commandName}`);
|
|
1322
|
+
}
|
|
1323
|
+
return Promise.resolve(typeof r === 'function' ? r(cmd) : r);
|
|
1324
|
+
});
|
|
1325
|
+
};
|
|
1326
|
+
const lastLambda = (name) => lambdaSendStub.getCalls()
|
|
1327
|
+
.filter((c) => c.args[0].commandName === name).pop()?.args[0];
|
|
1328
|
+
const notFound = () => Promise.reject(Object.assign(new Error('nf'), { name: 'ResourceNotFoundException' }));
|
|
1329
|
+
|
|
1330
|
+
it('returns provisioning WITHOUT CreateFunction when the role was just created', async () => {
|
|
1331
|
+
// Root-cause fix for the 503 first-byte timeout: a freshly-created IAM role needs time to
|
|
1332
|
+
// propagate, so we must NOT wait + CreateFunction in this same request. Return provisioning
|
|
1333
|
+
// immediately; the next poll (role now exists → roleIsNew false) performs the create.
|
|
1334
|
+
wireIam({
|
|
1335
|
+
GetRole: () => Promise.reject(Object.assign(new Error('no role'), { name: 'NoSuchEntityException' })),
|
|
1336
|
+
CreateRole: { Role: { Arn: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role' } },
|
|
1337
|
+
PutRolePolicy: {},
|
|
1338
|
+
});
|
|
1339
|
+
wireLambda({
|
|
1340
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1341
|
+
CreateFunction: { FunctionArn: 'arn:fn' },
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1345
|
+
|
|
1346
|
+
expect(result.status).to.equal('provisioning');
|
|
1347
|
+
expect(result.created).to.equal(true);
|
|
1348
|
+
expect(result.functionArn).to.equal(null);
|
|
1349
|
+
expect(result.versionArn).to.equal(null);
|
|
1350
|
+
expect(result.roleArn).to.include('edgeoptimize-origin-role');
|
|
1351
|
+
// The expensive work is deferred: no CreateFunction (and no PublishVersion) this call.
|
|
1352
|
+
expect(lastLambda('CreateFunction')).to.equal(undefined);
|
|
1353
|
+
expect(lastLambda('PublishVersion')).to.equal(undefined);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('creates the function (non-blocking) and returns provisioning when the role already exists', async () => {
|
|
1357
|
+
// Existing role + missing function: proceed to CreateFunction in the SAME call (unchanged).
|
|
1358
|
+
wireIam({
|
|
1359
|
+
GetRole: { Role: { Arn: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role' } },
|
|
1360
|
+
UpdateAssumeRolePolicy: {},
|
|
1361
|
+
PutRolePolicy: {},
|
|
1362
|
+
});
|
|
1363
|
+
wireLambda({
|
|
1364
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1365
|
+
CreateFunction: { FunctionArn: 'arn:fn' },
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1369
|
+
|
|
1370
|
+
// Does NOT block on the new function becoming Active — returns provisioning immediately.
|
|
1371
|
+
expect(result.status).to.equal('provisioning');
|
|
1372
|
+
expect(result.created).to.equal(true);
|
|
1373
|
+
expect(result.functionArn).to.equal('arn:fn');
|
|
1374
|
+
expect(result.versionArn).to.equal(null);
|
|
1375
|
+
expect(result.roleArn).to.include('edgeoptimize-origin-role');
|
|
1376
|
+
expect(lastLambda('CreateFunction').input.Role).to.include('edgeoptimize-origin-role');
|
|
1377
|
+
expect(lastLambda('PublishVersion')).to.equal(undefined); // never publishes while Pending
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it('retries CreateFunction on role-propagation then succeeds', async () => {
|
|
1381
|
+
let createAttempts = 0;
|
|
1382
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1383
|
+
wireLambda({
|
|
1384
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1385
|
+
CreateFunction: () => {
|
|
1386
|
+
createAttempts += 1;
|
|
1387
|
+
if (createAttempts === 1) {
|
|
1388
|
+
return Promise.reject(Object.assign(
|
|
1389
|
+
new Error('The role defined for the function cannot be assumed by Lambda.'),
|
|
1390
|
+
{ name: 'InvalidParameterValueException' },
|
|
1391
|
+
));
|
|
1392
|
+
}
|
|
1393
|
+
return Promise.resolve({ FunctionArn: 'arn:fn' });
|
|
1394
|
+
},
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { retryDelayMs: 1, distributionId: 'E2EXAMPLE' });
|
|
1398
|
+
expect(result.status).to.equal('provisioning');
|
|
1399
|
+
expect(createAttempts).to.equal(2);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it('rethrows a non-role-propagation CreateFunction error', async () => {
|
|
1403
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1404
|
+
wireLambda({
|
|
1405
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1406
|
+
CreateFunction: () => Promise.reject(Object.assign(new Error('boom'), { name: 'SomethingElse' })),
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
let error;
|
|
1410
|
+
try {
|
|
1411
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
error = e;
|
|
1414
|
+
}
|
|
1415
|
+
expect(error.message).to.equal('boom');
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
it('rethrows an InvalidParameterValue error with no message (not role propagation)', async () => {
|
|
1419
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1420
|
+
wireLambda({
|
|
1421
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1422
|
+
// name is InvalidParameterValueException but message is empty → `(message || '')` fallback,
|
|
1423
|
+
// `.includes('role')` is false → not role-propagation → rethrow immediately.
|
|
1424
|
+
CreateFunction: () => {
|
|
1425
|
+
const e = new Error('');
|
|
1426
|
+
e.name = 'InvalidParameterValueException';
|
|
1427
|
+
e.message = '';
|
|
1428
|
+
return Promise.reject(e);
|
|
1429
|
+
},
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
let error;
|
|
1433
|
+
try {
|
|
1434
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1435
|
+
} catch (e) {
|
|
1436
|
+
error = e;
|
|
1437
|
+
}
|
|
1438
|
+
expect(error.name).to.equal('InvalidParameterValueException');
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it('gives up after the retry budget on persistent role-propagation', async () => {
|
|
1442
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1443
|
+
wireLambda({
|
|
1444
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1445
|
+
CreateFunction: () => Promise.reject(Object.assign(
|
|
1446
|
+
new Error('The role defined for the function cannot be assumed by Lambda.'),
|
|
1447
|
+
{ name: 'InvalidParameterValueException' },
|
|
1448
|
+
)),
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
let error;
|
|
1452
|
+
try {
|
|
1453
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { retryDelayMs: 1, distributionId: 'E2EXAMPLE' });
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
error = e;
|
|
1456
|
+
}
|
|
1457
|
+
expect(error.name).to.equal('InvalidParameterValueException');
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('rethrows an unexpected GetRole error', async () => {
|
|
1461
|
+
wireIam({ GetRole: () => Promise.reject(Object.assign(new Error('access denied'), { name: 'AccessDenied' })) });
|
|
1462
|
+
lambdaSendStub.callsFake(() => Promise.resolve({}));
|
|
1463
|
+
|
|
1464
|
+
let error;
|
|
1465
|
+
try {
|
|
1466
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
error = e;
|
|
1469
|
+
}
|
|
1470
|
+
expect(error.message).to.equal('access denied');
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
it('rethrows an unexpected GetFunctionConfiguration error', async () => {
|
|
1474
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1475
|
+
wireLambda({
|
|
1476
|
+
GetFunctionConfiguration: () => Promise.reject(Object.assign(new Error('throttled'), { name: 'TooManyRequestsException' })),
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
let error;
|
|
1480
|
+
try {
|
|
1481
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
error = e;
|
|
1484
|
+
}
|
|
1485
|
+
expect(error.message).to.equal('throttled');
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
it('returns provisioning (no mutation) while the function is still finalizing', async () => {
|
|
1489
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1490
|
+
wireLambda({
|
|
1491
|
+
GetFunctionConfiguration: {
|
|
1492
|
+
FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'InProgress',
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1497
|
+
|
|
1498
|
+
expect(result.status).to.equal('provisioning');
|
|
1499
|
+
expect(result.versionArn).to.equal(null);
|
|
1500
|
+
expect(lastLambda('PublishVersion')).to.equal(undefined); // never touched while InProgress
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
it('is idempotent: reuses the existing version when the function is idle', async () => {
|
|
1504
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1505
|
+
wireLambda({
|
|
1506
|
+
GetFunctionConfiguration: {
|
|
1507
|
+
FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful',
|
|
1508
|
+
},
|
|
1509
|
+
ListVersionsByFunction: { Versions: [{ Version: '$LATEST' }, { Version: '3', FunctionArn: 'arn:fn:3' }] },
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1513
|
+
|
|
1514
|
+
expect(result.status).to.equal('ready');
|
|
1515
|
+
expect(result.alreadyExisted).to.equal(true);
|
|
1516
|
+
expect(result.versionArn).to.equal('arn:fn:3');
|
|
1517
|
+
expect(lastLambda('PublishVersion')).to.equal(undefined); // reused, not re-published
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it('publishes a version when the function is idle but unpublished', async () => {
|
|
1521
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1522
|
+
wireLambda({
|
|
1523
|
+
GetFunctionConfiguration: {
|
|
1524
|
+
FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful',
|
|
1525
|
+
},
|
|
1526
|
+
ListVersionsByFunction: { Versions: [{ Version: '$LATEST' }] },
|
|
1527
|
+
PublishVersion: { FunctionArn: 'arn:fn:1', Version: '1' },
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1531
|
+
|
|
1532
|
+
expect(result.status).to.equal('ready');
|
|
1533
|
+
expect(result.versionArn).to.equal('arn:fn:1');
|
|
1534
|
+
expect(lastLambda('PublishVersion')).to.not.equal(undefined);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it('treats a concurrent-create conflict as provisioning', async () => {
|
|
1538
|
+
wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} });
|
|
1539
|
+
wireLambda({
|
|
1540
|
+
GetFunctionConfiguration: () => notFound(),
|
|
1541
|
+
CreateFunction: () => Promise.reject(Object.assign(new Error('exists'), { name: 'ResourceConflictException' })),
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
const result = await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' });
|
|
1545
|
+
|
|
1546
|
+
expect(result.status).to.equal('provisioning');
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it('throws for an invalid account id', async () => {
|
|
1550
|
+
let error;
|
|
1551
|
+
try {
|
|
1552
|
+
await edgeOptimize.createLambdaAtEdge(creds, '123');
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
error = e;
|
|
1555
|
+
}
|
|
1556
|
+
expect(error.message).to.include('12-digit');
|
|
1557
|
+
expect(iamSendStub.called).to.equal(false);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it('throws when distributionId is missing', async () => {
|
|
1561
|
+
let error;
|
|
1562
|
+
try {
|
|
1563
|
+
await edgeOptimize.createLambdaAtEdge(creds, '120569600543', {});
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
error = e;
|
|
1566
|
+
}
|
|
1567
|
+
expect(error.message).to.include('distributionId');
|
|
1568
|
+
expect(iamSendStub.called).to.equal(false);
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
describe('getLambdaAtEdgeStatus', () => {
|
|
1573
|
+
it('reports roleExists:false + exists:false when nothing is provisioned', async () => {
|
|
1574
|
+
iamSendStub.callsFake(() => Promise.reject(Object.assign(new Error('no role'), { name: 'NoSuchEntityException' })));
|
|
1575
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1576
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1577
|
+
return Promise.reject(Object.assign(new Error('nf'), { name: 'ResourceNotFoundException' }));
|
|
1578
|
+
}
|
|
1579
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1583
|
+
|
|
1584
|
+
expect(result).to.deep.equal({
|
|
1585
|
+
roleExists: false, roleOk: false, exists: false, versionArn: null, ready: false,
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
it('reports the role (roleOk) + published version and ready:true when fully provisioned', async () => {
|
|
1590
|
+
const trust = encodeURIComponent(JSON.stringify({
|
|
1591
|
+
Version: '2012-10-17',
|
|
1592
|
+
Statement: [{
|
|
1593
|
+
Effect: 'Allow',
|
|
1594
|
+
Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] },
|
|
1595
|
+
Action: 'sts:AssumeRole',
|
|
1596
|
+
}],
|
|
1597
|
+
}));
|
|
1598
|
+
iamSendStub.callsFake((cmd) => {
|
|
1599
|
+
if (cmd.commandName === 'GetRole') {
|
|
1600
|
+
return Promise.resolve({ Role: { Arn: 'arn:role', AssumeRolePolicyDocument: trust } });
|
|
1601
|
+
}
|
|
1602
|
+
if (cmd.commandName === 'GetRolePolicy') {
|
|
1603
|
+
return Promise.resolve({ PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' });
|
|
1604
|
+
}
|
|
1605
|
+
throw new Error(`unexpected iam: ${cmd.commandName}`);
|
|
1606
|
+
});
|
|
1607
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1608
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1609
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful' });
|
|
1610
|
+
}
|
|
1611
|
+
if (cmd.commandName === 'ListVersionsByFunction') {
|
|
1612
|
+
return Promise.resolve({ Versions: [{ Version: '$LATEST' }, { Version: '2', FunctionArn: 'arn:fn:2' }] });
|
|
1613
|
+
}
|
|
1614
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1618
|
+
|
|
1619
|
+
expect(result.roleExists).to.equal(true);
|
|
1620
|
+
expect(result.roleOk).to.equal(true);
|
|
1621
|
+
expect(result.exists).to.equal(true);
|
|
1622
|
+
expect(result.state).to.equal('Active');
|
|
1623
|
+
expect(result.versionArn).to.equal('arn:fn:2');
|
|
1624
|
+
expect(result.version).to.equal('2');
|
|
1625
|
+
expect(result.ready).to.equal(true);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it('marks roleOk:false when the trust document does not parse', async () => {
|
|
1629
|
+
iamSendStub.callsFake((cmd) => {
|
|
1630
|
+
if (cmd.commandName === 'GetRole') {
|
|
1631
|
+
return Promise.resolve({ Role: { Arn: 'arn:role', AssumeRolePolicyDocument: '%ZZ-not-json' } });
|
|
1632
|
+
}
|
|
1633
|
+
if (cmd.commandName === 'GetRolePolicy') {
|
|
1634
|
+
return Promise.resolve({ PolicyName: 'EdgeOptimizeLambdaLogging' });
|
|
1635
|
+
}
|
|
1636
|
+
throw new Error(`unexpected iam: ${cmd.commandName}`);
|
|
1637
|
+
});
|
|
1638
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1639
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1640
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful' });
|
|
1641
|
+
}
|
|
1642
|
+
if (cmd.commandName === 'ListVersionsByFunction') {
|
|
1643
|
+
return Promise.resolve({ Versions: [{ Version: '1', FunctionArn: 'arn:fn:1' }] });
|
|
1644
|
+
}
|
|
1645
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1649
|
+
|
|
1650
|
+
expect(result.roleExists).to.equal(true);
|
|
1651
|
+
expect(result.roleOk).to.equal(false); // trust unparsable → trustOk false
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
it('marks roleOk:false when the logs policy is absent', async () => {
|
|
1655
|
+
const trust = encodeURIComponent(JSON.stringify({
|
|
1656
|
+
Statement: [{ Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] } }],
|
|
1657
|
+
}));
|
|
1658
|
+
iamSendStub.callsFake((cmd) => {
|
|
1659
|
+
if (cmd.commandName === 'GetRole') {
|
|
1660
|
+
return Promise.resolve({ Role: { Arn: 'arn:role', AssumeRolePolicyDocument: trust } });
|
|
1661
|
+
}
|
|
1662
|
+
if (cmd.commandName === 'GetRolePolicy') {
|
|
1663
|
+
return Promise.reject(Object.assign(new Error('no policy'), { name: 'NoSuchEntityException' }));
|
|
1664
|
+
}
|
|
1665
|
+
throw new Error(`unexpected iam: ${cmd.commandName}`);
|
|
1666
|
+
});
|
|
1667
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1668
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1669
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful' });
|
|
1670
|
+
}
|
|
1671
|
+
if (cmd.commandName === 'ListVersionsByFunction') {
|
|
1672
|
+
return Promise.resolve({ Versions: [{ Version: '1', FunctionArn: 'arn:fn:1' }] });
|
|
1673
|
+
}
|
|
1674
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1678
|
+
|
|
1679
|
+
expect(result.roleOk).to.equal(false);
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it('rethrows a non-NoSuchEntity GetRole error', async () => {
|
|
1683
|
+
iamSendStub.callsFake(() => Promise.reject(Object.assign(new Error('denied'), { name: 'AccessDenied' })));
|
|
1684
|
+
lambdaSendStub.callsFake(() => Promise.resolve({}));
|
|
1685
|
+
|
|
1686
|
+
let error;
|
|
1687
|
+
try {
|
|
1688
|
+
await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
error = e;
|
|
1691
|
+
}
|
|
1692
|
+
expect(error.message).to.equal('denied');
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
it('rethrows a non-NoSuchEntity GetRolePolicy error', async () => {
|
|
1696
|
+
const trust = encodeURIComponent(JSON.stringify({
|
|
1697
|
+
Statement: [{ Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] } }],
|
|
1698
|
+
}));
|
|
1699
|
+
iamSendStub.callsFake((cmd) => {
|
|
1700
|
+
if (cmd.commandName === 'GetRole') {
|
|
1701
|
+
return Promise.resolve({ Role: { Arn: 'arn:role', AssumeRolePolicyDocument: trust } });
|
|
1702
|
+
}
|
|
1703
|
+
return Promise.reject(Object.assign(new Error('denied'), { name: 'AccessDenied' }));
|
|
1704
|
+
});
|
|
1705
|
+
lambdaSendStub.callsFake(() => Promise.resolve({}));
|
|
1706
|
+
|
|
1707
|
+
let error;
|
|
1708
|
+
try {
|
|
1709
|
+
await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1710
|
+
} catch (e) {
|
|
1711
|
+
error = e;
|
|
1712
|
+
}
|
|
1713
|
+
expect(error.message).to.equal('denied');
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
it('rethrows a non-ResourceNotFound GetFunctionConfiguration error', async () => {
|
|
1717
|
+
iamSendStub.callsFake(() => Promise.reject(Object.assign(new Error('no role'), { name: 'NoSuchEntityException' })));
|
|
1718
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1719
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1720
|
+
return Promise.reject(Object.assign(new Error('throttled'), { name: 'TooManyRequestsException' }));
|
|
1721
|
+
}
|
|
1722
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
let error;
|
|
1726
|
+
try {
|
|
1727
|
+
await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1728
|
+
} catch (e) {
|
|
1729
|
+
error = e;
|
|
1730
|
+
}
|
|
1731
|
+
expect(error.message).to.equal('throttled');
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
it('handles a role with no trust document at all', async () => {
|
|
1735
|
+
iamSendStub.callsFake((cmd) => {
|
|
1736
|
+
if (cmd.commandName === 'GetRole') {
|
|
1737
|
+
return Promise.resolve({ Role: { Arn: 'arn:role' } });
|
|
1738
|
+
}
|
|
1739
|
+
if (cmd.commandName === 'GetRolePolicy') {
|
|
1740
|
+
return Promise.resolve({ PolicyName: 'EdgeOptimizeLambdaLogging' });
|
|
1741
|
+
}
|
|
1742
|
+
throw new Error(`unexpected iam: ${cmd.commandName}`);
|
|
1743
|
+
});
|
|
1744
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1745
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1746
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful' });
|
|
1747
|
+
}
|
|
1748
|
+
return Promise.resolve({ Versions: [{ Version: '1', FunctionArn: 'arn:fn:1' }] });
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1752
|
+
expect(result.roleOk).to.equal(false); // no trust doc → trustOk false
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
it('treats a missing Versions list as no published version (ready:false)', async () => {
|
|
1756
|
+
const trust = encodeURIComponent(JSON.stringify({
|
|
1757
|
+
Statement: [{ Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] } }],
|
|
1758
|
+
}));
|
|
1759
|
+
iamSendStub.callsFake((cmd) => {
|
|
1760
|
+
if (cmd.commandName === 'GetRole') {
|
|
1761
|
+
return Promise.resolve({ Role: { Arn: 'arn:role', AssumeRolePolicyDocument: trust } });
|
|
1762
|
+
}
|
|
1763
|
+
return Promise.resolve({ PolicyName: 'EdgeOptimizeLambdaLogging' });
|
|
1764
|
+
});
|
|
1765
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1766
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1767
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful' });
|
|
1768
|
+
}
|
|
1769
|
+
// ListVersionsByFunction returns NO Versions field → `resp.Versions || []` fallback.
|
|
1770
|
+
return Promise.resolve({});
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1774
|
+
|
|
1775
|
+
expect(result.exists).to.equal(true);
|
|
1776
|
+
expect(result.versionArn).to.equal(null);
|
|
1777
|
+
expect(result.ready).to.equal(false);
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it('reports ready:false (role created, still provisioning) when not yet published', async () => {
|
|
1781
|
+
iamSendStub.callsFake(() => Promise.resolve({ Role: { Arn: 'arn:role' } }));
|
|
1782
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
1783
|
+
if (cmd.commandName === 'GetFunctionConfiguration') {
|
|
1784
|
+
return Promise.resolve({ FunctionArn: 'arn:fn', State: 'Pending', LastUpdateStatus: 'InProgress' });
|
|
1785
|
+
}
|
|
1786
|
+
if (cmd.commandName === 'ListVersionsByFunction') {
|
|
1787
|
+
return Promise.resolve({ Versions: [{ Version: '$LATEST' }] });
|
|
1788
|
+
}
|
|
1789
|
+
throw new Error(`unexpected: ${cmd.commandName}`);
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
const result = await edgeOptimize.getLambdaAtEdgeStatus({}, 'E2EXAMPLE');
|
|
1793
|
+
|
|
1794
|
+
expect(result.roleExists).to.equal(true);
|
|
1795
|
+
expect(result.exists).to.equal(true);
|
|
1796
|
+
expect(result.versionArn).to.equal(null);
|
|
1797
|
+
expect(result.ready).to.equal(false);
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it('throws when distributionId is missing', async () => {
|
|
1801
|
+
let error;
|
|
1802
|
+
try {
|
|
1803
|
+
await edgeOptimize.getLambdaAtEdgeStatus({}, '');
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
error = e;
|
|
1806
|
+
}
|
|
1807
|
+
expect(error.message).to.include('distributionId');
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
describe('applyAssociations', () => {
|
|
1812
|
+
const lambdaArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:1';
|
|
1813
|
+
|
|
1814
|
+
it('wires the CF function (viewer-request) and Lambda (origin req/res) onto the behavior', async () => {
|
|
1815
|
+
cfSendStub.onFirstCall().resolves({
|
|
1816
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1817
|
+
});
|
|
1818
|
+
cfSendStub.onSecondCall().resolves({
|
|
1819
|
+
DistributionConfig: { DefaultCacheBehavior: {} },
|
|
1820
|
+
ETag: 'dist-etag',
|
|
1821
|
+
});
|
|
1822
|
+
cfSendStub.onThirdCall().resolves({});
|
|
1823
|
+
|
|
1824
|
+
const result = await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1825
|
+
|
|
1826
|
+
expect(result).to.deep.equal({ cloudFrontFunctionArn: 'arn:cf-fn', lambdaArn });
|
|
1827
|
+
const update = cfSendStub.thirdCall.args[0];
|
|
1828
|
+
expect(update.commandName).to.equal('UpdateDistribution');
|
|
1829
|
+
const behavior = update.input.DistributionConfig.DefaultCacheBehavior;
|
|
1830
|
+
expect(behavior.FunctionAssociations.Items[0]).to.deep.equal({ FunctionARN: 'arn:cf-fn', EventType: 'viewer-request' });
|
|
1831
|
+
expect(behavior.LambdaFunctionAssociations.Quantity).to.equal(2);
|
|
1832
|
+
expect(behavior.LambdaFunctionAssociations.Items.map((i) => i.EventType)).to.deep.equal(['origin-request', 'origin-response']);
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
it('throws when the CF function is not published to LIVE', async () => {
|
|
1836
|
+
cfSendStub.onFirstCall().resolves({ FunctionSummary: {} });
|
|
1837
|
+
let error;
|
|
1838
|
+
try {
|
|
1839
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1840
|
+
} catch (e) {
|
|
1841
|
+
error = e;
|
|
1842
|
+
}
|
|
1843
|
+
expect(error.message).to.include('not found or not published');
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
it('surfaces a conflicting viewer-request association', async () => {
|
|
1847
|
+
cfSendStub.onFirstCall().resolves({
|
|
1848
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1849
|
+
});
|
|
1850
|
+
cfSendStub.onSecondCall().resolves({
|
|
1851
|
+
DistributionConfig: {
|
|
1852
|
+
DefaultCacheBehavior: {
|
|
1853
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:other-fn' }] },
|
|
1854
|
+
},
|
|
1855
|
+
},
|
|
1856
|
+
ETag: 'dist-etag',
|
|
1857
|
+
});
|
|
1858
|
+
let error;
|
|
1859
|
+
try {
|
|
1860
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1861
|
+
} catch (e) {
|
|
1862
|
+
error = e;
|
|
1863
|
+
}
|
|
1864
|
+
expect(error.message).to.include('already has a different viewer-request function');
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
it('surfaces a conflicting viewer-request Lambda@Edge association', async () => {
|
|
1868
|
+
cfSendStub.onFirstCall().resolves({
|
|
1869
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1870
|
+
});
|
|
1871
|
+
cfSendStub.onSecondCall().resolves({
|
|
1872
|
+
DistributionConfig: {
|
|
1873
|
+
DefaultCacheBehavior: {
|
|
1874
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'viewer-request', LambdaFunctionARN: 'arn:cust-viewer-lambda' }] },
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
ETag: 'dist-etag',
|
|
1878
|
+
});
|
|
1879
|
+
let error;
|
|
1880
|
+
try {
|
|
1881
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1882
|
+
} catch (e) {
|
|
1883
|
+
error = e;
|
|
1884
|
+
}
|
|
1885
|
+
expect(error.message).to.include('viewer-request Lambda@Edge');
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
it('preserves the customer\'s other-slot associations (merge, not wholesale replace)', async () => {
|
|
1889
|
+
cfSendStub.onFirstCall().resolves({
|
|
1890
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1891
|
+
});
|
|
1892
|
+
cfSendStub.onSecondCall().resolves({
|
|
1893
|
+
DistributionConfig: {
|
|
1894
|
+
DefaultCacheBehavior: {
|
|
1895
|
+
FunctionAssociations: {
|
|
1896
|
+
Quantity: 1,
|
|
1897
|
+
Items: [{ EventType: 'viewer-response', FunctionARN: 'arn:cust-fn' }],
|
|
1898
|
+
},
|
|
1899
|
+
LambdaFunctionAssociations: {
|
|
1900
|
+
Quantity: 1,
|
|
1901
|
+
Items: [{ EventType: 'viewer-response', LambdaFunctionARN: 'arn:cust-lambda', IncludeBody: false }],
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
ETag: 'dist-etag',
|
|
1906
|
+
});
|
|
1907
|
+
cfSendStub.onThirdCall().resolves({});
|
|
1908
|
+
|
|
1909
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1910
|
+
const behavior = cfSendStub.thirdCall.args[0].input.DistributionConfig.DefaultCacheBehavior;
|
|
1911
|
+
// Customer's viewer-response function is preserved; EO's viewer-request function is added.
|
|
1912
|
+
expect(behavior.FunctionAssociations.Items)
|
|
1913
|
+
.to.deep.include({ EventType: 'viewer-response', FunctionARN: 'arn:cust-fn' });
|
|
1914
|
+
expect(behavior.FunctionAssociations.Items)
|
|
1915
|
+
.to.deep.include({ FunctionARN: 'arn:cf-fn', EventType: 'viewer-request' });
|
|
1916
|
+
// Customer's viewer-response lambda is preserved; EO's origin-request/response are added.
|
|
1917
|
+
const lambdaEvents = behavior.LambdaFunctionAssociations.Items.map((i) => i.EventType);
|
|
1918
|
+
expect(lambdaEvents).to.include.members(['viewer-response', 'origin-request', 'origin-response']);
|
|
1919
|
+
expect(behavior.LambdaFunctionAssociations.Items
|
|
1920
|
+
.find((i) => i.EventType === 'viewer-response').LambdaFunctionARN).to.equal('arn:cust-lambda');
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
it('targets a named (non-default) behavior', async () => {
|
|
1924
|
+
cfSendStub.onFirstCall().resolves({
|
|
1925
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1926
|
+
});
|
|
1927
|
+
cfSendStub.onSecondCall().resolves({
|
|
1928
|
+
DistributionConfig: { CacheBehaviors: { Items: [{ PathPattern: '/api/*' }] } },
|
|
1929
|
+
ETag: 'dist-etag',
|
|
1930
|
+
});
|
|
1931
|
+
cfSendStub.onThirdCall().resolves({});
|
|
1932
|
+
|
|
1933
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', '/api/*', lambdaArn);
|
|
1934
|
+
const { DistributionConfig } = cfSendStub.thirdCall.args[0].input;
|
|
1935
|
+
const behavior = DistributionConfig.CacheBehaviors.Items[0];
|
|
1936
|
+
expect(behavior.FunctionAssociations.Items.map((i) => i.EventType)).to.include('viewer-request');
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it('refuses an origin-request Lambda@Edge association that carries no ARN', async () => {
|
|
1940
|
+
cfSendStub.onFirstCall().resolves({
|
|
1941
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1942
|
+
});
|
|
1943
|
+
cfSendStub.onSecondCall().resolves({
|
|
1944
|
+
DistributionConfig: {
|
|
1945
|
+
DefaultCacheBehavior: {
|
|
1946
|
+
// origin-request lambda with NO LambdaFunctionARN → isEdgeOptimizeLambdaArn(undefined)
|
|
1947
|
+
// exercises the `arn || ''` fallback → not EO → conflict.
|
|
1948
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request' }] },
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
ETag: 'dist-etag',
|
|
1952
|
+
});
|
|
1953
|
+
let error;
|
|
1954
|
+
try {
|
|
1955
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1956
|
+
} catch (e) {
|
|
1957
|
+
error = e;
|
|
1958
|
+
}
|
|
1959
|
+
expect(error.message).to.include('different origin-request');
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
it('refuses to overwrite a customer origin-request Lambda@Edge', async () => {
|
|
1963
|
+
cfSendStub.onFirstCall().resolves({
|
|
1964
|
+
FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } },
|
|
1965
|
+
});
|
|
1966
|
+
cfSendStub.onSecondCall().resolves({
|
|
1967
|
+
DistributionConfig: {
|
|
1968
|
+
DefaultCacheBehavior: {
|
|
1969
|
+
LambdaFunctionAssociations: {
|
|
1970
|
+
Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:cust-origin-lambda' }],
|
|
1971
|
+
},
|
|
1972
|
+
},
|
|
1973
|
+
},
|
|
1974
|
+
ETag: 'dist-etag',
|
|
1975
|
+
});
|
|
1976
|
+
let error;
|
|
1977
|
+
try {
|
|
1978
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', lambdaArn);
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
error = e;
|
|
1981
|
+
}
|
|
1982
|
+
expect(error.message).to.include('different origin-request');
|
|
1983
|
+
expect(cfSendStub.thirdCall).to.equal(null); // never issued an UpdateDistribution
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
it('throws when distributionId is missing', async () => {
|
|
1987
|
+
let error;
|
|
1988
|
+
try {
|
|
1989
|
+
await edgeOptimize.applyAssociations({}, '', 'default', lambdaArn);
|
|
1990
|
+
} catch (e) {
|
|
1991
|
+
error = e;
|
|
1992
|
+
}
|
|
1993
|
+
expect(error.message).to.include('distributionId');
|
|
1994
|
+
expect(cfSendStub.called).to.equal(false);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
it('throws when pathPattern is missing', async () => {
|
|
1998
|
+
let error;
|
|
1999
|
+
try {
|
|
2000
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', '', lambdaArn);
|
|
2001
|
+
} catch (e) {
|
|
2002
|
+
error = e;
|
|
2003
|
+
}
|
|
2004
|
+
expect(error.message).to.include('pathPattern');
|
|
2005
|
+
expect(cfSendStub.called).to.equal(false);
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
it('throws when lambdaVersionArn is missing', async () => {
|
|
2009
|
+
let error;
|
|
2010
|
+
try {
|
|
2011
|
+
await edgeOptimize.applyAssociations({}, 'E2EXAMPLE', 'default', '');
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
error = e;
|
|
2014
|
+
}
|
|
2015
|
+
expect(error.message).to.include('lambdaVersionArn');
|
|
2016
|
+
expect(cfSendStub.called).to.equal(false);
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
describe('verifyRouting', () => {
|
|
2021
|
+
let fetchStub;
|
|
2022
|
+
|
|
2023
|
+
const makeResponse = (status, headerMap) => ({
|
|
2024
|
+
status,
|
|
2025
|
+
headers: { forEach: (cb) => Object.entries(headerMap).forEach(([k, v]) => cb(v, k)) },
|
|
2026
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
afterEach(() => {
|
|
2030
|
+
if (fetchStub) {
|
|
2031
|
+
fetchStub.restore();
|
|
2032
|
+
}
|
|
2033
|
+
fetchStub = undefined;
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
it('passes when the bot response carries x-edgeoptimize-request-id and the human does not', async () => {
|
|
2037
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2038
|
+
fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-123' }));
|
|
2039
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, {}));
|
|
2040
|
+
|
|
2041
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2042
|
+
|
|
2043
|
+
expect(result.passed).to.equal(true);
|
|
2044
|
+
expect(result.requestId).to.equal('req-123');
|
|
2045
|
+
expect(result.details.bot.status).to.equal(200);
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
it('ignores non-edgeoptimize response headers', async () => {
|
|
2049
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2050
|
+
fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-1', 'content-type': 'text/html' }));
|
|
2051
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, { 'cache-control': 'no-store' }));
|
|
2052
|
+
|
|
2053
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2054
|
+
|
|
2055
|
+
expect(result.details.bot.headers['content-type']).to.equal(undefined);
|
|
2056
|
+
expect(result.passed).to.equal(true);
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
it('does NOT pass when only failover (x-edgeoptimize-fo) is present', async () => {
|
|
2060
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2061
|
+
fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-fo': '1' }));
|
|
2062
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, {}));
|
|
2063
|
+
|
|
2064
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2065
|
+
|
|
2066
|
+
expect(result.passed).to.equal(false);
|
|
2067
|
+
expect(result.requestId).to.equal(null);
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
it('does NOT pass when the human response is also optimized', async () => {
|
|
2071
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2072
|
+
fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-123' }));
|
|
2073
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-999' }));
|
|
2074
|
+
|
|
2075
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2076
|
+
|
|
2077
|
+
expect(result.passed).to.equal(false);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
it('does NOT pass when the human response carries the proxy marker', async () => {
|
|
2081
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2082
|
+
fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-123' }));
|
|
2083
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, { 'x-edgeoptimize-proxy': '1' }));
|
|
2084
|
+
|
|
2085
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2086
|
+
|
|
2087
|
+
expect(result.passed).to.equal(false);
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
it('passes a bounded AbortSignal to each probe', async () => {
|
|
2091
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2092
|
+
fetchStub.resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'r' }));
|
|
2093
|
+
|
|
2094
|
+
await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2095
|
+
|
|
2096
|
+
// Each probe carries an AbortSignal so it can be cancelled at the bounded timeout.
|
|
2097
|
+
expect(fetchStub.firstCall.args[1].signal).to.be.instanceOf(AbortSignal);
|
|
2098
|
+
expect(edgeOptimize.EDGE_OPTIMIZE_VERIFY_PROBE_TIMEOUT_MS).to.equal(20000);
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
it('resolves passed:false on a network error instead of throwing', async () => {
|
|
2102
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2103
|
+
// Bot probe errors at the network layer; it must resolve to a non-passing { status: 0 }
|
|
2104
|
+
// result (NOT throw) so the FE poll loop simply retries. The human probe still runs.
|
|
2105
|
+
fetchStub.onFirstCall().rejects(new Error('ECONNREFUSED'));
|
|
2106
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, {}));
|
|
2107
|
+
|
|
2108
|
+
const result = await edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2109
|
+
|
|
2110
|
+
expect(result.passed).to.equal(false);
|
|
2111
|
+
expect(result.requestId).to.equal(null);
|
|
2112
|
+
expect(result.details.bot.status).to.equal(0);
|
|
2113
|
+
expect(result.details.bot.headers).to.deep.equal({});
|
|
2114
|
+
// A plain network error is not flagged as a timeout.
|
|
2115
|
+
expect(result.details.bot.timedOut).to.equal(undefined);
|
|
2116
|
+
expect(result.details.human.status).to.equal(200);
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
it('aborts a probe that exceeds the bounded timeout and resolves passed:false', async () => {
|
|
2120
|
+
const clock = sinon.useFakeTimers();
|
|
2121
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2122
|
+
// Bot probe hangs until its abort signal fires (the bounded-timeout abort); the human
|
|
2123
|
+
// probe resolves immediately. Driving the fake clock past the timeout triggers the abort.
|
|
2124
|
+
fetchStub.onFirstCall().callsFake((url, opts) => new Promise((resolve, reject) => {
|
|
2125
|
+
opts.signal.addEventListener('abort', () => {
|
|
2126
|
+
const err = new Error('aborted');
|
|
2127
|
+
err.name = 'AbortError';
|
|
2128
|
+
reject(err);
|
|
2129
|
+
});
|
|
2130
|
+
}));
|
|
2131
|
+
fetchStub.onSecondCall().resolves(makeResponse(200, {}));
|
|
2132
|
+
|
|
2133
|
+
const promise = edgeOptimize.verifyRouting('https://d.cloudfront.net/');
|
|
2134
|
+
await clock.tickAsync(edgeOptimize.EDGE_OPTIMIZE_VERIFY_PROBE_TIMEOUT_MS);
|
|
2135
|
+
const result = await promise;
|
|
2136
|
+
|
|
2137
|
+
expect(result.passed).to.equal(false);
|
|
2138
|
+
expect(result.requestId).to.equal(null);
|
|
2139
|
+
expect(result.details.bot.status).to.equal(0);
|
|
2140
|
+
// An abort (timeout) is flagged so callers can tell "still warming up" from a hard failure.
|
|
2141
|
+
expect(result.details.bot.timedOut).to.equal(true);
|
|
2142
|
+
clock.restore();
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
it('throws when url is missing', async () => {
|
|
2146
|
+
let error;
|
|
2147
|
+
try {
|
|
2148
|
+
await edgeOptimize.verifyRouting('');
|
|
2149
|
+
} catch (e) {
|
|
2150
|
+
error = e;
|
|
2151
|
+
}
|
|
2152
|
+
expect(error.message).to.include('url');
|
|
2153
|
+
});
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
describe('EDGE_OPTIMIZE_DEPLOY_STEPS', () => {
|
|
2157
|
+
it('exposes the ordered step contract', () => {
|
|
2158
|
+
expect(edgeOptimize.EDGE_OPTIMIZE_DEPLOY_STEPS.map((s) => s.key)).to.deep.equal([
|
|
2159
|
+
'origin', 'function', 'cache', 'lambda', 'associate', 'propagation', 'verify',
|
|
2160
|
+
]);
|
|
2161
|
+
});
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
describe('runDeployStep', () => {
|
|
2165
|
+
let fetchStub;
|
|
2166
|
+
const deployParams = {
|
|
2167
|
+
distributionId: 'E2EXAMPLE123',
|
|
2168
|
+
originId: 'origin-aem',
|
|
2169
|
+
behavior: 'default',
|
|
2170
|
+
originDomain: 'dev.edgeoptimize.net',
|
|
2171
|
+
originHeaders: { apiKey: 'eo-key', forwardedHost: 'www.example.com' },
|
|
2172
|
+
accountId: '120569600543',
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
// Dispatch each client's send() by command name; per-test overrides via the `r` map.
|
|
2176
|
+
const wire = (cf = {}, lambda = {}, iam = {}) => {
|
|
2177
|
+
cfSendStub.callsFake((cmd) => {
|
|
2178
|
+
const fn = cf[cmd.commandName];
|
|
2179
|
+
if (fn === undefined) {
|
|
2180
|
+
throw new Error(`unexpected cf command: ${cmd.commandName}`);
|
|
2181
|
+
}
|
|
2182
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
2183
|
+
});
|
|
2184
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
2185
|
+
const fn = lambda[cmd.commandName];
|
|
2186
|
+
if (fn === undefined) {
|
|
2187
|
+
throw new Error(`unexpected lambda command: ${cmd.commandName}`);
|
|
2188
|
+
}
|
|
2189
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
2190
|
+
});
|
|
2191
|
+
iamSendStub.callsFake((cmd) => {
|
|
2192
|
+
const fn = iam[cmd.commandName];
|
|
2193
|
+
if (fn === undefined) {
|
|
2194
|
+
throw new Error(`unexpected iam command: ${cmd.commandName}`);
|
|
2195
|
+
}
|
|
2196
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
2197
|
+
});
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
const statusOf = (steps, key) => steps.find((s) => s.key === key).status;
|
|
2201
|
+
const cfCalls = (name) => cfSendStub.getCalls().filter((c) => c.args[0].commandName === name);
|
|
2202
|
+
|
|
2203
|
+
// Returns a responder that throws an AWS-style named error (so the SDK error path triggers).
|
|
2204
|
+
const throwNamed = (name, message) => () => {
|
|
2205
|
+
const e = new Error(message);
|
|
2206
|
+
e.name = name;
|
|
2207
|
+
throw e;
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2210
|
+
const iamCalls = (name) => iamSendStub.getCalls().filter((c) => c.args[0].commandName === name);
|
|
2211
|
+
|
|
2212
|
+
// Encoded trust doc allowing both Lambda@Edge principals — what inspectRole treats as valid.
|
|
2213
|
+
const validTrust = encodeURIComponent(JSON.stringify({
|
|
2214
|
+
Version: '2012-10-17',
|
|
2215
|
+
Statement: [{
|
|
2216
|
+
Effect: 'Allow',
|
|
2217
|
+
Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] },
|
|
2218
|
+
Action: 'sts:AssumeRole',
|
|
2219
|
+
}],
|
|
2220
|
+
}));
|
|
2221
|
+
// IAM mock for an existing, correctly-configured role (roleExists + roleOk = true).
|
|
2222
|
+
const okRoleIam = (extra = {}) => ({
|
|
2223
|
+
GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: validTrust } },
|
|
2224
|
+
GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' },
|
|
2225
|
+
UpdateAssumeRolePolicy: {},
|
|
2226
|
+
PutRolePolicy: {},
|
|
2227
|
+
...extra,
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
// CF + Lambda wiring for "function already published, behavior not yet associated, propagation
|
|
2231
|
+
// still in progress" — the role-heal gate tests reuse this and only vary the IAM/role mock.
|
|
2232
|
+
const readyDeployCf = () => ({
|
|
2233
|
+
GetDistributionConfig: () => ({
|
|
2234
|
+
DistributionConfig: {
|
|
2235
|
+
Origins: {
|
|
2236
|
+
Items: [{
|
|
2237
|
+
Id: 'EdgeOptimize_Origin',
|
|
2238
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2239
|
+
CustomHeaders: {
|
|
2240
|
+
Items: [
|
|
2241
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2242
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2243
|
+
],
|
|
2244
|
+
},
|
|
2245
|
+
}],
|
|
2246
|
+
},
|
|
2247
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2248
|
+
},
|
|
2249
|
+
ETag: 'etag',
|
|
2250
|
+
}),
|
|
2251
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2252
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2253
|
+
GetCachePolicyConfig: {
|
|
2254
|
+
CachePolicyConfig: {
|
|
2255
|
+
Name: 'p',
|
|
2256
|
+
MinTTL: 0,
|
|
2257
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2258
|
+
HeadersConfig: {
|
|
2259
|
+
HeaderBehavior: 'whitelist',
|
|
2260
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
2261
|
+
},
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
ETag: 'cp-etag',
|
|
2265
|
+
},
|
|
2266
|
+
UpdateDistribution: {},
|
|
2267
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } },
|
|
2268
|
+
});
|
|
2269
|
+
const readyLambda = () => ({
|
|
2270
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2271
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: 'arn:lambda:3', CodeSha256: 'sha' }] },
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
const makeFetchResponse = (status, headerMap) => ({
|
|
2275
|
+
status,
|
|
2276
|
+
headers: { forEach: (cb) => Object.entries(headerMap).forEach(([k, v]) => cb(v, k)) },
|
|
2277
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
afterEach(() => {
|
|
2281
|
+
if (fetchStub) {
|
|
2282
|
+
fetchStub.restore();
|
|
2283
|
+
}
|
|
2284
|
+
fetchStub = undefined;
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
it('first call advances origin+function+cache and returns lambda in_progress (others pending)', async () => {
|
|
2288
|
+
wire(
|
|
2289
|
+
{
|
|
2290
|
+
// origin: existing with matching headers → idempotent no-op (no UpdateDistribution).
|
|
2291
|
+
GetDistributionConfig: {
|
|
2292
|
+
DistributionConfig: {
|
|
2293
|
+
Origins: {
|
|
2294
|
+
Items: [{
|
|
2295
|
+
Id: 'EdgeOptimize_Origin',
|
|
2296
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2297
|
+
CustomHeaders: {
|
|
2298
|
+
Items: [
|
|
2299
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2300
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2301
|
+
],
|
|
2302
|
+
},
|
|
2303
|
+
}],
|
|
2304
|
+
},
|
|
2305
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2306
|
+
},
|
|
2307
|
+
ETag: 'etag',
|
|
2308
|
+
},
|
|
2309
|
+
// function gate: already published to LIVE → skip create+publish.
|
|
2310
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2311
|
+
// cache: custom policy already forwards EO headers + MinTTL 0 → no-op.
|
|
2312
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2313
|
+
GetCachePolicyConfig: {
|
|
2314
|
+
CachePolicyConfig: {
|
|
2315
|
+
Name: 'p',
|
|
2316
|
+
MinTTL: 0,
|
|
2317
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2318
|
+
HeadersConfig: {
|
|
2319
|
+
HeaderBehavior: 'whitelist',
|
|
2320
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
2321
|
+
},
|
|
2322
|
+
},
|
|
2323
|
+
},
|
|
2324
|
+
ETag: 'cp-etag',
|
|
2325
|
+
},
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
// lambda: does not exist yet → kick off create → in_progress.
|
|
2329
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope'),
|
|
2330
|
+
ListVersionsByFunction: { Versions: [] },
|
|
2331
|
+
CreateFunction: { FunctionArn: 'arn:lambda', Version: '$LATEST' },
|
|
2332
|
+
},
|
|
2333
|
+
// Role already exists + correctly configured → no role-propagation wait.
|
|
2334
|
+
okRoleIam(),
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2338
|
+
|
|
2339
|
+
expect(statusOf(out.steps, 'origin')).to.equal('done');
|
|
2340
|
+
expect(statusOf(out.steps, 'function')).to.equal('done');
|
|
2341
|
+
expect(statusOf(out.steps, 'cache')).to.equal('done');
|
|
2342
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('in_progress');
|
|
2343
|
+
expect(statusOf(out.steps, 'associate')).to.equal('pending');
|
|
2344
|
+
expect(statusOf(out.steps, 'verify')).to.equal('pending');
|
|
2345
|
+
expect(out.routingDeployed).to.equal(false);
|
|
2346
|
+
expect(out.verified).to.equal(false);
|
|
2347
|
+
// function already LIVE → never created/published.
|
|
2348
|
+
expect(cfCalls('CreateFunction')).to.have.length(0);
|
|
2349
|
+
expect(cfCalls('PublishFunction')).to.have.length(0);
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
it('creates the routing function when not yet LIVE', async () => {
|
|
2353
|
+
wire(
|
|
2354
|
+
{
|
|
2355
|
+
GetDistributionConfig: {
|
|
2356
|
+
DistributionConfig: {
|
|
2357
|
+
Origins: {
|
|
2358
|
+
Items: [{
|
|
2359
|
+
Id: 'EdgeOptimize_Origin',
|
|
2360
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2361
|
+
CustomHeaders: {
|
|
2362
|
+
Items: [
|
|
2363
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2364
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2365
|
+
],
|
|
2366
|
+
},
|
|
2367
|
+
}],
|
|
2368
|
+
},
|
|
2369
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2370
|
+
},
|
|
2371
|
+
ETag: 'etag',
|
|
2372
|
+
},
|
|
2373
|
+
// function gate: not LIVE → create+publish path runs.
|
|
2374
|
+
DescribeFunction: (cmd) => {
|
|
2375
|
+
if (cmd.input.Stage === 'LIVE') {
|
|
2376
|
+
return Promise.reject(Object.assign(new Error('no live'), { name: 'NoSuchFunctionExists' }));
|
|
2377
|
+
}
|
|
2378
|
+
// DEVELOPMENT lookup inside createCloudFrontFunction → also missing.
|
|
2379
|
+
return Promise.reject(Object.assign(new Error('no dev'), { name: 'NoSuchFunctionExists' }));
|
|
2380
|
+
},
|
|
2381
|
+
CreateFunction: { ETag: 'fn-etag' },
|
|
2382
|
+
PublishFunction: {},
|
|
2383
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2384
|
+
GetCachePolicyConfig: {
|
|
2385
|
+
CachePolicyConfig: {
|
|
2386
|
+
Name: 'p',
|
|
2387
|
+
MinTTL: 0,
|
|
2388
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2389
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2390
|
+
},
|
|
2391
|
+
},
|
|
2392
|
+
ETag: 'cp-etag',
|
|
2393
|
+
},
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope'),
|
|
2397
|
+
ListVersionsByFunction: { Versions: [] },
|
|
2398
|
+
CreateFunction: { FunctionArn: 'arn:lambda' },
|
|
2399
|
+
},
|
|
2400
|
+
okRoleIam(),
|
|
2401
|
+
);
|
|
2402
|
+
|
|
2403
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2404
|
+
|
|
2405
|
+
expect(statusOf(out.steps, 'function')).to.equal('done');
|
|
2406
|
+
expect(cfCalls('CreateFunction')).to.have.length(1);
|
|
2407
|
+
expect(cfCalls('PublishFunction')).to.have.length(1);
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
it('with lambda ready proceeds to associate then verify (in_progress until propagation)', async () => {
|
|
2411
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2412
|
+
wire(
|
|
2413
|
+
{
|
|
2414
|
+
GetDistributionConfig: () => ({
|
|
2415
|
+
// origin exists (idempotent), default behavior NOT yet associated (associate must run).
|
|
2416
|
+
DistributionConfig: {
|
|
2417
|
+
Origins: {
|
|
2418
|
+
Items: [{
|
|
2419
|
+
Id: 'EdgeOptimize_Origin',
|
|
2420
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2421
|
+
CustomHeaders: {
|
|
2422
|
+
Items: [
|
|
2423
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2424
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2425
|
+
],
|
|
2426
|
+
},
|
|
2427
|
+
}],
|
|
2428
|
+
},
|
|
2429
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2430
|
+
},
|
|
2431
|
+
ETag: 'etag',
|
|
2432
|
+
}),
|
|
2433
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2434
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2435
|
+
GetCachePolicyConfig: {
|
|
2436
|
+
CachePolicyConfig: {
|
|
2437
|
+
Name: 'p',
|
|
2438
|
+
MinTTL: 0,
|
|
2439
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2440
|
+
HeadersConfig: {
|
|
2441
|
+
HeaderBehavior: 'whitelist',
|
|
2442
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
2443
|
+
},
|
|
2444
|
+
},
|
|
2445
|
+
},
|
|
2446
|
+
ETag: 'cp-etag',
|
|
2447
|
+
},
|
|
2448
|
+
UpdateDistribution: {},
|
|
2449
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2453
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2454
|
+
},
|
|
2455
|
+
okRoleIam(),
|
|
2456
|
+
);
|
|
2457
|
+
// verify probe: bot lacks request-id → not passed yet (propagation).
|
|
2458
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2459
|
+
fetchStub.onFirstCall().resolves(makeFetchResponse(200, {}));
|
|
2460
|
+
fetchStub.onSecondCall().resolves(makeFetchResponse(200, {}));
|
|
2461
|
+
|
|
2462
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2463
|
+
|
|
2464
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('done');
|
|
2465
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
2466
|
+
expect(statusOf(out.steps, 'verify')).to.equal('in_progress');
|
|
2467
|
+
expect(out.routingDeployed).to.equal(true);
|
|
2468
|
+
expect(out.verified).to.equal(false);
|
|
2469
|
+
// associate ran exactly one UpdateDistribution (behavior was not associated).
|
|
2470
|
+
expect(cfCalls('UpdateDistribution')).to.have.length(1);
|
|
2471
|
+
// verify probes the customer's REAL host (forwardedHost), not the dist domain.
|
|
2472
|
+
expect(out.steps.find((s) => s.key === 'verify').detail).to.equal('waiting for propagation');
|
|
2473
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://www.example.com/');
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
it('verify passes → verified true and verify done', async () => {
|
|
2477
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2478
|
+
wire(
|
|
2479
|
+
{
|
|
2480
|
+
GetDistributionConfig: () => ({
|
|
2481
|
+
DistributionConfig: {
|
|
2482
|
+
Origins: {
|
|
2483
|
+
Items: [{
|
|
2484
|
+
Id: 'EdgeOptimize_Origin',
|
|
2485
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2486
|
+
CustomHeaders: {
|
|
2487
|
+
Items: [
|
|
2488
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2489
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2490
|
+
],
|
|
2491
|
+
},
|
|
2492
|
+
}],
|
|
2493
|
+
},
|
|
2494
|
+
// already associated → associate gate skips UpdateDistribution.
|
|
2495
|
+
DefaultCacheBehavior: {
|
|
2496
|
+
CachePolicyId: 'cp-1',
|
|
2497
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2498
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2499
|
+
},
|
|
2500
|
+
},
|
|
2501
|
+
ETag: 'etag',
|
|
2502
|
+
}),
|
|
2503
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2504
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2505
|
+
GetCachePolicyConfig: {
|
|
2506
|
+
CachePolicyConfig: {
|
|
2507
|
+
Name: 'p',
|
|
2508
|
+
MinTTL: 0,
|
|
2509
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2510
|
+
HeadersConfig: {
|
|
2511
|
+
HeaderBehavior: 'whitelist',
|
|
2512
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
2513
|
+
},
|
|
2514
|
+
},
|
|
2515
|
+
},
|
|
2516
|
+
ETag: 'cp-etag',
|
|
2517
|
+
},
|
|
2518
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2522
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2523
|
+
},
|
|
2524
|
+
okRoleIam(),
|
|
2525
|
+
);
|
|
2526
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2527
|
+
fetchStub.onFirstCall().resolves(makeFetchResponse(200, { 'x-edgeoptimize-request-id': 'req-1' }));
|
|
2528
|
+
fetchStub.onSecondCall().resolves(makeFetchResponse(200, {}));
|
|
2529
|
+
|
|
2530
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2531
|
+
|
|
2532
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
2533
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('done');
|
|
2534
|
+
expect(statusOf(out.steps, 'verify')).to.equal('done');
|
|
2535
|
+
expect(out.routingDeployed).to.equal(true);
|
|
2536
|
+
expect(out.verified).to.equal(true);
|
|
2537
|
+
// verify probe surfaces the per-UA result the wizard renders.
|
|
2538
|
+
const verifyProbe = out.steps.find((s) => s.key === 'verify').probe;
|
|
2539
|
+
expect(verifyProbe.domain).to.equal('www.example.com'); // real host, not dist domain
|
|
2540
|
+
expect(verifyProbe.bot).to.deep.include({ ua: 'chatgpt-user', requestId: 'req-1', failover: false });
|
|
2541
|
+
expect(verifyProbe.human.requestId).to.equal(null);
|
|
2542
|
+
// idempotent gate: behavior already associated → no UpdateDistribution at all.
|
|
2543
|
+
expect(cfCalls('UpdateDistribution')).to.have.length(0);
|
|
2544
|
+
});
|
|
2545
|
+
|
|
2546
|
+
it('verify falls back to the distribution domain when no forwardedHost is supplied', async () => {
|
|
2547
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2548
|
+
wire(
|
|
2549
|
+
{
|
|
2550
|
+
GetDistributionConfig: () => ({
|
|
2551
|
+
DistributionConfig: {
|
|
2552
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2553
|
+
DefaultCacheBehavior: {
|
|
2554
|
+
CachePolicyId: 'cp-1',
|
|
2555
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2556
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2557
|
+
},
|
|
2558
|
+
},
|
|
2559
|
+
ETag: 'etag',
|
|
2560
|
+
}),
|
|
2561
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2562
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2563
|
+
GetCachePolicyConfig: {
|
|
2564
|
+
CachePolicyConfig: {
|
|
2565
|
+
Name: 'p',
|
|
2566
|
+
MinTTL: 0,
|
|
2567
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2568
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2569
|
+
},
|
|
2570
|
+
},
|
|
2571
|
+
ETag: 'cp-etag',
|
|
2572
|
+
},
|
|
2573
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2577
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2578
|
+
},
|
|
2579
|
+
okRoleIam(),
|
|
2580
|
+
);
|
|
2581
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2582
|
+
fetchStub.onFirstCall().resolves(makeFetchResponse(200, {}));
|
|
2583
|
+
fetchStub.onSecondCall().resolves(makeFetchResponse(200, {}));
|
|
2584
|
+
|
|
2585
|
+
// No originHeaders → forwardedHost empty → verify falls back to the dist domain.
|
|
2586
|
+
const noHostParams = { ...deployParams, originHeaders: undefined };
|
|
2587
|
+
const out = await edgeOptimize.runDeployStep({}, noHostParams);
|
|
2588
|
+
|
|
2589
|
+
expect(out.steps.find((s) => s.key === 'verify').probe.domain).to.equal('d123.cloudfront.net');
|
|
2590
|
+
expect(fetchStub.firstCall.args[0]).to.equal('https://d123.cloudfront.net/');
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
it('holds verify "waiting for domain" when neither forwardedHost nor a dist domain is known', async () => {
|
|
2594
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2595
|
+
wire(
|
|
2596
|
+
{
|
|
2597
|
+
GetDistributionConfig: () => ({
|
|
2598
|
+
DistributionConfig: {
|
|
2599
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2600
|
+
DefaultCacheBehavior: {
|
|
2601
|
+
CachePolicyId: 'cp-1',
|
|
2602
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2603
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2604
|
+
},
|
|
2605
|
+
},
|
|
2606
|
+
ETag: 'etag',
|
|
2607
|
+
}),
|
|
2608
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2609
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2610
|
+
GetCachePolicyConfig: {
|
|
2611
|
+
CachePolicyConfig: {
|
|
2612
|
+
Name: 'p',
|
|
2613
|
+
MinTTL: 0,
|
|
2614
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2615
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2616
|
+
},
|
|
2617
|
+
},
|
|
2618
|
+
ETag: 'cp-etag',
|
|
2619
|
+
},
|
|
2620
|
+
// distribution deployed but reports no domain name → no host to verify.
|
|
2621
|
+
GetDistribution: { Distribution: { DomainName: '', Status: 'Deployed' } },
|
|
2622
|
+
UpdateDistribution: {}, // origin self-heal write (api-key header added)
|
|
2623
|
+
},
|
|
2624
|
+
{
|
|
2625
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2626
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2627
|
+
},
|
|
2628
|
+
okRoleIam(),
|
|
2629
|
+
);
|
|
2630
|
+
|
|
2631
|
+
// originHeaders carries an apiKey but NO forwardedHost → verify domain falls back to the
|
|
2632
|
+
// (empty) dist domain → "waiting for domain".
|
|
2633
|
+
const out = await edgeOptimize.runDeployStep({}, { ...deployParams, originHeaders: { apiKey: 'k' } });
|
|
2634
|
+
|
|
2635
|
+
expect(statusOf(out.steps, 'verify')).to.equal('in_progress');
|
|
2636
|
+
expect(out.steps.find((s) => s.key === 'verify').detail).to.equal('waiting for domain');
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
it('surfaces verify failover (x-edgeoptimize-fo) as in_progress', async () => {
|
|
2640
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2641
|
+
wire(
|
|
2642
|
+
{
|
|
2643
|
+
GetDistributionConfig: () => ({
|
|
2644
|
+
DistributionConfig: {
|
|
2645
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2646
|
+
DefaultCacheBehavior: {
|
|
2647
|
+
CachePolicyId: 'cp-1',
|
|
2648
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2649
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2650
|
+
},
|
|
2651
|
+
},
|
|
2652
|
+
ETag: 'etag',
|
|
2653
|
+
}),
|
|
2654
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2655
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2656
|
+
GetCachePolicyConfig: {
|
|
2657
|
+
CachePolicyConfig: {
|
|
2658
|
+
Name: 'p',
|
|
2659
|
+
MinTTL: 0,
|
|
2660
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2661
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2662
|
+
},
|
|
2663
|
+
},
|
|
2664
|
+
ETag: 'cp-etag',
|
|
2665
|
+
},
|
|
2666
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
2667
|
+
},
|
|
2668
|
+
{
|
|
2669
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2670
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2671
|
+
},
|
|
2672
|
+
okRoleIam(),
|
|
2673
|
+
);
|
|
2674
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2675
|
+
fetchStub.onFirstCall().resolves(makeFetchResponse(200, { 'x-edgeoptimize-fo': '1' }));
|
|
2676
|
+
fetchStub.onSecondCall().resolves(makeFetchResponse(200, {}));
|
|
2677
|
+
|
|
2678
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2679
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2680
|
+
|
|
2681
|
+
expect(statusOf(out.steps, 'verify')).to.equal('in_progress');
|
|
2682
|
+
expect(out.steps.find((s) => s.key === 'verify').detail).to.include('failover');
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
it('keeps verify in_progress when a probe network error resolves to a non-passing result', async () => {
|
|
2686
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2687
|
+
wire(
|
|
2688
|
+
{
|
|
2689
|
+
GetDistributionConfig: () => ({
|
|
2690
|
+
DistributionConfig: {
|
|
2691
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2692
|
+
DefaultCacheBehavior: {
|
|
2693
|
+
CachePolicyId: 'cp-1',
|
|
2694
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2695
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2696
|
+
},
|
|
2697
|
+
},
|
|
2698
|
+
ETag: 'etag',
|
|
2699
|
+
}),
|
|
2700
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2701
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2702
|
+
GetCachePolicyConfig: {
|
|
2703
|
+
CachePolicyConfig: {
|
|
2704
|
+
Name: 'p',
|
|
2705
|
+
MinTTL: 0,
|
|
2706
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2707
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2708
|
+
},
|
|
2709
|
+
},
|
|
2710
|
+
ETag: 'cp-etag',
|
|
2711
|
+
},
|
|
2712
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2716
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2717
|
+
},
|
|
2718
|
+
okRoleIam(),
|
|
2719
|
+
);
|
|
2720
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
2721
|
+
// Both probes hit a network error. After the bounded-timeout fix they resolve to a
|
|
2722
|
+
// non-passing result ({ status: 0 }) instead of throwing, so the deploy never fails — verify
|
|
2723
|
+
// simply stays in_progress and the FE poll loop retries on the next poll.
|
|
2724
|
+
fetchStub.rejects(new Error('network down'));
|
|
2725
|
+
|
|
2726
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2727
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2728
|
+
|
|
2729
|
+
expect(statusOf(out.steps, 'verify')).to.equal('in_progress');
|
|
2730
|
+
expect(out.steps.find((s) => s.key === 'verify').detail).to.equal('waiting for propagation');
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it('holds at propagation (verify pending) while the distribution is still Deploying', async () => {
|
|
2734
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2735
|
+
wire(
|
|
2736
|
+
{
|
|
2737
|
+
GetDistributionConfig: () => ({
|
|
2738
|
+
DistributionConfig: {
|
|
2739
|
+
Origins: {
|
|
2740
|
+
Items: [{
|
|
2741
|
+
Id: 'EdgeOptimize_Origin',
|
|
2742
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2743
|
+
CustomHeaders: {
|
|
2744
|
+
Items: [
|
|
2745
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2746
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2747
|
+
],
|
|
2748
|
+
},
|
|
2749
|
+
}],
|
|
2750
|
+
},
|
|
2751
|
+
DefaultCacheBehavior: {
|
|
2752
|
+
CachePolicyId: 'cp-1',
|
|
2753
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2754
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2755
|
+
},
|
|
2756
|
+
},
|
|
2757
|
+
ETag: 'etag',
|
|
2758
|
+
}),
|
|
2759
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2760
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2761
|
+
GetCachePolicyConfig: {
|
|
2762
|
+
CachePolicyConfig: {
|
|
2763
|
+
Name: 'p',
|
|
2764
|
+
MinTTL: 0,
|
|
2765
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2766
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2767
|
+
},
|
|
2768
|
+
},
|
|
2769
|
+
ETag: 'cp-etag',
|
|
2770
|
+
},
|
|
2771
|
+
// distribution still deploying → propagation gate holds, verify never runs.
|
|
2772
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } },
|
|
2773
|
+
},
|
|
2774
|
+
{
|
|
2775
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2776
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2777
|
+
},
|
|
2778
|
+
okRoleIam(),
|
|
2779
|
+
);
|
|
2780
|
+
|
|
2781
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2782
|
+
|
|
2783
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
2784
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('in_progress');
|
|
2785
|
+
expect(statusOf(out.steps, 'verify')).to.equal('pending');
|
|
2786
|
+
expect(out.steps.find((s) => s.key === 'propagation').detail).to.include('Deploying');
|
|
2787
|
+
expect(out.verified).to.equal(false);
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
it('holds at propagation when the distribution is not yet returned', async () => {
|
|
2791
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2792
|
+
wire(
|
|
2793
|
+
{
|
|
2794
|
+
GetDistributionConfig: () => ({
|
|
2795
|
+
DistributionConfig: {
|
|
2796
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2797
|
+
DefaultCacheBehavior: {
|
|
2798
|
+
CachePolicyId: 'cp-1',
|
|
2799
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2800
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
ETag: 'etag',
|
|
2804
|
+
}),
|
|
2805
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2806
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2807
|
+
GetCachePolicyConfig: {
|
|
2808
|
+
CachePolicyConfig: {
|
|
2809
|
+
Name: 'p',
|
|
2810
|
+
MinTTL: 0,
|
|
2811
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2812
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2813
|
+
},
|
|
2814
|
+
},
|
|
2815
|
+
ETag: 'cp-etag',
|
|
2816
|
+
},
|
|
2817
|
+
// distribution not returned yet → "waiting for the distribution to appear".
|
|
2818
|
+
GetDistribution: {},
|
|
2819
|
+
},
|
|
2820
|
+
{
|
|
2821
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2822
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2823
|
+
},
|
|
2824
|
+
okRoleIam(),
|
|
2825
|
+
);
|
|
2826
|
+
|
|
2827
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2828
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2829
|
+
|
|
2830
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('in_progress');
|
|
2831
|
+
expect(out.steps.find((s) => s.key === 'propagation').detail).to.include('waiting for the distribution');
|
|
2832
|
+
});
|
|
2833
|
+
|
|
2834
|
+
it('marks propagation in_progress when getting the distribution fails', async () => {
|
|
2835
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2836
|
+
wire(
|
|
2837
|
+
{
|
|
2838
|
+
GetDistributionConfig: () => ({
|
|
2839
|
+
DistributionConfig: {
|
|
2840
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2841
|
+
DefaultCacheBehavior: {
|
|
2842
|
+
CachePolicyId: 'cp-1',
|
|
2843
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
2844
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
2845
|
+
},
|
|
2846
|
+
},
|
|
2847
|
+
ETag: 'etag',
|
|
2848
|
+
}),
|
|
2849
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2850
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2851
|
+
GetCachePolicyConfig: {
|
|
2852
|
+
CachePolicyConfig: {
|
|
2853
|
+
Name: 'p',
|
|
2854
|
+
MinTTL: 0,
|
|
2855
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
2856
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
2857
|
+
},
|
|
2858
|
+
},
|
|
2859
|
+
ETag: 'cp-etag',
|
|
2860
|
+
},
|
|
2861
|
+
GetDistribution: () => { throw new Error('get failed'); },
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
2865
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
2866
|
+
},
|
|
2867
|
+
okRoleIam(),
|
|
2868
|
+
);
|
|
2869
|
+
|
|
2870
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2871
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2872
|
+
|
|
2873
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('in_progress');
|
|
2874
|
+
expect(out.steps.find((s) => s.key === 'propagation').detail).to.equal('get failed');
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
it('marks the step error (earlier done, later pending) and does not throw when a step fails', async () => {
|
|
2878
|
+
wire(
|
|
2879
|
+
{
|
|
2880
|
+
GetDistributionConfig: {
|
|
2881
|
+
DistributionConfig: {
|
|
2882
|
+
Origins: {
|
|
2883
|
+
Items: [{
|
|
2884
|
+
Id: 'EdgeOptimize_Origin',
|
|
2885
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
2886
|
+
CustomHeaders: {
|
|
2887
|
+
Items: [
|
|
2888
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
2889
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
2890
|
+
],
|
|
2891
|
+
},
|
|
2892
|
+
}],
|
|
2893
|
+
},
|
|
2894
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2895
|
+
},
|
|
2896
|
+
ETag: 'etag',
|
|
2897
|
+
},
|
|
2898
|
+
// function gate DescribeFunction throws a non-NoSuchFunction error → step error.
|
|
2899
|
+
DescribeFunction: () => { throw new Error('AccessDenied on DescribeFunction'); },
|
|
2900
|
+
},
|
|
2901
|
+
);
|
|
2902
|
+
|
|
2903
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2904
|
+
|
|
2905
|
+
expect(statusOf(out.steps, 'origin')).to.equal('done');
|
|
2906
|
+
expect(statusOf(out.steps, 'function')).to.equal('error');
|
|
2907
|
+
expect(out.steps.find((s) => s.key === 'function').detail).to.include('AccessDenied');
|
|
2908
|
+
// later steps remain pending.
|
|
2909
|
+
expect(statusOf(out.steps, 'cache')).to.equal('pending');
|
|
2910
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('pending');
|
|
2911
|
+
expect(out.routingDeployed).to.equal(false);
|
|
2912
|
+
});
|
|
2913
|
+
|
|
2914
|
+
it('marks the origin step error (raw err.message) and stops the sequence', async () => {
|
|
2915
|
+
wire(
|
|
2916
|
+
{
|
|
2917
|
+
GetDistributionConfig: () => { throw new Error('GetDistributionConfig denied'); },
|
|
2918
|
+
},
|
|
2919
|
+
);
|
|
2920
|
+
|
|
2921
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
2922
|
+
|
|
2923
|
+
expect(statusOf(out.steps, 'origin')).to.equal('error');
|
|
2924
|
+
expect(out.steps.find((s) => s.key === 'origin').detail).to.equal('GetDistributionConfig denied');
|
|
2925
|
+
expect(statusOf(out.steps, 'function')).to.equal('pending');
|
|
2926
|
+
});
|
|
2927
|
+
|
|
2928
|
+
it('marks the cache step error (raw err.message) and stops the sequence', async () => {
|
|
2929
|
+
wire(
|
|
2930
|
+
{
|
|
2931
|
+
GetDistributionConfig: () => ({
|
|
2932
|
+
DistributionConfig: {
|
|
2933
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2934
|
+
// No CachePolicyId and a behavior lookup mismatch: target a missing named behavior.
|
|
2935
|
+
CacheBehaviors: { Items: [] },
|
|
2936
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
2937
|
+
},
|
|
2938
|
+
ETag: 'etag',
|
|
2939
|
+
}),
|
|
2940
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2941
|
+
ListCachePolicies: () => { throw new Error('ListCachePolicies denied'); },
|
|
2942
|
+
},
|
|
2943
|
+
);
|
|
2944
|
+
|
|
2945
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2946
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2947
|
+
|
|
2948
|
+
expect(statusOf(out.steps, 'cache')).to.equal('error');
|
|
2949
|
+
expect(out.steps.find((s) => s.key === 'cache').detail).to.equal('ListCachePolicies denied');
|
|
2950
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('pending');
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
it('marks the lambda step error (raw err.message) and stops the sequence', async () => {
|
|
2954
|
+
wire(
|
|
2955
|
+
{
|
|
2956
|
+
GetDistributionConfig: {
|
|
2957
|
+
DistributionConfig: {
|
|
2958
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
2959
|
+
DefaultCacheBehavior: {
|
|
2960
|
+
CachePolicyId: 'cp-1',
|
|
2961
|
+
ForwardedValues: undefined,
|
|
2962
|
+
},
|
|
2963
|
+
},
|
|
2964
|
+
ETag: 'etag',
|
|
2965
|
+
},
|
|
2966
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
2967
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
2968
|
+
GetCachePolicyConfig: {
|
|
2969
|
+
CachePolicyConfig: {
|
|
2970
|
+
Name: 'p',
|
|
2971
|
+
MinTTL: 0,
|
|
2972
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
2973
|
+
},
|
|
2974
|
+
ETag: 'cp-etag',
|
|
2975
|
+
},
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
GetFunctionConfiguration: () => { throw Object.assign(new Error('lambda denied'), { name: 'AccessDenied' }); },
|
|
2979
|
+
},
|
|
2980
|
+
okRoleIam(),
|
|
2981
|
+
);
|
|
2982
|
+
|
|
2983
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
2984
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
2985
|
+
|
|
2986
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('error');
|
|
2987
|
+
expect(out.steps.find((s) => s.key === 'lambda').detail).to.equal('lambda denied');
|
|
2988
|
+
expect(statusOf(out.steps, 'associate')).to.equal('pending');
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
it('marks the associate step error (raw err.message) and stops the sequence', async () => {
|
|
2992
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
2993
|
+
let getCfgCount = 0;
|
|
2994
|
+
wire(
|
|
2995
|
+
{
|
|
2996
|
+
GetDistributionConfig: () => {
|
|
2997
|
+
getCfgCount += 1;
|
|
2998
|
+
// 1st: origin (idempotent). 2nd: associate gate (not associated). 3rd: associate write.
|
|
2999
|
+
if (getCfgCount === 3) {
|
|
3000
|
+
throw new Error('associate read denied');
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
DistributionConfig: {
|
|
3004
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3005
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
3006
|
+
},
|
|
3007
|
+
ETag: 'etag',
|
|
3008
|
+
};
|
|
3009
|
+
},
|
|
3010
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3011
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3012
|
+
GetCachePolicyConfig: {
|
|
3013
|
+
CachePolicyConfig: {
|
|
3014
|
+
Name: 'p',
|
|
3015
|
+
MinTTL: 0,
|
|
3016
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3017
|
+
},
|
|
3018
|
+
ETag: 'cp-etag',
|
|
3019
|
+
},
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3023
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
3024
|
+
},
|
|
3025
|
+
okRoleIam(),
|
|
3026
|
+
);
|
|
3027
|
+
|
|
3028
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
3029
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
3030
|
+
|
|
3031
|
+
expect(statusOf(out.steps, 'associate')).to.equal('error');
|
|
3032
|
+
expect(out.steps.find((s) => s.key === 'associate').detail).to.equal('associate read denied');
|
|
3033
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('pending');
|
|
3034
|
+
});
|
|
3035
|
+
|
|
3036
|
+
it('holds the sequence when lambda exists but is not yet ready (no re-create)', async () => {
|
|
3037
|
+
wire(
|
|
3038
|
+
{
|
|
3039
|
+
GetDistributionConfig: {
|
|
3040
|
+
DistributionConfig: {
|
|
3041
|
+
Origins: {
|
|
3042
|
+
Items: [{
|
|
3043
|
+
Id: 'EdgeOptimize_Origin',
|
|
3044
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
3045
|
+
CustomHeaders: {
|
|
3046
|
+
Items: [
|
|
3047
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
3048
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
3049
|
+
],
|
|
3050
|
+
},
|
|
3051
|
+
}],
|
|
3052
|
+
},
|
|
3053
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
3054
|
+
},
|
|
3055
|
+
ETag: 'etag',
|
|
3056
|
+
},
|
|
3057
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3058
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3059
|
+
GetCachePolicyConfig: {
|
|
3060
|
+
CachePolicyConfig: {
|
|
3061
|
+
Name: 'p',
|
|
3062
|
+
MinTTL: 0,
|
|
3063
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
3064
|
+
HeadersConfig: {
|
|
3065
|
+
HeaderBehavior: 'whitelist',
|
|
3066
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
3067
|
+
},
|
|
3068
|
+
},
|
|
3069
|
+
},
|
|
3070
|
+
ETag: 'cp-etag',
|
|
3071
|
+
},
|
|
3072
|
+
},
|
|
3073
|
+
{
|
|
3074
|
+
// exists but still finalizing (Pending) → createLambdaAtEdge is called to drive the
|
|
3075
|
+
// state machine, but it must NOT CreateFunction or PublishVersion while still Pending.
|
|
3076
|
+
GetFunctionConfiguration: { State: 'Pending', LastUpdateStatus: 'InProgress', FunctionArn: 'arn:lambda' },
|
|
3077
|
+
ListVersionsByFunction: { Versions: [] },
|
|
3078
|
+
},
|
|
3079
|
+
okRoleIam(),
|
|
3080
|
+
);
|
|
3081
|
+
|
|
3082
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
3083
|
+
|
|
3084
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('in_progress');
|
|
3085
|
+
expect(statusOf(out.steps, 'associate')).to.equal('pending');
|
|
3086
|
+
// Pending → neither CreateFunction nor PublishVersion (no re-create, no premature publish).
|
|
3087
|
+
expect(lambdaSendStub.getCalls().filter((c) => c.args[0].commandName === 'CreateFunction')).to.have.length(0);
|
|
3088
|
+
expect(lambdaSendStub.getCalls().filter((c) => c.args[0].commandName === 'PublishVersion')).to.have.length(0);
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3091
|
+
it('reports lambda create started when the function does not exist yet', async () => {
|
|
3092
|
+
wire(
|
|
3093
|
+
{
|
|
3094
|
+
GetDistributionConfig: {
|
|
3095
|
+
DistributionConfig: {
|
|
3096
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3097
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
3098
|
+
},
|
|
3099
|
+
ETag: 'etag',
|
|
3100
|
+
},
|
|
3101
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3102
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3103
|
+
GetCachePolicyConfig: {
|
|
3104
|
+
CachePolicyConfig: {
|
|
3105
|
+
Name: 'p',
|
|
3106
|
+
MinTTL: 0,
|
|
3107
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3108
|
+
},
|
|
3109
|
+
ETag: 'cp-etag',
|
|
3110
|
+
},
|
|
3111
|
+
},
|
|
3112
|
+
{
|
|
3113
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope'),
|
|
3114
|
+
ListVersionsByFunction: { Versions: [] },
|
|
3115
|
+
CreateFunction: { FunctionArn: 'arn:lambda' },
|
|
3116
|
+
},
|
|
3117
|
+
okRoleIam(),
|
|
3118
|
+
);
|
|
3119
|
+
|
|
3120
|
+
const noHeaderParams = { ...deployParams, originHeaders: undefined };
|
|
3121
|
+
const out = await edgeOptimize.runDeployStep({}, noHeaderParams);
|
|
3122
|
+
|
|
3123
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('in_progress');
|
|
3124
|
+
expect(out.steps.find((s) => s.key === 'lambda').detail).to.equal('Lambda@Edge create started');
|
|
3125
|
+
});
|
|
3126
|
+
|
|
3127
|
+
it('publishes the version once the Lambda is Active, then proceeds to associate + verify', async () => {
|
|
3128
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:1';
|
|
3129
|
+
wire(
|
|
3130
|
+
{
|
|
3131
|
+
GetDistributionConfig: () => ({
|
|
3132
|
+
DistributionConfig: {
|
|
3133
|
+
Origins: {
|
|
3134
|
+
Items: [{
|
|
3135
|
+
Id: 'EdgeOptimize_Origin',
|
|
3136
|
+
DomainName: 'dev.edgeoptimize.net',
|
|
3137
|
+
CustomHeaders: {
|
|
3138
|
+
Items: [
|
|
3139
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
3140
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
3141
|
+
],
|
|
3142
|
+
},
|
|
3143
|
+
}],
|
|
3144
|
+
},
|
|
3145
|
+
// already associated → associate gate skips; the focus is the lambda publish path.
|
|
3146
|
+
DefaultCacheBehavior: {
|
|
3147
|
+
CachePolicyId: 'cp-1',
|
|
3148
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
3149
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:1' }] },
|
|
3150
|
+
},
|
|
3151
|
+
},
|
|
3152
|
+
ETag: 'etag',
|
|
3153
|
+
}),
|
|
3154
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3155
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3156
|
+
GetCachePolicyConfig: {
|
|
3157
|
+
CachePolicyConfig: {
|
|
3158
|
+
Name: 'p',
|
|
3159
|
+
MinTTL: 0,
|
|
3160
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
3161
|
+
HeadersConfig: {
|
|
3162
|
+
HeaderBehavior: 'whitelist',
|
|
3163
|
+
Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
3164
|
+
},
|
|
3165
|
+
},
|
|
3166
|
+
},
|
|
3167
|
+
ETag: 'cp-etag',
|
|
3168
|
+
},
|
|
3169
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
// Active + idle, NO published version yet → createLambdaAtEdge must publish one.
|
|
3173
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3174
|
+
ListVersionsByFunction: { Versions: [] },
|
|
3175
|
+
PublishVersion: { Version: '1', FunctionArn: lambdaVersionArn },
|
|
3176
|
+
},
|
|
3177
|
+
okRoleIam(),
|
|
3178
|
+
);
|
|
3179
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
3180
|
+
fetchStub.onFirstCall().resolves(makeFetchResponse(200, { 'x-edgeoptimize-request-id': 'req-1' }));
|
|
3181
|
+
fetchStub.onSecondCall().resolves(makeFetchResponse(200, {}));
|
|
3182
|
+
|
|
3183
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
3184
|
+
|
|
3185
|
+
// the fix: Active-without-version gets published → lambda flips to done (not stuck).
|
|
3186
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('done');
|
|
3187
|
+
expect(lambdaSendStub.getCalls().filter((c) => c.args[0].commandName === 'PublishVersion')).to.have.length(1);
|
|
3188
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
3189
|
+
expect(statusOf(out.steps, 'verify')).to.equal('done');
|
|
3190
|
+
expect(out.routingDeployed).to.equal(true);
|
|
3191
|
+
expect(out.verified).to.equal(true);
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
// Role-heal gate: the lambda step is "done" only when the function is ready AND the role is
|
|
3195
|
+
// present + correctly configured. The next three cover each role state on a ready function.
|
|
3196
|
+
it('lambda ready + role MISSING → recreates the role, then completes the lambda step', async () => {
|
|
3197
|
+
wire(readyDeployCf(), readyLambda(), {
|
|
3198
|
+
GetRole: throwNamed('NoSuchEntityException', 'no role'),
|
|
3199
|
+
CreateRole: { Role: { Arn: 'arn:role' } },
|
|
3200
|
+
PutRolePolicy: {},
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
3204
|
+
|
|
3205
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('done');
|
|
3206
|
+
expect(iamCalls('CreateRole')).to.have.length(1); // role recreated despite a ready function
|
|
3207
|
+
expect(iamCalls('PutRolePolicy')).to.have.length(1); // logs policy re-attached
|
|
3208
|
+
expect(out.routingDeployed).to.equal(true);
|
|
3209
|
+
expect(statusOf(out.steps, 'propagation')).to.equal('in_progress');
|
|
3210
|
+
});
|
|
3211
|
+
|
|
3212
|
+
it('lambda ready + role MIS-CONFIGURED → heals trust + logs, then completes', async () => {
|
|
3213
|
+
const badTrust = encodeURIComponent(JSON.stringify({
|
|
3214
|
+
Version: '2012-10-17',
|
|
3215
|
+
// missing edgelambda.amazonaws.com → roleOk is false
|
|
3216
|
+
Statement: [{ Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole' }],
|
|
3217
|
+
}));
|
|
3218
|
+
wire(readyDeployCf(), readyLambda(), {
|
|
3219
|
+
GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: badTrust } },
|
|
3220
|
+
GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' },
|
|
3221
|
+
UpdateAssumeRolePolicy: {},
|
|
3222
|
+
PutRolePolicy: {},
|
|
3223
|
+
});
|
|
3224
|
+
|
|
3225
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
3226
|
+
|
|
3227
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('done');
|
|
3228
|
+
expect(iamCalls('UpdateAssumeRolePolicy')).to.have.length(1); // trust corrected
|
|
3229
|
+
expect(iamCalls('PutRolePolicy')).to.have.length(1); // logs policy re-attached
|
|
3230
|
+
expect(iamCalls('CreateRole')).to.have.length(0); // role exists → not recreated
|
|
3231
|
+
expect(out.routingDeployed).to.equal(true);
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
it('lambda ready + role OK → completes WITHOUT touching the role (no churn)', async () => {
|
|
3235
|
+
wire(readyDeployCf(), readyLambda(), okRoleIam());
|
|
3236
|
+
|
|
3237
|
+
const out = await edgeOptimize.runDeployStep({}, deployParams);
|
|
3238
|
+
|
|
3239
|
+
expect(statusOf(out.steps, 'lambda')).to.equal('done');
|
|
3240
|
+
expect(iamCalls('CreateRole')).to.have.length(0);
|
|
3241
|
+
expect(iamCalls('UpdateAssumeRolePolicy')).to.have.length(0);
|
|
3242
|
+
expect(iamCalls('PutRolePolicy')).to.have.length(0); // gate passed → createLambda skipped
|
|
3243
|
+
expect(out.routingDeployed).to.equal(true);
|
|
3244
|
+
});
|
|
3245
|
+
|
|
3246
|
+
it('targets a NAMED behavior already associated (associate gate looks it up by name)', async () => {
|
|
3247
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
3248
|
+
const namedBehavior = {
|
|
3249
|
+
PathPattern: '/api/*',
|
|
3250
|
+
CachePolicyId: 'cp-1',
|
|
3251
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:fn/edgeoptimize-routing' }] },
|
|
3252
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:edgeoptimize-origin:3' }] },
|
|
3253
|
+
};
|
|
3254
|
+
wire(
|
|
3255
|
+
{
|
|
3256
|
+
GetDistributionConfig: () => ({
|
|
3257
|
+
DistributionConfig: {
|
|
3258
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3259
|
+
CacheBehaviors: { Items: [namedBehavior] },
|
|
3260
|
+
},
|
|
3261
|
+
ETag: 'etag',
|
|
3262
|
+
}),
|
|
3263
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3264
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3265
|
+
GetCachePolicyConfig: {
|
|
3266
|
+
CachePolicyConfig: {
|
|
3267
|
+
Name: 'p',
|
|
3268
|
+
MinTTL: 0,
|
|
3269
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3270
|
+
},
|
|
3271
|
+
ETag: 'cp-etag',
|
|
3272
|
+
},
|
|
3273
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } },
|
|
3274
|
+
},
|
|
3275
|
+
{
|
|
3276
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3277
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
3278
|
+
},
|
|
3279
|
+
okRoleIam(),
|
|
3280
|
+
);
|
|
3281
|
+
|
|
3282
|
+
const namedParams = { ...deployParams, behavior: '/api/*', originHeaders: undefined };
|
|
3283
|
+
const out = await edgeOptimize.runDeployStep({}, namedParams);
|
|
3284
|
+
|
|
3285
|
+
// associate gate's isBehaviorAlreadyAssociated resolves the named behavior → already wired.
|
|
3286
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
3287
|
+
expect(cfCalls('UpdateDistribution')).to.have.length(0); // already associated → no write
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
it('writes the association for a NAMED behavior the gate cannot find as associated', async () => {
|
|
3291
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
3292
|
+
// Named behavior exists (so cache + applyAssociations succeed) but has no EO associations,
|
|
3293
|
+
// so isBehaviorAlreadyAssociated returns false via the named-lookup path.
|
|
3294
|
+
const namedBehavior = { PathPattern: '/api/*', CachePolicyId: 'cp-1' };
|
|
3295
|
+
wire(
|
|
3296
|
+
{
|
|
3297
|
+
GetDistributionConfig: () => ({
|
|
3298
|
+
DistributionConfig: {
|
|
3299
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3300
|
+
CacheBehaviors: { Items: [{ ...namedBehavior }] },
|
|
3301
|
+
},
|
|
3302
|
+
ETag: 'etag',
|
|
3303
|
+
}),
|
|
3304
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3305
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3306
|
+
GetCachePolicyConfig: {
|
|
3307
|
+
CachePolicyConfig: {
|
|
3308
|
+
Name: 'p',
|
|
3309
|
+
MinTTL: 0,
|
|
3310
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3311
|
+
},
|
|
3312
|
+
ETag: 'cp-etag',
|
|
3313
|
+
},
|
|
3314
|
+
UpdateDistribution: {},
|
|
3315
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } },
|
|
3316
|
+
},
|
|
3317
|
+
{
|
|
3318
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3319
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
3320
|
+
},
|
|
3321
|
+
okRoleIam(),
|
|
3322
|
+
);
|
|
3323
|
+
|
|
3324
|
+
const namedParams = { ...deployParams, behavior: '/api/*', originHeaders: undefined };
|
|
3325
|
+
const out = await edgeOptimize.runDeployStep({}, namedParams);
|
|
3326
|
+
|
|
3327
|
+
// gate (named lookup) returns false → associate writes the association.
|
|
3328
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
3329
|
+
expect(cfCalls('UpdateDistribution')).to.have.length(1);
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
it('associate gate returns false when EO slots are held by non-EO ARNs', async () => {
|
|
3333
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
3334
|
+
// The gate's .some() predicates evaluate the ARN regex (right side of &&) on matching event
|
|
3335
|
+
// types whose ARNs are NOT edgeoptimize → returns false → associate runs (and then refuses).
|
|
3336
|
+
wire(
|
|
3337
|
+
{
|
|
3338
|
+
GetDistributionConfig: () => ({
|
|
3339
|
+
DistributionConfig: {
|
|
3340
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3341
|
+
DefaultCacheBehavior: {
|
|
3342
|
+
CachePolicyId: 'cp-1',
|
|
3343
|
+
// EventType matches the EO slot but the ARN fields are absent → the gate evaluates
|
|
3344
|
+
// the `a.FunctionARN || ''` / `a.LambdaFunctionARN || ''` fallbacks → regex false.
|
|
3345
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request' }, { EventType: 'viewer-request', FunctionARN: 'arn:other-fn' }] },
|
|
3346
|
+
LambdaFunctionAssociations: { Items: [{ EventType: 'origin-request' }] },
|
|
3347
|
+
},
|
|
3348
|
+
},
|
|
3349
|
+
ETag: 'etag',
|
|
3350
|
+
}),
|
|
3351
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3352
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3353
|
+
GetCachePolicyConfig: {
|
|
3354
|
+
CachePolicyConfig: {
|
|
3355
|
+
Name: 'p',
|
|
3356
|
+
MinTTL: 0,
|
|
3357
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3358
|
+
},
|
|
3359
|
+
ETag: 'cp-etag',
|
|
3360
|
+
},
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3364
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
3365
|
+
},
|
|
3366
|
+
okRoleIam(),
|
|
3367
|
+
);
|
|
3368
|
+
|
|
3369
|
+
const namedParams = { ...deployParams, originHeaders: undefined };
|
|
3370
|
+
const out = await edgeOptimize.runDeployStep({}, namedParams);
|
|
3371
|
+
|
|
3372
|
+
// gate → false (ARNs not EO); applyAssociations then refuses the non-EO slot.
|
|
3373
|
+
expect(statusOf(out.steps, 'associate')).to.equal('error');
|
|
3374
|
+
expect(out.steps.find((s) => s.key === 'associate').detail).to.include('different viewer-request function');
|
|
3375
|
+
});
|
|
3376
|
+
|
|
3377
|
+
it('returns associate gate false when the named behavior is absent from the gate config', async () => {
|
|
3378
|
+
const lambdaVersionArn = 'arn:aws:lambda:us-east-1:120569600543:function:edgeoptimize-origin:3';
|
|
3379
|
+
let getCfgCount = 0;
|
|
3380
|
+
wire(
|
|
3381
|
+
{
|
|
3382
|
+
GetDistributionConfig: () => {
|
|
3383
|
+
getCfgCount += 1;
|
|
3384
|
+
// 1st: origin. 2nd: cache. 3rd: associate-gate (named behavior ABSENT → !behavior).
|
|
3385
|
+
// 4th: applyAssociations read (named behavior present again).
|
|
3386
|
+
const hasNamed = getCfgCount !== 3;
|
|
3387
|
+
return {
|
|
3388
|
+
DistributionConfig: {
|
|
3389
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'dev.edgeoptimize.net' }] },
|
|
3390
|
+
CacheBehaviors: { Items: hasNamed ? [{ PathPattern: '/api/*', CachePolicyId: 'cp-1' }] : [] },
|
|
3391
|
+
},
|
|
3392
|
+
ETag: 'etag',
|
|
3393
|
+
};
|
|
3394
|
+
},
|
|
3395
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3396
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3397
|
+
GetCachePolicyConfig: {
|
|
3398
|
+
CachePolicyConfig: {
|
|
3399
|
+
Name: 'p',
|
|
3400
|
+
MinTTL: 0,
|
|
3401
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Quantity: 2, Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } } },
|
|
3402
|
+
},
|
|
3403
|
+
ETag: 'cp-etag',
|
|
3404
|
+
},
|
|
3405
|
+
UpdateDistribution: {},
|
|
3406
|
+
GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } },
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' },
|
|
3410
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] },
|
|
3411
|
+
},
|
|
3412
|
+
okRoleIam(),
|
|
3413
|
+
);
|
|
3414
|
+
|
|
3415
|
+
const namedParams = { ...deployParams, behavior: '/api/*', originHeaders: undefined };
|
|
3416
|
+
const out = await edgeOptimize.runDeployStep({}, namedParams);
|
|
3417
|
+
|
|
3418
|
+
// gate config lacks the named behavior (!behavior → false) → associate writes it.
|
|
3419
|
+
expect(statusOf(out.steps, 'associate')).to.equal('done');
|
|
3420
|
+
expect(cfCalls('UpdateDistribution')).to.have.length(1);
|
|
3421
|
+
});
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
describe('planDeploy', () => {
|
|
3425
|
+
const planParams = {
|
|
3426
|
+
distributionId: 'E2EXAMPLE123',
|
|
3427
|
+
originId: 'origin-aem',
|
|
3428
|
+
behavior: 'default',
|
|
3429
|
+
originDomain: 'live.edgeoptimize.net',
|
|
3430
|
+
originHeaders: { apiKey: 'eo-key', forwardedHost: 'www.example.com' },
|
|
3431
|
+
accountId: '120569600543',
|
|
3432
|
+
};
|
|
3433
|
+
|
|
3434
|
+
// Dispatch each client's send() by command name; per-test overrides via the maps.
|
|
3435
|
+
const wire = (cf = {}, lambda = {}, iam = {}) => {
|
|
3436
|
+
cfSendStub.callsFake((cmd) => {
|
|
3437
|
+
const fn = cf[cmd.commandName];
|
|
3438
|
+
if (fn === undefined) {
|
|
3439
|
+
throw new Error(`unexpected cf command: ${cmd.commandName}`);
|
|
3440
|
+
}
|
|
3441
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
3442
|
+
});
|
|
3443
|
+
lambdaSendStub.callsFake((cmd) => {
|
|
3444
|
+
const fn = lambda[cmd.commandName];
|
|
3445
|
+
if (fn === undefined) {
|
|
3446
|
+
throw new Error(`unexpected lambda command: ${cmd.commandName}`);
|
|
3447
|
+
}
|
|
3448
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
3449
|
+
});
|
|
3450
|
+
iamSendStub.callsFake((cmd) => {
|
|
3451
|
+
const fn = iam[cmd.commandName];
|
|
3452
|
+
if (fn === undefined) {
|
|
3453
|
+
throw new Error(`unexpected iam command: ${cmd.commandName}`);
|
|
3454
|
+
}
|
|
3455
|
+
return Promise.resolve(typeof fn === 'function' ? fn(cmd) : fn);
|
|
3456
|
+
});
|
|
3457
|
+
};
|
|
3458
|
+
|
|
3459
|
+
const throwNamed = (name, message) => () => {
|
|
3460
|
+
const e = new Error(message);
|
|
3461
|
+
e.name = name;
|
|
3462
|
+
throw e;
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
const stepOf = (steps, key) => steps.find((s) => s.key === key);
|
|
3466
|
+
|
|
3467
|
+
it('plans an all-create deploy (nothing exists yet, legacy cache)', async () => {
|
|
3468
|
+
wire(
|
|
3469
|
+
{
|
|
3470
|
+
GetDistributionConfig: {
|
|
3471
|
+
DistributionConfig: {
|
|
3472
|
+
Origins: { Items: [] },
|
|
3473
|
+
DefaultCacheBehavior: {
|
|
3474
|
+
ForwardedValues: { Headers: { Quantity: 0, Items: [] } },
|
|
3475
|
+
MinTTL: 60,
|
|
3476
|
+
},
|
|
3477
|
+
},
|
|
3478
|
+
},
|
|
3479
|
+
// function gate: not published to LIVE.
|
|
3480
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3481
|
+
},
|
|
3482
|
+
{
|
|
3483
|
+
// lambda: does not exist.
|
|
3484
|
+
GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope'),
|
|
3485
|
+
},
|
|
3486
|
+
{
|
|
3487
|
+
GetRole: throwNamed('NoSuchEntityException', 'no role'),
|
|
3488
|
+
},
|
|
3489
|
+
);
|
|
3490
|
+
|
|
3491
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3492
|
+
|
|
3493
|
+
expect(result.canProceed).to.equal(true);
|
|
3494
|
+
expect(result.blocker).to.equal(null);
|
|
3495
|
+
expect(result.steps.map((s) => s.key)).to.deep.equal(['origin', 'function', 'cache', 'lambda', 'associate']);
|
|
3496
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('create');
|
|
3497
|
+
expect(stepOf(result.steps, 'function').action).to.equal('create');
|
|
3498
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('update');
|
|
3499
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Add the Edge Optimize headers');
|
|
3500
|
+
expect(stepOf(result.steps, 'lambda').action).to.equal('create');
|
|
3501
|
+
expect(stepOf(result.steps, 'associate').action).to.equal('create');
|
|
3502
|
+
// role-will-be-created note surfaced on the lambda row
|
|
3503
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('will be created');
|
|
3504
|
+
// no `verify` row in the plan
|
|
3505
|
+
expect(result.steps.some((s) => s.key === 'verify')).to.equal(false);
|
|
3506
|
+
});
|
|
3507
|
+
|
|
3508
|
+
it('surfaces a config read failure on the origin + cache rows without blocking', async () => {
|
|
3509
|
+
wire(
|
|
3510
|
+
{
|
|
3511
|
+
GetDistributionConfig: () => { throw new Error('config read denied'); },
|
|
3512
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3513
|
+
},
|
|
3514
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3515
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3516
|
+
);
|
|
3517
|
+
|
|
3518
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3519
|
+
|
|
3520
|
+
expect(stepOf(result.steps, 'origin').detail).to.include('could not read distribution config');
|
|
3521
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('could not determine cache scenario');
|
|
3522
|
+
expect(result.canProceed).to.equal(true);
|
|
3523
|
+
});
|
|
3524
|
+
|
|
3525
|
+
it('plans an origin-headers patch when the EO origin exists header-less', async () => {
|
|
3526
|
+
wire(
|
|
3527
|
+
{
|
|
3528
|
+
GetDistributionConfig: {
|
|
3529
|
+
DistributionConfig: {
|
|
3530
|
+
Origins: { Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'live.edgeoptimize.net' }] },
|
|
3531
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } }, MinTTL: 0 },
|
|
3532
|
+
},
|
|
3533
|
+
},
|
|
3534
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3535
|
+
},
|
|
3536
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3537
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3538
|
+
);
|
|
3539
|
+
|
|
3540
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3541
|
+
|
|
3542
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('create');
|
|
3543
|
+
expect(stepOf(result.steps, 'origin').detail).to.include('patch Edge Optimize origin headers');
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
it('handles a config that has no Origins collection at all', async () => {
|
|
3547
|
+
wire(
|
|
3548
|
+
{
|
|
3549
|
+
GetDistributionConfig: {
|
|
3550
|
+
// config present but NO Origins → `config.Origins?.Items || []` fallback.
|
|
3551
|
+
DistributionConfig: {
|
|
3552
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3553
|
+
},
|
|
3554
|
+
},
|
|
3555
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3556
|
+
},
|
|
3557
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3558
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3559
|
+
);
|
|
3560
|
+
|
|
3561
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3562
|
+
|
|
3563
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('create');
|
|
3564
|
+
expect(stepOf(result.steps, 'origin').detail).to.include('add Edge Optimize origin');
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
it('blocks when a non-Edge Optimize origin uses the EO domain', async () => {
|
|
3568
|
+
wire(
|
|
3569
|
+
{
|
|
3570
|
+
GetDistributionConfig: {
|
|
3571
|
+
DistributionConfig: {
|
|
3572
|
+
// origin id is NOT EdgeOptimize_Origin, but the DomainName matches originDomain.
|
|
3573
|
+
Origins: { Items: [{ Id: 'some-other-id', DomainName: 'live.edgeoptimize.net' }] },
|
|
3574
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } }, MinTTL: 0 },
|
|
3575
|
+
},
|
|
3576
|
+
},
|
|
3577
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3578
|
+
},
|
|
3579
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3580
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3581
|
+
);
|
|
3582
|
+
|
|
3583
|
+
const result = await edgeOptimize.planDeploy(
|
|
3584
|
+
{},
|
|
3585
|
+
{ ...planParams, originHeaders: undefined },
|
|
3586
|
+
);
|
|
3587
|
+
|
|
3588
|
+
expect(result.canProceed).to.equal(false);
|
|
3589
|
+
expect(result.blocker).to.include('refusing to reuse a non-Edge Optimize origin');
|
|
3590
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('blocked');
|
|
3591
|
+
expect(stepOf(result.steps, 'origin').detail).to.include('some-other-id');
|
|
3592
|
+
});
|
|
3593
|
+
|
|
3594
|
+
it('marks the legacy cache step "exists" when the headers are already forwarded', async () => {
|
|
3595
|
+
wire(
|
|
3596
|
+
{
|
|
3597
|
+
GetDistributionConfig: {
|
|
3598
|
+
DistributionConfig: {
|
|
3599
|
+
Origins: { Items: [] },
|
|
3600
|
+
DefaultCacheBehavior: {
|
|
3601
|
+
ForwardedValues: { Headers: { Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
3602
|
+
MinTTL: 0,
|
|
3603
|
+
},
|
|
3604
|
+
},
|
|
3605
|
+
},
|
|
3606
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3607
|
+
},
|
|
3608
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3609
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3610
|
+
);
|
|
3611
|
+
|
|
3612
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3613
|
+
|
|
3614
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('exists');
|
|
3615
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('already forwards');
|
|
3616
|
+
});
|
|
3617
|
+
|
|
3618
|
+
it('blocks when the behavior is already associated (canProceed:false + exact blocker)', async () => {
|
|
3619
|
+
const associatedBehavior = {
|
|
3620
|
+
ForwardedValues: { Headers: { Items: [] } },
|
|
3621
|
+
FunctionAssociations: {
|
|
3622
|
+
Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:aws:cloudfront::1:function/edgeoptimize-routing-adobe-E2EXAMPLE123' }],
|
|
3623
|
+
},
|
|
3624
|
+
LambdaFunctionAssociations: {
|
|
3625
|
+
Items: [{ EventType: 'origin-request', LambdaFunctionARN: 'arn:aws:lambda:us-east-1:1:function:edgeoptimize-origin-adobe-E2EXAMPLE123:1' }],
|
|
3626
|
+
},
|
|
3627
|
+
};
|
|
3628
|
+
wire(
|
|
3629
|
+
{
|
|
3630
|
+
GetDistributionConfig: {
|
|
3631
|
+
DistributionConfig: {
|
|
3632
|
+
Origins: { Items: [] },
|
|
3633
|
+
DefaultCacheBehavior: associatedBehavior,
|
|
3634
|
+
},
|
|
3635
|
+
},
|
|
3636
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3637
|
+
},
|
|
3638
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3639
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3640
|
+
);
|
|
3641
|
+
|
|
3642
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3643
|
+
|
|
3644
|
+
expect(result.canProceed).to.equal(false);
|
|
3645
|
+
expect(result.blocker).to.equal(
|
|
3646
|
+
"This behaviour is already associated with routes, please recheck — can't proceed with this automation.",
|
|
3647
|
+
);
|
|
3648
|
+
expect(stepOf(result.steps, 'associate').action).to.equal('blocked');
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
it('blocks when the customer owns a conflicting slot (assocConflict)', async () => {
|
|
3652
|
+
wire(
|
|
3653
|
+
{
|
|
3654
|
+
GetDistributionConfig: {
|
|
3655
|
+
DistributionConfig: {
|
|
3656
|
+
Origins: { Items: [] },
|
|
3657
|
+
DefaultCacheBehavior: {
|
|
3658
|
+
ForwardedValues: { Headers: { Items: [] } },
|
|
3659
|
+
FunctionAssociations: { Items: [{ EventType: 'viewer-request', FunctionARN: 'arn:other-fn' }] },
|
|
3660
|
+
},
|
|
3661
|
+
},
|
|
3662
|
+
},
|
|
3663
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3664
|
+
},
|
|
3665
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3666
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3667
|
+
);
|
|
3668
|
+
|
|
3669
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3670
|
+
|
|
3671
|
+
expect(result.canProceed).to.equal(false);
|
|
3672
|
+
expect(result.blocker).to.include('already has a different viewer-request function');
|
|
3673
|
+
expect(stepOf(result.steps, 'associate').action).to.equal('blocked');
|
|
3674
|
+
});
|
|
3675
|
+
|
|
3676
|
+
it('surfaces a function-status read failure on the function row', async () => {
|
|
3677
|
+
wire(
|
|
3678
|
+
{
|
|
3679
|
+
GetDistributionConfig: {
|
|
3680
|
+
DistributionConfig: {
|
|
3681
|
+
Origins: { Items: [] },
|
|
3682
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3683
|
+
},
|
|
3684
|
+
},
|
|
3685
|
+
DescribeFunction: () => { throw new Error('describe denied'); },
|
|
3686
|
+
},
|
|
3687
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3688
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3689
|
+
);
|
|
3690
|
+
|
|
3691
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3692
|
+
|
|
3693
|
+
expect(stepOf(result.steps, 'function').detail).to.include('could not read routing function status');
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
it('describes a managed-policy clone in the cache step', async () => {
|
|
3697
|
+
wire(
|
|
3698
|
+
{
|
|
3699
|
+
GetDistributionConfig: {
|
|
3700
|
+
DistributionConfig: {
|
|
3701
|
+
Origins: { Items: [] },
|
|
3702
|
+
DefaultCacheBehavior: { CachePolicyId: 'managed-1' },
|
|
3703
|
+
},
|
|
3704
|
+
},
|
|
3705
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3706
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
3707
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
3708
|
+
: { CachePolicyList: { Items: [] } }), // no existing clone
|
|
3709
|
+
GetCachePolicy: {
|
|
3710
|
+
CachePolicy: { CachePolicyConfig: { Name: 'Managed-CachingOptimized' } },
|
|
3711
|
+
},
|
|
3712
|
+
},
|
|
3713
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3714
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3715
|
+
);
|
|
3716
|
+
|
|
3717
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3718
|
+
|
|
3719
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('create');
|
|
3720
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('CachingOptimized-adobe-E2EXAMPLE123');
|
|
3721
|
+
expect(result.canProceed).to.equal(true);
|
|
3722
|
+
});
|
|
3723
|
+
|
|
3724
|
+
it('mentions the MinTTL change when cloning a managed policy with a long MinTTL', async () => {
|
|
3725
|
+
wire(
|
|
3726
|
+
{
|
|
3727
|
+
GetDistributionConfig: {
|
|
3728
|
+
DistributionConfig: {
|
|
3729
|
+
Origins: { Items: [] },
|
|
3730
|
+
DefaultCacheBehavior: { CachePolicyId: 'managed-1' },
|
|
3731
|
+
},
|
|
3732
|
+
},
|
|
3733
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3734
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
3735
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
3736
|
+
: { CachePolicyList: { Items: [] } }),
|
|
3737
|
+
GetCachePolicy: {
|
|
3738
|
+
CachePolicy: { CachePolicyConfig: { Name: 'Managed-CachingOptimized', MinTTL: 9999 } },
|
|
3739
|
+
},
|
|
3740
|
+
},
|
|
3741
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3742
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3743
|
+
);
|
|
3744
|
+
|
|
3745
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3746
|
+
|
|
3747
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Minimum TTL will be set to 0');
|
|
3748
|
+
});
|
|
3749
|
+
|
|
3750
|
+
it('marks the managed cache step "update" when the clone exists but the behavior is not associated with it', async () => {
|
|
3751
|
+
wire(
|
|
3752
|
+
{
|
|
3753
|
+
GetDistributionConfig: {
|
|
3754
|
+
DistributionConfig: {
|
|
3755
|
+
Origins: { Items: [] },
|
|
3756
|
+
DefaultCacheBehavior: { CachePolicyId: 'managed-1' },
|
|
3757
|
+
},
|
|
3758
|
+
},
|
|
3759
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3760
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
3761
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
3762
|
+
: { CachePolicyList: { Items: [{ CachePolicy: { Id: 'eo-clone', CachePolicyConfig: { Name: 'CachingOptimized-adobe-E2EXAMPLE123' } } }] } }),
|
|
3763
|
+
GetCachePolicy: {
|
|
3764
|
+
CachePolicy: { CachePolicyConfig: { Name: 'Managed-CachingOptimized' } },
|
|
3765
|
+
},
|
|
3766
|
+
},
|
|
3767
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3768
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3769
|
+
);
|
|
3770
|
+
|
|
3771
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3772
|
+
// The clone exists but the behavior is still on the managed policy → the deploy will switch
|
|
3773
|
+
// the behavior to the existing copy, so this is an 'update' with a clear created-but-not-
|
|
3774
|
+
// associated message that names both the current policy and the copy.
|
|
3775
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('update');
|
|
3776
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('not associated');
|
|
3777
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('CachingOptimized-adobe-E2EXAMPLE123');
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
it('marks function + lambda + origin "exists" when already present', async () => {
|
|
3781
|
+
wire(
|
|
3782
|
+
{
|
|
3783
|
+
GetDistributionConfig: {
|
|
3784
|
+
DistributionConfig: {
|
|
3785
|
+
Origins: {
|
|
3786
|
+
Items: [{
|
|
3787
|
+
Id: 'EdgeOptimize_Origin',
|
|
3788
|
+
DomainName: 'live.edgeoptimize.net',
|
|
3789
|
+
CustomHeaders: {
|
|
3790
|
+
Items: [
|
|
3791
|
+
{ HeaderName: 'x-edgeoptimize-api-key', HeaderValue: 'eo-key' },
|
|
3792
|
+
{ HeaderName: 'x-forwarded-host', HeaderValue: 'www.example.com' },
|
|
3793
|
+
],
|
|
3794
|
+
},
|
|
3795
|
+
}],
|
|
3796
|
+
},
|
|
3797
|
+
DefaultCacheBehavior: {
|
|
3798
|
+
CachePolicyId: 'cp-custom',
|
|
3799
|
+
},
|
|
3800
|
+
},
|
|
3801
|
+
},
|
|
3802
|
+
// function gate: already published to LIVE.
|
|
3803
|
+
DescribeFunction: { FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } },
|
|
3804
|
+
// cache: custom (not managed), without our headers → update in place.
|
|
3805
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3806
|
+
GetCachePolicyConfig: {
|
|
3807
|
+
CachePolicyConfig: {
|
|
3808
|
+
Name: 'my-custom-policy',
|
|
3809
|
+
MinTTL: 0,
|
|
3810
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'none' } },
|
|
3811
|
+
},
|
|
3812
|
+
},
|
|
3813
|
+
},
|
|
3814
|
+
{
|
|
3815
|
+
// lambda: exists + has a published version → ready.
|
|
3816
|
+
GetFunctionConfiguration: { FunctionArn: 'arn:lambda', State: 'Active', LastUpdateStatus: 'Successful' },
|
|
3817
|
+
ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: 'arn:lambda:3', CodeSha256: 'sha' }] },
|
|
3818
|
+
},
|
|
3819
|
+
{
|
|
3820
|
+
GetRole: {
|
|
3821
|
+
Role: {
|
|
3822
|
+
Arn: 'arn:role',
|
|
3823
|
+
AssumeRolePolicyDocument: encodeURIComponent(JSON.stringify({
|
|
3824
|
+
Version: '2012-10-17',
|
|
3825
|
+
Statement: [{
|
|
3826
|
+
Effect: 'Allow',
|
|
3827
|
+
Principal: { Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'] },
|
|
3828
|
+
Action: 'sts:AssumeRole',
|
|
3829
|
+
}],
|
|
3830
|
+
})),
|
|
3831
|
+
},
|
|
3832
|
+
},
|
|
3833
|
+
GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' },
|
|
3834
|
+
},
|
|
3835
|
+
);
|
|
3836
|
+
|
|
3837
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3838
|
+
|
|
3839
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('exists');
|
|
3840
|
+
expect(stepOf(result.steps, 'function').action).to.equal('exists');
|
|
3841
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('update');
|
|
3842
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('my-custom-policy');
|
|
3843
|
+
expect(stepOf(result.steps, 'lambda').action).to.equal('exists');
|
|
3844
|
+
// Role visibility: an existing, correctly-configured execution role is surfaced + reused.
|
|
3845
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('Execution role');
|
|
3846
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('correctly configured');
|
|
3847
|
+
expect(stepOf(result.steps, 'associate').action).to.equal('create');
|
|
3848
|
+
expect(result.canProceed).to.equal(true);
|
|
3849
|
+
});
|
|
3850
|
+
|
|
3851
|
+
it('notes a provisioning lambda + a mis-configured existing role', async () => {
|
|
3852
|
+
wire(
|
|
3853
|
+
{
|
|
3854
|
+
GetDistributionConfig: {
|
|
3855
|
+
DistributionConfig: {
|
|
3856
|
+
Origins: { Items: [] },
|
|
3857
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3858
|
+
},
|
|
3859
|
+
},
|
|
3860
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3861
|
+
},
|
|
3862
|
+
{
|
|
3863
|
+
// function exists but not yet published → provisioning.
|
|
3864
|
+
GetFunctionConfiguration: { FunctionArn: 'arn:lambda', State: 'Pending', LastUpdateStatus: 'InProgress' },
|
|
3865
|
+
ListVersionsByFunction: { Versions: [] },
|
|
3866
|
+
},
|
|
3867
|
+
{
|
|
3868
|
+
GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: encodeURIComponent('{"Statement":[]}') } },
|
|
3869
|
+
GetRolePolicy: throwNamed('NoSuchEntityException', 'no policy'),
|
|
3870
|
+
},
|
|
3871
|
+
);
|
|
3872
|
+
|
|
3873
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3874
|
+
|
|
3875
|
+
expect(stepOf(result.steps, 'lambda').action).to.equal('exists');
|
|
3876
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('still provisioning');
|
|
3877
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('not correctly configured');
|
|
3878
|
+
});
|
|
3879
|
+
|
|
3880
|
+
it('surfaces a Lambda@Edge status read failure on the lambda row', async () => {
|
|
3881
|
+
wire(
|
|
3882
|
+
{
|
|
3883
|
+
GetDistributionConfig: {
|
|
3884
|
+
DistributionConfig: {
|
|
3885
|
+
Origins: { Items: [] },
|
|
3886
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3887
|
+
},
|
|
3888
|
+
},
|
|
3889
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3890
|
+
},
|
|
3891
|
+
{ GetFunctionConfiguration: () => { throw Object.assign(new Error('lambda denied'), { name: 'AccessDenied' }); } },
|
|
3892
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3893
|
+
);
|
|
3894
|
+
|
|
3895
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3896
|
+
|
|
3897
|
+
expect(stepOf(result.steps, 'lambda').detail).to.include('could not read Lambda@Edge status');
|
|
3898
|
+
});
|
|
3899
|
+
|
|
3900
|
+
it('marks the custom cache step "exists" when our headers are already present (idempotent re-deploy)', async () => {
|
|
3901
|
+
wire(
|
|
3902
|
+
{
|
|
3903
|
+
GetDistributionConfig: {
|
|
3904
|
+
DistributionConfig: {
|
|
3905
|
+
Origins: { Items: [] },
|
|
3906
|
+
DefaultCacheBehavior: { CachePolicyId: 'eo-clone' },
|
|
3907
|
+
},
|
|
3908
|
+
},
|
|
3909
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3910
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } }, // eo-clone not managed → custom
|
|
3911
|
+
GetCachePolicyConfig: {
|
|
3912
|
+
CachePolicyConfig: {
|
|
3913
|
+
Name: 'CachingOptimized-adobe-E2EXAMPLE123',
|
|
3914
|
+
MinTTL: 0,
|
|
3915
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
3916
|
+
HeadersConfig: {
|
|
3917
|
+
HeaderBehavior: 'whitelist',
|
|
3918
|
+
Headers: { Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] },
|
|
3919
|
+
},
|
|
3920
|
+
},
|
|
3921
|
+
},
|
|
3922
|
+
},
|
|
3923
|
+
},
|
|
3924
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3925
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3926
|
+
);
|
|
3927
|
+
|
|
3928
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3929
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('exists');
|
|
3930
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Already has the Edge Optimize headers');
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
it('marks a custom cache policy with allViewer headers "exists"', async () => {
|
|
3934
|
+
wire(
|
|
3935
|
+
{
|
|
3936
|
+
GetDistributionConfig: {
|
|
3937
|
+
DistributionConfig: {
|
|
3938
|
+
Origins: { Items: [] },
|
|
3939
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-av' },
|
|
3940
|
+
},
|
|
3941
|
+
},
|
|
3942
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3943
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
3944
|
+
GetCachePolicyConfig: {
|
|
3945
|
+
CachePolicyConfig: {
|
|
3946
|
+
Name: 'all-viewer-policy',
|
|
3947
|
+
MinTTL: 0,
|
|
3948
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'allViewer' } },
|
|
3949
|
+
},
|
|
3950
|
+
},
|
|
3951
|
+
},
|
|
3952
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3953
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3954
|
+
);
|
|
3955
|
+
|
|
3956
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
3957
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('exists');
|
|
3958
|
+
});
|
|
3959
|
+
|
|
3960
|
+
it('surfaces a behavior read failure on the associate row', async () => {
|
|
3961
|
+
// config available for origin/cache, but the named behavior is missing → assoc read fails.
|
|
3962
|
+
wire(
|
|
3963
|
+
{
|
|
3964
|
+
GetDistributionConfig: {
|
|
3965
|
+
DistributionConfig: {
|
|
3966
|
+
Origins: { Items: [] },
|
|
3967
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3968
|
+
CacheBehaviors: { Items: [] },
|
|
3969
|
+
},
|
|
3970
|
+
},
|
|
3971
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3972
|
+
},
|
|
3973
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3974
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3975
|
+
);
|
|
3976
|
+
|
|
3977
|
+
const result = await edgeOptimize.planDeploy({}, { ...planParams, behavior: '/missing/*' });
|
|
3978
|
+
|
|
3979
|
+
expect(stepOf(result.steps, 'associate').detail).to.include('could not read behavior associations');
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3982
|
+
it('uses the default origin domain when none is supplied', async () => {
|
|
3983
|
+
wire(
|
|
3984
|
+
{
|
|
3985
|
+
GetDistributionConfig: {
|
|
3986
|
+
DistributionConfig: {
|
|
3987
|
+
Origins: { Items: [] },
|
|
3988
|
+
DefaultCacheBehavior: { ForwardedValues: { Headers: { Items: [] } } },
|
|
3989
|
+
},
|
|
3990
|
+
},
|
|
3991
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
3992
|
+
},
|
|
3993
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
3994
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
3995
|
+
);
|
|
3996
|
+
|
|
3997
|
+
const noDomainParams = { ...planParams, originDomain: undefined, originHeaders: undefined };
|
|
3998
|
+
const result = await edgeOptimize.planDeploy({}, noDomainParams);
|
|
3999
|
+
|
|
4000
|
+
expect(stepOf(result.steps, 'origin').detail).to.include('live.edgeoptimize.net');
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
it('handles a legacy behavior with no ForwardedValues / Headers at all', async () => {
|
|
4004
|
+
wire(
|
|
4005
|
+
{
|
|
4006
|
+
GetDistributionConfig: {
|
|
4007
|
+
DistributionConfig: {
|
|
4008
|
+
Origins: { Items: [] },
|
|
4009
|
+
// legacy (no CachePolicyId) and NO ForwardedValues → both `|| {}` fallbacks.
|
|
4010
|
+
DefaultCacheBehavior: {},
|
|
4011
|
+
},
|
|
4012
|
+
},
|
|
4013
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4014
|
+
},
|
|
4015
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4016
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4017
|
+
);
|
|
4018
|
+
|
|
4019
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
4020
|
+
|
|
4021
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('update');
|
|
4022
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Add the Edge Optimize headers');
|
|
4023
|
+
});
|
|
4024
|
+
|
|
4025
|
+
it('handles sparse custom-policy reads (no CachePolicyList / CachePolicyConfig)', async () => {
|
|
4026
|
+
wire(
|
|
4027
|
+
{
|
|
4028
|
+
GetDistributionConfig: {
|
|
4029
|
+
DistributionConfig: {
|
|
4030
|
+
Origins: { Items: [] },
|
|
4031
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
4032
|
+
},
|
|
4033
|
+
},
|
|
4034
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4035
|
+
// no CachePolicyList → managedIds empty (`?.Items || []`) → custom branch.
|
|
4036
|
+
ListCachePolicies: {},
|
|
4037
|
+
// no CachePolicyConfig → `pc = {}`, no headers → update + `pc.Name || 'custom'`.
|
|
4038
|
+
GetCachePolicyConfig: {},
|
|
4039
|
+
},
|
|
4040
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4041
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4042
|
+
);
|
|
4043
|
+
|
|
4044
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
4045
|
+
|
|
4046
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('update');
|
|
4047
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Current policy: custom');
|
|
4048
|
+
});
|
|
4049
|
+
|
|
4050
|
+
it('marks a sparse custom policy "exists" using the default policy name', async () => {
|
|
4051
|
+
wire(
|
|
4052
|
+
{
|
|
4053
|
+
GetDistributionConfig: {
|
|
4054
|
+
DistributionConfig: {
|
|
4055
|
+
Origins: { Items: [] },
|
|
4056
|
+
DefaultCacheBehavior: { CachePolicyId: 'cp-1' },
|
|
4057
|
+
},
|
|
4058
|
+
},
|
|
4059
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4060
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
4061
|
+
// headers present (allViewer) + MinTTL 0 + NO Name → exists + `pc.Name || 'custom'`.
|
|
4062
|
+
GetCachePolicyConfig: {
|
|
4063
|
+
CachePolicyConfig: {
|
|
4064
|
+
MinTTL: 0,
|
|
4065
|
+
ParametersInCacheKeyAndForwardedToOrigin: { HeadersConfig: { HeaderBehavior: 'allViewer' } },
|
|
4066
|
+
},
|
|
4067
|
+
},
|
|
4068
|
+
},
|
|
4069
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4070
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4071
|
+
);
|
|
4072
|
+
|
|
4073
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
4074
|
+
|
|
4075
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('exists');
|
|
4076
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('Current policy: custom');
|
|
4077
|
+
});
|
|
4078
|
+
|
|
4079
|
+
it('handles sparse managed-policy reads (no CachePolicy / custom list)', async () => {
|
|
4080
|
+
wire(
|
|
4081
|
+
{
|
|
4082
|
+
GetDistributionConfig: {
|
|
4083
|
+
DistributionConfig: {
|
|
4084
|
+
Origins: { Items: [] },
|
|
4085
|
+
DefaultCacheBehavior: { CachePolicyId: 'managed-1' },
|
|
4086
|
+
},
|
|
4087
|
+
},
|
|
4088
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4089
|
+
ListCachePolicies: (cmd) => (cmd.input.Type === 'managed'
|
|
4090
|
+
? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } }
|
|
4091
|
+
: {}), // custom list absent → `?.Items || []`
|
|
4092
|
+
GetCachePolicy: {}, // no CachePolicy → `srcConfig = {}`, `srcConfig.Name || 'cache'`
|
|
4093
|
+
},
|
|
4094
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4095
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4096
|
+
);
|
|
4097
|
+
|
|
4098
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
4099
|
+
|
|
4100
|
+
expect(stepOf(result.steps, 'cache').action).to.equal('create');
|
|
4101
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('cache-adobe-E2EXAMPLE123');
|
|
4102
|
+
});
|
|
4103
|
+
|
|
4104
|
+
it('associate gate (plan) returns false when the named behavior is absent from its own read', async () => {
|
|
4105
|
+
let getCfgCount = 0;
|
|
4106
|
+
wire(
|
|
4107
|
+
{
|
|
4108
|
+
GetDistributionConfig: () => {
|
|
4109
|
+
getCfgCount += 1;
|
|
4110
|
+
// 1st (top read): named behavior present so origin/cache/conflict succeed.
|
|
4111
|
+
// 2nd (isBehaviorAlreadyAssociated): config has NO CacheBehaviors at all → the
|
|
4112
|
+
// `config.CacheBehaviors?.Items || []` fallback fires → !behavior → false.
|
|
4113
|
+
if (getCfgCount === 1) {
|
|
4114
|
+
return {
|
|
4115
|
+
DistributionConfig: {
|
|
4116
|
+
Origins: { Items: [] },
|
|
4117
|
+
CacheBehaviors: { Items: [{ PathPattern: '/api/*', CachePolicyId: 'cp-1' }] },
|
|
4118
|
+
},
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
return { DistributionConfig: { Origins: { Items: [] } } };
|
|
4122
|
+
},
|
|
4123
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4124
|
+
ListCachePolicies: { CachePolicyList: { Items: [] } },
|
|
4125
|
+
GetCachePolicyConfig: {
|
|
4126
|
+
CachePolicyConfig: {
|
|
4127
|
+
Name: 'p',
|
|
4128
|
+
MinTTL: 0,
|
|
4129
|
+
ParametersInCacheKeyAndForwardedToOrigin: {
|
|
4130
|
+
HeadersConfig: { HeaderBehavior: 'whitelist', Headers: { Items: ['x-edgeoptimize-config', 'x-edgeoptimize-url'] } },
|
|
4131
|
+
},
|
|
4132
|
+
},
|
|
4133
|
+
},
|
|
4134
|
+
},
|
|
4135
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4136
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4137
|
+
);
|
|
4138
|
+
|
|
4139
|
+
const result = await edgeOptimize.planDeploy({}, { ...planParams, behavior: '/api/*' });
|
|
4140
|
+
|
|
4141
|
+
// gate (plan path) hits the !behavior → false branch → associate is a normal 'create'.
|
|
4142
|
+
expect(stepOf(result.steps, 'associate').action).to.equal('create');
|
|
4143
|
+
expect(result.canProceed).to.equal(true);
|
|
4144
|
+
});
|
|
4145
|
+
|
|
4146
|
+
it('falls back to the add-origin note when the config is empty (no throw, null config)', async () => {
|
|
4147
|
+
// GetDistributionConfig resolves WITHOUT a DistributionConfig → config is null but no error
|
|
4148
|
+
// was caught, so the origin row falls through to the `else if (!detail)` add-origin branch.
|
|
4149
|
+
wire(
|
|
4150
|
+
{
|
|
4151
|
+
GetDistributionConfig: {},
|
|
4152
|
+
DescribeFunction: throwNamed('NoSuchFunctionExists', 'no fn'),
|
|
4153
|
+
},
|
|
4154
|
+
{ GetFunctionConfiguration: throwNamed('ResourceNotFoundException', 'nope') },
|
|
4155
|
+
{ GetRole: throwNamed('NoSuchEntityException', 'no role') },
|
|
4156
|
+
);
|
|
4157
|
+
|
|
4158
|
+
const result = await edgeOptimize.planDeploy({}, planParams);
|
|
4159
|
+
|
|
4160
|
+
expect(stepOf(result.steps, 'origin').action).to.equal('create');
|
|
4161
|
+
expect(stepOf(result.steps, 'origin').detail).to.equal('add Edge Optimize origin (live.edgeoptimize.net)');
|
|
4162
|
+
expect(stepOf(result.steps, 'cache').detail).to.include('could not determine cache scenario');
|
|
4163
|
+
});
|
|
4164
|
+
|
|
4165
|
+
it('throws when distributionId is missing', async () => {
|
|
4166
|
+
let error;
|
|
4167
|
+
try {
|
|
4168
|
+
await edgeOptimize.planDeploy({}, { ...planParams, distributionId: '' });
|
|
4169
|
+
} catch (e) {
|
|
4170
|
+
error = e;
|
|
4171
|
+
}
|
|
4172
|
+
expect(error.message).to.include('distributionId');
|
|
4173
|
+
});
|
|
4174
|
+
|
|
4175
|
+
it('throws when behavior is missing', async () => {
|
|
4176
|
+
let error;
|
|
4177
|
+
try {
|
|
4178
|
+
await edgeOptimize.planDeploy({}, { ...planParams, behavior: '' });
|
|
4179
|
+
} catch (e) {
|
|
4180
|
+
error = e;
|
|
4181
|
+
}
|
|
4182
|
+
expect(error.message).to.include('behavior');
|
|
4183
|
+
});
|
|
4184
|
+
});
|
|
4185
|
+
});
|