@friggframework/devtools 2.0.0--canary.474.6a0bba7.0 → 2.0.0--canary.474.ca45ad3.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/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -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__/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/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/package.json +6 -6
package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js
ADDED
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportTemplateGenerator Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for CloudFormation import template generation
|
|
5
|
+
* Domain Layer - Service Tests
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Generate CloudFormation template for import operation
|
|
9
|
+
* - Resolve template intrinsics (!Ref, !Sub, !GetAtt) with actual AWS values
|
|
10
|
+
* - Validate template matches current resource state
|
|
11
|
+
* - Merge resource definitions with existing template
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { ImportTemplateGenerator } = require('../import-template-generator');
|
|
15
|
+
|
|
16
|
+
describe('ImportTemplateGenerator', () => {
|
|
17
|
+
let generator;
|
|
18
|
+
let mockTemplateParser;
|
|
19
|
+
let mockResourceDetector;
|
|
20
|
+
let mockStackRepository;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Mock template parser
|
|
24
|
+
mockTemplateParser = {
|
|
25
|
+
parseTemplate: jest.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Mock resource detector (AWS state)
|
|
29
|
+
mockResourceDetector = {
|
|
30
|
+
getResourceDetails: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Mock stack repository (CloudFormation)
|
|
34
|
+
mockStackRepository = {
|
|
35
|
+
getTemplate: jest.fn(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
generator = new ImportTemplateGenerator({
|
|
39
|
+
templateParser: mockTemplateParser,
|
|
40
|
+
resourceDetector: mockResourceDetector,
|
|
41
|
+
stackRepository: mockStackRepository,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('generateImportTemplate', () => {
|
|
46
|
+
const stackIdentifier = {
|
|
47
|
+
stackName: 'test-stack',
|
|
48
|
+
region: 'us-east-1',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
it('should generate template with resolved !Ref VpcCidr intrinsic', async () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const resourcesToImport = [
|
|
54
|
+
{
|
|
55
|
+
logicalId: 'FriggVPC',
|
|
56
|
+
physicalId: 'vpc-12345678',
|
|
57
|
+
resourceType: 'AWS::EC2::VPC',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const buildTemplate = {
|
|
62
|
+
resources: {
|
|
63
|
+
FriggVPC: {
|
|
64
|
+
Type: 'AWS::EC2::VPC',
|
|
65
|
+
Properties: {
|
|
66
|
+
CidrBlock: { Ref: 'VpcCidr' }, // !Ref intrinsic
|
|
67
|
+
EnableDnsHostnames: true,
|
|
68
|
+
EnableDnsSupport: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const awsResourceDetails = {
|
|
75
|
+
properties: {
|
|
76
|
+
VpcId: 'vpc-12345678',
|
|
77
|
+
CidrBlock: '10.0.0.0/16', // Actual AWS value
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
82
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
83
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
84
|
+
|
|
85
|
+
// Act
|
|
86
|
+
const result = await generator.generateImportTemplate({
|
|
87
|
+
resourcesToImport,
|
|
88
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
89
|
+
stackIdentifier,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(result.template.Resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
|
|
94
|
+
expect(result.template.Resources.FriggVPC.Properties.EnableDnsHostnames).toBe(true);
|
|
95
|
+
expect(result.resourceIdentifiers).toHaveLength(1);
|
|
96
|
+
expect(result.resourceIdentifiers[0]).toEqual({
|
|
97
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
98
|
+
LogicalResourceId: 'FriggVPC',
|
|
99
|
+
ResourceIdentifier: { VpcId: 'vpc-12345678' },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should generate template with resolved !Sub ${AWS::StackName} intrinsic', async () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const resourcesToImport = [
|
|
106
|
+
{
|
|
107
|
+
logicalId: 'FriggVPC',
|
|
108
|
+
physicalId: 'vpc-12345678',
|
|
109
|
+
resourceType: 'AWS::EC2::VPC',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const buildTemplate = {
|
|
114
|
+
resources: {
|
|
115
|
+
FriggVPC: {
|
|
116
|
+
Type: 'AWS::EC2::VPC',
|
|
117
|
+
Properties: {
|
|
118
|
+
CidrBlock: '10.0.0.0/16',
|
|
119
|
+
Tags: [
|
|
120
|
+
{
|
|
121
|
+
Key: 'Name',
|
|
122
|
+
Value: { 'Fn::Sub': '${AWS::StackName}-vpc' }, // !Sub intrinsic
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const awsResourceDetails = {
|
|
131
|
+
properties: {
|
|
132
|
+
VpcId: 'vpc-12345678',
|
|
133
|
+
CidrBlock: '10.0.0.0/16',
|
|
134
|
+
Tags: [
|
|
135
|
+
{ Key: 'Name', Value: 'test-stack-vpc' },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
stackName: 'test-stack', // Used for !Sub resolution
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
142
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
143
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
144
|
+
|
|
145
|
+
// Act
|
|
146
|
+
const result = await generator.generateImportTemplate({
|
|
147
|
+
resourcesToImport,
|
|
148
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
149
|
+
stackIdentifier,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
expect(result.template.Resources.FriggVPC.Properties.Tags[0].Value).toBe(
|
|
154
|
+
'test-stack-vpc'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should generate template with resolved !GetAtt intrinsic', async () => {
|
|
159
|
+
// Arrange
|
|
160
|
+
const resourcesToImport = [
|
|
161
|
+
{
|
|
162
|
+
logicalId: 'FriggPublicSubnet',
|
|
163
|
+
physicalId: 'subnet-public-123',
|
|
164
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const buildTemplate = {
|
|
169
|
+
resources: {
|
|
170
|
+
FriggPublicSubnet: {
|
|
171
|
+
Type: 'AWS::EC2::Subnet',
|
|
172
|
+
Properties: {
|
|
173
|
+
VpcId: { 'Fn::GetAtt': ['FriggVPC', 'VpcId'] }, // !GetAtt intrinsic
|
|
174
|
+
CidrBlock: '10.0.1.0/24',
|
|
175
|
+
AvailabilityZone: { 'Fn::GetAtt': ['FriggVPC', 'AvailabilityZone'] },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const awsResourceDetails = {
|
|
182
|
+
properties: {
|
|
183
|
+
SubnetId: 'subnet-public-123',
|
|
184
|
+
VpcId: 'vpc-12345678', // Actual VPC ID from AWS
|
|
185
|
+
CidrBlock: '10.0.1.0/24',
|
|
186
|
+
AvailabilityZone: 'us-east-1a', // Actual AZ from AWS
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
191
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
192
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
193
|
+
|
|
194
|
+
// Act
|
|
195
|
+
const result = await generator.generateImportTemplate({
|
|
196
|
+
resourcesToImport,
|
|
197
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
198
|
+
stackIdentifier,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Assert
|
|
202
|
+
expect(result.template.Resources.FriggPublicSubnet.Properties.VpcId).toBe(
|
|
203
|
+
'vpc-12345678'
|
|
204
|
+
);
|
|
205
|
+
expect(result.template.Resources.FriggPublicSubnet.Properties.AvailabilityZone).toBe(
|
|
206
|
+
'us-east-1a'
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should resolve nested !Ref in array properties', async () => {
|
|
211
|
+
// Arrange
|
|
212
|
+
const resourcesToImport = [
|
|
213
|
+
{
|
|
214
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
215
|
+
physicalId: 'sg-07c01370e830b6ad6',
|
|
216
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const buildTemplate = {
|
|
221
|
+
resources: {
|
|
222
|
+
FriggLambdaSecurityGroup: {
|
|
223
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
224
|
+
Properties: {
|
|
225
|
+
GroupDescription: 'Lambda security group',
|
|
226
|
+
VpcId: { Ref: 'FriggVPC' }, // !Ref intrinsic
|
|
227
|
+
SecurityGroupIngress: [
|
|
228
|
+
{
|
|
229
|
+
IpProtocol: 'tcp',
|
|
230
|
+
FromPort: 443,
|
|
231
|
+
ToPort: 443,
|
|
232
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const awsResourceDetails = {
|
|
241
|
+
properties: {
|
|
242
|
+
GroupId: 'sg-07c01370e830b6ad6',
|
|
243
|
+
VpcId: 'vpc-12345678',
|
|
244
|
+
GroupDescription: 'Lambda security group',
|
|
245
|
+
SecurityGroupIngress: [
|
|
246
|
+
{
|
|
247
|
+
IpProtocol: 'tcp',
|
|
248
|
+
FromPort: 443,
|
|
249
|
+
ToPort: 443,
|
|
250
|
+
SourceSecurityGroupId: 'sg-07c01370e830b6ad6',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
257
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
258
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
259
|
+
|
|
260
|
+
// Act
|
|
261
|
+
const result = await generator.generateImportTemplate({
|
|
262
|
+
resourcesToImport,
|
|
263
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
264
|
+
stackIdentifier,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Assert
|
|
268
|
+
expect(result.template.Resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe(
|
|
269
|
+
'vpc-12345678'
|
|
270
|
+
);
|
|
271
|
+
expect(
|
|
272
|
+
result.template.Resources.FriggLambdaSecurityGroup.Properties.SecurityGroupIngress[0]
|
|
273
|
+
.SourceSecurityGroupId
|
|
274
|
+
).toBe('sg-07c01370e830b6ad6');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should merge resource definitions with existing CloudFormation template', async () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
const resourcesToImport = [
|
|
280
|
+
{
|
|
281
|
+
logicalId: 'FriggVPC',
|
|
282
|
+
physicalId: 'vpc-12345678',
|
|
283
|
+
resourceType: 'AWS::EC2::VPC',
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const buildTemplate = {
|
|
288
|
+
resources: {
|
|
289
|
+
FriggVPC: {
|
|
290
|
+
Type: 'AWS::EC2::VPC',
|
|
291
|
+
Properties: {
|
|
292
|
+
CidrBlock: '10.0.0.0/16',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Existing template already has other resources
|
|
299
|
+
const currentTemplate = {
|
|
300
|
+
Resources: {
|
|
301
|
+
MyLambdaFunction: {
|
|
302
|
+
Type: 'AWS::Lambda::Function',
|
|
303
|
+
Properties: {
|
|
304
|
+
FunctionName: 'my-function',
|
|
305
|
+
Runtime: 'nodejs18.x',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
MyDynamoDBTable: {
|
|
309
|
+
Type: 'AWS::DynamoDB::Table',
|
|
310
|
+
Properties: {
|
|
311
|
+
TableName: 'my-table',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const awsResourceDetails = {
|
|
318
|
+
properties: {
|
|
319
|
+
VpcId: 'vpc-12345678',
|
|
320
|
+
CidrBlock: '10.0.0.0/16',
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
325
|
+
mockStackRepository.getTemplate.mockResolvedValue(currentTemplate);
|
|
326
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
327
|
+
|
|
328
|
+
// Act
|
|
329
|
+
const result = await generator.generateImportTemplate({
|
|
330
|
+
resourcesToImport,
|
|
331
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
332
|
+
stackIdentifier,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Assert
|
|
336
|
+
expect(result.template.Resources.FriggVPC).toBeDefined();
|
|
337
|
+
expect(result.template.Resources.MyLambdaFunction).toBeDefined();
|
|
338
|
+
expect(result.template.Resources.MyDynamoDBTable).toBeDefined();
|
|
339
|
+
expect(Object.keys(result.template.Resources)).toHaveLength(3);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should throw error if logical ID not found in build template', async () => {
|
|
343
|
+
// Arrange
|
|
344
|
+
const resourcesToImport = [
|
|
345
|
+
{
|
|
346
|
+
logicalId: 'NonExistentResource',
|
|
347
|
+
physicalId: 'vpc-12345678',
|
|
348
|
+
resourceType: 'AWS::EC2::VPC',
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
const buildTemplate = {
|
|
353
|
+
resources: {
|
|
354
|
+
FriggVPC: {
|
|
355
|
+
Type: 'AWS::EC2::VPC',
|
|
356
|
+
Properties: {
|
|
357
|
+
CidrBlock: '10.0.0.0/16',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
364
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
365
|
+
|
|
366
|
+
// Act & Assert
|
|
367
|
+
await expect(
|
|
368
|
+
generator.generateImportTemplate({
|
|
369
|
+
resourcesToImport,
|
|
370
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
371
|
+
stackIdentifier,
|
|
372
|
+
})
|
|
373
|
+
).rejects.toThrow(
|
|
374
|
+
'Logical ID NonExistentResource not found in build template. Cannot generate import definition without template reference.'
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should throw error if build template file does not exist', async () => {
|
|
379
|
+
// Arrange
|
|
380
|
+
const resourcesToImport = [
|
|
381
|
+
{
|
|
382
|
+
logicalId: 'FriggVPC',
|
|
383
|
+
physicalId: 'vpc-12345678',
|
|
384
|
+
resourceType: 'AWS::EC2::VPC',
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
mockTemplateParser.parseTemplate.mockImplementation(() => {
|
|
389
|
+
throw new Error('ENOENT: no such file or directory');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Act & Assert
|
|
393
|
+
await expect(
|
|
394
|
+
generator.generateImportTemplate({
|
|
395
|
+
resourcesToImport,
|
|
396
|
+
buildTemplatePath: '/path/to/nonexistent-template.json',
|
|
397
|
+
stackIdentifier,
|
|
398
|
+
})
|
|
399
|
+
).rejects.toThrow('ENOENT: no such file or directory');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should generate correct resource identifiers for VPC, Subnet, SecurityGroup', async () => {
|
|
403
|
+
// Arrange
|
|
404
|
+
const resourcesToImport = [
|
|
405
|
+
{
|
|
406
|
+
logicalId: 'FriggVPC',
|
|
407
|
+
physicalId: 'vpc-12345678',
|
|
408
|
+
resourceType: 'AWS::EC2::VPC',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
412
|
+
physicalId: 'subnet-11111111',
|
|
413
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
417
|
+
physicalId: 'sg-07c01370e830b6ad6',
|
|
418
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
const buildTemplate = {
|
|
423
|
+
resources: {
|
|
424
|
+
FriggVPC: {
|
|
425
|
+
Type: 'AWS::EC2::VPC',
|
|
426
|
+
Properties: { CidrBlock: '10.0.0.0/16' },
|
|
427
|
+
},
|
|
428
|
+
FriggPrivateSubnet1: {
|
|
429
|
+
Type: 'AWS::EC2::Subnet',
|
|
430
|
+
Properties: { CidrBlock: '10.0.1.0/24', VpcId: { Ref: 'FriggVPC' } },
|
|
431
|
+
},
|
|
432
|
+
FriggLambdaSecurityGroup: {
|
|
433
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
434
|
+
Properties: { GroupDescription: 'Lambda SG', VpcId: { Ref: 'FriggVPC' } },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
440
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
441
|
+
mockResourceDetector.getResourceDetails
|
|
442
|
+
.mockResolvedValueOnce({
|
|
443
|
+
properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
|
|
444
|
+
})
|
|
445
|
+
.mockResolvedValueOnce({
|
|
446
|
+
properties: {
|
|
447
|
+
SubnetId: 'subnet-11111111',
|
|
448
|
+
CidrBlock: '10.0.1.0/24',
|
|
449
|
+
VpcId: 'vpc-12345678',
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
.mockResolvedValueOnce({
|
|
453
|
+
properties: {
|
|
454
|
+
GroupId: 'sg-07c01370e830b6ad6',
|
|
455
|
+
GroupDescription: 'Lambda SG',
|
|
456
|
+
VpcId: 'vpc-12345678',
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Act
|
|
461
|
+
const result = await generator.generateImportTemplate({
|
|
462
|
+
resourcesToImport,
|
|
463
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
464
|
+
stackIdentifier,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Assert
|
|
468
|
+
expect(result.resourceIdentifiers).toHaveLength(3);
|
|
469
|
+
expect(result.resourceIdentifiers[0]).toEqual({
|
|
470
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
471
|
+
LogicalResourceId: 'FriggVPC',
|
|
472
|
+
ResourceIdentifier: { VpcId: 'vpc-12345678' },
|
|
473
|
+
});
|
|
474
|
+
expect(result.resourceIdentifiers[1]).toEqual({
|
|
475
|
+
ResourceType: 'AWS::EC2::Subnet',
|
|
476
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
477
|
+
ResourceIdentifier: { SubnetId: 'subnet-11111111' },
|
|
478
|
+
});
|
|
479
|
+
expect(result.resourceIdentifiers[2]).toEqual({
|
|
480
|
+
ResourceType: 'AWS::EC2::SecurityGroup',
|
|
481
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
482
|
+
ResourceIdentifier: { Id: 'sg-07c01370e830b6ad6' },
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should generate correct resource identifier for InternetGateway', async () => {
|
|
487
|
+
// Arrange
|
|
488
|
+
const resourcesToImport = [
|
|
489
|
+
{
|
|
490
|
+
logicalId: 'FriggInternetGateway',
|
|
491
|
+
physicalId: 'igw-0abc123def456',
|
|
492
|
+
resourceType: 'AWS::EC2::InternetGateway',
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
const buildTemplate = {
|
|
497
|
+
resources: {
|
|
498
|
+
FriggInternetGateway: {
|
|
499
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
500
|
+
Properties: {
|
|
501
|
+
Tags: [{ Key: 'Name', Value: 'Frigg IGW' }],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
508
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
509
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
510
|
+
properties: {
|
|
511
|
+
InternetGatewayId: 'igw-0abc123def456',
|
|
512
|
+
Tags: [{ Key: 'Name', Value: 'Frigg IGW' }],
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Act
|
|
517
|
+
const result = await generator.generateImportTemplate({
|
|
518
|
+
resourcesToImport,
|
|
519
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
520
|
+
stackIdentifier,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Assert
|
|
524
|
+
expect(result.resourceIdentifiers[0]).toEqual({
|
|
525
|
+
ResourceType: 'AWS::EC2::InternetGateway',
|
|
526
|
+
LogicalResourceId: 'FriggInternetGateway',
|
|
527
|
+
ResourceIdentifier: { InternetGatewayId: 'igw-0abc123def456' },
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should handle empty current template when stack does not exist yet', async () => {
|
|
532
|
+
// Arrange
|
|
533
|
+
const resourcesToImport = [
|
|
534
|
+
{
|
|
535
|
+
logicalId: 'FriggVPC',
|
|
536
|
+
physicalId: 'vpc-12345678',
|
|
537
|
+
resourceType: 'AWS::EC2::VPC',
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
const buildTemplate = {
|
|
542
|
+
resources: {
|
|
543
|
+
FriggVPC: {
|
|
544
|
+
Type: 'AWS::EC2::VPC',
|
|
545
|
+
Properties: { CidrBlock: '10.0.0.0/16' },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
551
|
+
// Stack doesn't exist yet
|
|
552
|
+
mockStackRepository.getTemplate.mockRejectedValue(new Error('Stack does not exist'));
|
|
553
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
554
|
+
properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Act
|
|
558
|
+
const result = await generator.generateImportTemplate({
|
|
559
|
+
resourcesToImport,
|
|
560
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
561
|
+
stackIdentifier,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Assert
|
|
565
|
+
expect(result.template.Resources.FriggVPC).toBeDefined();
|
|
566
|
+
expect(Object.keys(result.template.Resources)).toHaveLength(1);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('should resolve complex nested intrinsics', async () => {
|
|
570
|
+
// Arrange
|
|
571
|
+
const resourcesToImport = [
|
|
572
|
+
{
|
|
573
|
+
logicalId: 'FriggPublicSubnet',
|
|
574
|
+
physicalId: 'subnet-public-123',
|
|
575
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
576
|
+
},
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
const buildTemplate = {
|
|
580
|
+
resources: {
|
|
581
|
+
FriggPublicSubnet: {
|
|
582
|
+
Type: 'AWS::EC2::Subnet',
|
|
583
|
+
Properties: {
|
|
584
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
585
|
+
CidrBlock: '10.0.1.0/24',
|
|
586
|
+
Tags: [
|
|
587
|
+
{
|
|
588
|
+
Key: 'Name',
|
|
589
|
+
Value: { 'Fn::Sub': '${AWS::StackName}-public-subnet' },
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
Key: 'Type',
|
|
593
|
+
Value: 'public',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
Key: 'VpcCidr',
|
|
597
|
+
Value: { 'Fn::GetAtt': ['FriggVPC', 'CidrBlock'] },
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const awsResourceDetails = {
|
|
606
|
+
properties: {
|
|
607
|
+
SubnetId: 'subnet-public-123',
|
|
608
|
+
VpcId: 'vpc-12345678',
|
|
609
|
+
CidrBlock: '10.0.1.0/24',
|
|
610
|
+
Tags: [
|
|
611
|
+
{ Key: 'Name', Value: 'test-stack-public-subnet' },
|
|
612
|
+
{ Key: 'Type', Value: 'public' },
|
|
613
|
+
{ Key: 'VpcCidr', Value: '10.0.0.0/16' },
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
stackName: 'test-stack',
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
620
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
621
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
|
|
622
|
+
|
|
623
|
+
// Act
|
|
624
|
+
const result = await generator.generateImportTemplate({
|
|
625
|
+
resourcesToImport,
|
|
626
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
627
|
+
stackIdentifier,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Assert
|
|
631
|
+
const tags = result.template.Resources.FriggPublicSubnet.Properties.Tags;
|
|
632
|
+
expect(tags[0].Value).toBe('test-stack-public-subnet');
|
|
633
|
+
expect(tags[1].Value).toBe('public');
|
|
634
|
+
expect(tags[2].Value).toBe('10.0.0.0/16');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should handle multiple resources with different resource types', async () => {
|
|
638
|
+
// Arrange
|
|
639
|
+
const resourcesToImport = [
|
|
640
|
+
{
|
|
641
|
+
logicalId: 'FriggVPC',
|
|
642
|
+
physicalId: 'vpc-12345678',
|
|
643
|
+
resourceType: 'AWS::EC2::VPC',
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
logicalId: 'FriggRouteTable',
|
|
647
|
+
physicalId: 'rtb-0123456789',
|
|
648
|
+
resourceType: 'AWS::EC2::RouteTable',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
logicalId: 'FriggVPCEndpoint',
|
|
652
|
+
physicalId: 'vpce-0987654321',
|
|
653
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
654
|
+
},
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
const buildTemplate = {
|
|
658
|
+
resources: {
|
|
659
|
+
FriggVPC: {
|
|
660
|
+
Type: 'AWS::EC2::VPC',
|
|
661
|
+
Properties: { CidrBlock: '10.0.0.0/16' },
|
|
662
|
+
},
|
|
663
|
+
FriggRouteTable: {
|
|
664
|
+
Type: 'AWS::EC2::RouteTable',
|
|
665
|
+
Properties: { VpcId: { Ref: 'FriggVPC' } },
|
|
666
|
+
},
|
|
667
|
+
FriggVPCEndpoint: {
|
|
668
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
669
|
+
Properties: { VpcId: { Ref: 'FriggVPC' }, ServiceName: 's3' },
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
675
|
+
mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
|
|
676
|
+
mockResourceDetector.getResourceDetails
|
|
677
|
+
.mockResolvedValueOnce({
|
|
678
|
+
properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
|
|
679
|
+
})
|
|
680
|
+
.mockResolvedValueOnce({
|
|
681
|
+
properties: { RouteTableId: 'rtb-0123456789', VpcId: 'vpc-12345678' },
|
|
682
|
+
})
|
|
683
|
+
.mockResolvedValueOnce({
|
|
684
|
+
properties: {
|
|
685
|
+
VpcEndpointId: 'vpce-0987654321',
|
|
686
|
+
VpcId: 'vpc-12345678',
|
|
687
|
+
ServiceName: 's3',
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Act
|
|
692
|
+
const result = await generator.generateImportTemplate({
|
|
693
|
+
resourcesToImport,
|
|
694
|
+
buildTemplatePath: '/path/to/build-template.json',
|
|
695
|
+
stackIdentifier,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Assert
|
|
699
|
+
expect(result.resourceIdentifiers).toHaveLength(3);
|
|
700
|
+
expect(result.resourceIdentifiers[0].ResourceIdentifier).toEqual({
|
|
701
|
+
VpcId: 'vpc-12345678',
|
|
702
|
+
});
|
|
703
|
+
expect(result.resourceIdentifiers[1].ResourceIdentifier).toEqual({
|
|
704
|
+
RouteTableId: 'rtb-0123456789',
|
|
705
|
+
});
|
|
706
|
+
expect(result.resourceIdentifiers[2].ResourceIdentifier).toEqual({
|
|
707
|
+
VpcEndpointId: 'vpce-0987654321',
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe('_getResourceIdentifier', () => {
|
|
713
|
+
it('should return correct identifier for VPC', () => {
|
|
714
|
+
// Act
|
|
715
|
+
const result = generator._getResourceIdentifier('AWS::EC2::VPC', 'vpc-123');
|
|
716
|
+
|
|
717
|
+
// Assert
|
|
718
|
+
expect(result).toEqual({ VpcId: 'vpc-123' });
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should return correct identifier for Subnet', () => {
|
|
722
|
+
// Act
|
|
723
|
+
const result = generator._getResourceIdentifier('AWS::EC2::Subnet', 'subnet-456');
|
|
724
|
+
|
|
725
|
+
// Assert
|
|
726
|
+
expect(result).toEqual({ SubnetId: 'subnet-456' });
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('should return correct identifier for SecurityGroup', () => {
|
|
730
|
+
// Act
|
|
731
|
+
const result = generator._getResourceIdentifier('AWS::EC2::SecurityGroup', 'sg-789');
|
|
732
|
+
|
|
733
|
+
// Assert
|
|
734
|
+
expect(result).toEqual({ Id: 'sg-789' });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should return correct identifier for InternetGateway', () => {
|
|
738
|
+
// Act
|
|
739
|
+
const result = generator._getResourceIdentifier('AWS::EC2::InternetGateway', 'igw-abc');
|
|
740
|
+
|
|
741
|
+
// Assert
|
|
742
|
+
expect(result).toEqual({ InternetGatewayId: 'igw-abc' });
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('should return correct identifier for NatGateway', () => {
|
|
746
|
+
// Act
|
|
747
|
+
const result = generator._getResourceIdentifier('AWS::EC2::NatGateway', 'nat-def');
|
|
748
|
+
|
|
749
|
+
// Assert
|
|
750
|
+
expect(result).toEqual({ NatGatewayId: 'nat-def' });
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('should return correct identifier for RouteTable', () => {
|
|
754
|
+
// Act
|
|
755
|
+
const result = generator._getResourceIdentifier('AWS::EC2::RouteTable', 'rtb-ghi');
|
|
756
|
+
|
|
757
|
+
// Assert
|
|
758
|
+
expect(result).toEqual({ RouteTableId: 'rtb-ghi' });
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('should return correct identifier for VPCEndpoint', () => {
|
|
762
|
+
// Act
|
|
763
|
+
const result = generator._getResourceIdentifier('AWS::EC2::VPCEndpoint', 'vpce-jkl');
|
|
764
|
+
|
|
765
|
+
// Assert
|
|
766
|
+
expect(result).toEqual({ VpcEndpointId: 'vpce-jkl' });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('should return generic identifier for unknown resource type', () => {
|
|
770
|
+
// Act
|
|
771
|
+
const result = generator._getResourceIdentifier('AWS::Unknown::Type', 'unknown-123');
|
|
772
|
+
|
|
773
|
+
// Assert
|
|
774
|
+
expect(result).toEqual({ Id: 'unknown-123' });
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe('_resolveRef', () => {
|
|
779
|
+
it('should resolve VpcCidr to actual CIDR block', () => {
|
|
780
|
+
// Arrange
|
|
781
|
+
const awsResourceDetails = {
|
|
782
|
+
properties: {
|
|
783
|
+
CidrBlock: '10.0.0.0/16',
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// Act
|
|
788
|
+
const result = generator._resolveRef('VpcCidr', awsResourceDetails, 'AWS::EC2::VPC');
|
|
789
|
+
|
|
790
|
+
// Assert
|
|
791
|
+
expect(result).toBe('10.0.0.0/16');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should resolve VpcId to actual VPC ID', () => {
|
|
795
|
+
// Arrange
|
|
796
|
+
const awsResourceDetails = {
|
|
797
|
+
properties: {
|
|
798
|
+
VpcId: 'vpc-12345678',
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// Act
|
|
803
|
+
const result = generator._resolveRef('VpcId', awsResourceDetails, 'AWS::EC2::Subnet');
|
|
804
|
+
|
|
805
|
+
// Assert
|
|
806
|
+
expect(result).toBe('vpc-12345678');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('should return property value from AWS details for unmapped refs', () => {
|
|
810
|
+
// Arrange
|
|
811
|
+
const awsResourceDetails = {
|
|
812
|
+
properties: {
|
|
813
|
+
CustomProperty: 'custom-value',
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// Act
|
|
818
|
+
const result = generator._resolveRef(
|
|
819
|
+
'CustomProperty',
|
|
820
|
+
awsResourceDetails,
|
|
821
|
+
'AWS::Custom::Type'
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Assert
|
|
825
|
+
expect(result).toBe('custom-value');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should return ref name if not found in properties', () => {
|
|
829
|
+
// Arrange
|
|
830
|
+
const awsResourceDetails = {
|
|
831
|
+
properties: {},
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// Act
|
|
835
|
+
const result = generator._resolveRef('UnknownRef', awsResourceDetails, 'AWS::EC2::VPC');
|
|
836
|
+
|
|
837
|
+
// Assert
|
|
838
|
+
expect(result).toBe('UnknownRef');
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
describe('_resolveSub', () => {
|
|
843
|
+
it('should resolve ${AWS::StackName} to actual stack name', () => {
|
|
844
|
+
// Arrange
|
|
845
|
+
const awsResourceDetails = {
|
|
846
|
+
stackName: 'test-stack',
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// Act
|
|
850
|
+
const result = generator._resolveSub('${AWS::StackName}-vpc', awsResourceDetails);
|
|
851
|
+
|
|
852
|
+
// Assert
|
|
853
|
+
expect(result).toBe('test-stack-vpc');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should resolve multiple ${AWS::StackName} occurrences', () => {
|
|
857
|
+
// Arrange
|
|
858
|
+
const awsResourceDetails = {
|
|
859
|
+
stackName: 'my-stack',
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// Act
|
|
863
|
+
const result = generator._resolveSub(
|
|
864
|
+
'${AWS::StackName}-${AWS::StackName}-resource',
|
|
865
|
+
awsResourceDetails
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
// Assert
|
|
869
|
+
expect(result).toBe('my-stack-my-stack-resource');
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('should resolve tag variables from AWS resource tags', () => {
|
|
873
|
+
// Arrange
|
|
874
|
+
const awsResourceDetails = {
|
|
875
|
+
stackName: 'my-stack',
|
|
876
|
+
tags: {
|
|
877
|
+
Environment: 'production',
|
|
878
|
+
Team: 'platform',
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
// Act
|
|
883
|
+
const result = generator._resolveSub(
|
|
884
|
+
'${AWS::StackName}-${Environment}-${Team}',
|
|
885
|
+
awsResourceDetails
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
// Assert
|
|
889
|
+
expect(result).toBe('my-stack-production-platform');
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('should return empty string if stack name missing', () => {
|
|
893
|
+
// Arrange
|
|
894
|
+
const awsResourceDetails = {};
|
|
895
|
+
|
|
896
|
+
// Act
|
|
897
|
+
const result = generator._resolveSub('${AWS::StackName}-vpc', awsResourceDetails);
|
|
898
|
+
|
|
899
|
+
// Assert
|
|
900
|
+
expect(result).toBe('-vpc');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should return non-string values as-is', () => {
|
|
904
|
+
// Arrange
|
|
905
|
+
const awsResourceDetails = {};
|
|
906
|
+
|
|
907
|
+
// Act
|
|
908
|
+
const result = generator._resolveSub({ some: 'object' }, awsResourceDetails);
|
|
909
|
+
|
|
910
|
+
// Assert
|
|
911
|
+
expect(result).toEqual({ some: 'object' });
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
describe('_resolveGetAtt', () => {
|
|
916
|
+
it('should resolve attribute from AWS resource properties', () => {
|
|
917
|
+
// Arrange
|
|
918
|
+
const awsResourceDetails = {
|
|
919
|
+
properties: {
|
|
920
|
+
VpcId: 'vpc-12345678',
|
|
921
|
+
CidrBlock: '10.0.0.0/16',
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
// Act
|
|
926
|
+
const result = generator._resolveGetAtt(['FriggVPC', 'VpcId'], awsResourceDetails);
|
|
927
|
+
|
|
928
|
+
// Assert
|
|
929
|
+
expect(result).toBe('vpc-12345678');
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('should return null if attribute not found', () => {
|
|
933
|
+
// Arrange
|
|
934
|
+
const awsResourceDetails = {
|
|
935
|
+
properties: {
|
|
936
|
+
VpcId: 'vpc-12345678',
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// Act
|
|
941
|
+
const result = generator._resolveGetAtt(['FriggVPC', 'NonExistentAttribute'], awsResourceDetails);
|
|
942
|
+
|
|
943
|
+
// Assert
|
|
944
|
+
expect(result).toBeNull();
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
describe('_resolveValue', () => {
|
|
949
|
+
it('should resolve literal values unchanged', () => {
|
|
950
|
+
// Arrange
|
|
951
|
+
const awsResourceDetails = { properties: {} };
|
|
952
|
+
|
|
953
|
+
// Act
|
|
954
|
+
const result = generator._resolveValue('literal-value', awsResourceDetails, 'AWS::EC2::VPC');
|
|
955
|
+
|
|
956
|
+
// Assert
|
|
957
|
+
expect(result).toBe('literal-value');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('should resolve nested object with intrinsics', () => {
|
|
961
|
+
// Arrange
|
|
962
|
+
const value = {
|
|
963
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
964
|
+
CidrBlock: '10.0.1.0/24',
|
|
965
|
+
};
|
|
966
|
+
const awsResourceDetails = {
|
|
967
|
+
properties: {
|
|
968
|
+
VpcId: 'vpc-12345678',
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// Act
|
|
973
|
+
const result = generator._resolveValue(value, awsResourceDetails, 'AWS::EC2::Subnet');
|
|
974
|
+
|
|
975
|
+
// Assert
|
|
976
|
+
expect(result.VpcId).toBe('vpc-12345678');
|
|
977
|
+
expect(result.CidrBlock).toBe('10.0.1.0/24');
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('should resolve array of values with intrinsics', () => {
|
|
981
|
+
// Arrange
|
|
982
|
+
const value = [
|
|
983
|
+
{ Ref: 'Subnet1' },
|
|
984
|
+
{ Ref: 'Subnet2' },
|
|
985
|
+
'literal-subnet-id',
|
|
986
|
+
];
|
|
987
|
+
const awsResourceDetails = {
|
|
988
|
+
properties: {
|
|
989
|
+
Subnet1: 'subnet-111',
|
|
990
|
+
Subnet2: 'subnet-222',
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// Act
|
|
995
|
+
const result = generator._resolveValue(value, awsResourceDetails, 'AWS::Lambda::Function');
|
|
996
|
+
|
|
997
|
+
// Assert
|
|
998
|
+
expect(result).toEqual(['subnet-111', 'subnet-222', 'literal-subnet-id']);
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
describe('_generateResourceDefinition', () => {
|
|
1003
|
+
it('should throw error if logical ID not in build template', () => {
|
|
1004
|
+
// Arrange
|
|
1005
|
+
const buildTemplate = {
|
|
1006
|
+
resources: {
|
|
1007
|
+
FriggVPC: { Type: 'AWS::EC2::VPC' },
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
const awsResourceDetails = {
|
|
1011
|
+
properties: { VpcId: 'vpc-123' },
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// Act & Assert
|
|
1015
|
+
expect(() => {
|
|
1016
|
+
generator._generateResourceDefinition({
|
|
1017
|
+
logicalId: 'NonExistentResource',
|
|
1018
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1019
|
+
physicalId: 'vpc-123',
|
|
1020
|
+
buildTemplate,
|
|
1021
|
+
awsResourceDetails,
|
|
1022
|
+
});
|
|
1023
|
+
}).toThrow(
|
|
1024
|
+
'Logical ID NonExistentResource not found in build template. Cannot generate import definition without template reference.'
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it('should generate resource definition with resolved properties', () => {
|
|
1029
|
+
// Arrange
|
|
1030
|
+
const buildTemplate = {
|
|
1031
|
+
resources: {
|
|
1032
|
+
FriggVPC: {
|
|
1033
|
+
Type: 'AWS::EC2::VPC',
|
|
1034
|
+
Properties: {
|
|
1035
|
+
CidrBlock: { Ref: 'VpcCidr' },
|
|
1036
|
+
EnableDnsHostnames: true,
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
const awsResourceDetails = {
|
|
1042
|
+
properties: {
|
|
1043
|
+
VpcId: 'vpc-12345678',
|
|
1044
|
+
CidrBlock: '10.0.0.0/16',
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// Act
|
|
1049
|
+
const result = generator._generateResourceDefinition({
|
|
1050
|
+
logicalId: 'FriggVPC',
|
|
1051
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1052
|
+
physicalId: 'vpc-12345678',
|
|
1053
|
+
buildTemplate,
|
|
1054
|
+
awsResourceDetails,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Assert
|
|
1058
|
+
expect(result.Type).toBe('AWS::EC2::VPC');
|
|
1059
|
+
expect(result.Properties.CidrBlock).toBe('10.0.0.0/16');
|
|
1060
|
+
expect(result.Properties.EnableDnsHostnames).toBe(true);
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
describe('Resource Protection Policies', () => {
|
|
1065
|
+
it('should add DeletionPolicy: Retain to all imported resources', () => {
|
|
1066
|
+
// Arrange
|
|
1067
|
+
const buildTemplate = {
|
|
1068
|
+
resources: {
|
|
1069
|
+
FriggVPC: {
|
|
1070
|
+
Type: 'AWS::EC2::VPC',
|
|
1071
|
+
Properties: { CidrBlock: '10.0.0.0/16' },
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
const awsResourceDetails = {
|
|
1076
|
+
properties: { VpcId: 'vpc-123', CidrBlock: '10.0.0.0/16' },
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Act
|
|
1080
|
+
const result = generator._generateResourceDefinition({
|
|
1081
|
+
logicalId: 'FriggVPC',
|
|
1082
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1083
|
+
physicalId: 'vpc-123',
|
|
1084
|
+
buildTemplate,
|
|
1085
|
+
awsResourceDetails,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// Assert
|
|
1089
|
+
expect(result.DeletionPolicy).toBe('Retain');
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('should add UpdateReplacePolicy: Retain to all imported resources', () => {
|
|
1093
|
+
// Arrange
|
|
1094
|
+
const buildTemplate = {
|
|
1095
|
+
resources: {
|
|
1096
|
+
FriggVPC: {
|
|
1097
|
+
Type: 'AWS::EC2::VPC',
|
|
1098
|
+
Properties: { CidrBlock: '10.0.0.0/16' },
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
const awsResourceDetails = {
|
|
1103
|
+
properties: { VpcId: 'vpc-123', CidrBlock: '10.0.0.0/16' },
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// Act
|
|
1107
|
+
const result = generator._generateResourceDefinition({
|
|
1108
|
+
logicalId: 'FriggVPC',
|
|
1109
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1110
|
+
physicalId: 'vpc-123',
|
|
1111
|
+
buildTemplate,
|
|
1112
|
+
awsResourceDetails,
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// Assert
|
|
1116
|
+
expect(result.UpdateReplacePolicy).toBe('Retain');
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('should protect resources from deletion during stack updates', () => {
|
|
1120
|
+
// Arrange
|
|
1121
|
+
const buildTemplate = {
|
|
1122
|
+
resources: {
|
|
1123
|
+
FriggLambdaSecurityGroup: {
|
|
1124
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1125
|
+
Properties: { GroupDescription: 'Lambda SG' },
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
const awsResourceDetails = {
|
|
1130
|
+
properties: { GroupId: 'sg-123', GroupDescription: 'Lambda SG' },
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// Act
|
|
1134
|
+
const result = generator._generateResourceDefinition({
|
|
1135
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
1136
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
1137
|
+
physicalId: 'sg-123',
|
|
1138
|
+
buildTemplate,
|
|
1139
|
+
awsResourceDetails,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Assert: Both policies protect resources
|
|
1143
|
+
expect(result.DeletionPolicy).toBe('Retain');
|
|
1144
|
+
expect(result.UpdateReplacePolicy).toBe('Retain');
|
|
1145
|
+
// This prevents:
|
|
1146
|
+
// - Stack deletion from destroying the physical resource (DeletionPolicy)
|
|
1147
|
+
// - Stack update replacement from destroying the old physical resource (UpdateReplacePolicy)
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
});
|