@friggframework/devtools 2.0.0-next.52 → 2.0.0-next.54

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