@friggframework/devtools 2.0.0--canary.428.d004aeb.0 → 2.0.0--canary.428.1a6e465.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.
|
@@ -9,7 +9,8 @@ const {
|
|
|
9
9
|
DescribeSecurityGroupsCommand,
|
|
10
10
|
DescribeRouteTablesCommand,
|
|
11
11
|
DescribeNatGatewaysCommand,
|
|
12
|
-
DescribeAddressesCommand
|
|
12
|
+
DescribeAddressesCommand,
|
|
13
|
+
DescribeInternetGatewaysCommand,
|
|
13
14
|
} = require('@aws-sdk/client-ec2');
|
|
14
15
|
const {
|
|
15
16
|
KMSClient,
|
|
@@ -77,6 +78,18 @@ describe('AWSDiscovery', () => {
|
|
|
77
78
|
expect(vpc).toEqual(mockVpc);
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
it('should retry without default filter when default VPC query returns empty', async () => {
|
|
82
|
+
const mockVpc = { VpcId: 'vpc-23456789', IsDefault: false };
|
|
83
|
+
|
|
84
|
+
ec2Mock
|
|
85
|
+
.on(DescribeVpcsCommand)
|
|
86
|
+
.resolvesOnce({ Vpcs: [] })
|
|
87
|
+
.resolves({ Vpcs: [mockVpc] });
|
|
88
|
+
|
|
89
|
+
const vpc = await discovery.findDefaultVpc();
|
|
90
|
+
expect(vpc).toEqual(mockVpc);
|
|
91
|
+
});
|
|
92
|
+
|
|
80
93
|
it('should throw error when no VPCs found', async () => {
|
|
81
94
|
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
82
95
|
Vpcs: []
|
|
@@ -127,6 +140,39 @@ describe('AWSDiscovery', () => {
|
|
|
127
140
|
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
128
141
|
expect(isPrivate).toBe(false);
|
|
129
142
|
});
|
|
143
|
+
|
|
144
|
+
it('should default to private when no route table found', async () => {
|
|
145
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
146
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({ RouteTables: [] });
|
|
150
|
+
|
|
151
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
152
|
+
expect(isPrivate).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should default to private when AWS throws describing subnet', async () => {
|
|
156
|
+
ec2Mock.on(DescribeSubnetsCommand).rejects(new Error('boom'));
|
|
157
|
+
|
|
158
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
159
|
+
expect(isPrivate).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should log warning when subnet cannot be found', async () => {
|
|
163
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: [] });
|
|
164
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
|
|
166
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId);
|
|
167
|
+
|
|
168
|
+
expect(isPrivate).toBe(true);
|
|
169
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
170
|
+
`Could not determine if subnet ${mockSubnetId} is private:`,
|
|
171
|
+
expect.any(Error)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
warnSpy.mockRestore();
|
|
175
|
+
});
|
|
130
176
|
});
|
|
131
177
|
|
|
132
178
|
describe('findPrivateSubnets', () => {
|
|
@@ -159,6 +205,26 @@ describe('AWSDiscovery', () => {
|
|
|
159
205
|
expect(subnets).toEqual(mockSubnets);
|
|
160
206
|
});
|
|
161
207
|
|
|
208
|
+
it('should duplicate single private subnet when only one available', async () => {
|
|
209
|
+
const mockSubnets = [
|
|
210
|
+
{ SubnetId: 'subnet-private-1', AvailabilityZone: 'us-east-1a' },
|
|
211
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1b' }
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: mockSubnets });
|
|
215
|
+
|
|
216
|
+
jest
|
|
217
|
+
.spyOn(discovery, 'isSubnetPrivate')
|
|
218
|
+
.mockResolvedValueOnce(true)
|
|
219
|
+
.mockResolvedValueOnce(false);
|
|
220
|
+
|
|
221
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId);
|
|
222
|
+
expect(subnets.map((s) => s.SubnetId)).toEqual([
|
|
223
|
+
'subnet-private-1',
|
|
224
|
+
'subnet-public-1'
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
162
228
|
it('should throw error when no private subnets found and autoConvert is false', async () => {
|
|
163
229
|
const mockSubnets = [
|
|
164
230
|
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
@@ -215,6 +281,31 @@ describe('AWSDiscovery', () => {
|
|
|
215
281
|
expect(subnets).toHaveLength(2);
|
|
216
282
|
expect(subnets[0].SubnetId).toBe('subnet-public-1');
|
|
217
283
|
});
|
|
284
|
+
|
|
285
|
+
it('should select converted subnets when autoConvert handles three public subnets', async () => {
|
|
286
|
+
const mockSubnets = [
|
|
287
|
+
{ SubnetId: 'subnet-public-1' },
|
|
288
|
+
{ SubnetId: 'subnet-public-2' },
|
|
289
|
+
{ SubnetId: 'subnet-public-3' }
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: mockSubnets });
|
|
293
|
+
jest.spyOn(discovery, 'isSubnetPrivate').mockResolvedValue(false);
|
|
294
|
+
|
|
295
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId, true);
|
|
296
|
+
expect(subnets.map((s) => s.SubnetId)).toEqual([
|
|
297
|
+
'subnet-public-2',
|
|
298
|
+
'subnet-public-3'
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should throw when AWS returns no subnets', async () => {
|
|
303
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: [] });
|
|
304
|
+
|
|
305
|
+
await expect(discovery.findPrivateSubnets(mockVpcId)).rejects.toThrow(
|
|
306
|
+
`No subnets found in VPC ${mockVpcId}`
|
|
307
|
+
);
|
|
308
|
+
});
|
|
218
309
|
});
|
|
219
310
|
|
|
220
311
|
describe('findPublicSubnets', () => {
|
|
@@ -246,6 +337,21 @@ describe('AWSDiscovery', () => {
|
|
|
246
337
|
await expect(discovery.findPublicSubnets(mockVpcId))
|
|
247
338
|
.rejects.toThrow('No subnets found in VPC');
|
|
248
339
|
});
|
|
340
|
+
|
|
341
|
+
it('should return null when no public subnets identified', async () => {
|
|
342
|
+
const mockSubnet = { SubnetId: 'subnet-private-1' };
|
|
343
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: [mockSubnet] });
|
|
344
|
+
|
|
345
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
346
|
+
RouteTables: [{
|
|
347
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
348
|
+
Routes: [{ GatewayId: 'local' }]
|
|
349
|
+
}]
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const subnet = await discovery.findPublicSubnets(mockVpcId);
|
|
353
|
+
expect(subnet).toBeNull();
|
|
354
|
+
});
|
|
249
355
|
});
|
|
250
356
|
|
|
251
357
|
describe('findDefaultSecurityGroup', () => {
|
|
@@ -257,14 +363,29 @@ describe('AWSDiscovery', () => {
|
|
|
257
363
|
GroupName: 'default'
|
|
258
364
|
};
|
|
259
365
|
|
|
260
|
-
ec2Mock
|
|
261
|
-
|
|
262
|
-
|
|
366
|
+
ec2Mock
|
|
367
|
+
.on(DescribeSecurityGroupsCommand)
|
|
368
|
+
.resolvesOnce({ SecurityGroups: [] })
|
|
369
|
+
.resolves({ SecurityGroups: [mockSecurityGroup] });
|
|
263
370
|
|
|
264
371
|
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
265
372
|
expect(sg).toEqual(mockSecurityGroup);
|
|
266
373
|
});
|
|
267
374
|
|
|
375
|
+
it('should prefer Frigg-managed security group when present', async () => {
|
|
376
|
+
const friggSg = {
|
|
377
|
+
GroupId: 'sg-frigg',
|
|
378
|
+
GroupName: 'frigg-lambda-sg',
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
ec2Mock
|
|
382
|
+
.on(DescribeSecurityGroupsCommand)
|
|
383
|
+
.resolves({ SecurityGroups: [friggSg] });
|
|
384
|
+
|
|
385
|
+
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
386
|
+
expect(sg).toEqual(friggSg);
|
|
387
|
+
});
|
|
388
|
+
|
|
268
389
|
it('should throw error when no default security group found', async () => {
|
|
269
390
|
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
270
391
|
SecurityGroups: []
|
|
@@ -309,6 +430,14 @@ describe('AWSDiscovery', () => {
|
|
|
309
430
|
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
310
431
|
expect(rt).toEqual(mockRouteTable);
|
|
311
432
|
});
|
|
433
|
+
|
|
434
|
+
it('should throw error when no route tables found', async () => {
|
|
435
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({ RouteTables: [] });
|
|
436
|
+
|
|
437
|
+
await expect(
|
|
438
|
+
discovery.findPrivateRouteTable(mockVpcId)
|
|
439
|
+
).rejects.toThrow(`No route tables found for VPC ${mockVpcId}`);
|
|
440
|
+
});
|
|
312
441
|
});
|
|
313
442
|
|
|
314
443
|
describe('findDefaultKmsKey', () => {
|
|
@@ -340,6 +469,69 @@ describe('AWSDiscovery', () => {
|
|
|
340
469
|
const keyArn = await discovery.findDefaultKmsKey();
|
|
341
470
|
expect(keyArn).toBeNull();
|
|
342
471
|
});
|
|
472
|
+
|
|
473
|
+
it('should return null when only AWS-managed keys exist', async () => {
|
|
474
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
475
|
+
Keys: [{ KeyId: 'aws-key' }]
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
479
|
+
KeyMetadata: {
|
|
480
|
+
Arn: 'arn:aws:kms:us-east-1:123456789012:key/aws-key',
|
|
481
|
+
KeyManager: 'AWS',
|
|
482
|
+
KeyState: 'Enabled'
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
487
|
+
expect(keyArn).toBeNull();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should skip keys that fail to describe', async () => {
|
|
491
|
+
kmsMock.on(ListKeysCommand).resolves({ Keys: [{ KeyId: 'bad-key' }] });
|
|
492
|
+
kmsMock.on(DescribeKeyCommand).rejects(new Error('describe failure'));
|
|
493
|
+
|
|
494
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
495
|
+
expect(keyArn).toBeNull();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should return null when ListKeys fails', async () => {
|
|
499
|
+
kmsMock.on(ListKeysCommand).rejects(new Error('list failure'));
|
|
500
|
+
|
|
501
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
502
|
+
expect(keyArn).toBeNull();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should skip customer keys pending deletion', async () => {
|
|
506
|
+
const mockKeyId = 'pending-key';
|
|
507
|
+
kmsMock.on(ListKeysCommand).resolves({ Keys: [{ KeyId: mockKeyId }] });
|
|
508
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
509
|
+
KeyMetadata: {
|
|
510
|
+
Arn: `arn:aws:kms:us-east-1:123456789012:key/${mockKeyId}`,
|
|
511
|
+
KeyManager: 'CUSTOMER',
|
|
512
|
+
KeyState: 'PendingDeletion',
|
|
513
|
+
DeletionDate: '2024-01-01T00:00:00Z',
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
518
|
+
expect(keyArn).toBeNull();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should return null when all customer keys are disabled', async () => {
|
|
522
|
+
const mockKeyId = 'disabled-key';
|
|
523
|
+
kmsMock.on(ListKeysCommand).resolves({ Keys: [{ KeyId: mockKeyId }] });
|
|
524
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
525
|
+
KeyMetadata: {
|
|
526
|
+
Arn: `arn:aws:kms:us-east-1:123456789012:key/${mockKeyId}`,
|
|
527
|
+
KeyManager: 'CUSTOMER',
|
|
528
|
+
KeyState: 'Disabled',
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
533
|
+
expect(keyArn).toBeNull();
|
|
534
|
+
});
|
|
343
535
|
});
|
|
344
536
|
|
|
345
537
|
describe('findAvailableElasticIP', () => {
|
|
@@ -365,6 +557,32 @@ describe('AWSDiscovery', () => {
|
|
|
365
557
|
const eip = await discovery.findAvailableElasticIP();
|
|
366
558
|
expect(eip).toBeNull();
|
|
367
559
|
});
|
|
560
|
+
|
|
561
|
+
it('should return Frigg-tagged Elastic IP when present', async () => {
|
|
562
|
+
const friggAddress = {
|
|
563
|
+
AllocationId: 'eipalloc-frigg',
|
|
564
|
+
PublicIp: '52.0.0.1',
|
|
565
|
+
NetworkInterfaceId: 'eni-12345',
|
|
566
|
+
Tags: [{ Key: 'Name', Value: 'frigg-shared-ip' }]
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
570
|
+
Addresses: [
|
|
571
|
+
{ AllocationId: 'eipalloc-associated', AssociationId: 'assoc-1' },
|
|
572
|
+
friggAddress,
|
|
573
|
+
]
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
577
|
+
expect(eip).toEqual(friggAddress);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should return null when DescribeAddresses fails', async () => {
|
|
581
|
+
ec2Mock.on(DescribeAddressesCommand).rejects(new Error('addr failure'));
|
|
582
|
+
|
|
583
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
584
|
+
expect(eip).toBeNull();
|
|
585
|
+
});
|
|
368
586
|
});
|
|
369
587
|
|
|
370
588
|
describe('findExistingNatGateway', () => {
|
|
@@ -541,6 +759,197 @@ describe('AWSDiscovery', () => {
|
|
|
541
759
|
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
542
760
|
expect(result).toBeNull();
|
|
543
761
|
});
|
|
762
|
+
|
|
763
|
+
it('should skip NAT Gateways that are not available', async () => {
|
|
764
|
+
const mockNatGateways = [
|
|
765
|
+
{
|
|
766
|
+
NatGatewayId: 'nat-pending',
|
|
767
|
+
SubnetId: 'subnet-public-1',
|
|
768
|
+
State: 'pending',
|
|
769
|
+
Tags: [],
|
|
770
|
+
},
|
|
771
|
+
];
|
|
772
|
+
|
|
773
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
774
|
+
NatGateways: mockNatGateways,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
778
|
+
expect(result).toBeNull();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should return null when only non-Frigg NAT Gateways are in private subnets', async () => {
|
|
782
|
+
const mockNatGateways = [
|
|
783
|
+
{
|
|
784
|
+
NatGatewayId: 'nat-private-only',
|
|
785
|
+
SubnetId: 'subnet-private-1',
|
|
786
|
+
State: 'available',
|
|
787
|
+
Tags: [],
|
|
788
|
+
},
|
|
789
|
+
];
|
|
790
|
+
|
|
791
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
792
|
+
NatGateways: mockNatGateways,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
796
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }],
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
800
|
+
RouteTables: [
|
|
801
|
+
{
|
|
802
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
803
|
+
Routes: [{ GatewayId: 'local' }],
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
809
|
+
expect(result).toBeNull();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should return null when DescribeNatGateways fails', async () => {
|
|
813
|
+
ec2Mock.on(DescribeNatGatewaysCommand).rejects(new Error('nat failure'));
|
|
814
|
+
|
|
815
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
816
|
+
expect(result).toBeNull();
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
describe('findInternetGateway', () => {
|
|
821
|
+
const mockVpcId = 'vpc-12345678';
|
|
822
|
+
|
|
823
|
+
it('should return existing Internet Gateway', async () => {
|
|
824
|
+
const mockIgw = { InternetGatewayId: 'igw-12345' };
|
|
825
|
+
|
|
826
|
+
ec2Mock.on(DescribeInternetGatewaysCommand).resolves({
|
|
827
|
+
InternetGateways: [mockIgw]
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const igw = await discovery.findInternetGateway(mockVpcId);
|
|
831
|
+
expect(igw).toEqual(mockIgw);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should return null when no Internet Gateway found', async () => {
|
|
835
|
+
ec2Mock.on(DescribeInternetGatewaysCommand).resolves({ InternetGateways: [] });
|
|
836
|
+
|
|
837
|
+
const igw = await discovery.findInternetGateway(mockVpcId);
|
|
838
|
+
expect(igw).toBeNull();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should return null when DescribeInternetGateways fails', async () => {
|
|
842
|
+
ec2Mock.on(DescribeInternetGatewaysCommand).rejects(new Error('igw failure'));
|
|
843
|
+
|
|
844
|
+
const igw = await discovery.findInternetGateway(mockVpcId);
|
|
845
|
+
expect(igw).toBeNull();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe('findFriggManagedResources', () => {
|
|
850
|
+
it('should return tagged resources', async () => {
|
|
851
|
+
const mockNat = { NatGatewayId: 'nat-frigg', Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }] };
|
|
852
|
+
const mockEip = { AllocationId: 'eip-frigg', Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }] };
|
|
853
|
+
const mockRouteTable = { RouteTableId: 'rtb-frigg', Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }] };
|
|
854
|
+
const mockSubnet = { SubnetId: 'subnet-frigg', Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }] };
|
|
855
|
+
const mockSg = { GroupId: 'sg-frigg', Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }] };
|
|
856
|
+
|
|
857
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({ NatGateways: [mockNat] });
|
|
858
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({ Addresses: [mockEip] });
|
|
859
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({ RouteTables: [mockRouteTable] });
|
|
860
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: [mockSubnet] });
|
|
861
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({ SecurityGroups: [mockSg] });
|
|
862
|
+
|
|
863
|
+
const result = await discovery.findFriggManagedResources('service', 'stage');
|
|
864
|
+
expect(result).toMatchObject({
|
|
865
|
+
natGateways: [mockNat],
|
|
866
|
+
elasticIps: [mockEip],
|
|
867
|
+
routeTables: [mockRouteTable],
|
|
868
|
+
subnets: [mockSubnet],
|
|
869
|
+
securityGroups: [mockSg],
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should return empty arrays when calls fail', async () => {
|
|
874
|
+
ec2Mock.on(DescribeNatGatewaysCommand).rejects(new Error('error'));
|
|
875
|
+
ec2Mock.on(DescribeAddressesCommand).rejects(new Error('error'));
|
|
876
|
+
ec2Mock.on(DescribeRouteTablesCommand).rejects(new Error('error'));
|
|
877
|
+
ec2Mock.on(DescribeSubnetsCommand).rejects(new Error('error'));
|
|
878
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).rejects(new Error('error'));
|
|
879
|
+
|
|
880
|
+
const result = await discovery.findFriggManagedResources('service', 'stage');
|
|
881
|
+
expect(result).toEqual({
|
|
882
|
+
natGateways: [],
|
|
883
|
+
elasticIps: [],
|
|
884
|
+
routeTables: [],
|
|
885
|
+
subnets: [],
|
|
886
|
+
securityGroups: [],
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
describe('detectMisconfiguredResources', () => {
|
|
892
|
+
const mockVpcId = 'vpc-12345678';
|
|
893
|
+
|
|
894
|
+
it('should capture misconfigured resources', async () => {
|
|
895
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
896
|
+
NatGateways: [
|
|
897
|
+
{ NatGatewayId: 'nat-private', SubnetId: 'subnet-private', State: 'available' },
|
|
898
|
+
],
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
902
|
+
Addresses: [
|
|
903
|
+
{
|
|
904
|
+
AllocationId: 'eip-123',
|
|
905
|
+
PublicIp: '52.0.0.1',
|
|
906
|
+
Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }],
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
discovery.findPrivateSubnets = jest
|
|
912
|
+
.fn()
|
|
913
|
+
.mockResolvedValue([{ SubnetId: 'subnet-private', AvailabilityZone: 'us-east-1a' }]);
|
|
914
|
+
discovery.findRouteTables = jest.fn().mockResolvedValue([
|
|
915
|
+
{
|
|
916
|
+
Associations: [],
|
|
917
|
+
Routes: [],
|
|
918
|
+
},
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
jest.spyOn(discovery, 'isSubnetPrivate').mockResolvedValue(true);
|
|
922
|
+
|
|
923
|
+
const misconfigs = await discovery.detectMisconfiguredResources(mockVpcId);
|
|
924
|
+
expect(misconfigs.natGatewaysInPrivateSubnets).toHaveLength(1);
|
|
925
|
+
expect(misconfigs.orphanedElasticIps).toHaveLength(1);
|
|
926
|
+
expect(misconfigs.privateSubnetsWithoutNatRoute).toHaveLength(1);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
describe('getHealingRecommendations', () => {
|
|
931
|
+
it('should produce ordered recommendations', () => {
|
|
932
|
+
const recs = discovery.getHealingRecommendations({
|
|
933
|
+
natGatewaysInPrivateSubnets: [{ natGatewayId: 'nat-1' }],
|
|
934
|
+
orphanedElasticIps: [{ allocationId: 'eip-1' }],
|
|
935
|
+
privateSubnetsWithoutNatRoute: [{ subnetId: 'subnet-1' }],
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
expect(recs[0].severity).toBe('critical');
|
|
939
|
+
expect(recs[0].issue).toContain('NAT Gateway');
|
|
940
|
+
expect(recs[1].severity).toBe('critical');
|
|
941
|
+
expect(recs[2].severity).toBe('warning');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('should return empty array for no issues', () => {
|
|
945
|
+
const recs = discovery.getHealingRecommendations({
|
|
946
|
+
natGatewaysInPrivateSubnets: [],
|
|
947
|
+
orphanedElasticIps: [],
|
|
948
|
+
privateSubnetsWithoutNatRoute: [],
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
expect(recs).toEqual([]);
|
|
952
|
+
});
|
|
544
953
|
});
|
|
545
954
|
|
|
546
955
|
describe('discoverResources', () => {
|
|
@@ -664,6 +1073,36 @@ describe('AWSDiscovery', () => {
|
|
|
664
1073
|
});
|
|
665
1074
|
});
|
|
666
1075
|
|
|
1076
|
+
it('should reuse available Elastic IP when no NAT Gateway exists and no public subnet is found', async () => {
|
|
1077
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
1078
|
+
const mockSubnets = [
|
|
1079
|
+
{ SubnetId: 'subnet-a' },
|
|
1080
|
+
{ SubnetId: 'subnet-b' }
|
|
1081
|
+
];
|
|
1082
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
1083
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
1084
|
+
const mockElasticIp = { AllocationId: 'eipalloc-available' };
|
|
1085
|
+
|
|
1086
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1087
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1088
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(null);
|
|
1089
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1090
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1091
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1092
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
1093
|
+
const findAvailableElasticIPSpy = jest
|
|
1094
|
+
.spyOn(discovery, 'findAvailableElasticIP')
|
|
1095
|
+
.mockResolvedValue(mockElasticIp);
|
|
1096
|
+
jest.spyOn(discovery, 'isSubnetPrivate').mockResolvedValue(true);
|
|
1097
|
+
|
|
1098
|
+
const result = await discovery.discoverResources();
|
|
1099
|
+
|
|
1100
|
+
expect(result.publicSubnetId).toBeNull();
|
|
1101
|
+
expect(result.existingNatGatewayId).toBeNull();
|
|
1102
|
+
expect(result.existingElasticIpAllocationId).toBe('eipalloc-available');
|
|
1103
|
+
expect(findAvailableElasticIPSpy).toHaveBeenCalled();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
667
1106
|
it('should detect NAT Gateway in private subnet in discoverResources', async () => {
|
|
668
1107
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
669
1108
|
const mockSubnets = [
|
|
@@ -740,4 +1179,4 @@ describe('AWSDiscovery', () => {
|
|
|
740
1179
|
expect(customDiscovery.region).toBe('us-west-2');
|
|
741
1180
|
});
|
|
742
1181
|
});
|
|
743
|
-
});
|
|
1182
|
+
});
|