@friggframework/devtools 2.0.0--canary.474.898a56c.0 → 2.0.0--canary.474.a794ea3.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/infrastructure/domains/database/migration-builder.js +199 -1
- package/infrastructure/domains/database/migration-builder.test.js +73 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +397 -29
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +6 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +162 -9
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +19 -1
- package/infrastructure/domains/health/domain/entities/issue.js +50 -1
- package/infrastructure/domains/health/domain/entities/issue.test.js +111 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +55 -28
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +21 -6
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +407 -20
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +698 -26
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
- package/package.json +6 -6
|
@@ -26,12 +26,16 @@ describe('LogicalIdMapper', () => {
|
|
|
26
26
|
// Arrange
|
|
27
27
|
const orphanedResources = [
|
|
28
28
|
{
|
|
29
|
-
physicalId: 'vpc-
|
|
29
|
+
physicalId: 'vpc-12345678',
|
|
30
30
|
resourceType: 'AWS::EC2::VPC',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
properties: {
|
|
32
|
+
VpcId: 'vpc-12345678',
|
|
33
|
+
CidrBlock: '10.0.0.0/16',
|
|
34
|
+
tags: {
|
|
35
|
+
'aws:cloudformation:stack-name': 'acme-integrations-dev',
|
|
36
|
+
'aws:cloudformation:logical-id': 'FriggVPC',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
35
39
|
},
|
|
36
40
|
];
|
|
37
41
|
|
|
@@ -49,7 +53,7 @@ describe('LogicalIdMapper', () => {
|
|
|
49
53
|
expect(result).toHaveLength(1);
|
|
50
54
|
expect(result[0]).toEqual({
|
|
51
55
|
logicalId: 'FriggVPC',
|
|
52
|
-
physicalId: 'vpc-
|
|
56
|
+
physicalId: 'vpc-12345678',
|
|
53
57
|
resourceType: 'AWS::EC2::VPC',
|
|
54
58
|
matchMethod: 'tag',
|
|
55
59
|
confidence: 'high',
|
|
@@ -60,9 +64,13 @@ describe('LogicalIdMapper', () => {
|
|
|
60
64
|
// Arrange
|
|
61
65
|
const orphanedResources = [
|
|
62
66
|
{
|
|
63
|
-
physicalId: 'vpc-
|
|
67
|
+
physicalId: 'vpc-12345678',
|
|
64
68
|
resourceType: 'AWS::EC2::VPC',
|
|
65
|
-
|
|
69
|
+
properties: {
|
|
70
|
+
VpcId: 'vpc-12345678',
|
|
71
|
+
CidrBlock: '10.0.0.0/16',
|
|
72
|
+
tags: {},
|
|
73
|
+
},
|
|
66
74
|
},
|
|
67
75
|
];
|
|
68
76
|
|
|
@@ -89,7 +97,7 @@ describe('LogicalIdMapper', () => {
|
|
|
89
97
|
Type: 'AWS::Lambda::Function',
|
|
90
98
|
Properties: {
|
|
91
99
|
VpcConfig: {
|
|
92
|
-
SubnetIds: ['subnet-
|
|
100
|
+
SubnetIds: ['subnet-11111111', 'subnet-22222222'],
|
|
93
101
|
},
|
|
94
102
|
},
|
|
95
103
|
},
|
|
@@ -99,8 +107,8 @@ describe('LogicalIdMapper', () => {
|
|
|
99
107
|
// Mock EC2 describe-subnets response
|
|
100
108
|
mockEc2Client.send.mockResolvedValueOnce({
|
|
101
109
|
Subnets: [
|
|
102
|
-
{ SubnetId: 'subnet-
|
|
103
|
-
{ SubnetId: 'subnet-
|
|
110
|
+
{ SubnetId: 'subnet-11111111', VpcId: 'vpc-12345678' },
|
|
111
|
+
{ SubnetId: 'subnet-22222222', VpcId: 'vpc-12345678' },
|
|
104
112
|
],
|
|
105
113
|
});
|
|
106
114
|
|
|
@@ -115,7 +123,7 @@ describe('LogicalIdMapper', () => {
|
|
|
115
123
|
expect(result).toHaveLength(1);
|
|
116
124
|
expect(result[0]).toEqual({
|
|
117
125
|
logicalId: 'FriggVPC',
|
|
118
|
-
physicalId: 'vpc-
|
|
126
|
+
physicalId: 'vpc-12345678',
|
|
119
127
|
resourceType: 'AWS::EC2::VPC',
|
|
120
128
|
matchMethod: 'contained-resources',
|
|
121
129
|
confidence: 'high',
|
|
@@ -126,9 +134,13 @@ describe('LogicalIdMapper', () => {
|
|
|
126
134
|
// Arrange
|
|
127
135
|
const orphanedResources = [
|
|
128
136
|
{
|
|
129
|
-
physicalId: 'subnet-
|
|
137
|
+
physicalId: 'subnet-11111111',
|
|
130
138
|
resourceType: 'AWS::EC2::Subnet',
|
|
131
|
-
|
|
139
|
+
properties: {
|
|
140
|
+
SubnetId: 'subnet-11111111',
|
|
141
|
+
VpcId: 'vpc-12345678',
|
|
142
|
+
tags: {},
|
|
143
|
+
},
|
|
132
144
|
},
|
|
133
145
|
];
|
|
134
146
|
|
|
@@ -152,7 +164,7 @@ describe('LogicalIdMapper', () => {
|
|
|
152
164
|
Type: 'AWS::Lambda::Function',
|
|
153
165
|
Properties: {
|
|
154
166
|
VpcConfig: {
|
|
155
|
-
SubnetIds: ['subnet-
|
|
167
|
+
SubnetIds: ['subnet-11111111'],
|
|
156
168
|
},
|
|
157
169
|
},
|
|
158
170
|
},
|
|
@@ -170,7 +182,7 @@ describe('LogicalIdMapper', () => {
|
|
|
170
182
|
expect(result).toHaveLength(1);
|
|
171
183
|
expect(result[0]).toEqual({
|
|
172
184
|
logicalId: 'FriggPrivateSubnet1',
|
|
173
|
-
physicalId: 'subnet-
|
|
185
|
+
physicalId: 'subnet-11111111',
|
|
174
186
|
resourceType: 'AWS::EC2::Subnet',
|
|
175
187
|
matchMethod: 'vpc-usage',
|
|
176
188
|
confidence: 'high',
|
|
@@ -183,7 +195,10 @@ describe('LogicalIdMapper', () => {
|
|
|
183
195
|
{
|
|
184
196
|
physicalId: 'sg-07c01370e830b6ad6',
|
|
185
197
|
resourceType: 'AWS::EC2::SecurityGroup',
|
|
186
|
-
|
|
198
|
+
properties: {
|
|
199
|
+
GroupId: 'sg-07c01370e830b6ad6',
|
|
200
|
+
tags: {},
|
|
201
|
+
},
|
|
187
202
|
},
|
|
188
203
|
];
|
|
189
204
|
|
|
@@ -238,7 +253,10 @@ describe('LogicalIdMapper', () => {
|
|
|
238
253
|
{
|
|
239
254
|
physicalId: 'vpc-unknown',
|
|
240
255
|
resourceType: 'AWS::EC2::VPC',
|
|
241
|
-
|
|
256
|
+
properties: {
|
|
257
|
+
VpcId: 'vpc-unknown',
|
|
258
|
+
tags: {},
|
|
259
|
+
},
|
|
242
260
|
},
|
|
243
261
|
];
|
|
244
262
|
|
|
@@ -267,14 +285,23 @@ describe('LogicalIdMapper', () => {
|
|
|
267
285
|
// Arrange
|
|
268
286
|
const orphanedResources = [
|
|
269
287
|
{
|
|
270
|
-
physicalId: 'vpc-
|
|
288
|
+
physicalId: 'vpc-12345678',
|
|
271
289
|
resourceType: 'AWS::EC2::VPC',
|
|
272
|
-
|
|
290
|
+
properties: {
|
|
291
|
+
VpcId: 'vpc-12345678',
|
|
292
|
+
tags: {
|
|
293
|
+
'aws:cloudformation:logical-id': 'FriggVPC',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
273
296
|
},
|
|
274
297
|
{
|
|
275
|
-
physicalId: 'subnet-
|
|
298
|
+
physicalId: 'subnet-11111111',
|
|
276
299
|
resourceType: 'AWS::EC2::Subnet',
|
|
277
|
-
|
|
300
|
+
properties: {
|
|
301
|
+
SubnetId: 'subnet-11111111',
|
|
302
|
+
VpcId: 'vpc-12345678',
|
|
303
|
+
tags: {},
|
|
304
|
+
},
|
|
278
305
|
},
|
|
279
306
|
];
|
|
280
307
|
|
|
@@ -298,7 +325,7 @@ describe('LogicalIdMapper', () => {
|
|
|
298
325
|
Type: 'AWS::Lambda::Function',
|
|
299
326
|
Properties: {
|
|
300
327
|
VpcConfig: {
|
|
301
|
-
SubnetIds: ['subnet-
|
|
328
|
+
SubnetIds: ['subnet-11111111'],
|
|
302
329
|
},
|
|
303
330
|
},
|
|
304
331
|
},
|
|
@@ -374,7 +401,7 @@ describe('LogicalIdMapper', () => {
|
|
|
374
401
|
it('should match VPC that contains all expected subnets', async () => {
|
|
375
402
|
// Arrange
|
|
376
403
|
const vpc = {
|
|
377
|
-
physicalId: 'vpc-
|
|
404
|
+
physicalId: 'vpc-12345678',
|
|
378
405
|
resourceType: 'AWS::EC2::VPC',
|
|
379
406
|
};
|
|
380
407
|
|
|
@@ -400,9 +427,9 @@ describe('LogicalIdMapper', () => {
|
|
|
400
427
|
// Mock EC2 describe-subnets response
|
|
401
428
|
mockEc2Client.send.mockResolvedValueOnce({
|
|
402
429
|
Subnets: [
|
|
403
|
-
{ SubnetId: 'subnet-111', VpcId: 'vpc-
|
|
404
|
-
{ SubnetId: 'subnet-222', VpcId: 'vpc-
|
|
405
|
-
{ SubnetId: 'subnet-333', VpcId: 'vpc-
|
|
430
|
+
{ SubnetId: 'subnet-111', VpcId: 'vpc-12345678' },
|
|
431
|
+
{ SubnetId: 'subnet-222', VpcId: 'vpc-12345678' },
|
|
432
|
+
{ SubnetId: 'subnet-333', VpcId: 'vpc-12345678' },
|
|
406
433
|
],
|
|
407
434
|
});
|
|
408
435
|
|
|
@@ -465,7 +492,7 @@ describe('LogicalIdMapper', () => {
|
|
|
465
492
|
it('should return null if no expected subnets in deployed template', async () => {
|
|
466
493
|
// Arrange
|
|
467
494
|
const vpc = {
|
|
468
|
-
physicalId: 'vpc-
|
|
495
|
+
physicalId: 'vpc-12345678',
|
|
469
496
|
resourceType: 'AWS::EC2::VPC',
|
|
470
497
|
};
|
|
471
498
|
|
package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpdateProgressMonitor Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for monitoring CloudFormation UPDATE operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { UpdateProgressMonitor } = require('../update-progress-monitor');
|
|
8
|
+
const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
|
|
9
|
+
|
|
10
|
+
// Mock timers for testing delays and timeouts
|
|
11
|
+
jest.useFakeTimers();
|
|
12
|
+
|
|
13
|
+
describe('UpdateProgressMonitor', () => {
|
|
14
|
+
let monitor;
|
|
15
|
+
let mockCFRepo;
|
|
16
|
+
let onProgressCallback;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Reset mock CloudFormation repository
|
|
20
|
+
mockCFRepo = {
|
|
21
|
+
getStackEvents: jest.fn(),
|
|
22
|
+
getStackStatus: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Reset progress callback
|
|
26
|
+
onProgressCallback = jest.fn();
|
|
27
|
+
|
|
28
|
+
// Create monitor instance
|
|
29
|
+
monitor = new UpdateProgressMonitor({
|
|
30
|
+
cloudFormationRepository: mockCFRepo,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Clear all timers
|
|
34
|
+
jest.clearAllTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
jest.clearAllTimers();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('constructor', () => {
|
|
42
|
+
it('should require cloudFormationRepository', () => {
|
|
43
|
+
expect(() => new UpdateProgressMonitor({})).toThrow(
|
|
44
|
+
'cloudFormationRepository is required'
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should create instance with valid dependencies', () => {
|
|
49
|
+
const monitor = new UpdateProgressMonitor({
|
|
50
|
+
cloudFormationRepository: mockCFRepo,
|
|
51
|
+
});
|
|
52
|
+
expect(monitor).toBeInstanceOf(UpdateProgressMonitor);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('monitorUpdate - successful update', () => {
|
|
57
|
+
it('should monitor single resource update to completion', async () => {
|
|
58
|
+
const stackIdentifier = new StackIdentifier({
|
|
59
|
+
stackName: 'test-stack',
|
|
60
|
+
region: 'us-east-1',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
64
|
+
|
|
65
|
+
// Mock stack events sequence
|
|
66
|
+
mockCFRepo.getStackEvents
|
|
67
|
+
// First poll: UPDATE_IN_PROGRESS
|
|
68
|
+
.mockResolvedValueOnce([
|
|
69
|
+
{
|
|
70
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
71
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
72
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
// Second poll: UPDATE_COMPLETE
|
|
76
|
+
.mockResolvedValueOnce([
|
|
77
|
+
{
|
|
78
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
79
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
80
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
84
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
85
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// Mock stack status
|
|
90
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
91
|
+
|
|
92
|
+
// Start monitoring in background
|
|
93
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
94
|
+
stackIdentifier,
|
|
95
|
+
resourceLogicalIds,
|
|
96
|
+
onProgress: onProgressCallback,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Advance timers to trigger first poll (2 seconds)
|
|
100
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
101
|
+
|
|
102
|
+
// Advance timers to trigger second poll (2 more seconds)
|
|
103
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
104
|
+
|
|
105
|
+
// Wait for monitoring to complete
|
|
106
|
+
const result = await monitorPromise;
|
|
107
|
+
|
|
108
|
+
// Verify result
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
expect(result.updatedCount).toBe(1);
|
|
111
|
+
expect(result.failedCount).toBe(0);
|
|
112
|
+
|
|
113
|
+
// Verify progress callbacks
|
|
114
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
115
|
+
logicalId: 'AttioLambdaFunction',
|
|
116
|
+
status: 'IN_PROGRESS',
|
|
117
|
+
});
|
|
118
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
119
|
+
logicalId: 'AttioLambdaFunction',
|
|
120
|
+
status: 'COMPLETE',
|
|
121
|
+
progress: 1,
|
|
122
|
+
total: 1,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should monitor multiple resources updating simultaneously', async () => {
|
|
127
|
+
const stackIdentifier = new StackIdentifier({
|
|
128
|
+
stackName: 'test-stack',
|
|
129
|
+
region: 'us-east-1',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resourceLogicalIds = [
|
|
133
|
+
'AttioLambdaFunction',
|
|
134
|
+
'PipedriveLambdaFunction',
|
|
135
|
+
'ZohoCrmLambdaFunction',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Mock stack events - all resources update together
|
|
139
|
+
mockCFRepo.getStackEvents
|
|
140
|
+
// First poll: All IN_PROGRESS
|
|
141
|
+
.mockResolvedValueOnce([
|
|
142
|
+
{
|
|
143
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
144
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
145
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
149
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
150
|
+
Timestamp: new Date('2025-01-01T00:00:02Z'),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
154
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
155
|
+
Timestamp: new Date('2025-01-01T00:00:03Z'),
|
|
156
|
+
},
|
|
157
|
+
])
|
|
158
|
+
// Second poll: First complete
|
|
159
|
+
.mockResolvedValueOnce([
|
|
160
|
+
{
|
|
161
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
162
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
163
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
167
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
168
|
+
Timestamp: new Date('2025-01-01T00:00:10Z'),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
172
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
173
|
+
Timestamp: new Date('2025-01-01T00:00:02Z'),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
177
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
178
|
+
Timestamp: new Date('2025-01-01T00:00:03Z'),
|
|
179
|
+
},
|
|
180
|
+
])
|
|
181
|
+
// Third poll: All complete
|
|
182
|
+
.mockResolvedValueOnce([
|
|
183
|
+
{
|
|
184
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
185
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
186
|
+
Timestamp: new Date('2025-01-01T00:00:10Z'),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
190
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
191
|
+
Timestamp: new Date('2025-01-01T00:00:11Z'),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
195
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
196
|
+
Timestamp: new Date('2025-01-01T00:00:12Z'),
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
201
|
+
|
|
202
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
203
|
+
stackIdentifier,
|
|
204
|
+
resourceLogicalIds,
|
|
205
|
+
onProgress: onProgressCallback,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Advance through polling intervals
|
|
209
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
210
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
211
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
212
|
+
|
|
213
|
+
const result = await monitorPromise;
|
|
214
|
+
|
|
215
|
+
expect(result.success).toBe(true);
|
|
216
|
+
expect(result.updatedCount).toBe(3);
|
|
217
|
+
expect(result.failedCount).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('monitorUpdate - failed updates', () => {
|
|
222
|
+
it('should detect and report UPDATE_FAILED resources', async () => {
|
|
223
|
+
const stackIdentifier = new StackIdentifier({
|
|
224
|
+
stackName: 'test-stack',
|
|
225
|
+
region: 'us-east-1',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
229
|
+
|
|
230
|
+
mockCFRepo.getStackEvents
|
|
231
|
+
.mockResolvedValueOnce([
|
|
232
|
+
{
|
|
233
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
234
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
235
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
236
|
+
},
|
|
237
|
+
])
|
|
238
|
+
.mockResolvedValueOnce([
|
|
239
|
+
{
|
|
240
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
241
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
242
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
246
|
+
ResourceStatus: 'UPDATE_FAILED',
|
|
247
|
+
ResourceStatusReason: 'Subnet does not exist: subnet-invalid',
|
|
248
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_COMPLETE');
|
|
253
|
+
|
|
254
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
255
|
+
stackIdentifier,
|
|
256
|
+
resourceLogicalIds,
|
|
257
|
+
onProgress: onProgressCallback,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
261
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
262
|
+
|
|
263
|
+
const result = await monitorPromise;
|
|
264
|
+
|
|
265
|
+
expect(result.success).toBe(false);
|
|
266
|
+
expect(result.updatedCount).toBe(0);
|
|
267
|
+
expect(result.failedCount).toBe(1);
|
|
268
|
+
expect(result.failedResources).toHaveLength(1);
|
|
269
|
+
expect(result.failedResources[0].logicalId).toBe('AttioLambdaFunction');
|
|
270
|
+
expect(result.failedResources[0].reason).toBe('Subnet does not exist: subnet-invalid');
|
|
271
|
+
|
|
272
|
+
// Verify FAILED callback was triggered
|
|
273
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
274
|
+
logicalId: 'AttioLambdaFunction',
|
|
275
|
+
status: 'FAILED',
|
|
276
|
+
reason: 'Subnet does not exist: subnet-invalid',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should detect stack rollback during update', async () => {
|
|
281
|
+
const stackIdentifier = new StackIdentifier({
|
|
282
|
+
stackName: 'test-stack',
|
|
283
|
+
region: 'us-east-1',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
287
|
+
|
|
288
|
+
mockCFRepo.getStackEvents.mockResolvedValue([
|
|
289
|
+
{
|
|
290
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
291
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
292
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
// Stack status shows rollback in progress
|
|
297
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_IN_PROGRESS');
|
|
298
|
+
|
|
299
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
300
|
+
stackIdentifier,
|
|
301
|
+
resourceLogicalIds,
|
|
302
|
+
onProgress: onProgressCallback,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
306
|
+
|
|
307
|
+
await expect(monitorPromise).rejects.toThrow('Update operation failed and rolled back');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('monitorUpdate - timeout handling', () => {
|
|
312
|
+
it('should timeout after 5 minutes', async () => {
|
|
313
|
+
const stackIdentifier = new StackIdentifier({
|
|
314
|
+
stackName: 'test-stack',
|
|
315
|
+
region: 'us-east-1',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
319
|
+
|
|
320
|
+
// Mock events that never complete
|
|
321
|
+
mockCFRepo.getStackEvents.mockResolvedValue([
|
|
322
|
+
{
|
|
323
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
324
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
325
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_IN_PROGRESS');
|
|
330
|
+
|
|
331
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
332
|
+
stackIdentifier,
|
|
333
|
+
resourceLogicalIds,
|
|
334
|
+
onProgress: onProgressCallback,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Advance past 5 minute timeout (300000ms)
|
|
338
|
+
await jest.advanceTimersByTimeAsync(300000 + 2000);
|
|
339
|
+
|
|
340
|
+
await expect(monitorPromise).rejects.toThrow('Update operation timed out');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('monitorUpdate - event deduplication', () => {
|
|
345
|
+
it('should not process duplicate events', async () => {
|
|
346
|
+
const stackIdentifier = new StackIdentifier({
|
|
347
|
+
stackName: 'test-stack',
|
|
348
|
+
region: 'us-east-1',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
352
|
+
|
|
353
|
+
// Mock duplicate events (same timestamp + logicalId + status)
|
|
354
|
+
mockCFRepo.getStackEvents
|
|
355
|
+
.mockResolvedValueOnce([
|
|
356
|
+
{
|
|
357
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
358
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
359
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
360
|
+
},
|
|
361
|
+
])
|
|
362
|
+
.mockResolvedValueOnce([
|
|
363
|
+
// Duplicate event
|
|
364
|
+
{
|
|
365
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
366
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
367
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
368
|
+
},
|
|
369
|
+
// New event
|
|
370
|
+
{
|
|
371
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
372
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
373
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
378
|
+
|
|
379
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
380
|
+
stackIdentifier,
|
|
381
|
+
resourceLogicalIds,
|
|
382
|
+
onProgress: onProgressCallback,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
386
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
387
|
+
|
|
388
|
+
await monitorPromise;
|
|
389
|
+
|
|
390
|
+
// IN_PROGRESS callback should only be called once (not twice for duplicate)
|
|
391
|
+
const inProgressCalls = onProgressCallback.mock.calls.filter(
|
|
392
|
+
(call) => call[0].status === 'IN_PROGRESS'
|
|
393
|
+
);
|
|
394
|
+
expect(inProgressCalls).toHaveLength(1);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('monitorUpdate - no resources to track', () => {
|
|
399
|
+
it('should return immediately if no resources to track', async () => {
|
|
400
|
+
const stackIdentifier = new StackIdentifier({
|
|
401
|
+
stackName: 'test-stack',
|
|
402
|
+
region: 'us-east-1',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await monitor.monitorUpdate({
|
|
406
|
+
stackIdentifier,
|
|
407
|
+
resourceLogicalIds: [],
|
|
408
|
+
onProgress: onProgressCallback,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(result.success).toBe(true);
|
|
412
|
+
expect(result.updatedCount).toBe(0);
|
|
413
|
+
expect(result.failedCount).toBe(0);
|
|
414
|
+
|
|
415
|
+
// Should not have polled CloudFormation
|
|
416
|
+
expect(mockCFRepo.getStackEvents).not.toHaveBeenCalled();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|