@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.6a0bba7.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__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -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 +307 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +38 -5
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- 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__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -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-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
- package/package.json +6 -6
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepairViaImportUseCase Tests - importWithLogicalIdMapping
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for the application layer use case orchestrating template comparison
|
|
5
|
+
* and logical ID mapping for the `frigg repair --import` command.
|
|
6
|
+
*
|
|
7
|
+
* Application Layer - Use Case Tests
|
|
8
|
+
* Following TDD, DDD, and Hexagonal Architecture principles
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const RepairViaImportUseCase = require('../repair-via-import-use-case');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
describe('RepairViaImportUseCase - importWithLogicalIdMapping', () => {
|
|
16
|
+
let useCase;
|
|
17
|
+
let mockResourceImporter;
|
|
18
|
+
let mockResourceDetector;
|
|
19
|
+
let mockStackRepository;
|
|
20
|
+
let mockTemplateParser;
|
|
21
|
+
let mockLogicalIdMapper;
|
|
22
|
+
let tempDir;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Create temp directory for test files
|
|
26
|
+
tempDir = path.join(__dirname, 'temp-test-files');
|
|
27
|
+
if (!fs.existsSync(tempDir)) {
|
|
28
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mock dependencies
|
|
32
|
+
mockResourceImporter = {
|
|
33
|
+
validateImport: jest.fn(),
|
|
34
|
+
importResource: jest.fn(),
|
|
35
|
+
importMultipleResources: jest.fn(),
|
|
36
|
+
getImportStatus: jest.fn(),
|
|
37
|
+
generateTemplateSnippet: jest.fn(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
mockResourceDetector = {
|
|
41
|
+
getResourceDetails: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
mockStackRepository = {
|
|
45
|
+
getTemplate: jest.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
mockTemplateParser = {
|
|
49
|
+
parseTemplate: jest.fn(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
mockLogicalIdMapper = {
|
|
53
|
+
mapOrphanedResourcesToLogicalIds: jest.fn(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useCase = new RepairViaImportUseCase({
|
|
57
|
+
resourceImporter: mockResourceImporter,
|
|
58
|
+
resourceDetector: mockResourceDetector,
|
|
59
|
+
stackRepository: mockStackRepository,
|
|
60
|
+
templateParser: mockTemplateParser,
|
|
61
|
+
logicalIdMapper: mockLogicalIdMapper,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
// Cleanup temp directory
|
|
67
|
+
if (fs.existsSync(tempDir)) {
|
|
68
|
+
const files = fs.readdirSync(tempDir);
|
|
69
|
+
files.forEach((file) => {
|
|
70
|
+
fs.unlinkSync(path.join(tempDir, file));
|
|
71
|
+
});
|
|
72
|
+
fs.rmdirSync(tempDir);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('importWithLogicalIdMapping', () => {
|
|
77
|
+
it('should throw error if buildTemplatePath not provided', async () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const stackIdentifier = {
|
|
80
|
+
stackName: 'acme-integrations-dev',
|
|
81
|
+
region: 'us-east-1',
|
|
82
|
+
};
|
|
83
|
+
const orphanedResources = [];
|
|
84
|
+
|
|
85
|
+
// Act & Assert
|
|
86
|
+
await expect(
|
|
87
|
+
useCase.importWithLogicalIdMapping({
|
|
88
|
+
stackIdentifier,
|
|
89
|
+
orphanedResources,
|
|
90
|
+
buildTemplatePath: null,
|
|
91
|
+
})
|
|
92
|
+
).rejects.toThrow('buildTemplatePath is required');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw error if build template file does not exist', async () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const stackIdentifier = {
|
|
98
|
+
stackName: 'acme-integrations-dev',
|
|
99
|
+
region: 'us-east-1',
|
|
100
|
+
};
|
|
101
|
+
const orphanedResources = [];
|
|
102
|
+
const nonExistentPath = '/path/to/nonexistent/template.json';
|
|
103
|
+
|
|
104
|
+
// Act & Assert
|
|
105
|
+
await expect(
|
|
106
|
+
useCase.importWithLogicalIdMapping({
|
|
107
|
+
stackIdentifier,
|
|
108
|
+
orphanedResources,
|
|
109
|
+
buildTemplatePath: nonExistentPath,
|
|
110
|
+
})
|
|
111
|
+
).rejects.toThrow(/Build template not found at/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should successfully map orphaned resources to logical IDs', async () => {
|
|
115
|
+
// Arrange
|
|
116
|
+
const stackIdentifier = {
|
|
117
|
+
stackName: 'acme-integrations-dev',
|
|
118
|
+
region: 'us-east-1',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const orphanedResources = [
|
|
122
|
+
{
|
|
123
|
+
physicalId: 'vpc-12345678',
|
|
124
|
+
resourceType: 'AWS::EC2::VPC',
|
|
125
|
+
properties: {
|
|
126
|
+
VpcId: 'vpc-12345678',
|
|
127
|
+
CidrBlock: '10.0.0.0/16',
|
|
128
|
+
tags: {
|
|
129
|
+
'aws:cloudformation:stack-name': 'acme-integrations-dev',
|
|
130
|
+
'aws:cloudformation:logical-id': 'FriggVPC',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
physicalId: 'subnet-11111111',
|
|
136
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
137
|
+
properties: {
|
|
138
|
+
SubnetId: 'subnet-11111111',
|
|
139
|
+
VpcId: 'vpc-12345678',
|
|
140
|
+
tags: {
|
|
141
|
+
'aws:cloudformation:stack-name': 'acme-integrations-dev',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const buildTemplate = {
|
|
148
|
+
resources: {
|
|
149
|
+
FriggVPC: { Type: 'AWS::EC2::VPC' },
|
|
150
|
+
FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const deployedTemplate = {
|
|
155
|
+
resources: {
|
|
156
|
+
FriggVPC: { Type: 'AWS::EC2::VPC' },
|
|
157
|
+
MyLambda: {
|
|
158
|
+
Type: 'AWS::Lambda::Function',
|
|
159
|
+
Properties: {
|
|
160
|
+
VpcConfig: {
|
|
161
|
+
SubnetIds: ['subnet-11111111'],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const mappings = [
|
|
169
|
+
{
|
|
170
|
+
logicalId: 'FriggVPC',
|
|
171
|
+
physicalId: 'vpc-12345678',
|
|
172
|
+
resourceType: 'AWS::EC2::VPC',
|
|
173
|
+
matchMethod: 'tag',
|
|
174
|
+
confidence: 'high',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
178
|
+
physicalId: 'subnet-11111111',
|
|
179
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
180
|
+
matchMethod: 'vpc-usage',
|
|
181
|
+
confidence: 'high',
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// Create temporary build template file
|
|
186
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
187
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
188
|
+
|
|
189
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
190
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
191
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
195
|
+
stackIdentifier,
|
|
196
|
+
orphanedResources,
|
|
197
|
+
buildTemplatePath,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
expect(result.mappedCount).toBe(2);
|
|
203
|
+
expect(result.unmappedCount).toBe(0);
|
|
204
|
+
expect(result.mappings).toHaveLength(2);
|
|
205
|
+
expect(result.resourcesToImport).toHaveLength(2);
|
|
206
|
+
|
|
207
|
+
// Verify template parser was called
|
|
208
|
+
expect(mockTemplateParser.parseTemplate).toHaveBeenCalledWith(buildTemplatePath);
|
|
209
|
+
|
|
210
|
+
// Verify stack repository was called
|
|
211
|
+
expect(mockStackRepository.getTemplate).toHaveBeenCalledWith(stackIdentifier);
|
|
212
|
+
|
|
213
|
+
// Verify logical ID mapper was called
|
|
214
|
+
expect(mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds).toHaveBeenCalledWith({
|
|
215
|
+
orphanedResources,
|
|
216
|
+
buildTemplate,
|
|
217
|
+
deployedTemplate,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should generate correct CloudFormation import format', async () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
const stackIdentifier = {
|
|
224
|
+
stackName: 'acme-integrations-dev',
|
|
225
|
+
region: 'us-east-1',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const orphanedResources = [
|
|
229
|
+
{
|
|
230
|
+
physicalId: 'vpc-12345678',
|
|
231
|
+
resourceType: 'AWS::EC2::VPC',
|
|
232
|
+
properties: {
|
|
233
|
+
VpcId: 'vpc-12345678',
|
|
234
|
+
CidrBlock: '10.0.0.0/16',
|
|
235
|
+
tags: {},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const buildTemplate = { resources: { FriggVPC: { Type: 'AWS::EC2::VPC' } } };
|
|
241
|
+
const deployedTemplate = { resources: {} };
|
|
242
|
+
|
|
243
|
+
const mappings = [
|
|
244
|
+
{
|
|
245
|
+
logicalId: 'FriggVPC',
|
|
246
|
+
physicalId: 'vpc-12345678',
|
|
247
|
+
resourceType: 'AWS::EC2::VPC',
|
|
248
|
+
matchMethod: 'tag',
|
|
249
|
+
confidence: 'high',
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
254
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
255
|
+
|
|
256
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
257
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
258
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
259
|
+
|
|
260
|
+
// Act
|
|
261
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
262
|
+
stackIdentifier,
|
|
263
|
+
orphanedResources,
|
|
264
|
+
buildTemplatePath,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Assert
|
|
268
|
+
expect(result.resourcesToImport).toEqual([
|
|
269
|
+
{
|
|
270
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
271
|
+
LogicalResourceId: 'FriggVPC',
|
|
272
|
+
ResourceIdentifier: { VpcId: 'vpc-12345678' },
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle multiple resource types with correct identifiers', async () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
const stackIdentifier = {
|
|
280
|
+
stackName: 'acme-integrations-dev',
|
|
281
|
+
region: 'us-east-1',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const orphanedResources = [
|
|
285
|
+
{
|
|
286
|
+
physicalId: 'vpc-123',
|
|
287
|
+
resourceType: 'AWS::EC2::VPC',
|
|
288
|
+
properties: {
|
|
289
|
+
VpcId: 'vpc-123',
|
|
290
|
+
CidrBlock: '10.0.0.0/16',
|
|
291
|
+
tags: {},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
physicalId: 'subnet-456',
|
|
296
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
297
|
+
properties: {
|
|
298
|
+
SubnetId: 'subnet-456',
|
|
299
|
+
VpcId: 'vpc-123',
|
|
300
|
+
tags: {},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
physicalId: 'sg-789',
|
|
305
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
306
|
+
properties: {
|
|
307
|
+
GroupId: 'sg-789',
|
|
308
|
+
VpcId: 'vpc-123',
|
|
309
|
+
tags: {},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
physicalId: 'igw-abc',
|
|
314
|
+
resourceType: 'AWS::EC2::InternetGateway',
|
|
315
|
+
properties: {
|
|
316
|
+
InternetGatewayId: 'igw-abc',
|
|
317
|
+
tags: {},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const buildTemplate = { resources: {} };
|
|
323
|
+
const deployedTemplate = { resources: {} };
|
|
324
|
+
|
|
325
|
+
const mappings = [
|
|
326
|
+
{
|
|
327
|
+
logicalId: 'FriggVPC',
|
|
328
|
+
physicalId: 'vpc-123',
|
|
329
|
+
resourceType: 'AWS::EC2::VPC',
|
|
330
|
+
matchMethod: 'tag',
|
|
331
|
+
confidence: 'high',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
335
|
+
physicalId: 'subnet-456',
|
|
336
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
337
|
+
matchMethod: 'vpc-usage',
|
|
338
|
+
confidence: 'high',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
342
|
+
physicalId: 'sg-789',
|
|
343
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
344
|
+
matchMethod: 'usage',
|
|
345
|
+
confidence: 'medium',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
logicalId: 'FriggInternetGateway',
|
|
349
|
+
physicalId: 'igw-abc',
|
|
350
|
+
resourceType: 'AWS::EC2::InternetGateway',
|
|
351
|
+
matchMethod: 'tag',
|
|
352
|
+
confidence: 'high',
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
357
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
358
|
+
|
|
359
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
360
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
361
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
362
|
+
|
|
363
|
+
// Act
|
|
364
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
365
|
+
stackIdentifier,
|
|
366
|
+
orphanedResources,
|
|
367
|
+
buildTemplatePath,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Assert
|
|
371
|
+
expect(result.resourcesToImport).toEqual([
|
|
372
|
+
{
|
|
373
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
374
|
+
LogicalResourceId: 'FriggVPC',
|
|
375
|
+
ResourceIdentifier: { VpcId: 'vpc-123' },
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
ResourceType: 'AWS::EC2::Subnet',
|
|
379
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
380
|
+
ResourceIdentifier: { SubnetId: 'subnet-456' },
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
ResourceType: 'AWS::EC2::SecurityGroup',
|
|
384
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
385
|
+
ResourceIdentifier: { GroupId: 'sg-789' },
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
ResourceType: 'AWS::EC2::InternetGateway',
|
|
389
|
+
LogicalResourceId: 'FriggInternetGateway',
|
|
390
|
+
ResourceIdentifier: { InternetGatewayId: 'igw-abc' },
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should separate mapped and unmapped resources', async () => {
|
|
396
|
+
// Arrange
|
|
397
|
+
const stackIdentifier = {
|
|
398
|
+
stackName: 'acme-integrations-dev',
|
|
399
|
+
region: 'us-east-1',
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const orphanedResources = [
|
|
403
|
+
{
|
|
404
|
+
physicalId: 'vpc-123',
|
|
405
|
+
resourceType: 'AWS::EC2::VPC',
|
|
406
|
+
properties: {
|
|
407
|
+
VpcId: 'vpc-123',
|
|
408
|
+
CidrBlock: '10.0.0.0/16',
|
|
409
|
+
tags: {},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
physicalId: 'vpc-456',
|
|
414
|
+
resourceType: 'AWS::EC2::VPC',
|
|
415
|
+
properties: {
|
|
416
|
+
VpcId: 'vpc-456',
|
|
417
|
+
CidrBlock: '10.0.0.0/16',
|
|
418
|
+
tags: {},
|
|
419
|
+
},
|
|
420
|
+
}, // No match
|
|
421
|
+
{
|
|
422
|
+
physicalId: 'subnet-789',
|
|
423
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
424
|
+
properties: {
|
|
425
|
+
SubnetId: 'subnet-789',
|
|
426
|
+
VpcId: 'vpc-123',
|
|
427
|
+
tags: {},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
const buildTemplate = { resources: {} };
|
|
433
|
+
const deployedTemplate = { resources: {} };
|
|
434
|
+
|
|
435
|
+
const mappings = [
|
|
436
|
+
{
|
|
437
|
+
logicalId: 'FriggVPC',
|
|
438
|
+
physicalId: 'vpc-123',
|
|
439
|
+
resourceType: 'AWS::EC2::VPC',
|
|
440
|
+
matchMethod: 'tag',
|
|
441
|
+
confidence: 'high',
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
logicalId: null, // Unmapped
|
|
445
|
+
physicalId: 'vpc-456',
|
|
446
|
+
resourceType: 'AWS::EC2::VPC',
|
|
447
|
+
matchMethod: 'none',
|
|
448
|
+
confidence: 'none',
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
452
|
+
physicalId: 'subnet-789',
|
|
453
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
454
|
+
matchMethod: 'vpc-usage',
|
|
455
|
+
confidence: 'high',
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
460
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
461
|
+
|
|
462
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
463
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
464
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
465
|
+
|
|
466
|
+
// Act
|
|
467
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
468
|
+
stackIdentifier,
|
|
469
|
+
orphanedResources,
|
|
470
|
+
buildTemplatePath,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Assert
|
|
474
|
+
expect(result.success).toBe(true);
|
|
475
|
+
expect(result.mappedCount).toBe(2);
|
|
476
|
+
expect(result.unmappedCount).toBe(1);
|
|
477
|
+
expect(result.mappings).toHaveLength(2);
|
|
478
|
+
expect(result.unmappedResources).toHaveLength(1);
|
|
479
|
+
expect(result.unmappedResources[0].physicalId).toBe('vpc-456');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should return failure if no resources could be mapped', async () => {
|
|
483
|
+
// Arrange
|
|
484
|
+
const stackIdentifier = {
|
|
485
|
+
stackName: 'acme-integrations-dev',
|
|
486
|
+
region: 'us-east-1',
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const orphanedResources = [
|
|
490
|
+
{
|
|
491
|
+
physicalId: 'vpc-123',
|
|
492
|
+
resourceType: 'AWS::EC2::VPC',
|
|
493
|
+
properties: {
|
|
494
|
+
VpcId: 'vpc-123',
|
|
495
|
+
CidrBlock: '10.0.0.0/16',
|
|
496
|
+
tags: {},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
physicalId: 'vpc-456',
|
|
501
|
+
resourceType: 'AWS::EC2::VPC',
|
|
502
|
+
properties: {
|
|
503
|
+
VpcId: 'vpc-456',
|
|
504
|
+
CidrBlock: '10.0.0.0/16',
|
|
505
|
+
tags: {},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
const buildTemplate = { resources: {} };
|
|
511
|
+
const deployedTemplate = { resources: {} };
|
|
512
|
+
|
|
513
|
+
// All resources unmapped
|
|
514
|
+
const mappings = [
|
|
515
|
+
{
|
|
516
|
+
logicalId: null,
|
|
517
|
+
physicalId: 'vpc-123',
|
|
518
|
+
resourceType: 'AWS::EC2::VPC',
|
|
519
|
+
matchMethod: 'none',
|
|
520
|
+
confidence: 'none',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
logicalId: null,
|
|
524
|
+
physicalId: 'vpc-456',
|
|
525
|
+
resourceType: 'AWS::EC2::VPC',
|
|
526
|
+
matchMethod: 'none',
|
|
527
|
+
confidence: 'none',
|
|
528
|
+
},
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
532
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
533
|
+
|
|
534
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
535
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
536
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
537
|
+
|
|
538
|
+
// Act
|
|
539
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
540
|
+
stackIdentifier,
|
|
541
|
+
orphanedResources,
|
|
542
|
+
buildTemplatePath,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Assert
|
|
546
|
+
expect(result.success).toBe(false);
|
|
547
|
+
expect(result.message).toBe('No resources could be mapped to logical IDs');
|
|
548
|
+
expect(result.unmappedCount).toBe(2);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should generate warnings for multiple resources of same type', async () => {
|
|
552
|
+
// Arrange
|
|
553
|
+
const stackIdentifier = {
|
|
554
|
+
stackName: 'acme-integrations-dev',
|
|
555
|
+
region: 'us-east-1',
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const orphanedResources = [
|
|
559
|
+
{
|
|
560
|
+
physicalId: 'vpc-123',
|
|
561
|
+
resourceType: 'AWS::EC2::VPC',
|
|
562
|
+
properties: {
|
|
563
|
+
VpcId: 'vpc-123',
|
|
564
|
+
CidrBlock: '10.0.0.0/16',
|
|
565
|
+
tags: {},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
physicalId: 'vpc-456',
|
|
570
|
+
resourceType: 'AWS::EC2::VPC',
|
|
571
|
+
properties: {
|
|
572
|
+
VpcId: 'vpc-456',
|
|
573
|
+
CidrBlock: '10.0.0.0/16',
|
|
574
|
+
tags: {},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
physicalId: 'vpc-789',
|
|
579
|
+
resourceType: 'AWS::EC2::VPC',
|
|
580
|
+
properties: {
|
|
581
|
+
VpcId: 'vpc-789',
|
|
582
|
+
CidrBlock: '10.0.0.0/16',
|
|
583
|
+
tags: {},
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
const buildTemplate = { resources: {} };
|
|
589
|
+
const deployedTemplate = { resources: {} };
|
|
590
|
+
|
|
591
|
+
// Three VPCs mapped
|
|
592
|
+
const mappings = [
|
|
593
|
+
{
|
|
594
|
+
logicalId: 'FriggVPC',
|
|
595
|
+
physicalId: 'vpc-123',
|
|
596
|
+
resourceType: 'AWS::EC2::VPC',
|
|
597
|
+
matchMethod: 'tag',
|
|
598
|
+
confidence: 'high',
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
logicalId: 'FriggVPC2',
|
|
602
|
+
physicalId: 'vpc-456',
|
|
603
|
+
resourceType: 'AWS::EC2::VPC',
|
|
604
|
+
matchMethod: 'contained-resources',
|
|
605
|
+
confidence: 'high',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
logicalId: 'FriggVPC3',
|
|
609
|
+
physicalId: 'vpc-789',
|
|
610
|
+
resourceType: 'AWS::EC2::VPC',
|
|
611
|
+
matchMethod: 'tag',
|
|
612
|
+
confidence: 'high',
|
|
613
|
+
},
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
617
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
618
|
+
|
|
619
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
620
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
621
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
622
|
+
|
|
623
|
+
// Act
|
|
624
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
625
|
+
stackIdentifier,
|
|
626
|
+
orphanedResources,
|
|
627
|
+
buildTemplatePath,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Assert
|
|
631
|
+
expect(result.success).toBe(true);
|
|
632
|
+
expect(result.warnings).toHaveLength(1);
|
|
633
|
+
expect(result.warnings[0].type).toBe('MULTIPLE_RESOURCES');
|
|
634
|
+
expect(result.warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
635
|
+
expect(result.warnings[0].count).toBe(3);
|
|
636
|
+
expect(result.warnings[0].message).toContain('Multiple VPCs detected (3)');
|
|
637
|
+
expect(result.warnings[0].resources).toHaveLength(3);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should include build and deployed template paths in result', async () => {
|
|
641
|
+
// Arrange
|
|
642
|
+
const stackIdentifier = {
|
|
643
|
+
stackName: 'acme-integrations-dev',
|
|
644
|
+
region: 'us-east-1',
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const orphanedResources = [
|
|
648
|
+
{
|
|
649
|
+
physicalId: 'vpc-123',
|
|
650
|
+
resourceType: 'AWS::EC2::VPC',
|
|
651
|
+
properties: {
|
|
652
|
+
VpcId: 'vpc-123',
|
|
653
|
+
CidrBlock: '10.0.0.0/16',
|
|
654
|
+
tags: {},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
const buildTemplate = { resources: { FriggVPC: { Type: 'AWS::EC2::VPC' } } };
|
|
660
|
+
const deployedTemplate = { resources: {} };
|
|
661
|
+
|
|
662
|
+
const mappings = [
|
|
663
|
+
{
|
|
664
|
+
logicalId: 'FriggVPC',
|
|
665
|
+
physicalId: 'vpc-123',
|
|
666
|
+
resourceType: 'AWS::EC2::VPC',
|
|
667
|
+
matchMethod: 'tag',
|
|
668
|
+
confidence: 'high',
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
673
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
674
|
+
|
|
675
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
676
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
677
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
678
|
+
|
|
679
|
+
// Act
|
|
680
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
681
|
+
stackIdentifier,
|
|
682
|
+
orphanedResources,
|
|
683
|
+
buildTemplatePath,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Assert
|
|
687
|
+
expect(result.buildTemplatePath).toBe(buildTemplatePath);
|
|
688
|
+
expect(result.deployedTemplatePath).toBe('CloudFormation (deployed)');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should throw error if stackRepository not provided', async () => {
|
|
692
|
+
// Arrange
|
|
693
|
+
const useCaseWithoutStackRepo = new RepairViaImportUseCase({
|
|
694
|
+
resourceImporter: mockResourceImporter,
|
|
695
|
+
resourceDetector: mockResourceDetector,
|
|
696
|
+
// stackRepository: NOT PROVIDED
|
|
697
|
+
templateParser: mockTemplateParser,
|
|
698
|
+
logicalIdMapper: mockLogicalIdMapper,
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const stackIdentifier = {
|
|
702
|
+
stackName: 'acme-integrations-dev',
|
|
703
|
+
region: 'us-east-1',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
707
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify({ resources: {} }));
|
|
708
|
+
|
|
709
|
+
mockTemplateParser.parseTemplate.mockReturnValue({ resources: {} });
|
|
710
|
+
|
|
711
|
+
// Act & Assert
|
|
712
|
+
await expect(
|
|
713
|
+
useCaseWithoutStackRepo.importWithLogicalIdMapping({
|
|
714
|
+
stackIdentifier,
|
|
715
|
+
orphanedResources: [],
|
|
716
|
+
buildTemplatePath,
|
|
717
|
+
})
|
|
718
|
+
).rejects.toThrow('stackRepository is required for template comparison');
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('_getResourceIdentifier', () => {
|
|
723
|
+
it('should return correct identifier for VPC', () => {
|
|
724
|
+
// Arrange
|
|
725
|
+
const mapping = {
|
|
726
|
+
physicalId: 'vpc-123',
|
|
727
|
+
resourceType: 'AWS::EC2::VPC',
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Act
|
|
731
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
732
|
+
|
|
733
|
+
// Assert
|
|
734
|
+
expect(result).toEqual({ VpcId: 'vpc-123' });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should return correct identifier for Subnet', () => {
|
|
738
|
+
// Arrange
|
|
739
|
+
const mapping = {
|
|
740
|
+
physicalId: 'subnet-456',
|
|
741
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// Act
|
|
745
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
746
|
+
|
|
747
|
+
// Assert
|
|
748
|
+
expect(result).toEqual({ SubnetId: 'subnet-456' });
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('should return correct identifier for SecurityGroup', () => {
|
|
752
|
+
// Arrange
|
|
753
|
+
const mapping = {
|
|
754
|
+
physicalId: 'sg-789',
|
|
755
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// Act
|
|
759
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
760
|
+
|
|
761
|
+
// Assert
|
|
762
|
+
expect(result).toEqual({ GroupId: 'sg-789' });
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should return generic identifier for unknown resource type', () => {
|
|
766
|
+
// Arrange
|
|
767
|
+
const mapping = {
|
|
768
|
+
physicalId: 'unknown-123',
|
|
769
|
+
resourceType: 'AWS::Unknown::Type',
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Act
|
|
773
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
774
|
+
|
|
775
|
+
// Assert
|
|
776
|
+
expect(result).toEqual({ Id: 'unknown-123' });
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
describe('_checkForMultipleResources', () => {
|
|
781
|
+
it('should return no warnings for single resource per type', () => {
|
|
782
|
+
// Arrange
|
|
783
|
+
const mappings = [
|
|
784
|
+
{
|
|
785
|
+
logicalId: 'FriggVPC',
|
|
786
|
+
physicalId: 'vpc-123',
|
|
787
|
+
resourceType: 'AWS::EC2::VPC',
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
791
|
+
physicalId: 'subnet-456',
|
|
792
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
793
|
+
},
|
|
794
|
+
];
|
|
795
|
+
|
|
796
|
+
// Act
|
|
797
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
798
|
+
|
|
799
|
+
// Assert
|
|
800
|
+
expect(warnings).toHaveLength(0);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should return warning for multiple resources of same type', () => {
|
|
804
|
+
// Arrange
|
|
805
|
+
const mappings = [
|
|
806
|
+
{
|
|
807
|
+
logicalId: 'FriggVPC',
|
|
808
|
+
physicalId: 'vpc-123',
|
|
809
|
+
resourceType: 'AWS::EC2::VPC',
|
|
810
|
+
matchMethod: 'tag',
|
|
811
|
+
confidence: 'high',
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
logicalId: 'FriggVPC2',
|
|
815
|
+
physicalId: 'vpc-456',
|
|
816
|
+
resourceType: 'AWS::EC2::VPC',
|
|
817
|
+
matchMethod: 'contained-resources',
|
|
818
|
+
confidence: 'high',
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
// Act
|
|
823
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
824
|
+
|
|
825
|
+
// Assert
|
|
826
|
+
expect(warnings).toHaveLength(1);
|
|
827
|
+
expect(warnings[0].type).toBe('MULTIPLE_RESOURCES');
|
|
828
|
+
expect(warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
829
|
+
expect(warnings[0].count).toBe(2);
|
|
830
|
+
expect(warnings[0].resources).toHaveLength(2);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should return multiple warnings for different resource types with multiples', () => {
|
|
834
|
+
// Arrange
|
|
835
|
+
const mappings = [
|
|
836
|
+
{
|
|
837
|
+
logicalId: 'FriggVPC',
|
|
838
|
+
physicalId: 'vpc-123',
|
|
839
|
+
resourceType: 'AWS::EC2::VPC',
|
|
840
|
+
matchMethod: 'tag',
|
|
841
|
+
confidence: 'high',
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
logicalId: 'FriggVPC2',
|
|
845
|
+
physicalId: 'vpc-456',
|
|
846
|
+
resourceType: 'AWS::EC2::VPC',
|
|
847
|
+
matchMethod: 'tag',
|
|
848
|
+
confidence: 'high',
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
852
|
+
physicalId: 'subnet-111',
|
|
853
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
854
|
+
matchMethod: 'vpc-usage',
|
|
855
|
+
confidence: 'high',
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
logicalId: 'FriggPrivateSubnet2',
|
|
859
|
+
physicalId: 'subnet-222',
|
|
860
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
861
|
+
matchMethod: 'vpc-usage',
|
|
862
|
+
confidence: 'high',
|
|
863
|
+
},
|
|
864
|
+
];
|
|
865
|
+
|
|
866
|
+
// Act
|
|
867
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
868
|
+
|
|
869
|
+
// Assert
|
|
870
|
+
expect(warnings).toHaveLength(2);
|
|
871
|
+
expect(warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
872
|
+
expect(warnings[0].count).toBe(2);
|
|
873
|
+
expect(warnings[1].resourceType).toBe('AWS::EC2::Subnet');
|
|
874
|
+
expect(warnings[1].count).toBe(2);
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
describe('_deduplicateResourcesByLogicalId', () => {
|
|
879
|
+
it('should select ONE resource per logical ID when multiple map to same ID', () => {
|
|
880
|
+
// Arrange: 3 VPCs all tagged with "FriggVPC", but only vpc-123 is in deployed template
|
|
881
|
+
const mappedResources = [
|
|
882
|
+
{
|
|
883
|
+
logicalId: 'FriggVPC',
|
|
884
|
+
physicalId: 'vpc-123',
|
|
885
|
+
resourceType: 'AWS::EC2::VPC',
|
|
886
|
+
matchMethod: 'tag',
|
|
887
|
+
confidence: 'high',
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
logicalId: 'FriggVPC',
|
|
891
|
+
physicalId: 'vpc-456',
|
|
892
|
+
resourceType: 'AWS::EC2::VPC',
|
|
893
|
+
matchMethod: 'tag',
|
|
894
|
+
confidence: 'high',
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
logicalId: 'FriggVPC',
|
|
898
|
+
physicalId: 'vpc-789',
|
|
899
|
+
resourceType: 'AWS::EC2::VPC',
|
|
900
|
+
matchMethod: 'tag',
|
|
901
|
+
confidence: 'high',
|
|
902
|
+
},
|
|
903
|
+
];
|
|
904
|
+
|
|
905
|
+
const deployedTemplate = {
|
|
906
|
+
resources: {
|
|
907
|
+
MyLambda: {
|
|
908
|
+
Type: 'AWS::Lambda::Function',
|
|
909
|
+
Properties: {
|
|
910
|
+
VpcConfig: {
|
|
911
|
+
SubnetIds: ['subnet-in-vpc-123'], // This subnet belongs to vpc-123
|
|
912
|
+
SecurityGroupIds: [],
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// Act
|
|
920
|
+
const result = useCase._deduplicateResourcesByLogicalId(
|
|
921
|
+
mappedResources,
|
|
922
|
+
deployedTemplate
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
// Assert: Should select ONLY the VPC that's actually referenced
|
|
926
|
+
expect(result.selectedResources).toHaveLength(1);
|
|
927
|
+
expect(result.selectedResources[0].physicalId).toBe('vpc-123');
|
|
928
|
+
expect(result.selectedResources[0].logicalId).toBe('FriggVPC');
|
|
929
|
+
|
|
930
|
+
// The other 2 VPCs should be marked as duplicates
|
|
931
|
+
expect(result.duplicates).toHaveLength(2);
|
|
932
|
+
expect(result.duplicates.map((d) => d.physicalId)).toEqual(
|
|
933
|
+
expect.arrayContaining(['vpc-456', 'vpc-789'])
|
|
934
|
+
);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it('should keep all resources when they have unique logical IDs', () => {
|
|
938
|
+
// Arrange: Different logical IDs, no duplication
|
|
939
|
+
const mappedResources = [
|
|
940
|
+
{
|
|
941
|
+
logicalId: 'FriggVPC',
|
|
942
|
+
physicalId: 'vpc-123',
|
|
943
|
+
resourceType: 'AWS::EC2::VPC',
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
947
|
+
physicalId: 'subnet-456',
|
|
948
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
952
|
+
physicalId: 'sg-789',
|
|
953
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
954
|
+
},
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
const deployedTemplate = { resources: {} };
|
|
958
|
+
|
|
959
|
+
// Act
|
|
960
|
+
const result = useCase._deduplicateResourcesByLogicalId(
|
|
961
|
+
mappedResources,
|
|
962
|
+
deployedTemplate
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Assert: All 3 resources should be selected
|
|
966
|
+
expect(result.selectedResources).toHaveLength(3);
|
|
967
|
+
expect(result.duplicates).toHaveLength(0);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('should use deployed template references to select correct resource', () => {
|
|
971
|
+
// Arrange: 5 subnets with same logical ID, need to pick the ones in deployed template
|
|
972
|
+
const mappedResources = [
|
|
973
|
+
{
|
|
974
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
975
|
+
physicalId: 'subnet-111',
|
|
976
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
980
|
+
physicalId: 'subnet-222', // THIS ONE is in deployed template
|
|
981
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
985
|
+
physicalId: 'subnet-333',
|
|
986
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
990
|
+
physicalId: 'subnet-444',
|
|
991
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
995
|
+
physicalId: 'subnet-555',
|
|
996
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
997
|
+
},
|
|
998
|
+
];
|
|
999
|
+
|
|
1000
|
+
const deployedTemplate = {
|
|
1001
|
+
resources: {
|
|
1002
|
+
MyLambda: {
|
|
1003
|
+
Type: 'AWS::Lambda::Function',
|
|
1004
|
+
Properties: {
|
|
1005
|
+
VpcConfig: {
|
|
1006
|
+
SubnetIds: ['subnet-222', 'subnet-other'], // subnet-222 is THE correct one
|
|
1007
|
+
SecurityGroupIds: [],
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// Act
|
|
1015
|
+
const result = useCase._deduplicateResourcesByLogicalId(
|
|
1016
|
+
mappedResources,
|
|
1017
|
+
deployedTemplate
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
// Assert: Should select subnet-222 because it's in deployed template
|
|
1021
|
+
expect(result.selectedResources).toHaveLength(1);
|
|
1022
|
+
expect(result.selectedResources[0].physicalId).toBe('subnet-222');
|
|
1023
|
+
|
|
1024
|
+
// Other 4 subnets should be duplicates
|
|
1025
|
+
expect(result.duplicates).toHaveLength(4);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it('should handle multiple logical IDs with duplicates', () => {
|
|
1029
|
+
// Arrange: 3 VPCs + 2 SecurityGroups, all duplicates
|
|
1030
|
+
// Note: VPC selection falls back to first when no direct reference exists
|
|
1031
|
+
const mappedResources = [
|
|
1032
|
+
{
|
|
1033
|
+
logicalId: 'FriggVPC',
|
|
1034
|
+
physicalId: 'vpc-111', // Will be selected (fallback to first)
|
|
1035
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
logicalId: 'FriggVPC',
|
|
1039
|
+
physicalId: 'vpc-222',
|
|
1040
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
logicalId: 'FriggVPC',
|
|
1044
|
+
physicalId: 'vpc-333',
|
|
1045
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
1049
|
+
physicalId: 'sg-aaa', // Will be selected (in deployed template)
|
|
1050
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
1054
|
+
physicalId: 'sg-bbb',
|
|
1055
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
1056
|
+
},
|
|
1057
|
+
];
|
|
1058
|
+
|
|
1059
|
+
const deployedTemplate = {
|
|
1060
|
+
resources: {
|
|
1061
|
+
MyLambda: {
|
|
1062
|
+
Type: 'AWS::Lambda::Function',
|
|
1063
|
+
Properties: {
|
|
1064
|
+
VpcConfig: {
|
|
1065
|
+
SubnetIds: ['subnet-xxx'], // Not matching any VPC
|
|
1066
|
+
SecurityGroupIds: ['sg-aaa'], // sg-aaa is the correct one
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// Act
|
|
1074
|
+
const result = useCase._deduplicateResourcesByLogicalId(
|
|
1075
|
+
mappedResources,
|
|
1076
|
+
deployedTemplate
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
// Assert: Should select ONE of each logical ID
|
|
1080
|
+
expect(result.selectedResources).toHaveLength(2);
|
|
1081
|
+
expect(result.selectedResources.map((r) => r.physicalId)).toEqual(
|
|
1082
|
+
expect.arrayContaining(['vpc-111', 'sg-aaa']) // VPC fallback + SG match
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// 3 resources should be duplicates (2 VPCs + 1 SG)
|
|
1086
|
+
expect(result.duplicates).toHaveLength(3);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('should fall back to first resource if none match deployed template', () => {
|
|
1090
|
+
// Arrange: Multiple resources but none are in deployed template
|
|
1091
|
+
const mappedResources = [
|
|
1092
|
+
{
|
|
1093
|
+
logicalId: 'FriggVPC',
|
|
1094
|
+
physicalId: 'vpc-111',
|
|
1095
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
logicalId: 'FriggVPC',
|
|
1099
|
+
physicalId: 'vpc-222',
|
|
1100
|
+
resourceType: 'AWS::EC2::VPC',
|
|
1101
|
+
},
|
|
1102
|
+
];
|
|
1103
|
+
|
|
1104
|
+
const deployedTemplate = {
|
|
1105
|
+
resources: {
|
|
1106
|
+
MyLambda: {
|
|
1107
|
+
Type: 'AWS::Lambda::Function',
|
|
1108
|
+
Properties: {
|
|
1109
|
+
VpcConfig: {
|
|
1110
|
+
SubnetIds: ['subnet-other'], // Different VPC
|
|
1111
|
+
SecurityGroupIds: [],
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// Act
|
|
1119
|
+
const result = useCase._deduplicateResourcesByLogicalId(
|
|
1120
|
+
mappedResources,
|
|
1121
|
+
deployedTemplate
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
// Assert: Should pick first one as fallback
|
|
1125
|
+
expect(result.selectedResources).toHaveLength(1);
|
|
1126
|
+
expect(result.selectedResources[0].physicalId).toBe('vpc-111');
|
|
1127
|
+
expect(result.duplicates).toHaveLength(1);
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
});
|