@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,496 @@
1
+ /**
2
+ * TemplateParser Tests
3
+ *
4
+ * TDD tests for CloudFormation template parsing functionality
5
+ * Domain Layer - Service Tests
6
+ */
7
+
8
+ const { TemplateParser } = require('../template-parser');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ describe('TemplateParser', () => {
13
+ let parser;
14
+
15
+ beforeEach(() => {
16
+ parser = new TemplateParser();
17
+ });
18
+
19
+ describe('parseTemplate', () => {
20
+ it('should parse template from file path', () => {
21
+ // Arrange
22
+ const mockTemplate = {
23
+ AWSTemplateFormatVersion: '2010-09-09',
24
+ Description: 'Test template',
25
+ Resources: {
26
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
27
+ },
28
+ };
29
+
30
+ const tempFile = path.join(__dirname, 'temp-template.json');
31
+ fs.writeFileSync(tempFile, JSON.stringify(mockTemplate));
32
+
33
+ // Act
34
+ const result = parser.parseTemplate(tempFile);
35
+
36
+ // Assert
37
+ expect(result.resources).toEqual(mockTemplate.Resources);
38
+ expect(result.version).toBe('2010-09-09');
39
+ expect(result.description).toBe('Test template');
40
+
41
+ // Cleanup
42
+ fs.unlinkSync(tempFile);
43
+ });
44
+
45
+ it('should parse template from object', () => {
46
+ // Arrange
47
+ const mockTemplate = {
48
+ AWSTemplateFormatVersion: '2010-09-09',
49
+ Resources: {
50
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
51
+ },
52
+ };
53
+
54
+ // Act
55
+ const result = parser.parseTemplate(mockTemplate);
56
+
57
+ // Assert
58
+ expect(result.resources).toEqual(mockTemplate.Resources);
59
+ expect(result.version).toBe('2010-09-09');
60
+ });
61
+
62
+ it('should throw error if template file not found', () => {
63
+ // Arrange
64
+ const nonExistentPath = '/path/to/nonexistent/template.json';
65
+
66
+ // Act & Assert
67
+ expect(() => parser.parseTemplate(nonExistentPath)).toThrow(
68
+ 'Template not found at path'
69
+ );
70
+ });
71
+
72
+ it('should return empty resources if Resources key missing', () => {
73
+ // Arrange
74
+ const mockTemplate = {
75
+ AWSTemplateFormatVersion: '2010-09-09',
76
+ };
77
+
78
+ // Act
79
+ const result = parser.parseTemplate(mockTemplate);
80
+
81
+ // Assert
82
+ expect(result.resources).toEqual({});
83
+ });
84
+ });
85
+
86
+ describe('getVpcResources', () => {
87
+ it('should extract VPC resources from template', () => {
88
+ // Arrange
89
+ const template = {
90
+ resources: {
91
+ FriggVPC: {
92
+ Type: 'AWS::EC2::VPC',
93
+ Properties: { CidrBlock: '10.0.0.0/16' },
94
+ },
95
+ FriggPrivateSubnet1: {
96
+ Type: 'AWS::EC2::Subnet',
97
+ Properties: { CidrBlock: '10.0.0.0/24' },
98
+ },
99
+ FriggLambdaSecurityGroup: {
100
+ Type: 'AWS::EC2::SecurityGroup',
101
+ Properties: { GroupDescription: 'Lambda SG' },
102
+ },
103
+ SomeOtherResource: {
104
+ Type: 'AWS::Lambda::Function',
105
+ Properties: {},
106
+ },
107
+ },
108
+ };
109
+
110
+ // Act
111
+ const result = parser.getVpcResources(template);
112
+
113
+ // Assert
114
+ expect(result).toHaveLength(3);
115
+ expect(result[0]).toEqual({
116
+ logicalId: 'FriggVPC',
117
+ resourceType: 'AWS::EC2::VPC',
118
+ properties: { CidrBlock: '10.0.0.0/16' },
119
+ });
120
+ expect(result[1]).toEqual({
121
+ logicalId: 'FriggPrivateSubnet1',
122
+ resourceType: 'AWS::EC2::Subnet',
123
+ properties: { CidrBlock: '10.0.0.0/24' },
124
+ });
125
+ expect(result[2]).toEqual({
126
+ logicalId: 'FriggLambdaSecurityGroup',
127
+ resourceType: 'AWS::EC2::SecurityGroup',
128
+ properties: { GroupDescription: 'Lambda SG' },
129
+ });
130
+ });
131
+
132
+ it('should return empty array if no VPC resources', () => {
133
+ // Arrange
134
+ const template = {
135
+ resources: {
136
+ MyLambda: { Type: 'AWS::Lambda::Function' },
137
+ },
138
+ };
139
+
140
+ // Act
141
+ const result = parser.getVpcResources(template);
142
+
143
+ // Assert
144
+ expect(result).toEqual([]);
145
+ });
146
+
147
+ it('should include all VPC-related resource types', () => {
148
+ // Arrange
149
+ const template = {
150
+ resources: {
151
+ MyVPC: { Type: 'AWS::EC2::VPC', Properties: {} },
152
+ MySubnet: { Type: 'AWS::EC2::Subnet', Properties: {} },
153
+ MySG: { Type: 'AWS::EC2::SecurityGroup', Properties: {} },
154
+ MyIGW: { Type: 'AWS::EC2::InternetGateway', Properties: {} },
155
+ MyNAT: { Type: 'AWS::EC2::NatGateway', Properties: {} },
156
+ MyRT: { Type: 'AWS::EC2::RouteTable', Properties: {} },
157
+ MyEndpoint: { Type: 'AWS::EC2::VPCEndpoint', Properties: {} },
158
+ },
159
+ };
160
+
161
+ // Act
162
+ const result = parser.getVpcResources(template);
163
+
164
+ // Assert
165
+ expect(result).toHaveLength(7);
166
+ });
167
+ });
168
+
169
+ describe('extractHardcodedIds', () => {
170
+ it('should extract hardcoded VPC IDs from deployed template', () => {
171
+ // Arrange
172
+ const template = {
173
+ resources: {
174
+ MyLambda: {
175
+ Type: 'AWS::Lambda::Function',
176
+ Properties: {
177
+ VpcConfig: {
178
+ VpcId: 'vpc-0eadd96976d29ede7',
179
+ SubnetIds: ['subnet-00ab9e0502e66aac3', 'subnet-00d085a52937aaf91'],
180
+ SecurityGroupIds: ['sg-07c01370e830b6ad6'],
181
+ },
182
+ },
183
+ },
184
+ },
185
+ };
186
+
187
+ // Act
188
+ const result = parser.extractHardcodedIds(template);
189
+
190
+ // Assert
191
+ expect(result.vpcIds).toEqual(['vpc-0eadd96976d29ede7']);
192
+ expect(result.subnetIds).toEqual([
193
+ 'subnet-00ab9e0502e66aac3',
194
+ 'subnet-00d085a52937aaf91',
195
+ ]);
196
+ expect(result.securityGroupIds).toEqual(['sg-07c01370e830b6ad6']);
197
+ });
198
+
199
+ it('should handle nested VPC configurations', () => {
200
+ // Arrange
201
+ const template = {
202
+ resources: {
203
+ Lambda1: {
204
+ Type: 'AWS::Lambda::Function',
205
+ Properties: {
206
+ VpcConfig: {
207
+ SubnetIds: ['subnet-111', 'subnet-222'],
208
+ },
209
+ },
210
+ },
211
+ Lambda2: {
212
+ Type: 'AWS::Lambda::Function',
213
+ Properties: {
214
+ VpcConfig: {
215
+ SubnetIds: ['subnet-333'],
216
+ SecurityGroupIds: ['sg-444'],
217
+ },
218
+ },
219
+ },
220
+ },
221
+ };
222
+
223
+ // Act
224
+ const result = parser.extractHardcodedIds(template);
225
+
226
+ // Assert
227
+ expect(result.subnetIds).toEqual(['subnet-111', 'subnet-222', 'subnet-333']);
228
+ expect(result.securityGroupIds).toEqual(['sg-444']);
229
+ });
230
+
231
+ it('should return empty arrays if no hardcoded IDs found', () => {
232
+ // Arrange
233
+ const template = {
234
+ resources: {
235
+ MyLambda: {
236
+ Type: 'AWS::Lambda::Function',
237
+ Properties: {},
238
+ },
239
+ },
240
+ };
241
+
242
+ // Act
243
+ const result = parser.extractHardcodedIds(template);
244
+
245
+ // Assert
246
+ expect(result.vpcIds).toEqual([]);
247
+ expect(result.subnetIds).toEqual([]);
248
+ expect(result.securityGroupIds).toEqual([]);
249
+ });
250
+
251
+ it('should deduplicate hardcoded IDs', () => {
252
+ // Arrange
253
+ const template = {
254
+ resources: {
255
+ Lambda1: {
256
+ Type: 'AWS::Lambda::Function',
257
+ Properties: {
258
+ VpcConfig: {
259
+ SubnetIds: ['subnet-111', 'subnet-222'],
260
+ },
261
+ },
262
+ },
263
+ Lambda2: {
264
+ Type: 'AWS::Lambda::Function',
265
+ Properties: {
266
+ VpcConfig: {
267
+ SubnetIds: ['subnet-111', 'subnet-333'],
268
+ },
269
+ },
270
+ },
271
+ },
272
+ };
273
+
274
+ // Act
275
+ const result = parser.extractHardcodedIds(template);
276
+
277
+ // Assert
278
+ expect(result.subnetIds).toEqual(['subnet-111', 'subnet-222', 'subnet-333']);
279
+ });
280
+ });
281
+
282
+ describe('extractRefs', () => {
283
+ it('should extract Refs from build template', () => {
284
+ // Arrange
285
+ const template = {
286
+ resources: {
287
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
288
+ FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
289
+ MyLambda: {
290
+ Type: 'AWS::Lambda::Function',
291
+ Properties: {
292
+ VpcConfig: {
293
+ SubnetIds: [
294
+ { Ref: 'FriggPrivateSubnet1' },
295
+ { Ref: 'FriggPrivateSubnet2' },
296
+ ],
297
+ SecurityGroupIds: [{ Ref: 'FriggLambdaSecurityGroup' }],
298
+ },
299
+ },
300
+ },
301
+ },
302
+ };
303
+
304
+ // Act
305
+ const result = parser.extractRefs(template);
306
+
307
+ // Assert
308
+ expect(result.subnetRefs).toEqual([
309
+ 'FriggPrivateSubnet1',
310
+ 'FriggPrivateSubnet2',
311
+ ]);
312
+ expect(result.securityGroupRefs).toEqual(['FriggLambdaSecurityGroup']);
313
+ });
314
+
315
+ it('should identify VPC Refs correctly', () => {
316
+ // Arrange
317
+ const template = {
318
+ resources: {
319
+ MySubnet: {
320
+ Type: 'AWS::EC2::Subnet',
321
+ Properties: {
322
+ VpcId: { Ref: 'FriggVPC' },
323
+ },
324
+ },
325
+ },
326
+ };
327
+
328
+ // Act
329
+ const result = parser.extractRefs(template);
330
+
331
+ // Assert
332
+ expect(result.vpcRefs).toEqual(['FriggVPC']);
333
+ });
334
+
335
+ it('should not confuse VPCEndpoint with VPC refs', () => {
336
+ // Arrange
337
+ const template = {
338
+ resources: {
339
+ MyEndpoint: {
340
+ Type: 'AWS::EC2::VPCEndpoint',
341
+ Properties: {
342
+ VpcId: { Ref: 'FriggVPCEndpoint' },
343
+ },
344
+ },
345
+ },
346
+ };
347
+
348
+ // Act
349
+ const result = parser.extractRefs(template);
350
+
351
+ // Assert
352
+ expect(result.vpcRefs).toEqual([]); // VPCEndpoint should be excluded
353
+ });
354
+
355
+ it('should return empty arrays if no Refs found', () => {
356
+ // Arrange
357
+ const template = {
358
+ resources: {
359
+ MyLambda: {
360
+ Type: 'AWS::Lambda::Function',
361
+ Properties: {
362
+ VpcConfig: {
363
+ SubnetIds: ['subnet-111'],
364
+ },
365
+ },
366
+ },
367
+ },
368
+ };
369
+
370
+ // Act
371
+ const result = parser.extractRefs(template);
372
+
373
+ // Assert
374
+ expect(result.vpcRefs).toEqual([]);
375
+ expect(result.subnetRefs).toEqual([]);
376
+ expect(result.securityGroupRefs).toEqual([]);
377
+ });
378
+ });
379
+
380
+ describe('findLogicalIdForPhysicalId', () => {
381
+ it('should match physical ID to logical ID via template comparison', () => {
382
+ // Arrange
383
+ const deployedTemplate = {
384
+ resources: {
385
+ MyLambda: {
386
+ Type: 'AWS::Lambda::Function',
387
+ Properties: {
388
+ VpcConfig: {
389
+ SubnetIds: ['subnet-00ab9e0502e66aac3'],
390
+ },
391
+ },
392
+ },
393
+ },
394
+ };
395
+
396
+ const buildTemplate = {
397
+ resources: {
398
+ FriggPrivateSubnet1: { Type: 'AWS::EC2::Subnet' },
399
+ MyLambda: {
400
+ Type: 'AWS::Lambda::Function',
401
+ Properties: {
402
+ VpcConfig: {
403
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }],
404
+ },
405
+ },
406
+ },
407
+ },
408
+ };
409
+
410
+ // Act
411
+ const result = parser.findLogicalIdForPhysicalId(
412
+ 'subnet-00ab9e0502e66aac3',
413
+ deployedTemplate,
414
+ buildTemplate
415
+ );
416
+
417
+ // Assert
418
+ expect(result).toBe('FriggPrivateSubnet1');
419
+ });
420
+
421
+ it('should return null if no match found', () => {
422
+ // Arrange
423
+ const deployedTemplate = { resources: {} };
424
+ const buildTemplate = { resources: {} };
425
+
426
+ // Act
427
+ const result = parser.findLogicalIdForPhysicalId(
428
+ 'subnet-unknown',
429
+ deployedTemplate,
430
+ buildTemplate
431
+ );
432
+
433
+ // Assert
434
+ expect(result).toBeNull();
435
+ });
436
+ });
437
+
438
+ describe('static methods', () => {
439
+ describe('getBuildTemplatePath', () => {
440
+ it('should return correct path to build template', () => {
441
+ // Act
442
+ const result = TemplateParser.getBuildTemplatePath('/project/root');
443
+
444
+ // Assert
445
+ expect(result).toBe(
446
+ '/project/root/.serverless/cloudformation-template-update-stack.json'
447
+ );
448
+ });
449
+
450
+ it('should use current directory if no path provided', () => {
451
+ // Act
452
+ const result = TemplateParser.getBuildTemplatePath();
453
+
454
+ // Assert
455
+ expect(result).toContain('.serverless/cloudformation-template-update-stack.json');
456
+ });
457
+ });
458
+
459
+ describe('buildTemplateExists', () => {
460
+ it('should return true if build template exists', () => {
461
+ // Arrange
462
+ const tempDir = path.join(__dirname, 'temp-project');
463
+ const serverlessDir = path.join(tempDir, '.serverless');
464
+ const templatePath = path.join(
465
+ serverlessDir,
466
+ 'cloudformation-template-update-stack.json'
467
+ );
468
+
469
+ fs.mkdirSync(serverlessDir, { recursive: true });
470
+ fs.writeFileSync(templatePath, '{}');
471
+
472
+ // Act
473
+ const result = TemplateParser.buildTemplateExists(tempDir);
474
+
475
+ // Assert
476
+ expect(result).toBe(true);
477
+
478
+ // Cleanup
479
+ fs.unlinkSync(templatePath);
480
+ fs.rmdirSync(serverlessDir);
481
+ fs.rmdirSync(tempDir);
482
+ });
483
+
484
+ it('should return false if build template does not exist', () => {
485
+ // Arrange
486
+ const tempDir = path.join(__dirname, 'nonexistent-project');
487
+
488
+ // Act
489
+ const result = TemplateParser.buildTemplateExists(tempDir);
490
+
491
+ // Assert
492
+ expect(result).toBe(false);
493
+ });
494
+ });
495
+ });
496
+ });