@friggframework/devtools 2.0.0--canary.474.a0b734c.0 → 2.0.0--canary.474.898a56c.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__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +762 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -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/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -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 +330 -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-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/package.json +6 -6
|
@@ -0,0 +1,762 @@
|
|
|
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-0eadd96976d29ede7',
|
|
124
|
+
resourceType: 'AWS::EC2::VPC',
|
|
125
|
+
tags: [
|
|
126
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' },
|
|
127
|
+
{ Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
physicalId: 'subnet-00ab9e0502e66aac3',
|
|
132
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
133
|
+
tags: [
|
|
134
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const buildTemplate = {
|
|
140
|
+
resources: {
|
|
141
|
+
FriggVPC: { Type: 'AWS::EC2::VPC' },
|
|
142
|
+
FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const deployedTemplate = {
|
|
147
|
+
resources: {
|
|
148
|
+
FriggVPC: { Type: 'AWS::EC2::VPC' },
|
|
149
|
+
MyLambda: {
|
|
150
|
+
Type: 'AWS::Lambda::Function',
|
|
151
|
+
Properties: {
|
|
152
|
+
VpcConfig: {
|
|
153
|
+
SubnetIds: ['subnet-00ab9e0502e66aac3'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const mappings = [
|
|
161
|
+
{
|
|
162
|
+
logicalId: 'FriggVPC',
|
|
163
|
+
physicalId: 'vpc-0eadd96976d29ede7',
|
|
164
|
+
resourceType: 'AWS::EC2::VPC',
|
|
165
|
+
matchMethod: 'tag',
|
|
166
|
+
confidence: 'high',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
170
|
+
physicalId: 'subnet-00ab9e0502e66aac3',
|
|
171
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
172
|
+
matchMethod: 'vpc-usage',
|
|
173
|
+
confidence: 'high',
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// Create temporary build template file
|
|
178
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
179
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
180
|
+
|
|
181
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
182
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
183
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
187
|
+
stackIdentifier,
|
|
188
|
+
orphanedResources,
|
|
189
|
+
buildTemplatePath,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Assert
|
|
193
|
+
expect(result.success).toBe(true);
|
|
194
|
+
expect(result.mappedCount).toBe(2);
|
|
195
|
+
expect(result.unmappedCount).toBe(0);
|
|
196
|
+
expect(result.mappings).toHaveLength(2);
|
|
197
|
+
expect(result.resourcesToImport).toHaveLength(2);
|
|
198
|
+
|
|
199
|
+
// Verify template parser was called
|
|
200
|
+
expect(mockTemplateParser.parseTemplate).toHaveBeenCalledWith(buildTemplatePath);
|
|
201
|
+
|
|
202
|
+
// Verify stack repository was called
|
|
203
|
+
expect(mockStackRepository.getTemplate).toHaveBeenCalledWith(stackIdentifier);
|
|
204
|
+
|
|
205
|
+
// Verify logical ID mapper was called
|
|
206
|
+
expect(mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds).toHaveBeenCalledWith({
|
|
207
|
+
orphanedResources,
|
|
208
|
+
buildTemplate,
|
|
209
|
+
deployedTemplate,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should generate correct CloudFormation import format', async () => {
|
|
214
|
+
// Arrange
|
|
215
|
+
const stackIdentifier = {
|
|
216
|
+
stackName: 'acme-integrations-dev',
|
|
217
|
+
region: 'us-east-1',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const orphanedResources = [
|
|
221
|
+
{
|
|
222
|
+
physicalId: 'vpc-0eadd96976d29ede7',
|
|
223
|
+
resourceType: 'AWS::EC2::VPC',
|
|
224
|
+
tags: [],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const buildTemplate = { resources: { FriggVPC: { Type: 'AWS::EC2::VPC' } } };
|
|
229
|
+
const deployedTemplate = { resources: {} };
|
|
230
|
+
|
|
231
|
+
const mappings = [
|
|
232
|
+
{
|
|
233
|
+
logicalId: 'FriggVPC',
|
|
234
|
+
physicalId: 'vpc-0eadd96976d29ede7',
|
|
235
|
+
resourceType: 'AWS::EC2::VPC',
|
|
236
|
+
matchMethod: 'tag',
|
|
237
|
+
confidence: 'high',
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
242
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
243
|
+
|
|
244
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
245
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
246
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
247
|
+
|
|
248
|
+
// Act
|
|
249
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
250
|
+
stackIdentifier,
|
|
251
|
+
orphanedResources,
|
|
252
|
+
buildTemplatePath,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Assert
|
|
256
|
+
expect(result.resourcesToImport).toEqual([
|
|
257
|
+
{
|
|
258
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
259
|
+
LogicalResourceId: 'FriggVPC',
|
|
260
|
+
ResourceIdentifier: { VpcId: 'vpc-0eadd96976d29ede7' },
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should handle multiple resource types with correct identifiers', async () => {
|
|
266
|
+
// Arrange
|
|
267
|
+
const stackIdentifier = {
|
|
268
|
+
stackName: 'acme-integrations-dev',
|
|
269
|
+
region: 'us-east-1',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const orphanedResources = [
|
|
273
|
+
{ physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
274
|
+
{ physicalId: 'subnet-456', resourceType: 'AWS::EC2::Subnet', tags: [] },
|
|
275
|
+
{ physicalId: 'sg-789', resourceType: 'AWS::EC2::SecurityGroup', tags: [] },
|
|
276
|
+
{ physicalId: 'igw-abc', resourceType: 'AWS::EC2::InternetGateway', tags: [] },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const buildTemplate = { resources: {} };
|
|
280
|
+
const deployedTemplate = { resources: {} };
|
|
281
|
+
|
|
282
|
+
const mappings = [
|
|
283
|
+
{
|
|
284
|
+
logicalId: 'FriggVPC',
|
|
285
|
+
physicalId: 'vpc-123',
|
|
286
|
+
resourceType: 'AWS::EC2::VPC',
|
|
287
|
+
matchMethod: 'tag',
|
|
288
|
+
confidence: 'high',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
292
|
+
physicalId: 'subnet-456',
|
|
293
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
294
|
+
matchMethod: 'vpc-usage',
|
|
295
|
+
confidence: 'high',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
logicalId: 'FriggLambdaSecurityGroup',
|
|
299
|
+
physicalId: 'sg-789',
|
|
300
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
301
|
+
matchMethod: 'usage',
|
|
302
|
+
confidence: 'medium',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
logicalId: 'FriggInternetGateway',
|
|
306
|
+
physicalId: 'igw-abc',
|
|
307
|
+
resourceType: 'AWS::EC2::InternetGateway',
|
|
308
|
+
matchMethod: 'tag',
|
|
309
|
+
confidence: 'high',
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
314
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
315
|
+
|
|
316
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
317
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
318
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
319
|
+
|
|
320
|
+
// Act
|
|
321
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
322
|
+
stackIdentifier,
|
|
323
|
+
orphanedResources,
|
|
324
|
+
buildTemplatePath,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Assert
|
|
328
|
+
expect(result.resourcesToImport).toEqual([
|
|
329
|
+
{
|
|
330
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
331
|
+
LogicalResourceId: 'FriggVPC',
|
|
332
|
+
ResourceIdentifier: { VpcId: 'vpc-123' },
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
ResourceType: 'AWS::EC2::Subnet',
|
|
336
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
337
|
+
ResourceIdentifier: { SubnetId: 'subnet-456' },
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
ResourceType: 'AWS::EC2::SecurityGroup',
|
|
341
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
342
|
+
ResourceIdentifier: { GroupId: 'sg-789' },
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
ResourceType: 'AWS::EC2::InternetGateway',
|
|
346
|
+
LogicalResourceId: 'FriggInternetGateway',
|
|
347
|
+
ResourceIdentifier: { InternetGatewayId: 'igw-abc' },
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should separate mapped and unmapped resources', async () => {
|
|
353
|
+
// Arrange
|
|
354
|
+
const stackIdentifier = {
|
|
355
|
+
stackName: 'acme-integrations-dev',
|
|
356
|
+
region: 'us-east-1',
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const orphanedResources = [
|
|
360
|
+
{ physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
361
|
+
{ physicalId: 'vpc-456', resourceType: 'AWS::EC2::VPC', tags: [] }, // No match
|
|
362
|
+
{ physicalId: 'subnet-789', resourceType: 'AWS::EC2::Subnet', tags: [] },
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
const buildTemplate = { resources: {} };
|
|
366
|
+
const deployedTemplate = { resources: {} };
|
|
367
|
+
|
|
368
|
+
const mappings = [
|
|
369
|
+
{
|
|
370
|
+
logicalId: 'FriggVPC',
|
|
371
|
+
physicalId: 'vpc-123',
|
|
372
|
+
resourceType: 'AWS::EC2::VPC',
|
|
373
|
+
matchMethod: 'tag',
|
|
374
|
+
confidence: 'high',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
logicalId: null, // Unmapped
|
|
378
|
+
physicalId: 'vpc-456',
|
|
379
|
+
resourceType: 'AWS::EC2::VPC',
|
|
380
|
+
matchMethod: 'none',
|
|
381
|
+
confidence: 'none',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
385
|
+
physicalId: 'subnet-789',
|
|
386
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
387
|
+
matchMethod: 'vpc-usage',
|
|
388
|
+
confidence: 'high',
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
393
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
394
|
+
|
|
395
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
396
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
397
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
398
|
+
|
|
399
|
+
// Act
|
|
400
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
401
|
+
stackIdentifier,
|
|
402
|
+
orphanedResources,
|
|
403
|
+
buildTemplatePath,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Assert
|
|
407
|
+
expect(result.success).toBe(true);
|
|
408
|
+
expect(result.mappedCount).toBe(2);
|
|
409
|
+
expect(result.unmappedCount).toBe(1);
|
|
410
|
+
expect(result.mappings).toHaveLength(2);
|
|
411
|
+
expect(result.unmappedResources).toHaveLength(1);
|
|
412
|
+
expect(result.unmappedResources[0].physicalId).toBe('vpc-456');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should return failure if no resources could be mapped', async () => {
|
|
416
|
+
// Arrange
|
|
417
|
+
const stackIdentifier = {
|
|
418
|
+
stackName: 'acme-integrations-dev',
|
|
419
|
+
region: 'us-east-1',
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const orphanedResources = [
|
|
423
|
+
{ physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
424
|
+
{ physicalId: 'vpc-456', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
const buildTemplate = { resources: {} };
|
|
428
|
+
const deployedTemplate = { resources: {} };
|
|
429
|
+
|
|
430
|
+
// All resources unmapped
|
|
431
|
+
const mappings = [
|
|
432
|
+
{
|
|
433
|
+
logicalId: null,
|
|
434
|
+
physicalId: 'vpc-123',
|
|
435
|
+
resourceType: 'AWS::EC2::VPC',
|
|
436
|
+
matchMethod: 'none',
|
|
437
|
+
confidence: 'none',
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
logicalId: null,
|
|
441
|
+
physicalId: 'vpc-456',
|
|
442
|
+
resourceType: 'AWS::EC2::VPC',
|
|
443
|
+
matchMethod: 'none',
|
|
444
|
+
confidence: 'none',
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
449
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
450
|
+
|
|
451
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
452
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
453
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
454
|
+
|
|
455
|
+
// Act
|
|
456
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
457
|
+
stackIdentifier,
|
|
458
|
+
orphanedResources,
|
|
459
|
+
buildTemplatePath,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Assert
|
|
463
|
+
expect(result.success).toBe(false);
|
|
464
|
+
expect(result.message).toBe('No resources could be mapped to logical IDs');
|
|
465
|
+
expect(result.unmappedCount).toBe(2);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should generate warnings for multiple resources of same type', async () => {
|
|
469
|
+
// Arrange
|
|
470
|
+
const stackIdentifier = {
|
|
471
|
+
stackName: 'acme-integrations-dev',
|
|
472
|
+
region: 'us-east-1',
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const orphanedResources = [
|
|
476
|
+
{ physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
477
|
+
{ physicalId: 'vpc-456', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
478
|
+
{ physicalId: 'vpc-789', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const buildTemplate = { resources: {} };
|
|
482
|
+
const deployedTemplate = { resources: {} };
|
|
483
|
+
|
|
484
|
+
// Three VPCs mapped
|
|
485
|
+
const mappings = [
|
|
486
|
+
{
|
|
487
|
+
logicalId: 'FriggVPC',
|
|
488
|
+
physicalId: 'vpc-123',
|
|
489
|
+
resourceType: 'AWS::EC2::VPC',
|
|
490
|
+
matchMethod: 'tag',
|
|
491
|
+
confidence: 'high',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
logicalId: 'FriggVPC2',
|
|
495
|
+
physicalId: 'vpc-456',
|
|
496
|
+
resourceType: 'AWS::EC2::VPC',
|
|
497
|
+
matchMethod: 'contained-resources',
|
|
498
|
+
confidence: 'high',
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
logicalId: 'FriggVPC3',
|
|
502
|
+
physicalId: 'vpc-789',
|
|
503
|
+
resourceType: 'AWS::EC2::VPC',
|
|
504
|
+
matchMethod: 'tag',
|
|
505
|
+
confidence: 'high',
|
|
506
|
+
},
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
510
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
511
|
+
|
|
512
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
513
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
514
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
515
|
+
|
|
516
|
+
// Act
|
|
517
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
518
|
+
stackIdentifier,
|
|
519
|
+
orphanedResources,
|
|
520
|
+
buildTemplatePath,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Assert
|
|
524
|
+
expect(result.success).toBe(true);
|
|
525
|
+
expect(result.warnings).toHaveLength(1);
|
|
526
|
+
expect(result.warnings[0].type).toBe('MULTIPLE_RESOURCES');
|
|
527
|
+
expect(result.warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
528
|
+
expect(result.warnings[0].count).toBe(3);
|
|
529
|
+
expect(result.warnings[0].message).toContain('Multiple VPCs detected (3)');
|
|
530
|
+
expect(result.warnings[0].resources).toHaveLength(3);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should include build and deployed template paths in result', async () => {
|
|
534
|
+
// Arrange
|
|
535
|
+
const stackIdentifier = {
|
|
536
|
+
stackName: 'acme-integrations-dev',
|
|
537
|
+
region: 'us-east-1',
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const orphanedResources = [
|
|
541
|
+
{ physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC', tags: [] },
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
const buildTemplate = { resources: { FriggVPC: { Type: 'AWS::EC2::VPC' } } };
|
|
545
|
+
const deployedTemplate = { resources: {} };
|
|
546
|
+
|
|
547
|
+
const mappings = [
|
|
548
|
+
{
|
|
549
|
+
logicalId: 'FriggVPC',
|
|
550
|
+
physicalId: 'vpc-123',
|
|
551
|
+
resourceType: 'AWS::EC2::VPC',
|
|
552
|
+
matchMethod: 'tag',
|
|
553
|
+
confidence: 'high',
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
558
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify(buildTemplate));
|
|
559
|
+
|
|
560
|
+
mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
|
|
561
|
+
mockStackRepository.getTemplate.mockResolvedValue(deployedTemplate);
|
|
562
|
+
mockLogicalIdMapper.mapOrphanedResourcesToLogicalIds.mockResolvedValue(mappings);
|
|
563
|
+
|
|
564
|
+
// Act
|
|
565
|
+
const result = await useCase.importWithLogicalIdMapping({
|
|
566
|
+
stackIdentifier,
|
|
567
|
+
orphanedResources,
|
|
568
|
+
buildTemplatePath,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Assert
|
|
572
|
+
expect(result.buildTemplatePath).toBe(buildTemplatePath);
|
|
573
|
+
expect(result.deployedTemplatePath).toBe('CloudFormation (deployed)');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should throw error if stackRepository not provided', async () => {
|
|
577
|
+
// Arrange
|
|
578
|
+
const useCaseWithoutStackRepo = new RepairViaImportUseCase({
|
|
579
|
+
resourceImporter: mockResourceImporter,
|
|
580
|
+
resourceDetector: mockResourceDetector,
|
|
581
|
+
// stackRepository: NOT PROVIDED
|
|
582
|
+
templateParser: mockTemplateParser,
|
|
583
|
+
logicalIdMapper: mockLogicalIdMapper,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const stackIdentifier = {
|
|
587
|
+
stackName: 'acme-integrations-dev',
|
|
588
|
+
region: 'us-east-1',
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const buildTemplatePath = path.join(tempDir, 'build-template.json');
|
|
592
|
+
fs.writeFileSync(buildTemplatePath, JSON.stringify({ resources: {} }));
|
|
593
|
+
|
|
594
|
+
mockTemplateParser.parseTemplate.mockReturnValue({ resources: {} });
|
|
595
|
+
|
|
596
|
+
// Act & Assert
|
|
597
|
+
await expect(
|
|
598
|
+
useCaseWithoutStackRepo.importWithLogicalIdMapping({
|
|
599
|
+
stackIdentifier,
|
|
600
|
+
orphanedResources: [],
|
|
601
|
+
buildTemplatePath,
|
|
602
|
+
})
|
|
603
|
+
).rejects.toThrow('stackRepository is required for template comparison');
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
describe('_getResourceIdentifier', () => {
|
|
608
|
+
it('should return correct identifier for VPC', () => {
|
|
609
|
+
// Arrange
|
|
610
|
+
const mapping = {
|
|
611
|
+
physicalId: 'vpc-123',
|
|
612
|
+
resourceType: 'AWS::EC2::VPC',
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Act
|
|
616
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
617
|
+
|
|
618
|
+
// Assert
|
|
619
|
+
expect(result).toEqual({ VpcId: 'vpc-123' });
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should return correct identifier for Subnet', () => {
|
|
623
|
+
// Arrange
|
|
624
|
+
const mapping = {
|
|
625
|
+
physicalId: 'subnet-456',
|
|
626
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Act
|
|
630
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
631
|
+
|
|
632
|
+
// Assert
|
|
633
|
+
expect(result).toEqual({ SubnetId: 'subnet-456' });
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should return correct identifier for SecurityGroup', () => {
|
|
637
|
+
// Arrange
|
|
638
|
+
const mapping = {
|
|
639
|
+
physicalId: 'sg-789',
|
|
640
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// Act
|
|
644
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
645
|
+
|
|
646
|
+
// Assert
|
|
647
|
+
expect(result).toEqual({ GroupId: 'sg-789' });
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should return generic identifier for unknown resource type', () => {
|
|
651
|
+
// Arrange
|
|
652
|
+
const mapping = {
|
|
653
|
+
physicalId: 'unknown-123',
|
|
654
|
+
resourceType: 'AWS::Unknown::Type',
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Act
|
|
658
|
+
const result = useCase._getResourceIdentifier(mapping);
|
|
659
|
+
|
|
660
|
+
// Assert
|
|
661
|
+
expect(result).toEqual({ Id: 'unknown-123' });
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('_checkForMultipleResources', () => {
|
|
666
|
+
it('should return no warnings for single resource per type', () => {
|
|
667
|
+
// Arrange
|
|
668
|
+
const mappings = [
|
|
669
|
+
{
|
|
670
|
+
logicalId: 'FriggVPC',
|
|
671
|
+
physicalId: 'vpc-123',
|
|
672
|
+
resourceType: 'AWS::EC2::VPC',
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
676
|
+
physicalId: 'subnet-456',
|
|
677
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
678
|
+
},
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
// Act
|
|
682
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
683
|
+
|
|
684
|
+
// Assert
|
|
685
|
+
expect(warnings).toHaveLength(0);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('should return warning for multiple resources of same type', () => {
|
|
689
|
+
// Arrange
|
|
690
|
+
const mappings = [
|
|
691
|
+
{
|
|
692
|
+
logicalId: 'FriggVPC',
|
|
693
|
+
physicalId: 'vpc-123',
|
|
694
|
+
resourceType: 'AWS::EC2::VPC',
|
|
695
|
+
matchMethod: 'tag',
|
|
696
|
+
confidence: 'high',
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
logicalId: 'FriggVPC2',
|
|
700
|
+
physicalId: 'vpc-456',
|
|
701
|
+
resourceType: 'AWS::EC2::VPC',
|
|
702
|
+
matchMethod: 'contained-resources',
|
|
703
|
+
confidence: 'high',
|
|
704
|
+
},
|
|
705
|
+
];
|
|
706
|
+
|
|
707
|
+
// Act
|
|
708
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
709
|
+
|
|
710
|
+
// Assert
|
|
711
|
+
expect(warnings).toHaveLength(1);
|
|
712
|
+
expect(warnings[0].type).toBe('MULTIPLE_RESOURCES');
|
|
713
|
+
expect(warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
714
|
+
expect(warnings[0].count).toBe(2);
|
|
715
|
+
expect(warnings[0].resources).toHaveLength(2);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should return multiple warnings for different resource types with multiples', () => {
|
|
719
|
+
// Arrange
|
|
720
|
+
const mappings = [
|
|
721
|
+
{
|
|
722
|
+
logicalId: 'FriggVPC',
|
|
723
|
+
physicalId: 'vpc-123',
|
|
724
|
+
resourceType: 'AWS::EC2::VPC',
|
|
725
|
+
matchMethod: 'tag',
|
|
726
|
+
confidence: 'high',
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
logicalId: 'FriggVPC2',
|
|
730
|
+
physicalId: 'vpc-456',
|
|
731
|
+
resourceType: 'AWS::EC2::VPC',
|
|
732
|
+
matchMethod: 'tag',
|
|
733
|
+
confidence: 'high',
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
737
|
+
physicalId: 'subnet-111',
|
|
738
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
739
|
+
matchMethod: 'vpc-usage',
|
|
740
|
+
confidence: 'high',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
logicalId: 'FriggPrivateSubnet2',
|
|
744
|
+
physicalId: 'subnet-222',
|
|
745
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
746
|
+
matchMethod: 'vpc-usage',
|
|
747
|
+
confidence: 'high',
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
// Act
|
|
752
|
+
const warnings = useCase._checkForMultipleResources(mappings);
|
|
753
|
+
|
|
754
|
+
// Assert
|
|
755
|
+
expect(warnings).toHaveLength(2);
|
|
756
|
+
expect(warnings[0].resourceType).toBe('AWS::EC2::VPC');
|
|
757
|
+
expect(warnings[0].count).toBe(2);
|
|
758
|
+
expect(warnings[1].resourceType).toBe('AWS::EC2::Subnet');
|
|
759
|
+
expect(warnings[1].count).toBe(2);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
});
|