@friggframework/devtools 2.0.0--canary.490.3cb2625.0 → 2.0.0--canary.496.a94ee46.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 (26) hide show
  1. package/frigg-cli/deploy-command/index.js +3 -9
  2. package/infrastructure/README.md +0 -28
  3. package/infrastructure/domains/database/migration-builder.js +13 -19
  4. package/infrastructure/domains/database/migration-builder.test.js +0 -57
  5. package/infrastructure/domains/integration/integration-builder.js +14 -19
  6. package/infrastructure/domains/integration/integration-builder.test.js +74 -0
  7. package/infrastructure/domains/networking/vpc-builder.js +18 -238
  8. package/infrastructure/domains/networking/vpc-builder.test.js +13 -682
  9. package/infrastructure/domains/networking/vpc-resolver.js +40 -221
  10. package/infrastructure/domains/networking/vpc-resolver.test.js +18 -318
  11. package/infrastructure/domains/security/kms-builder.js +6 -55
  12. package/infrastructure/domains/security/kms-builder.test.js +1 -19
  13. package/infrastructure/domains/shared/cloudformation-discovery.js +13 -310
  14. package/infrastructure/domains/shared/cloudformation-discovery.test.js +0 -395
  15. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +6 -41
  16. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +0 -39
  17. package/infrastructure/domains/shared/resource-discovery.js +5 -17
  18. package/infrastructure/domains/shared/resource-discovery.test.js +0 -36
  19. package/infrastructure/domains/shared/utilities/base-definition-factory.js +17 -27
  20. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +0 -73
  21. package/infrastructure/infrastructure-composer.js +3 -11
  22. package/infrastructure/scripts/build-prisma-layer.js +81 -8
  23. package/infrastructure/scripts/build-prisma-layer.test.js +53 -1
  24. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  25. package/package.json +7 -7
  26. package/layers/prisma/.build-complete +0 -3
@@ -398,93 +398,19 @@ describe('VpcBuilder', () => {
398
398
  expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
399
399
  });
400
400
 
401
- it('should add stack-managed security group back to template to prevent deletion', async () => {
401
+ it('should reuse stack-managed VPC endpoints without creating CloudFormation resources', async () => {
402
402
  const appDefinition = {
403
- vpc: { enable: true },
404
- };
405
-
406
- const discoveredResources = {
407
- fromCloudFormationStack: true,
408
- stackName: 'test-stack',
409
- existingLogicalIds: ['FriggLambdaSecurityGroup'],
410
- defaultVpcId: 'vpc-123',
411
- privateSubnetId1: 'subnet-1',
412
- privateSubnetId2: 'subnet-2',
413
- lambdaSecurityGroupId: 'sg-existing-stack', // Existing in stack
414
- };
415
-
416
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
417
-
418
- // CRITICAL: Must RE-ADD stack-managed SG to template or CloudFormation will DELETE it
419
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
420
- expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
421
- expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-123');
422
- expect(result.resources.FriggLambdaSecurityGroup.Properties.GroupDescription).toBeDefined();
423
-
424
- // Should use Ref in Lambda config (not recreating)
425
- expect(result.vpcConfig.securityGroupIds).toContainEqual({ Ref: 'FriggLambdaSecurityGroup' });
426
- });
427
-
428
- it('should add stack-managed subnets back to template to prevent deletion', async () => {
429
- const appDefinition = {
430
- vpc: { enable: true },
431
- };
432
-
433
- const discoveredResources = {
434
- fromCloudFormationStack: true,
435
- stackName: 'test-stack',
436
- existingLogicalIds: ['FriggPrivateSubnet1', 'FriggPrivateSubnet2'],
437
- defaultVpcId: 'vpc-123',
438
- // Subnets exist in stack with specific IDs
439
- privateSubnetId1: 'subnet-existing-1',
440
- privateSubnetId2: 'subnet-existing-2',
441
- };
442
-
443
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
444
-
445
- // CRITICAL: Must RE-ADD stack-managed subnets to template or CloudFormation will DELETE them
446
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
447
- expect(result.resources.FriggPrivateSubnet1.Type).toBe('AWS::EC2::Subnet');
448
- expect(result.resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-123');
449
-
450
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
451
- expect(result.resources.FriggPrivateSubnet2.Type).toBe('AWS::EC2::Subnet');
452
- expect(result.resources.FriggPrivateSubnet2.Properties.VpcId).toBe('vpc-123');
453
-
454
- // Should use Refs (not external IDs)
455
- expect(result.vpcConfig.subnetIds).toEqual([
456
- { Ref: 'FriggPrivateSubnet1' },
457
- { Ref: 'FriggPrivateSubnet2' }
458
- ]);
459
- });
460
-
461
- it('should add stack-managed VPC endpoints back to template to prevent deletion', async () => {
462
- const appDefinition = {
463
- vpc: { enable: true },
403
+ vpc: { enable: true, enableVPCEndpoints: true },
464
404
  encryption: { fieldLevelEncryptionMethod: 'kms' },
405
+ database: { postgres: { enable: true } },
465
406
  };
466
-
467
- // Structured discovery from CloudFormation
468
407
  const discoveredResources = {
469
- fromCloudFormationStack: true,
470
- stackName: 'test-stack',
471
- existingLogicalIds: [
472
- 'FriggLambdaSecurityGroup',
473
- 'FriggLambdaRouteTable',
474
- 'FriggS3VPCEndpoint',
475
- 'FriggDynamoDBVPCEndpoint',
476
- 'FriggKMSVPCEndpoint',
477
- 'FriggSecretsManagerVPCEndpoint',
478
- 'FriggSQSVPCEndpoint'
479
- ],
480
408
  defaultVpcId: 'vpc-123',
481
409
  privateSubnetId1: 'subnet-1',
482
410
  privateSubnetId2: 'subnet-2',
483
- routeTableId: 'rtb-123',
484
- lambdaSecurityGroupId: 'sg-123',
485
- // VPC endpoints discovered in stack
411
+ // VPC endpoints from CloudFormation stack (string IDs)
486
412
  s3VpcEndpointId: 'vpce-s3-stack',
487
- dynamodbVpcEndpointId: 'vpce-ddb-stack',
413
+ dynamoDbVpcEndpointId: 'vpce-ddb-stack',
488
414
  kmsVpcEndpointId: 'vpce-kms-stack',
489
415
  secretsManagerVpcEndpointId: 'vpce-sm-stack',
490
416
  sqsVpcEndpointId: 'vpce-sqs-stack',
@@ -492,30 +418,15 @@ describe('VpcBuilder', () => {
492
418
 
493
419
  const result = await vpcBuilder.build(appDefinition, discoveredResources);
494
420
 
495
- // CRITICAL: Must RE-ADD stack-managed endpoints to template or CloudFormation will DELETE them
496
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
497
- expect(result.resources.FriggS3VPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
498
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
499
-
500
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
501
- expect(result.resources.FriggDynamoDBVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
502
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
503
-
504
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
505
- expect(result.resources.FriggKMSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
506
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
507
-
508
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
509
- expect(result.resources.FriggSecretsManagerVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
510
- expect(result.resources.FriggSecretsManagerVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
511
-
512
- expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
513
- expect(result.resources.FriggSQSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
514
- expect(result.resources.FriggSQSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
421
+ // Should NOT create CloudFormation resources (reuse stack endpoints)
422
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
423
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined();
424
+ expect(result.resources.FriggKMSVPCEndpoint).toBeUndefined();
425
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeUndefined();
426
+ expect(result.resources.FriggSQSVPCEndpoint).toBeUndefined();
515
427
 
516
- // Should create VPC Endpoint Security Group for interface endpoints
517
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
518
- expect(result.resources.FriggVPCEndpointSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
428
+ // Should still NOT create VPC Endpoint Security Group
429
+ expect(result.resources.FriggVPCEndpointSecurityGroup).toBeUndefined();
519
430
  });
520
431
 
521
432
  it('should create VPC endpoints when discovered from AWS but not stack', async () => {
@@ -1347,585 +1258,5 @@ describe('VpcBuilder', () => {
1347
1258
  expect(result.outputs.PrivateSubnet2Id).toBeDefined();
1348
1259
  });
1349
1260
  });
1350
-
1351
- describe('External VPC with stack-managed routing infrastructure pattern', () => {
1352
- it('should correctly handle external VPC with NEW logical IDs (FriggPrivateRoute pattern)', async () => {
1353
- // This pattern occurs when VPC/subnets/NAT are external but routing (route tables,
1354
- // VPC endpoints, security groups) are managed by CloudFormation stack
1355
- // This tests the NEWER naming convention
1356
- const appDefinition = {
1357
- vpc: { enable: true },
1358
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1359
- database: {
1360
- dynamodb: { enable: true } // Enable DynamoDB to create DynamoDB VPC endpoint
1361
- }
1362
- };
1363
-
1364
- // Discovery results from real-world production scenario (newer stack)
1365
- const discoveredResources = {
1366
- fromCloudFormationStack: true,
1367
- stackName: 'test-production-stack',
1368
- existingLogicalIds: [
1369
- 'FriggLambdaSecurityGroup',
1370
- 'FriggLambdaRouteTable',
1371
- 'FriggPrivateRoute', // NEW naming
1372
- 'FriggPrivateSubnet1RouteTableAssociation', // NEW naming
1373
- 'FriggPrivateSubnet2RouteTableAssociation', // NEW naming
1374
- 'FriggS3VPCEndpoint', // NEW naming
1375
- 'FriggDynamoDBVPCEndpoint', // NEW naming
1376
- 'FriggKMSVPCEndpoint' // NEW naming
1377
- ],
1378
- // Stack resources (from CloudFormation)
1379
- lambdaSecurityGroupId: 'sg-01002240c6a446202',
1380
- routeTableId: 'rtb-08af43bbf0775602d',
1381
- s3VpcEndpointId: 'vpce-0d1ecb2c53ce9b4b8',
1382
- dynamodbVpcEndpointId: 'vpce-0fb749b207f1020b0',
1383
- kmsVpcEndpointId: 'vpce-0e38c25155b86de22',
1384
- // External resources (discovered via queries)
1385
- defaultVpcId: 'vpc-0cd17c0e06cb28b28',
1386
- privateSubnetId1: 'subnet-034f6562dbbc16348',
1387
- privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
1388
- existingNatGatewayId: 'nat-022660c36a47e2d79'
1389
- };
1390
-
1391
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1392
-
1393
- // === ASSERTIONS: Template Structure ===
1394
-
1395
- // 1. VPC should be external (not in template)
1396
- expect(result.resources.FriggVPC).toBeUndefined();
1397
- expect(result.vpcId).toBe('vpc-0cd17c0e06cb28b28');
1398
-
1399
- // 2. Security Group MUST be in template (stack-managed)
1400
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
1401
- expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
1402
- expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
1403
-
1404
- // 3. Subnets should be external (use hardcoded IDs, not in template)
1405
- expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
1406
- expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
1407
- expect(result.vpcConfig.subnetIds).toEqual([
1408
- 'subnet-034f6562dbbc16348',
1409
- 'subnet-0b8be2b82aeb5cdec'
1410
- ]);
1411
-
1412
- // 4. NAT Gateway should be external (not in template)
1413
- expect(result.resources.FriggNATGateway).toBeUndefined();
1414
- expect(result.resources.FriggNATGatewayEIP).toBeUndefined();
1415
- expect(result.natGatewayId).toBe('nat-022660c36a47e2d79');
1416
-
1417
- // 5. Route table MUST be in template (stack-managed)
1418
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1419
- expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
1420
-
1421
- // 6. Route table associations MUST be in template
1422
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
1423
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
1424
-
1425
- // 7. VPC Endpoints MUST be in template (stack-managed, prevents deletion)
1426
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
1427
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1428
-
1429
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
1430
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1431
-
1432
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
1433
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
1434
-
1435
- // 8. VPC Endpoint Security Group needed for interface endpoints
1436
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1437
-
1438
- // === ASSERTIONS: Resource Count ===
1439
- const resourceKeys = Object.keys(result.resources);
1440
- const friggResources = resourceKeys.filter(k => k.startsWith('Frigg') || k.startsWith('VPC'));
1441
-
1442
- // Should have routing infrastructure + endpoints + security groups
1443
- // NOT full VPC (no FriggVPC, FriggPrivateSubnet1/2, FriggNATGateway)
1444
- expect(friggResources).toContain('FriggLambdaSecurityGroup');
1445
- expect(friggResources).toContain('FriggLambdaRouteTable');
1446
- expect(friggResources).toContain('FriggS3VPCEndpoint');
1447
- expect(friggResources).toContain('FriggDynamoDBVPCEndpoint');
1448
- expect(friggResources).toContain('FriggKMSVPCEndpoint');
1449
- expect(friggResources).not.toContain('FriggVPC');
1450
- expect(friggResources).not.toContain('FriggPrivateSubnet1');
1451
- expect(friggResources).not.toContain('FriggNATGateway');
1452
- });
1453
-
1454
- it('should use OLD logical IDs for backwards compatibility (FriggNATRoute, VPCEndpointS3 pattern)', async () => {
1455
- // CRITICAL TEST: Real Frontify production stack uses OLD naming convention
1456
- // Stack currently has: FriggNATRoute, VPCEndpointS3, VPCEndpointDynamoDB
1457
- // We MUST use these same logical IDs to avoid AlreadyExists errors
1458
- const appDefinition = {
1459
- vpc: {
1460
- enable: true,
1461
- ownership: {
1462
- securityGroup: 'external'
1463
- },
1464
- external: {
1465
- securityGroupIds: ['sg-0c5e0d0e4a2f5efcf']
1466
- }
1467
- },
1468
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1469
- database: {
1470
- dynamodb: { enable: true }
1471
- }
1472
- };
1473
-
1474
- // Discovery results matching ACTUAL Frontify production stack
1475
- const discoveredResources = {
1476
- fromCloudFormationStack: true,
1477
- stackName: 'create-frigg-app-production',
1478
- existingLogicalIds: [
1479
- 'FriggLambdaRouteTable',
1480
- 'FriggNATRoute', // OLD naming
1481
- 'FriggSubnet1RouteAssociation', // OLD naming
1482
- 'FriggSubnet2RouteAssociation', // OLD naming
1483
- 'VPCEndpointS3', // OLD naming
1484
- 'VPCEndpointDynamoDB' // OLD naming
1485
- ],
1486
- // Structured discovery (what resolver needs)
1487
- _structured: {
1488
- stackManaged: [
1489
- { logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d', resourceType: 'AWS::EC2::RouteTable' },
1490
- { logicalId: 'FriggNATRoute', physicalId: 'rtb-08af43bbf0775602d|0.0.0.0/0', resourceType: 'AWS::EC2::Route' },
1491
- { logicalId: 'VPCEndpointS3', physicalId: 'vpce-0352ceac2124c14be', resourceType: 'AWS::EC2::VPCEndpoint' },
1492
- { logicalId: 'VPCEndpointDynamoDB', physicalId: 'vpce-0b06c4f631199ea68', resourceType: 'AWS::EC2::VPCEndpoint' }
1493
- ],
1494
- external: [
1495
- { physicalId: 'vpc-01cd124575c683a17', resourceType: 'AWS::EC2::VPC' },
1496
- { physicalId: 'sg-0c5e0d0e4a2f5efcf', resourceType: 'AWS::EC2::SecurityGroup' },
1497
- { physicalId: 'subnet-0bbca02e9981df72c', resourceType: 'AWS::EC2::Subnet' },
1498
- { physicalId: 'subnet-005f7092b91efaaeb', resourceType: 'AWS::EC2::Subnet' },
1499
- { physicalId: 'nat-05a536cbe7056325f', resourceType: 'AWS::EC2::NatGateway' }
1500
- ]
1501
- },
1502
- // Flat discovery (for backwards compatibility)
1503
- routeTableId: 'rtb-08af43bbf0775602d',
1504
- natRoute: 'rtb-08af43bbf0775602d|0.0.0.0/0',
1505
- s3VpcEndpointId: 'vpce-0352ceac2124c14be',
1506
- dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
1507
- // External resources
1508
- defaultVpcId: 'vpc-01cd124575c683a17',
1509
- defaultSecurityGroupId: 'sg-0c5e0d0e4a2f5efcf',
1510
- privateSubnetId1: 'subnet-0bbca02e9981df72c',
1511
- privateSubnetId2: 'subnet-005f7092b91efaaeb',
1512
- existingNatGatewayId: 'nat-05a536cbe7056325f'
1513
- };
1514
-
1515
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1516
-
1517
- // CRITICAL: Must use OLD logical IDs to match existing stack
1518
- expect(result.resources.FriggNATRoute).toBeDefined();
1519
- expect(result.resources.FriggNATRoute.Type).toBe('AWS::EC2::Route');
1520
- expect(result.resources.FriggPrivateRoute).toBeUndefined(); // Should NOT create new ID
1521
-
1522
- expect(result.resources.FriggSubnet1RouteAssociation).toBeDefined();
1523
- expect(result.resources.FriggSubnet2RouteAssociation).toBeDefined();
1524
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeUndefined();
1525
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeUndefined();
1526
-
1527
- expect(result.resources.VPCEndpointS3).toBeDefined();
1528
- expect(result.resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
1529
- expect(result.resources.FriggS3VPCEndpoint).toBeUndefined(); // Should NOT create new ID
1530
-
1531
- expect(result.resources.VPCEndpointDynamoDB).toBeDefined();
1532
- expect(result.resources.VPCEndpointDynamoDB.Type).toBe('AWS::EC2::VPCEndpoint');
1533
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined(); // Should NOT create new ID
1534
-
1535
- // Route table should still be created
1536
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1537
- });
1538
-
1539
- it('should convert OLD logical IDs to structured discovery stackManaged array', () => {
1540
- // TDD test: Verify that VPCEndpointS3 in existingLogicalIds gets added to stackManaged
1541
- const flatDiscovery = {
1542
- fromCloudFormationStack: true,
1543
- stackName: 'create-frigg-app-production',
1544
- existingLogicalIds: [
1545
- 'VPCEndpointS3', // OLD naming
1546
- 'VPCEndpointDynamoDB', // OLD naming
1547
- 'FriggNATRoute' // OLD naming
1548
- ],
1549
- s3VpcEndpointId: 'vpce-0352ceac2124c14be',
1550
- dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
1551
- natRoute: 'rtb-xxx|0.0.0.0/0'
1552
- };
1553
-
1554
- const structured = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1555
-
1556
- // CRITICAL: Old logical IDs should be in stackManaged array
1557
- expect(structured.stackManaged).toContainEqual(
1558
- expect.objectContaining({
1559
- logicalId: 'VPCEndpointS3',
1560
- physicalId: 'vpce-0352ceac2124c14be',
1561
- resourceType: 'AWS::EC2::VPCEndpoint'
1562
- })
1563
- );
1564
- expect(structured.stackManaged).toContainEqual(
1565
- expect.objectContaining({
1566
- logicalId: 'VPCEndpointDynamoDB',
1567
- physicalId: 'vpce-0b06c4f631199ea68',
1568
- resourceType: 'AWS::EC2::VPCEndpoint'
1569
- })
1570
- );
1571
- expect(structured.stackManaged).toContainEqual(
1572
- expect.objectContaining({
1573
- logicalId: 'FriggNATRoute',
1574
- physicalId: 'rtb-xxx|0.0.0.0/0',
1575
- resourceType: 'AWS::EC2::Route'
1576
- })
1577
- );
1578
- });
1579
- });
1580
-
1581
- describe('convertFlatDiscoveryToStructured - Direct Properties', () => {
1582
- it('should copy flat discovery properties to structured discovery for resolver access', () => {
1583
- const flatDiscovery = {
1584
- fromCloudFormationStack: true,
1585
- defaultVpcId: 'vpc-123',
1586
- defaultSecurityGroupId: 'sg-default-456',
1587
- lambdaSecurityGroupId: 'sg-lambda-789',
1588
- privateSubnetId1: 'subnet-1',
1589
- privateSubnetId2: 'subnet-2',
1590
- natGatewayId: 'nat-123'
1591
- };
1592
-
1593
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1594
-
1595
- // Direct properties should be copied for resolver access
1596
- expect(result.defaultVpcId).toBe('vpc-123');
1597
- expect(result.defaultSecurityGroupId).toBe('sg-default-456');
1598
- expect(result.lambdaSecurityGroupId).toBe('sg-lambda-789');
1599
- expect(result.privateSubnetId1).toBe('subnet-1');
1600
- expect(result.privateSubnetId2).toBe('subnet-2');
1601
- expect(result.natGatewayId).toBe('nat-123');
1602
- });
1603
- });
1604
-
1605
- describe('VPC Endpoint Security Group with External Lambda SG', () => {
1606
- it('should use external Lambda SG ID (not Ref) for VPC endpoint SG when Lambda SG is external', async () => {
1607
- const appDefinition = {
1608
- vpc: {
1609
- enable: true,
1610
- enableVPCEndpoints: true,
1611
- ownership: {
1612
- securityGroup: 'external' // External Lambda SG
1613
- }
1614
- },
1615
- encryption: { fieldLevelEncryptionMethod: 'kms' }
1616
- };
1617
- const discoveredResources = {
1618
- fromCloudFormationStack: true,
1619
- defaultVpcId: 'vpc-123',
1620
- defaultSecurityGroupId: 'sg-default-456', // Default VPC SG
1621
- lambdaSecurityGroupId: 'sg-stack-789', // Stack-managed SG (will be ignored)
1622
- privateSubnetId1: 'subnet-1',
1623
- privateSubnetId2: 'subnet-2',
1624
- natGatewayId: 'nat-123',
1625
- existingLogicalIds: ['FriggS3VPCEndpoint', 'FriggKMSVPCEndpoint'],
1626
- s3VpcEndpointId: 'vpce-s3-stack',
1627
- kmsVpcEndpointId: 'vpce-kms-stack'
1628
- };
1629
-
1630
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1631
-
1632
- // VPC Endpoint SG should be created
1633
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1634
-
1635
- // CRITICAL: Should use external Lambda SG ID directly, NOT a CloudFormation Ref
1636
- const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
1637
- expect(ingressRule.SourceSecurityGroupId).toBe('sg-default-456'); // Direct ID, not { Ref: 'FriggLambdaSecurityGroup' }
1638
- expect(typeof ingressRule.SourceSecurityGroupId).toBe('string');
1639
-
1640
- // Verify FriggLambdaSecurityGroup is NOT in the template
1641
- expect(result.resources.FriggLambdaSecurityGroup).toBeUndefined();
1642
- });
1643
-
1644
- it('should use CloudFormation Ref when Lambda SG is stack-managed', async () => {
1645
- const appDefinition = {
1646
- vpc: {
1647
- enable: true,
1648
- enableVPCEndpoints: true,
1649
- ownership: {
1650
- securityGroup: 'stack' // Stack-managed Lambda SG
1651
- }
1652
- },
1653
- encryption: { fieldLevelEncryptionMethod: 'kms' }
1654
- };
1655
- const discoveredResources = {
1656
- defaultVpcId: 'vpc-123',
1657
- privateSubnetId1: 'subnet-1',
1658
- privateSubnetId2: 'subnet-2'
1659
- };
1660
-
1661
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1662
-
1663
- // VPC Endpoint SG should be created
1664
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1665
-
1666
- // Should use CloudFormation Ref when Lambda SG is in stack
1667
- const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
1668
- expect(ingressRule.SourceSecurityGroupId).toEqual({ Ref: 'FriggLambdaSecurityGroup' });
1669
-
1670
- // Verify FriggLambdaSecurityGroup IS in the template
1671
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
1672
- });
1673
- });
1674
-
1675
- describe('convertFlatDiscoveryToStructured - VPC Endpoints from CloudFormation', () => {
1676
- it('should add VPC endpoints to stackManaged when in existingLogicalIds', () => {
1677
- const flatDiscovery = {
1678
- fromCloudFormationStack: true,
1679
- stackName: 'test-stack',
1680
- existingLogicalIds: [
1681
- 'FriggS3VPCEndpoint',
1682
- 'FriggDynamoDBVPCEndpoint',
1683
- 'FriggKMSVPCEndpoint'
1684
- ],
1685
- s3VpcEndpointId: 'vpce-s3-stack',
1686
- dynamodbVpcEndpointId: 'vpce-ddb-stack',
1687
- kmsVpcEndpointId: 'vpce-kms-stack'
1688
- };
1689
-
1690
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1691
-
1692
- // VPC endpoints should be in stackManaged (not external)
1693
- expect(result.stackManaged).toContainEqual(
1694
- expect.objectContaining({
1695
- logicalId: 'FriggS3VPCEndpoint',
1696
- physicalId: 'vpce-s3-stack',
1697
- resourceType: 'AWS::EC2::VPCEndpoint'
1698
- })
1699
- );
1700
- expect(result.stackManaged).toContainEqual(
1701
- expect.objectContaining({
1702
- logicalId: 'FriggDynamoDBVPCEndpoint',
1703
- physicalId: 'vpce-ddb-stack',
1704
- resourceType: 'AWS::EC2::VPCEndpoint'
1705
- })
1706
- );
1707
- expect(result.stackManaged).toContainEqual(
1708
- expect.objectContaining({
1709
- logicalId: 'FriggKMSVPCEndpoint',
1710
- physicalId: 'vpce-kms-stack',
1711
- resourceType: 'AWS::EC2::VPCEndpoint'
1712
- })
1713
- );
1714
-
1715
- // Should NOT be in external array
1716
- expect(result.external.some(r => r.physicalId === 'vpce-s3-stack')).toBe(false);
1717
- expect(result.external.some(r => r.physicalId === 'vpce-ddb-stack')).toBe(false);
1718
- expect(result.external.some(r => r.physicalId === 'vpce-kms-stack')).toBe(false);
1719
- });
1720
-
1721
- it('should add VPC endpoints to external when NOT in existingLogicalIds', () => {
1722
- const flatDiscovery = {
1723
- fromCloudFormationStack: false, // AWS API discovery
1724
- s3VpcEndpointId: 'vpce-s3-external',
1725
- dynamodbVpcEndpointId: 'vpce-ddb-external'
1726
- };
1727
-
1728
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1729
-
1730
- // Should be in external (AWS discovery)
1731
- expect(result.external).toContainEqual(
1732
- expect.objectContaining({
1733
- physicalId: 'vpce-s3-external',
1734
- resourceType: 'AWS::EC2::VPCEndpoint',
1735
- source: 'aws-discovery'
1736
- })
1737
- );
1738
-
1739
- // Should NOT be in stackManaged
1740
- expect(result.stackManaged.some(r => r.physicalId === 'vpce-s3-external')).toBe(false);
1741
- });
1742
-
1743
- it('should preserve existing VPC endpoints and only create missing ones', async () => {
1744
- const appDefinition = {
1745
- vpc: { enable: true },
1746
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1747
- };
1748
-
1749
- const discoveredResources = {
1750
- fromCloudFormationStack: true,
1751
- stackName: 'test-stack',
1752
- existingLogicalIds: [
1753
- 'FriggS3VPCEndpoint', // In stack
1754
- 'FriggDynamoDBVPCEndpoint', // In stack
1755
- 'FriggKMSVPCEndpoint' // In stack
1756
- // SecretsManager and SQS NOT in stack (were deleted)
1757
- ],
1758
- defaultVpcId: 'vpc-123',
1759
- privateSubnetId1: 'subnet-1',
1760
- privateSubnetId2: 'subnet-2',
1761
- lambdaSecurityGroupId: 'sg-123',
1762
- routeTableId: 'rtb-123',
1763
- // Endpoints in stack
1764
- s3VpcEndpointId: 'vpce-s3-existing',
1765
- dynamodbVpcEndpointId: 'vpce-ddb-existing',
1766
- kmsVpcEndpointId: 'vpce-kms-existing'
1767
- // secretsManagerVpcEndpointId and sqsVpcEndpointId NOT present
1768
- };
1769
-
1770
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1771
-
1772
- // Existing endpoints MUST be in template (re-added)
1773
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
1774
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
1775
-
1776
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
1777
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcId).toBe('vpc-123');
1778
-
1779
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
1780
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcId).toBe('vpc-123');
1781
-
1782
- // Missing endpoints should also be created
1783
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
1784
- expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
1785
-
1786
- // VPC Endpoint Security Group should be created
1787
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1788
- });
1789
- });
1790
-
1791
- describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
1792
- it('should add VPC from CloudFormation query to external array', () => {
1793
- const flatDiscovery = {
1794
- fromCloudFormationStack: true,
1795
- stackName: 'test-stack',
1796
- existingLogicalIds: ['FriggLambdaRouteTable', 'FriggLambdaSecurityGroup'],
1797
- // VPC ID was extracted from security group query (NOT a stack resource)
1798
- defaultVpcId: 'vpc-extracted-from-sg',
1799
- lambdaSecurityGroupId: 'sg-123',
1800
- routeTableId: 'rtb-123'
1801
- };
1802
-
1803
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1804
-
1805
- // VPC should be in external array (discovered via query, not in stack)
1806
- const vpcExternal = result.external.find(r => r.resourceType === 'AWS::EC2::VPC');
1807
- expect(vpcExternal).toBeDefined();
1808
- expect(vpcExternal.physicalId).toBe('vpc-extracted-from-sg');
1809
- expect(vpcExternal.source).toBe('cloudformation-query');
1810
-
1811
- // Security group SHOULD be in stackManaged (is in stack)
1812
- const sgStack = result.stackManaged.find(r => r.logicalId === 'FriggLambdaSecurityGroup');
1813
- expect(sgStack).toBeDefined();
1814
- expect(sgStack.physicalId).toBe('sg-123');
1815
- });
1816
-
1817
- it('should add subnets from route table associations to external array', () => {
1818
- const flatDiscovery = {
1819
- fromCloudFormationStack: true,
1820
- stackName: 'test-stack',
1821
- existingLogicalIds: ['FriggLambdaRouteTable'],
1822
- routeTableId: 'rtb-123',
1823
- // Subnets extracted from route table associations (NOT stack resources)
1824
- privateSubnetId1: 'subnet-1',
1825
- privateSubnetId2: 'subnet-2'
1826
- };
1827
-
1828
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1829
-
1830
- // Subnets should be in external array
1831
- const subnet1 = result.external.find(r => r.physicalId === 'subnet-1');
1832
- const subnet2 = result.external.find(r => r.physicalId === 'subnet-2');
1833
-
1834
- expect(subnet1).toBeDefined();
1835
- expect(subnet1.resourceType).toBe('AWS::EC2::Subnet');
1836
- expect(subnet1.source).toBe('cloudformation-query');
1837
-
1838
- expect(subnet2).toBeDefined();
1839
- expect(subnet2.resourceType).toBe('AWS::EC2::Subnet');
1840
- expect(subnet2.source).toBe('cloudformation-query');
1841
- });
1842
-
1843
- it('should add NAT Gateway from route table queries to external array', () => {
1844
- const flatDiscovery = {
1845
- fromCloudFormationStack: true,
1846
- stackName: 'test-stack',
1847
- existingLogicalIds: ['FriggLambdaRouteTable', 'FriggPrivateRoute'],
1848
- routeTableId: 'rtb-123',
1849
- // NAT Gateway extracted from route table routes (NOT a stack resource)
1850
- existingNatGatewayId: 'nat-extracted'
1851
- };
1852
-
1853
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1854
-
1855
- // NAT should be in external array
1856
- const natExternal = result.external.find(r => r.resourceType === 'AWS::EC2::NatGateway');
1857
- expect(natExternal).toBeDefined();
1858
- expect(natExternal.physicalId).toBe('nat-extracted');
1859
- expect(natExternal.source).toBe('cloudformation-query');
1860
- });
1861
-
1862
- it('should NOT add resources to external if they are in stack', () => {
1863
- const flatDiscovery = {
1864
- fromCloudFormationStack: true,
1865
- stackName: 'test-stack',
1866
- existingLogicalIds: ['FriggVPC', 'FriggPrivateSubnet1'],
1867
- // These ARE in the stack
1868
- defaultVpcId: 'vpc-in-stack',
1869
- privateSubnetId1: 'subnet-in-stack'
1870
- };
1871
-
1872
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1873
-
1874
- // Should be in stackManaged, NOT external
1875
- expect(result.stackManaged.some(r => r.logicalId === 'FriggVPC')).toBe(true);
1876
- expect(result.stackManaged.some(r => r.logicalId === 'FriggPrivateSubnet1')).toBe(true);
1877
-
1878
- // Should NOT be in external
1879
- expect(result.external.some(r => r.physicalId === 'vpc-in-stack')).toBe(false);
1880
- expect(result.external.some(r => r.physicalId === 'subnet-in-stack')).toBe(false);
1881
- });
1882
-
1883
- it('should handle external VPC pattern: stack resources + queried external references', () => {
1884
- const flatDiscovery = {
1885
- fromCloudFormationStack: true,
1886
- stackName: 'test-production-stack',
1887
- existingLogicalIds: [
1888
- 'FriggLambdaSecurityGroup',
1889
- 'FriggLambdaRouteTable',
1890
- 'FriggPrivateRoute',
1891
- 'FriggPrivateSubnet1RouteTableAssociation',
1892
- 'FriggPrivateSubnet2RouteTableAssociation',
1893
- 'FriggS3VPCEndpoint',
1894
- 'FriggDynamoDBVPCEndpoint',
1895
- 'FriggKMSVPCEndpoint'
1896
- ],
1897
- // Stack resources
1898
- lambdaSecurityGroupId: 'sg-stack-123',
1899
- routeTableId: 'rtb-stack-456',
1900
- s3VpcEndpointId: 'vpce-s3-stack',
1901
- // External resources (discovered via queries)
1902
- defaultVpcId: 'vpc-external-123',
1903
- privateSubnetId1: 'subnet-external-1',
1904
- privateSubnetId2: 'subnet-external-2',
1905
- existingNatGatewayId: 'nat-external-789'
1906
- };
1907
-
1908
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1909
-
1910
- // Stack resources should be in stackManaged
1911
- expect(result.stackManaged).toEqual(
1912
- expect.arrayContaining([
1913
- expect.objectContaining({ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack-123' }),
1914
- expect.objectContaining({ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-stack-456' }),
1915
- expect.objectContaining({ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack' })
1916
- ])
1917
- );
1918
-
1919
- // External resources should be in external array
1920
- expect(result.external).toEqual(
1921
- expect.arrayContaining([
1922
- expect.objectContaining({ physicalId: 'vpc-external-123', resourceType: 'AWS::EC2::VPC', source: 'cloudformation-query' }),
1923
- expect.objectContaining({ physicalId: 'subnet-external-1', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
1924
- expect.objectContaining({ physicalId: 'subnet-external-2', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
1925
- expect.objectContaining({ physicalId: 'nat-external-789', resourceType: 'AWS::EC2::NatGateway', source: 'cloudformation-query' })
1926
- ])
1927
- );
1928
- });
1929
- });
1930
1261
  });
1931
1262