@friggframework/devtools 2.0.0--canary.474.86c5119.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.
Files changed (24) hide show
  1. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  2. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +762 -0
  3. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
  4. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -5
  5. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
  6. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  7. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  8. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  9. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  10. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  11. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  12. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  13. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -0
  14. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  15. package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
  16. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
  17. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +330 -0
  18. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  19. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  20. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  21. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  22. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
  23. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
  24. package/package.json +6 -6
@@ -0,0 +1,645 @@
1
+ /**
2
+ * LogicalIdMapper Tests
3
+ *
4
+ * TDD tests for logical ID mapping functionality
5
+ * Domain Layer - Service Tests
6
+ */
7
+
8
+ const { LogicalIdMapper } = require('../logical-id-mapper');
9
+
10
+ describe('LogicalIdMapper', () => {
11
+ let mapper;
12
+ let mockEc2Client;
13
+
14
+ beforeEach(() => {
15
+ // Mock EC2 client
16
+ mockEc2Client = {
17
+ send: jest.fn(),
18
+ };
19
+
20
+ mapper = new LogicalIdMapper({ region: 'us-east-1' });
21
+ mapper.ec2Client = mockEc2Client;
22
+ });
23
+
24
+ describe('mapOrphanedResourcesToLogicalIds', () => {
25
+ it('should map orphaned resources using CloudFormation tags (highest confidence)', async () => {
26
+ // Arrange
27
+ const orphanedResources = [
28
+ {
29
+ physicalId: 'vpc-0eadd96976d29ede7',
30
+ resourceType: 'AWS::EC2::VPC',
31
+ tags: [
32
+ { Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' },
33
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
34
+ ],
35
+ },
36
+ ];
37
+
38
+ const buildTemplate = { resources: {} };
39
+ const deployedTemplate = { resources: {} };
40
+
41
+ // Act
42
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
43
+ orphanedResources,
44
+ buildTemplate,
45
+ deployedTemplate,
46
+ });
47
+
48
+ // Assert
49
+ expect(result).toHaveLength(1);
50
+ expect(result[0]).toEqual({
51
+ logicalId: 'FriggVPC',
52
+ physicalId: 'vpc-0eadd96976d29ede7',
53
+ resourceType: 'AWS::EC2::VPC',
54
+ matchMethod: 'tag',
55
+ confidence: 'high',
56
+ });
57
+ });
58
+
59
+ it('should map VPC by contained resources when no tag found', async () => {
60
+ // Arrange
61
+ const orphanedResources = [
62
+ {
63
+ physicalId: 'vpc-0eadd96976d29ede7',
64
+ resourceType: 'AWS::EC2::VPC',
65
+ tags: [],
66
+ },
67
+ ];
68
+
69
+ const buildTemplate = {
70
+ resources: {
71
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
72
+ MyLambda: {
73
+ Type: 'AWS::Lambda::Function',
74
+ Properties: {
75
+ VpcConfig: {
76
+ SubnetIds: [
77
+ { Ref: 'FriggPrivateSubnet1' },
78
+ { Ref: 'FriggPrivateSubnet2' },
79
+ ],
80
+ },
81
+ },
82
+ },
83
+ },
84
+ };
85
+
86
+ const deployedTemplate = {
87
+ resources: {
88
+ MyLambda: {
89
+ Type: 'AWS::Lambda::Function',
90
+ Properties: {
91
+ VpcConfig: {
92
+ SubnetIds: ['subnet-00ab9e0502e66aac3', 'subnet-00d085a52937aaf91'],
93
+ },
94
+ },
95
+ },
96
+ },
97
+ };
98
+
99
+ // Mock EC2 describe-subnets response
100
+ mockEc2Client.send.mockResolvedValueOnce({
101
+ Subnets: [
102
+ { SubnetId: 'subnet-00ab9e0502e66aac3', VpcId: 'vpc-0eadd96976d29ede7' },
103
+ { SubnetId: 'subnet-00d085a52937aaf91', VpcId: 'vpc-0eadd96976d29ede7' },
104
+ ],
105
+ });
106
+
107
+ // Act
108
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
109
+ orphanedResources,
110
+ buildTemplate,
111
+ deployedTemplate,
112
+ });
113
+
114
+ // Assert
115
+ expect(result).toHaveLength(1);
116
+ expect(result[0]).toEqual({
117
+ logicalId: 'FriggVPC',
118
+ physicalId: 'vpc-0eadd96976d29ede7',
119
+ resourceType: 'AWS::EC2::VPC',
120
+ matchMethod: 'contained-resources',
121
+ confidence: 'high',
122
+ });
123
+ });
124
+
125
+ it('should map subnet by VPC usage in Lambda functions', async () => {
126
+ // Arrange
127
+ const orphanedResources = [
128
+ {
129
+ physicalId: 'subnet-00ab9e0502e66aac3',
130
+ resourceType: 'AWS::EC2::Subnet',
131
+ tags: [],
132
+ },
133
+ ];
134
+
135
+ const buildTemplate = {
136
+ resources: {
137
+ FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
138
+ MyLambda: {
139
+ Type: 'AWS::Lambda::Function',
140
+ Properties: {
141
+ VpcConfig: {
142
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }],
143
+ },
144
+ },
145
+ },
146
+ },
147
+ };
148
+
149
+ const deployedTemplate = {
150
+ resources: {
151
+ MyLambda: {
152
+ Type: 'AWS::Lambda::Function',
153
+ Properties: {
154
+ VpcConfig: {
155
+ SubnetIds: ['subnet-00ab9e0502e66aac3'],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ };
161
+
162
+ // Act
163
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
164
+ orphanedResources,
165
+ buildTemplate,
166
+ deployedTemplate,
167
+ });
168
+
169
+ // Assert
170
+ expect(result).toHaveLength(1);
171
+ expect(result[0]).toEqual({
172
+ logicalId: 'FriggPrivateSubnet1',
173
+ physicalId: 'subnet-00ab9e0502e66aac3',
174
+ resourceType: 'AWS::EC2::Subnet',
175
+ matchMethod: 'vpc-usage',
176
+ confidence: 'high',
177
+ });
178
+ });
179
+
180
+ it('should map security group by usage in Lambda functions', async () => {
181
+ // Arrange
182
+ const orphanedResources = [
183
+ {
184
+ physicalId: 'sg-07c01370e830b6ad6',
185
+ resourceType: 'AWS::EC2::SecurityGroup',
186
+ tags: [],
187
+ },
188
+ ];
189
+
190
+ const buildTemplate = {
191
+ resources: {
192
+ FriggLambdaSecurityGroup: { Type: 'AWS::EC2::SecurityGroup' },
193
+ MyLambda: {
194
+ Type: 'AWS::Lambda::Function',
195
+ Properties: {
196
+ VpcConfig: {
197
+ SecurityGroupIds: [{ Ref: 'FriggLambdaSecurityGroup' }],
198
+ },
199
+ },
200
+ },
201
+ },
202
+ };
203
+
204
+ const deployedTemplate = {
205
+ resources: {
206
+ MyLambda: {
207
+ Type: 'AWS::Lambda::Function',
208
+ Properties: {
209
+ VpcConfig: {
210
+ SecurityGroupIds: ['sg-07c01370e830b6ad6'],
211
+ },
212
+ },
213
+ },
214
+ },
215
+ };
216
+
217
+ // Act
218
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
219
+ orphanedResources,
220
+ buildTemplate,
221
+ deployedTemplate,
222
+ });
223
+
224
+ // Assert
225
+ expect(result).toHaveLength(1);
226
+ expect(result[0]).toEqual({
227
+ logicalId: 'FriggLambdaSecurityGroup',
228
+ physicalId: 'sg-07c01370e830b6ad6',
229
+ resourceType: 'AWS::EC2::SecurityGroup',
230
+ matchMethod: 'usage',
231
+ confidence: 'medium',
232
+ });
233
+ });
234
+
235
+ it('should return null logical ID if no match found', async () => {
236
+ // Arrange
237
+ const orphanedResources = [
238
+ {
239
+ physicalId: 'vpc-unknown',
240
+ resourceType: 'AWS::EC2::VPC',
241
+ tags: [],
242
+ },
243
+ ];
244
+
245
+ const buildTemplate = { resources: {} };
246
+ const deployedTemplate = { resources: {} };
247
+
248
+ // Act
249
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
250
+ orphanedResources,
251
+ buildTemplate,
252
+ deployedTemplate,
253
+ });
254
+
255
+ // Assert
256
+ expect(result).toHaveLength(1);
257
+ expect(result[0]).toEqual({
258
+ logicalId: null,
259
+ physicalId: 'vpc-unknown',
260
+ resourceType: 'AWS::EC2::VPC',
261
+ matchMethod: 'none',
262
+ confidence: 'none',
263
+ });
264
+ });
265
+
266
+ it('should map multiple orphaned resources with different strategies', async () => {
267
+ // Arrange
268
+ const orphanedResources = [
269
+ {
270
+ physicalId: 'vpc-0eadd96976d29ede7',
271
+ resourceType: 'AWS::EC2::VPC',
272
+ tags: [{ Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' }],
273
+ },
274
+ {
275
+ physicalId: 'subnet-00ab9e0502e66aac3',
276
+ resourceType: 'AWS::EC2::Subnet',
277
+ tags: [],
278
+ },
279
+ ];
280
+
281
+ const buildTemplate = {
282
+ resources: {
283
+ FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
284
+ MyLambda: {
285
+ Type: 'AWS::Lambda::Function',
286
+ Properties: {
287
+ VpcConfig: {
288
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }],
289
+ },
290
+ },
291
+ },
292
+ },
293
+ };
294
+
295
+ const deployedTemplate = {
296
+ resources: {
297
+ MyLambda: {
298
+ Type: 'AWS::Lambda::Function',
299
+ Properties: {
300
+ VpcConfig: {
301
+ SubnetIds: ['subnet-00ab9e0502e66aac3'],
302
+ },
303
+ },
304
+ },
305
+ },
306
+ };
307
+
308
+ // Act
309
+ const result = await mapper.mapOrphanedResourcesToLogicalIds({
310
+ orphanedResources,
311
+ buildTemplate,
312
+ deployedTemplate,
313
+ });
314
+
315
+ // Assert
316
+ expect(result).toHaveLength(2);
317
+ expect(result[0].matchMethod).toBe('tag');
318
+ expect(result[0].confidence).toBe('high');
319
+ expect(result[1].matchMethod).toBe('vpc-usage');
320
+ expect(result[1].confidence).toBe('high');
321
+ });
322
+ });
323
+
324
+ describe('_getLogicalIdFromTags', () => {
325
+ it('should extract logical ID from CloudFormation tags', () => {
326
+ // Arrange
327
+ const tags = [
328
+ { Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' },
329
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
330
+ { Key: 'Name', Value: 'My VPC' },
331
+ ];
332
+
333
+ // Act
334
+ const result = mapper._getLogicalIdFromTags(tags);
335
+
336
+ // Assert
337
+ expect(result).toBe('FriggVPC');
338
+ });
339
+
340
+ it('should return null if logical-id tag not found', () => {
341
+ // Arrange
342
+ const tags = [
343
+ { Key: 'Name', Value: 'My VPC' },
344
+ { Key: 'Environment', Value: 'dev' },
345
+ ];
346
+
347
+ // Act
348
+ const result = mapper._getLogicalIdFromTags(tags);
349
+
350
+ // Assert
351
+ expect(result).toBeNull();
352
+ });
353
+
354
+ it('should return null if tags is null or undefined', () => {
355
+ // Act
356
+ const resultNull = mapper._getLogicalIdFromTags(null);
357
+ const resultUndefined = mapper._getLogicalIdFromTags(undefined);
358
+
359
+ // Assert
360
+ expect(resultNull).toBeNull();
361
+ expect(resultUndefined).toBeNull();
362
+ });
363
+
364
+ it('should return null if tags is not an array', () => {
365
+ // Act
366
+ const result = mapper._getLogicalIdFromTags('not-an-array');
367
+
368
+ // Assert
369
+ expect(result).toBeNull();
370
+ });
371
+ });
372
+
373
+ describe('_matchVpcByContainedResources', () => {
374
+ it('should match VPC that contains all expected subnets', async () => {
375
+ // Arrange
376
+ const vpc = {
377
+ physicalId: 'vpc-0eadd96976d29ede7',
378
+ resourceType: 'AWS::EC2::VPC',
379
+ };
380
+
381
+ const buildTemplate = {
382
+ resources: {
383
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
384
+ },
385
+ };
386
+
387
+ const deployedTemplate = {
388
+ resources: {
389
+ MyLambda: {
390
+ Type: 'AWS::Lambda::Function',
391
+ Properties: {
392
+ VpcConfig: {
393
+ SubnetIds: ['subnet-111', 'subnet-222'],
394
+ },
395
+ },
396
+ },
397
+ },
398
+ };
399
+
400
+ // Mock EC2 describe-subnets response
401
+ mockEc2Client.send.mockResolvedValueOnce({
402
+ Subnets: [
403
+ { SubnetId: 'subnet-111', VpcId: 'vpc-0eadd96976d29ede7' },
404
+ { SubnetId: 'subnet-222', VpcId: 'vpc-0eadd96976d29ede7' },
405
+ { SubnetId: 'subnet-333', VpcId: 'vpc-0eadd96976d29ede7' },
406
+ ],
407
+ });
408
+
409
+ // Act
410
+ const result = await mapper._matchVpcByContainedResources(
411
+ vpc,
412
+ buildTemplate,
413
+ deployedTemplate
414
+ );
415
+
416
+ // Assert
417
+ expect(result).toBe('FriggVPC');
418
+ });
419
+
420
+ it('should return null if VPC does not contain expected subnets', async () => {
421
+ // Arrange
422
+ const vpc = {
423
+ physicalId: 'vpc-wrong',
424
+ resourceType: 'AWS::EC2::VPC',
425
+ };
426
+
427
+ const buildTemplate = {
428
+ resources: {
429
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
430
+ },
431
+ };
432
+
433
+ const deployedTemplate = {
434
+ resources: {
435
+ MyLambda: {
436
+ Type: 'AWS::Lambda::Function',
437
+ Properties: {
438
+ VpcConfig: {
439
+ SubnetIds: ['subnet-111', 'subnet-222'],
440
+ },
441
+ },
442
+ },
443
+ },
444
+ };
445
+
446
+ // Mock EC2 describe-subnets response - VPC has different subnets
447
+ mockEc2Client.send.mockResolvedValueOnce({
448
+ Subnets: [
449
+ { SubnetId: 'subnet-999', VpcId: 'vpc-wrong' },
450
+ { SubnetId: 'subnet-888', VpcId: 'vpc-wrong' },
451
+ ],
452
+ });
453
+
454
+ // Act
455
+ const result = await mapper._matchVpcByContainedResources(
456
+ vpc,
457
+ buildTemplate,
458
+ deployedTemplate
459
+ );
460
+
461
+ // Assert
462
+ expect(result).toBeNull();
463
+ });
464
+
465
+ it('should return null if no expected subnets in deployed template', async () => {
466
+ // Arrange
467
+ const vpc = {
468
+ physicalId: 'vpc-0eadd96976d29ede7',
469
+ resourceType: 'AWS::EC2::VPC',
470
+ };
471
+
472
+ const buildTemplate = { resources: {} };
473
+ const deployedTemplate = { resources: {} };
474
+
475
+ // Act
476
+ const result = await mapper._matchVpcByContainedResources(
477
+ vpc,
478
+ buildTemplate,
479
+ deployedTemplate
480
+ );
481
+
482
+ // Assert
483
+ expect(result).toBeNull();
484
+ });
485
+ });
486
+
487
+ describe('_extractSubnetIdsFromTemplate', () => {
488
+ it('should extract subnet IDs from Lambda VpcConfig', () => {
489
+ // Arrange
490
+ const template = {
491
+ resources: {
492
+ Lambda1: {
493
+ Type: 'AWS::Lambda::Function',
494
+ Properties: {
495
+ VpcConfig: {
496
+ SubnetIds: ['subnet-111', 'subnet-222'],
497
+ },
498
+ },
499
+ },
500
+ Lambda2: {
501
+ Type: 'AWS::Lambda::Function',
502
+ Properties: {
503
+ VpcConfig: {
504
+ SubnetIds: ['subnet-333'],
505
+ },
506
+ },
507
+ },
508
+ },
509
+ };
510
+
511
+ // Act
512
+ const result = mapper._extractSubnetIdsFromTemplate(template);
513
+
514
+ // Assert
515
+ expect(result).toEqual(['subnet-111', 'subnet-222', 'subnet-333']);
516
+ });
517
+
518
+ it('should deduplicate subnet IDs', () => {
519
+ // Arrange
520
+ const template = {
521
+ resources: {
522
+ Lambda1: {
523
+ Type: 'AWS::Lambda::Function',
524
+ Properties: {
525
+ VpcConfig: {
526
+ SubnetIds: ['subnet-111', 'subnet-222'],
527
+ },
528
+ },
529
+ },
530
+ Lambda2: {
531
+ Type: 'AWS::Lambda::Function',
532
+ Properties: {
533
+ VpcConfig: {
534
+ SubnetIds: ['subnet-111', 'subnet-333'],
535
+ },
536
+ },
537
+ },
538
+ },
539
+ };
540
+
541
+ // Act
542
+ const result = mapper._extractSubnetIdsFromTemplate(template);
543
+
544
+ // Assert
545
+ expect(result).toEqual(['subnet-111', 'subnet-222', 'subnet-333']);
546
+ });
547
+
548
+ it('should return empty array if no Lambda VPC configs', () => {
549
+ // Arrange
550
+ const template = {
551
+ resources: {
552
+ MyQueue: { Type: 'AWS::SQS::Queue' },
553
+ },
554
+ };
555
+
556
+ // Act
557
+ const result = mapper._extractSubnetIdsFromTemplate(template);
558
+
559
+ // Assert
560
+ expect(result).toEqual([]);
561
+ });
562
+ });
563
+
564
+ describe('_extractSubnetRefsFromTemplate', () => {
565
+ it('should extract subnet Refs from build template', () => {
566
+ // Arrange
567
+ const template = {
568
+ resources: {
569
+ MyLambda: {
570
+ Type: 'AWS::Lambda::Function',
571
+ Properties: {
572
+ VpcConfig: {
573
+ SubnetIds: [
574
+ { Ref: 'FriggPrivateSubnet1' },
575
+ { Ref: 'FriggPrivateSubnet2' },
576
+ ],
577
+ },
578
+ },
579
+ },
580
+ },
581
+ };
582
+
583
+ // Act
584
+ const result = mapper._extractSubnetRefsFromTemplate(template);
585
+
586
+ // Assert
587
+ expect(result).toEqual(['FriggPrivateSubnet1', 'FriggPrivateSubnet2']);
588
+ });
589
+
590
+ it('should return empty array if no Refs found', () => {
591
+ // Arrange
592
+ const template = {
593
+ resources: {
594
+ MyLambda: {
595
+ Type: 'AWS::Lambda::Function',
596
+ Properties: {
597
+ VpcConfig: {
598
+ SubnetIds: ['subnet-111'],
599
+ },
600
+ },
601
+ },
602
+ },
603
+ };
604
+
605
+ // Act
606
+ const result = mapper._extractSubnetRefsFromTemplate(template);
607
+
608
+ // Assert
609
+ expect(result).toEqual([]);
610
+ });
611
+ });
612
+
613
+ describe('_findVpcLogicalIdInTemplate', () => {
614
+ it('should find VPC logical ID in build template', () => {
615
+ // Arrange
616
+ const template = {
617
+ resources: {
618
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
619
+ MySubnet: { Type: 'AWS::EC2::Subnet' },
620
+ },
621
+ };
622
+
623
+ // Act
624
+ const result = mapper._findVpcLogicalIdInTemplate(template);
625
+
626
+ // Assert
627
+ expect(result).toBe('FriggVPC');
628
+ });
629
+
630
+ it('should return null if no VPC in template', () => {
631
+ // Arrange
632
+ const template = {
633
+ resources: {
634
+ MyLambda: { Type: 'AWS::Lambda::Function' },
635
+ },
636
+ };
637
+
638
+ // Act
639
+ const result = mapper._findVpcLogicalIdInTemplate(template);
640
+
641
+ // Assert
642
+ expect(result).toBeNull();
643
+ });
644
+ });
645
+ });