@friggframework/devtools 2.0.0--canary.461.84ff4f5.0 → 2.0.0--canary.461.ec909cf.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.
@@ -0,0 +1,772 @@
1
+ /**
2
+ * Tests for VPC Builder
3
+ *
4
+ * Tests VPC infrastructure building with various management modes
5
+ */
6
+
7
+ const { VpcBuilder } = require('./vpc-builder');
8
+ const { ValidationResult } = require('../shared/base-builder');
9
+
10
+ describe('VpcBuilder', () => {
11
+ let vpcBuilder;
12
+
13
+ beforeEach(() => {
14
+ vpcBuilder = new VpcBuilder();
15
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
16
+ });
17
+
18
+ afterEach(() => {
19
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
20
+ });
21
+
22
+ describe('shouldExecute()', () => {
23
+ it('should return true when VPC is enabled', () => {
24
+ const appDefinition = {
25
+ vpc: { enable: true },
26
+ };
27
+
28
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(true);
29
+ });
30
+
31
+ it('should return false when VPC is disabled', () => {
32
+ const appDefinition = {
33
+ vpc: { enable: false },
34
+ };
35
+
36
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
37
+ });
38
+
39
+ it('should return false when VPC is not defined', () => {
40
+ const appDefinition = {};
41
+
42
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
43
+ });
44
+
45
+ it('should return false when FRIGG_SKIP_AWS_DISCOVERY is set (local mode)', () => {
46
+ process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
47
+ const appDefinition = {
48
+ vpc: { enable: true },
49
+ };
50
+
51
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('validate()', () => {
56
+ it('should pass validation for valid discover mode config', () => {
57
+ const appDefinition = {
58
+ vpc: {
59
+ enable: true,
60
+ management: 'discover',
61
+ },
62
+ };
63
+
64
+ const result = vpcBuilder.validate(appDefinition);
65
+
66
+ expect(result).toBeInstanceOf(ValidationResult);
67
+ expect(result.valid).toBe(true);
68
+ expect(result.errors).toEqual([]);
69
+ });
70
+
71
+ it('should pass validation for valid create-new mode config', () => {
72
+ const appDefinition = {
73
+ vpc: {
74
+ enable: true,
75
+ management: 'create-new',
76
+ },
77
+ };
78
+
79
+ const result = vpcBuilder.validate(appDefinition);
80
+
81
+ expect(result.valid).toBe(true);
82
+ });
83
+
84
+ it('should pass validation for valid use-existing mode with vpcId', () => {
85
+ const appDefinition = {
86
+ vpc: {
87
+ enable: true,
88
+ management: 'use-existing',
89
+ vpcId: 'vpc-123456',
90
+ securityGroupIds: ['sg-123'],
91
+ },
92
+ };
93
+
94
+ const result = vpcBuilder.validate(appDefinition);
95
+
96
+ expect(result.valid).toBe(true);
97
+ });
98
+
99
+ it('should error if VPC configuration is missing', () => {
100
+ const appDefinition = {};
101
+
102
+ const result = vpcBuilder.validate(appDefinition);
103
+
104
+ expect(result.valid).toBe(false);
105
+ expect(result.errors).toContain('VPC configuration is missing');
106
+ });
107
+
108
+ it('should error for invalid management mode', () => {
109
+ const appDefinition = {
110
+ vpc: {
111
+ enable: true,
112
+ management: 'invalid-mode',
113
+ },
114
+ };
115
+
116
+ const result = vpcBuilder.validate(appDefinition);
117
+
118
+ expect(result.valid).toBe(false);
119
+ expect(result.errors).toContain(
120
+ expect.stringContaining('Invalid vpc.management')
121
+ );
122
+ });
123
+
124
+ it('should error when use-existing mode without vpcId', () => {
125
+ const appDefinition = {
126
+ vpc: {
127
+ enable: true,
128
+ management: 'use-existing',
129
+ },
130
+ };
131
+
132
+ const result = vpcBuilder.validate(appDefinition);
133
+
134
+ expect(result.valid).toBe(false);
135
+ expect(result.errors).toContain(
136
+ 'vpc.vpcId is required when management="use-existing"'
137
+ );
138
+ });
139
+
140
+ it('should warn when use-existing mode without security groups', () => {
141
+ const appDefinition = {
142
+ vpc: {
143
+ enable: true,
144
+ management: 'use-existing',
145
+ vpcId: 'vpc-123',
146
+ },
147
+ };
148
+
149
+ const result = vpcBuilder.validate(appDefinition);
150
+
151
+ expect(result.warnings).toContain(
152
+ expect.stringContaining('securityGroupIds not provided')
153
+ );
154
+ });
155
+
156
+ it('should error for invalid CIDR block format', () => {
157
+ const appDefinition = {
158
+ vpc: {
159
+ enable: true,
160
+ cidrBlock: 'invalid-cidr',
161
+ },
162
+ };
163
+
164
+ const result = vpcBuilder.validate(appDefinition);
165
+
166
+ expect(result.valid).toBe(false);
167
+ expect(result.errors).toContain(
168
+ expect.stringContaining('Invalid CIDR block format')
169
+ );
170
+ });
171
+
172
+ it('should accept valid CIDR block formats', () => {
173
+ const validCidrs = ['10.0.0.0/16', '172.31.0.0/16', '192.168.0.0/24'];
174
+
175
+ validCidrs.forEach(cidr => {
176
+ const appDefinition = {
177
+ vpc: {
178
+ enable: true,
179
+ cidrBlock: cidr,
180
+ },
181
+ };
182
+
183
+ const result = vpcBuilder.validate(appDefinition);
184
+ expect(result.valid).toBe(true);
185
+ });
186
+ });
187
+
188
+ it('should error when use-existing subnets without subnet IDs', () => {
189
+ const appDefinition = {
190
+ vpc: {
191
+ enable: true,
192
+ subnets: {
193
+ management: 'use-existing',
194
+ },
195
+ },
196
+ };
197
+
198
+ const result = vpcBuilder.validate(appDefinition);
199
+
200
+ expect(result.valid).toBe(false);
201
+ expect(result.errors).toContain(
202
+ expect.stringContaining('At least 2 subnet IDs required')
203
+ );
204
+ });
205
+
206
+ it('should error when use-existing subnets with only 1 subnet', () => {
207
+ const appDefinition = {
208
+ vpc: {
209
+ enable: true,
210
+ subnets: {
211
+ management: 'use-existing',
212
+ ids: ['subnet-1'],
213
+ },
214
+ },
215
+ };
216
+
217
+ const result = vpcBuilder.validate(appDefinition);
218
+
219
+ expect(result.valid).toBe(false);
220
+ });
221
+
222
+ it('should pass when use-existing subnets with 2+ subnets', () => {
223
+ const appDefinition = {
224
+ vpc: {
225
+ enable: true,
226
+ subnets: {
227
+ management: 'use-existing',
228
+ ids: ['subnet-1', 'subnet-2'],
229
+ },
230
+ },
231
+ };
232
+
233
+ const result = vpcBuilder.validate(appDefinition);
234
+
235
+ expect(result.valid).toBe(true);
236
+ });
237
+
238
+ it('should default to discover mode when management not specified', () => {
239
+ const appDefinition = {
240
+ vpc: {
241
+ enable: true,
242
+ },
243
+ };
244
+
245
+ const result = vpcBuilder.validate(appDefinition);
246
+
247
+ expect(result.valid).toBe(true);
248
+ });
249
+ });
250
+
251
+ describe('build() - discover mode', () => {
252
+ it('should use discovered VPC resources', async () => {
253
+ const appDefinition = {
254
+ vpc: {
255
+ enable: true,
256
+ management: 'discover',
257
+ },
258
+ };
259
+
260
+ const discoveredResources = {
261
+ defaultVpcId: 'vpc-discovered',
262
+ privateSubnetId1: 'subnet-private1',
263
+ privateSubnetId2: 'subnet-private2',
264
+ defaultSecurityGroupId: 'sg-discovered',
265
+ };
266
+
267
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
268
+
269
+ expect(result.vpcConfig.subnetIds).toEqual(['subnet-private1', 'subnet-private2']);
270
+ // In discover mode, we create FriggLambdaSecurityGroup in the discovered VPC
271
+ expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
272
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
273
+ expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-discovered');
274
+ });
275
+
276
+ it('should create VPC endpoints in discover mode with selfHeal when none exist', async () => {
277
+ const appDefinition = {
278
+ vpc: {
279
+ enable: true,
280
+ management: 'discover',
281
+ selfHeal: true,
282
+ },
283
+ };
284
+
285
+ const discoveredResources = {
286
+ defaultVpcId: 'vpc-discovered',
287
+ privateSubnetId1: 'subnet-private1',
288
+ privateSubnetId2: 'subnet-private2',
289
+ existingNatGatewayId: 'nat-123',
290
+ // No VPC endpoints discovered
291
+ };
292
+
293
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
294
+
295
+ // With selfHeal enabled and no VPC endpoints found, they should be created
296
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
297
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
298
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
299
+ });
300
+
301
+ it('should NOT create VPC endpoints in discover mode when they already exist', async () => {
302
+ const appDefinition = {
303
+ vpc: {
304
+ enable: true,
305
+ management: 'discover',
306
+ selfHeal: true,
307
+ },
308
+ };
309
+
310
+ const discoveredResources = {
311
+ defaultVpcId: 'vpc-discovered',
312
+ privateSubnetId1: 'subnet-private1',
313
+ privateSubnetId2: 'subnet-private2',
314
+ existingNatGatewayId: 'nat-123',
315
+ // VPC endpoints already exist
316
+ s3VpcEndpointId: 'vpce-s3-123',
317
+ dynamodbVpcEndpointId: 'vpce-dynamodb-456',
318
+ };
319
+
320
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
321
+
322
+ // With existing VPC endpoints discovered, they should NOT be recreated
323
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
324
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined();
325
+ });
326
+
327
+ it('should create VPC endpoints with selfHeal when missing', async () => {
328
+ const appDefinition = {
329
+ vpc: {
330
+ enable: true,
331
+ management: 'discover',
332
+ selfHeal: true,
333
+ },
334
+ };
335
+
336
+ const discoveredResources = {
337
+ defaultVpcId: 'vpc-123',
338
+ privateSubnetId1: 'subnet-1',
339
+ privateSubnetId2: 'subnet-2',
340
+ defaultSecurityGroupId: 'sg-123',
341
+ // No VPC endpoints discovered - selfHeal should create them
342
+ };
343
+
344
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
345
+
346
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
347
+ expect(result.resources.FriggS3VPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
348
+ expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
349
+ });
350
+
351
+ it('should skip VPC endpoints when disabled', async () => {
352
+ const appDefinition = {
353
+ vpc: {
354
+ enable: true,
355
+ management: 'discover',
356
+ enableVPCEndpoints: false,
357
+ },
358
+ };
359
+
360
+ const discoveredResources = {
361
+ defaultVpcId: 'vpc-123',
362
+ privateSubnetId1: 'subnet-1',
363
+ privateSubnetId2: 'subnet-2',
364
+ };
365
+
366
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
367
+
368
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
369
+ });
370
+
371
+ it('should include IAM permissions for VPC operations', async () => {
372
+ const appDefinition = {
373
+ vpc: {
374
+ enable: true,
375
+ },
376
+ };
377
+
378
+ const discoveredResources = {
379
+ defaultVpcId: 'vpc-123',
380
+ };
381
+
382
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
383
+
384
+ const vpcPermissions = result.iamStatements.find(stmt =>
385
+ stmt.Action.includes('ec2:CreateNetworkInterface')
386
+ );
387
+
388
+ expect(vpcPermissions).toBeDefined();
389
+ expect(vpcPermissions.Action).toContain('ec2:DescribeNetworkInterfaces');
390
+ expect(vpcPermissions.Action).toContain('ec2:DeleteNetworkInterface');
391
+ });
392
+ });
393
+
394
+ describe('build() - create-new mode', () => {
395
+ it('should create complete VPC infrastructure', async () => {
396
+ const appDefinition = {
397
+ vpc: {
398
+ enable: true,
399
+ management: 'create-new',
400
+ },
401
+ };
402
+
403
+ const result = await vpcBuilder.build(appDefinition, {});
404
+
405
+ expect(result.resources.FriggVPC).toBeDefined();
406
+ expect(result.resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
407
+ expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
408
+ });
409
+
410
+ it('should create private and public subnets', async () => {
411
+ const appDefinition = {
412
+ vpc: {
413
+ enable: true,
414
+ management: 'create-new',
415
+ },
416
+ };
417
+
418
+ const result = await vpcBuilder.build(appDefinition, {});
419
+
420
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
421
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
422
+ expect(result.resources.FriggPublicSubnet).toBeDefined();
423
+ });
424
+
425
+ it('should create security group for Lambda functions', async () => {
426
+ const appDefinition = {
427
+ vpc: {
428
+ enable: true,
429
+ management: 'create-new',
430
+ },
431
+ };
432
+
433
+ const result = await vpcBuilder.build(appDefinition, {});
434
+
435
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
436
+ expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
437
+ });
438
+
439
+ it('should use CloudFormation references for new resources', async () => {
440
+ const appDefinition = {
441
+ vpc: {
442
+ enable: true,
443
+ management: 'create-new',
444
+ },
445
+ };
446
+
447
+ const result = await vpcBuilder.build(appDefinition, {});
448
+
449
+ expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
450
+ expect(result.vpcConfig.subnetIds).toContain({ Ref: 'FriggPrivateSubnet1' });
451
+ expect(result.vpcConfig.subnetIds).toContain({ Ref: 'FriggPrivateSubnet2' });
452
+ });
453
+
454
+ it('should use custom CIDR block if provided', async () => {
455
+ const appDefinition = {
456
+ vpc: {
457
+ enable: true,
458
+ management: 'create-new',
459
+ cidrBlock: '192.168.0.0/16',
460
+ },
461
+ };
462
+
463
+ const result = await vpcBuilder.build(appDefinition, {});
464
+
465
+ expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('192.168.0.0/16');
466
+ });
467
+ });
468
+
469
+ describe('build() - use-existing mode', () => {
470
+ it('should use provided VPC and subnet IDs', async () => {
471
+ const appDefinition = {
472
+ vpc: {
473
+ enable: true,
474
+ management: 'use-existing',
475
+ vpcId: 'vpc-custom',
476
+ subnets: {
477
+ ids: ['subnet-a', 'subnet-b'],
478
+ },
479
+ securityGroupIds: ['sg-custom'],
480
+ },
481
+ };
482
+
483
+ const result = await vpcBuilder.build(appDefinition, {});
484
+
485
+ expect(result.vpcConfig.subnetIds).toEqual(['subnet-a', 'subnet-b']);
486
+ expect(result.vpcConfig.securityGroupIds).toEqual(['sg-custom']);
487
+ });
488
+
489
+ it('should not create VPC resources in use-existing mode', async () => {
490
+ const appDefinition = {
491
+ vpc: {
492
+ enable: true,
493
+ management: 'use-existing',
494
+ vpcId: 'vpc-custom',
495
+ subnets: {
496
+ ids: ['subnet-a', 'subnet-b'],
497
+ },
498
+ },
499
+ };
500
+
501
+ const result = await vpcBuilder.build(appDefinition, {});
502
+
503
+ expect(result.resources.FriggVPC).toBeUndefined();
504
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
505
+ });
506
+ });
507
+
508
+ describe('getDependencies()', () => {
509
+ it('should have no dependencies', () => {
510
+ const deps = vpcBuilder.getDependencies();
511
+
512
+ expect(deps).toEqual([]);
513
+ });
514
+ });
515
+
516
+ describe('getName()', () => {
517
+ it('should return VpcBuilder', () => {
518
+ expect(vpcBuilder.getName()).toBe('VpcBuilder');
519
+ });
520
+ });
521
+
522
+ describe('NAT Gateway handling', () => {
523
+ it('should create NAT gateway when management is createAndManage', async () => {
524
+ const appDefinition = {
525
+ vpc: {
526
+ enable: true,
527
+ management: 'discover',
528
+ natGateway: {
529
+ management: 'createAndManage',
530
+ },
531
+ selfHeal: true,
532
+ },
533
+ };
534
+
535
+ const discoveredResources = {
536
+ defaultVpcId: 'vpc-123',
537
+ publicSubnetId: 'subnet-public',
538
+ };
539
+
540
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
541
+
542
+ expect(result.resources.FriggNATGateway).toBeDefined();
543
+ expect(result.resources.FriggNATGateway.Type).toBe('AWS::EC2::NatGateway');
544
+ });
545
+
546
+ it('should not create NAT when existing NAT is properly placed', async () => {
547
+ const appDefinition = {
548
+ vpc: {
549
+ enable: true,
550
+ natGateway: {
551
+ management: 'createAndManage',
552
+ },
553
+ selfHeal: true,
554
+ },
555
+ };
556
+
557
+ const discoveredResources = {
558
+ defaultVpcId: 'vpc-123',
559
+ publicSubnetId: 'subnet-public',
560
+ existingNatGatewayId: 'nat-good',
561
+ natGatewayInPrivateSubnet: false,
562
+ };
563
+
564
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
565
+
566
+ expect(result.resources.FriggNATGateway).toBeUndefined();
567
+ });
568
+
569
+ it('should create new NAT when existing is in private subnet', async () => {
570
+ const appDefinition = {
571
+ vpc: {
572
+ enable: true,
573
+ natGateway: {
574
+ management: 'createAndManage',
575
+ },
576
+ selfHeal: true,
577
+ },
578
+ };
579
+
580
+ const discoveredResources = {
581
+ defaultVpcId: 'vpc-123',
582
+ publicSubnetId: 'subnet-public',
583
+ existingNatGatewayId: 'nat-misplaced',
584
+ natGatewayInPrivateSubnet: true, // WRONG placement
585
+ };
586
+
587
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
588
+
589
+ expect(result.resources.FriggNATGateway).toBeDefined();
590
+ });
591
+
592
+ it('should throw error when NAT in private subnet without selfHeal', async () => {
593
+ const appDefinition = {
594
+ vpc: {
595
+ enable: true,
596
+ natGateway: {
597
+ management: 'createAndManage',
598
+ },
599
+ selfHeal: false,
600
+ },
601
+ };
602
+
603
+ const discoveredResources = {
604
+ defaultVpcId: 'vpc-123',
605
+ existingNatGatewayId: 'nat-misplaced',
606
+ natGatewayInPrivateSubnet: true,
607
+ };
608
+
609
+ await expect(vpcBuilder.build(appDefinition, discoveredResources)).rejects.toThrow(
610
+ 'CRITICAL: NAT Gateway is in PRIVATE subnet'
611
+ );
612
+ });
613
+
614
+ it('should reuse existing elastic IP allocation', async () => {
615
+ const appDefinition = {
616
+ vpc: {
617
+ enable: true,
618
+ natGateway: {
619
+ management: 'createAndManage',
620
+ },
621
+ selfHeal: true,
622
+ },
623
+ };
624
+
625
+ const discoveredResources = {
626
+ defaultVpcId: 'vpc-123',
627
+ publicSubnetId: 'subnet-public',
628
+ existingElasticIpAllocationId: 'eipalloc-123',
629
+ };
630
+
631
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
632
+
633
+ if (result.resources.FriggNATGateway) {
634
+ expect(result.resources.FriggNATGateway.Properties.AllocationId).toBe('eipalloc-123');
635
+ expect(result.resources.FriggNATGatewayEIP).toBeUndefined();
636
+ }
637
+ });
638
+ });
639
+
640
+ describe('VPC Endpoints', () => {
641
+ it('should create KMS endpoint when KMS encryption is enabled', async () => {
642
+ const appDefinition = {
643
+ vpc: {
644
+ enable: true,
645
+ },
646
+ encryption: {
647
+ fieldLevelEncryptionMethod: 'kms',
648
+ },
649
+ };
650
+
651
+ const discoveredResources = {
652
+ defaultVpcId: 'vpc-123',
653
+ };
654
+
655
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
656
+
657
+ expect(result.resources.VPCEndpointKMS).toBeDefined();
658
+ });
659
+
660
+ it('should create Secrets Manager endpoint when enabled', async () => {
661
+ const appDefinition = {
662
+ vpc: {
663
+ enable: true,
664
+ },
665
+ secretsManager: {
666
+ enable: true,
667
+ },
668
+ };
669
+
670
+ const discoveredResources = {
671
+ defaultVpcId: 'vpc-123',
672
+ };
673
+
674
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
675
+
676
+ expect(result.resources.VPCEndpointSecretsManager).toBeDefined();
677
+ });
678
+
679
+ it('should not create KMS endpoint when encryption is AES', async () => {
680
+ const appDefinition = {
681
+ vpc: {
682
+ enable: true,
683
+ },
684
+ encryption: {
685
+ fieldLevelEncryptionMethod: 'aes',
686
+ },
687
+ };
688
+
689
+ const discoveredResources = {
690
+ defaultVpcId: 'vpc-123',
691
+ };
692
+
693
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
694
+
695
+ expect(result.resources.VPCEndpointKMS).toBeUndefined();
696
+ });
697
+ });
698
+
699
+ describe('Self-healing', () => {
700
+ it('should create missing subnets when selfHeal is enabled', async () => {
701
+ const appDefinition = {
702
+ vpc: {
703
+ enable: true,
704
+ management: 'discover',
705
+ selfHeal: true,
706
+ },
707
+ };
708
+
709
+ const discoveredResources = {
710
+ defaultVpcId: 'vpc-123',
711
+ privateSubnetId1: null,
712
+ privateSubnetId2: null,
713
+ };
714
+
715
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
716
+
717
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
718
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
719
+ });
720
+
721
+ it('should throw error for missing subnets without selfHeal', async () => {
722
+ const appDefinition = {
723
+ vpc: {
724
+ enable: true,
725
+ management: 'discover',
726
+ subnets: { management: 'discover' },
727
+ selfHeal: false,
728
+ },
729
+ };
730
+
731
+ const discoveredResources = {
732
+ defaultVpcId: 'vpc-123',
733
+ privateSubnetId1: null,
734
+ privateSubnetId2: null,
735
+ };
736
+
737
+ await expect(vpcBuilder.build(appDefinition, discoveredResources)).rejects.toThrow(
738
+ 'No subnets discovered'
739
+ );
740
+ });
741
+ });
742
+
743
+ describe('Outputs', () => {
744
+ it('should generate VPC ID output', async () => {
745
+ const appDefinition = {
746
+ vpc: {
747
+ enable: true,
748
+ management: 'create-new',
749
+ },
750
+ };
751
+
752
+ const result = await vpcBuilder.build(appDefinition, {});
753
+
754
+ expect(result.outputs.VpcId).toBeDefined();
755
+ });
756
+
757
+ it('should generate subnet outputs', async () => {
758
+ const appDefinition = {
759
+ vpc: {
760
+ enable: true,
761
+ management: 'create-new',
762
+ },
763
+ };
764
+
765
+ const result = await vpcBuilder.build(appDefinition, {});
766
+
767
+ expect(result.outputs.PrivateSubnet1Id).toBeDefined();
768
+ expect(result.outputs.PrivateSubnet2Id).toBeDefined();
769
+ });
770
+ });
771
+ });
772
+