@friggframework/devtools 2.0.0-next.39 → 2.0.0-next.40
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.
- package/infrastructure/README.md +19 -8
- package/infrastructure/aws-discovery.js +951 -345
- package/infrastructure/aws-discovery.test.js +1031 -184
- package/infrastructure/build-time-discovery.test.js +3 -0
- package/infrastructure/iam-generator.js +46 -0
- package/infrastructure/iam-generator.test.js +7 -4
- package/infrastructure/serverless-template.js +1096 -813
- package/infrastructure/serverless-template.test.js +1036 -21
- package/package.json +8 -6
- package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +0 -245
- package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +0 -627
- package/infrastructure/README-TESTING.md +0 -332
|
@@ -1,298 +1,954 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { mockClient } = require('aws-sdk-client-mock');
|
|
2
2
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
// Import AWS SDK commands
|
|
5
|
+
const {
|
|
6
|
+
EC2Client,
|
|
7
|
+
DescribeVpcsCommand,
|
|
8
|
+
DescribeSubnetsCommand,
|
|
9
|
+
DescribeSecurityGroupsCommand,
|
|
10
|
+
DescribeRouteTablesCommand,
|
|
11
|
+
DescribeNatGatewaysCommand,
|
|
12
|
+
DescribeAddressesCommand,
|
|
13
|
+
DescribeInternetGatewaysCommand,
|
|
14
|
+
} = require('@aws-sdk/client-ec2');
|
|
15
|
+
const {
|
|
16
|
+
KMSClient,
|
|
17
|
+
ListKeysCommand,
|
|
18
|
+
DescribeKeyCommand
|
|
19
|
+
} = require('@aws-sdk/client-kms');
|
|
20
|
+
const {
|
|
21
|
+
STSClient,
|
|
22
|
+
GetCallerIdentityCommand
|
|
23
|
+
} = require('@aws-sdk/client-sts');
|
|
24
|
+
|
|
25
|
+
// Create mock clients
|
|
26
|
+
const ec2Mock = mockClient(EC2Client);
|
|
27
|
+
const kmsMock = mockClient(KMSClient);
|
|
28
|
+
const stsMock = mockClient(STSClient);
|
|
12
29
|
|
|
13
30
|
describe('AWSDiscovery', () => {
|
|
14
31
|
let discovery;
|
|
15
|
-
let mockEC2Send;
|
|
16
|
-
let mockKMSSend;
|
|
17
|
-
let mockSTSSend;
|
|
18
32
|
|
|
19
33
|
beforeEach(() => {
|
|
34
|
+
// Reset all mocks before each test
|
|
35
|
+
ec2Mock.reset();
|
|
36
|
+
kmsMock.reset();
|
|
37
|
+
stsMock.reset();
|
|
38
|
+
|
|
20
39
|
discovery = new AWSDiscovery('us-east-1');
|
|
21
|
-
|
|
22
|
-
// Create mock send functions
|
|
23
|
-
mockEC2Send = jest.fn();
|
|
24
|
-
mockKMSSend = jest.fn();
|
|
25
|
-
mockSTSSend = jest.fn();
|
|
26
|
-
|
|
27
|
-
// Mock the client constructors and send methods
|
|
28
|
-
EC2Client.mockImplementation(() => ({
|
|
29
|
-
send: mockEC2Send
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
KMSClient.mockImplementation(() => ({
|
|
33
|
-
send: mockKMSSend
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
STSClient.mockImplementation(() => ({
|
|
37
|
-
send: mockSTSSend
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// Reset mocks
|
|
41
|
-
jest.clearAllMocks();
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
describe('getAccountId', () => {
|
|
45
43
|
it('should return AWS account ID', async () => {
|
|
46
44
|
const mockAccountId = '123456789012';
|
|
47
|
-
|
|
45
|
+
stsMock.on(GetCallerIdentityCommand).resolves({
|
|
48
46
|
Account: mockAccountId
|
|
49
47
|
});
|
|
50
48
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
expect(result).toBe(mockAccountId);
|
|
54
|
-
expect(mockSTSSend).toHaveBeenCalledWith(expect.any(GetCallerIdentityCommand));
|
|
49
|
+
const accountId = await discovery.getAccountId();
|
|
50
|
+
expect(accountId).toBe(mockAccountId);
|
|
55
51
|
});
|
|
56
52
|
|
|
57
53
|
it('should throw error when STS call fails', async () => {
|
|
58
|
-
|
|
59
|
-
mockSTSSend.mockRejectedValue(error);
|
|
54
|
+
stsMock.on(GetCallerIdentityCommand).rejects(new Error('STS error'));
|
|
60
55
|
|
|
61
|
-
await expect(discovery.getAccountId()).rejects.toThrow('STS
|
|
56
|
+
await expect(discovery.getAccountId()).rejects.toThrow('STS error');
|
|
62
57
|
});
|
|
63
58
|
});
|
|
64
59
|
|
|
65
60
|
describe('findDefaultVpc', () => {
|
|
66
|
-
it('should return default VPC when
|
|
67
|
-
const mockVpc = {
|
|
68
|
-
|
|
69
|
-
IsDefault: true,
|
|
70
|
-
State: 'available'
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
mockEC2Send.mockResolvedValue({
|
|
61
|
+
it('should return default VPC when available', async () => {
|
|
62
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: true };
|
|
63
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
74
64
|
Vpcs: [mockVpc]
|
|
75
65
|
});
|
|
76
66
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
expect(result).toEqual(mockVpc);
|
|
80
|
-
expect(mockEC2Send).toHaveBeenCalledWith(expect.objectContaining({
|
|
81
|
-
input: {
|
|
82
|
-
Filters: [{
|
|
83
|
-
Name: 'is-default',
|
|
84
|
-
Values: ['true']
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
67
|
+
const vpc = await discovery.findDefaultVpc();
|
|
68
|
+
expect(vpc).toEqual(mockVpc);
|
|
88
69
|
});
|
|
89
70
|
|
|
90
|
-
it('should return first
|
|
91
|
-
const mockVpc = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
71
|
+
it('should return first VPC when no default VPC exists', async () => {
|
|
72
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: false };
|
|
73
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
74
|
+
Vpcs: [mockVpc]
|
|
75
|
+
});
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
77
|
+
const vpc = await discovery.findDefaultVpc();
|
|
78
|
+
expect(vpc).toEqual(mockVpc);
|
|
79
|
+
});
|
|
100
80
|
|
|
101
|
-
|
|
81
|
+
it('should retry without default filter when default VPC query returns empty', async () => {
|
|
82
|
+
const mockVpc = { VpcId: 'vpc-23456789', IsDefault: false };
|
|
102
83
|
|
|
103
|
-
|
|
104
|
-
|
|
84
|
+
ec2Mock
|
|
85
|
+
.on(DescribeVpcsCommand)
|
|
86
|
+
.resolvesOnce({ Vpcs: [] })
|
|
87
|
+
.resolves({ Vpcs: [mockVpc] });
|
|
88
|
+
|
|
89
|
+
const vpc = await discovery.findDefaultVpc();
|
|
90
|
+
expect(vpc).toEqual(mockVpc);
|
|
105
91
|
});
|
|
106
92
|
|
|
107
93
|
it('should throw error when no VPCs found', async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
94
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
95
|
+
Vpcs: []
|
|
96
|
+
});
|
|
111
97
|
|
|
112
98
|
await expect(discovery.findDefaultVpc()).rejects.toThrow('No VPC found in the account');
|
|
113
99
|
});
|
|
114
100
|
});
|
|
115
101
|
|
|
102
|
+
describe('isSubnetPrivate', () => {
|
|
103
|
+
const mockVpcId = 'vpc-12345678';
|
|
104
|
+
const mockSubnetId = 'subnet-12345678';
|
|
105
|
+
|
|
106
|
+
it('should return true for private subnet', async () => {
|
|
107
|
+
// Mock subnet lookup first
|
|
108
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
109
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
113
|
+
RouteTables: [{
|
|
114
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
115
|
+
Routes: [
|
|
116
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
117
|
+
]
|
|
118
|
+
}]
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
122
|
+
expect(isPrivate).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return false for public subnet', async () => {
|
|
126
|
+
// Mock subnet lookup first
|
|
127
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
128
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
132
|
+
RouteTables: [{
|
|
133
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
134
|
+
Routes: [
|
|
135
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
136
|
+
]
|
|
137
|
+
}]
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
141
|
+
expect(isPrivate).toBe(false);
|
|
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
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
116
178
|
describe('findPrivateSubnets', () => {
|
|
117
179
|
const mockVpcId = 'vpc-12345678';
|
|
118
180
|
|
|
119
|
-
it('should return private subnets
|
|
181
|
+
it('should return private subnets', async () => {
|
|
120
182
|
const mockSubnets = [
|
|
121
|
-
{ SubnetId: 'subnet-private-1',
|
|
122
|
-
{ SubnetId: 'subnet-private-2',
|
|
183
|
+
{ SubnetId: 'subnet-private-1', AvailabilityZone: 'us-east-1a' },
|
|
184
|
+
{ SubnetId: 'subnet-private-2', AvailabilityZone: 'us-east-1b' }
|
|
123
185
|
];
|
|
124
186
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
187
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
188
|
+
Subnets: mockSubnets
|
|
189
|
+
});
|
|
128
190
|
|
|
129
|
-
|
|
191
|
+
// Mock route tables - no IGW routes (private)
|
|
192
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
193
|
+
RouteTables: [{
|
|
194
|
+
Associations: [
|
|
195
|
+
{ SubnetId: 'subnet-private-1' },
|
|
196
|
+
{ SubnetId: 'subnet-private-2' }
|
|
197
|
+
],
|
|
198
|
+
Routes: [
|
|
199
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
200
|
+
]
|
|
201
|
+
}]
|
|
202
|
+
});
|
|
130
203
|
|
|
131
|
-
|
|
132
|
-
expect(
|
|
133
|
-
expect(result[1].SubnetId).toBe('subnet-private-2');
|
|
204
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId);
|
|
205
|
+
expect(subnets).toEqual(mockSubnets);
|
|
134
206
|
});
|
|
135
207
|
|
|
136
|
-
it('should
|
|
208
|
+
it('should duplicate single private subnet when only one available', async () => {
|
|
137
209
|
const mockSubnets = [
|
|
138
|
-
{ SubnetId: 'subnet-1',
|
|
139
|
-
{ SubnetId: 'subnet-
|
|
140
|
-
{ SubnetId: 'subnet-3', VpcId: mockVpcId }
|
|
210
|
+
{ SubnetId: 'subnet-private-1', AvailabilityZone: 'us-east-1a' },
|
|
211
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1b' }
|
|
141
212
|
];
|
|
142
213
|
|
|
143
|
-
|
|
144
|
-
.mockResolvedValueOnce({ Subnets: mockSubnets }) // DescribeSubnets
|
|
145
|
-
.mockResolvedValue({ // Only one private subnet
|
|
146
|
-
RouteTables: [{
|
|
147
|
-
Routes: [] // No IGW route = private
|
|
148
|
-
}]
|
|
149
|
-
});
|
|
214
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({ Subnets: mockSubnets });
|
|
150
215
|
|
|
151
|
-
|
|
216
|
+
jest
|
|
217
|
+
.spyOn(discovery, 'isSubnetPrivate')
|
|
218
|
+
.mockResolvedValueOnce(true)
|
|
219
|
+
.mockResolvedValueOnce(false);
|
|
152
220
|
|
|
153
|
-
|
|
221
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId);
|
|
222
|
+
expect(subnets.map((s) => s.SubnetId)).toEqual([
|
|
223
|
+
'subnet-private-1',
|
|
224
|
+
'subnet-public-1'
|
|
225
|
+
]);
|
|
154
226
|
});
|
|
155
227
|
|
|
156
|
-
it('should throw error when no subnets found', async () => {
|
|
157
|
-
|
|
228
|
+
it('should throw error when no private subnets found and autoConvert is false', async () => {
|
|
229
|
+
const mockSubnets = [
|
|
230
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
231
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' },
|
|
232
|
+
{ SubnetId: 'subnet-public-3', AvailabilityZone: 'us-east-1c' }
|
|
233
|
+
];
|
|
158
234
|
|
|
159
|
-
|
|
235
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
236
|
+
Subnets: mockSubnets
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Mock route tables - has IGW routes (public)
|
|
240
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
241
|
+
RouteTables: [{
|
|
242
|
+
Associations: [
|
|
243
|
+
{ SubnetId: 'subnet-public-1' },
|
|
244
|
+
{ SubnetId: 'subnet-public-2' },
|
|
245
|
+
{ SubnetId: 'subnet-public-3' }
|
|
246
|
+
],
|
|
247
|
+
Routes: [
|
|
248
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
249
|
+
]
|
|
250
|
+
}]
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await expect(discovery.findPrivateSubnets(mockVpcId, false))
|
|
254
|
+
.rejects.toThrow('No private subnets found in VPC');
|
|
160
255
|
});
|
|
161
|
-
});
|
|
162
256
|
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
it('should return public subnets with warning when autoConvert is true', async () => {
|
|
258
|
+
const mockSubnets = [
|
|
259
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
260
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' }
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
264
|
+
Subnets: mockSubnets
|
|
265
|
+
});
|
|
165
266
|
|
|
166
|
-
|
|
167
|
-
|
|
267
|
+
// Mock route tables - has IGW routes (public)
|
|
268
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
168
269
|
RouteTables: [{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
270
|
+
Associations: [
|
|
271
|
+
{ SubnetId: 'subnet-public-1' },
|
|
272
|
+
{ SubnetId: 'subnet-public-2' }
|
|
273
|
+
],
|
|
274
|
+
Routes: [
|
|
275
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
276
|
+
]
|
|
173
277
|
}]
|
|
174
278
|
});
|
|
175
279
|
|
|
176
|
-
const
|
|
280
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId, true);
|
|
281
|
+
expect(subnets).toHaveLength(2);
|
|
282
|
+
expect(subnets[0].SubnetId).toBe('subnet-public-1');
|
|
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: [] });
|
|
177
304
|
|
|
178
|
-
expect(
|
|
305
|
+
await expect(discovery.findPrivateSubnets(mockVpcId)).rejects.toThrow(
|
|
306
|
+
`No subnets found in VPC ${mockVpcId}`
|
|
307
|
+
);
|
|
179
308
|
});
|
|
309
|
+
});
|
|
180
310
|
|
|
181
|
-
|
|
182
|
-
|
|
311
|
+
describe('findPublicSubnets', () => {
|
|
312
|
+
const mockVpcId = 'vpc-12345678';
|
|
313
|
+
|
|
314
|
+
it('should return public subnet', async () => {
|
|
315
|
+
const mockSubnet = { SubnetId: 'subnet-public-1' };
|
|
316
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
317
|
+
Subnets: [mockSubnet]
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Mock route table check to show it's public
|
|
321
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
183
322
|
RouteTables: [{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
DestinationCidrBlock: '10.0.0.0/16'
|
|
187
|
-
}]
|
|
323
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
324
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Has IGW = public
|
|
188
325
|
}]
|
|
189
326
|
});
|
|
190
327
|
|
|
191
|
-
const
|
|
328
|
+
const subnet = await discovery.findPublicSubnets(mockVpcId);
|
|
329
|
+
expect(subnet).toEqual(mockSubnet);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should throw error when no subnets found', async () => {
|
|
333
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
334
|
+
Subnets: []
|
|
335
|
+
});
|
|
192
336
|
|
|
193
|
-
expect(
|
|
337
|
+
await expect(discovery.findPublicSubnets(mockVpcId))
|
|
338
|
+
.rejects.toThrow('No subnets found in VPC');
|
|
194
339
|
});
|
|
195
340
|
|
|
196
|
-
it('should
|
|
197
|
-
|
|
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] });
|
|
198
344
|
|
|
199
|
-
|
|
345
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
346
|
+
RouteTables: [{
|
|
347
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
348
|
+
Routes: [{ GatewayId: 'local' }]
|
|
349
|
+
}]
|
|
350
|
+
});
|
|
200
351
|
|
|
201
|
-
|
|
352
|
+
const subnet = await discovery.findPublicSubnets(mockVpcId);
|
|
353
|
+
expect(subnet).toBeNull();
|
|
202
354
|
});
|
|
203
355
|
});
|
|
204
356
|
|
|
205
357
|
describe('findDefaultSecurityGroup', () => {
|
|
206
358
|
const mockVpcId = 'vpc-12345678';
|
|
207
359
|
|
|
208
|
-
it('should return
|
|
209
|
-
const
|
|
210
|
-
GroupId: 'sg-
|
|
360
|
+
it('should return default security group', async () => {
|
|
361
|
+
const mockSecurityGroup = {
|
|
362
|
+
GroupId: 'sg-12345678',
|
|
363
|
+
GroupName: 'default'
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
ec2Mock
|
|
367
|
+
.on(DescribeSecurityGroupsCommand)
|
|
368
|
+
.resolvesOnce({ SecurityGroups: [] })
|
|
369
|
+
.resolves({ SecurityGroups: [mockSecurityGroup] });
|
|
370
|
+
|
|
371
|
+
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
372
|
+
expect(sg).toEqual(mockSecurityGroup);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should prefer Frigg-managed security group when present', async () => {
|
|
376
|
+
const friggSg = {
|
|
377
|
+
GroupId: 'sg-frigg',
|
|
211
378
|
GroupName: 'frigg-lambda-sg',
|
|
212
|
-
VpcId: mockVpcId
|
|
213
379
|
};
|
|
214
380
|
|
|
215
|
-
|
|
216
|
-
|
|
381
|
+
ec2Mock
|
|
382
|
+
.on(DescribeSecurityGroupsCommand)
|
|
383
|
+
.resolves({ SecurityGroups: [friggSg] });
|
|
384
|
+
|
|
385
|
+
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
386
|
+
expect(sg).toEqual(friggSg);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should throw error when no default security group found', async () => {
|
|
390
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
391
|
+
SecurityGroups: []
|
|
217
392
|
});
|
|
218
393
|
|
|
219
|
-
|
|
394
|
+
await expect(discovery.findDefaultSecurityGroup(mockVpcId))
|
|
395
|
+
.rejects.toThrow('No security group found for VPC');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
220
398
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
399
|
+
describe('findPrivateRouteTable', () => {
|
|
400
|
+
const mockVpcId = 'vpc-12345678';
|
|
401
|
+
|
|
402
|
+
it('should return private route table', async () => {
|
|
403
|
+
const mockRouteTable = {
|
|
404
|
+
RouteTableId: 'rtb-12345678',
|
|
405
|
+
Routes: [
|
|
406
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
407
|
+
]
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
411
|
+
RouteTables: [mockRouteTable]
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
415
|
+
expect(rt).toEqual(mockRouteTable);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should return first route table when no private route table found', async () => {
|
|
419
|
+
const mockRouteTable = {
|
|
420
|
+
RouteTableId: 'rtb-12345678',
|
|
421
|
+
Routes: [
|
|
422
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
423
|
+
]
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
427
|
+
RouteTables: [mockRouteTable]
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
431
|
+
expect(rt).toEqual(mockRouteTable);
|
|
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
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('findDefaultKmsKey', () => {
|
|
444
|
+
it('should return default KMS key ARN', async () => {
|
|
445
|
+
const mockKeyId = '12345678-1234-1234-1234-123456789012';
|
|
446
|
+
const mockKeyArn = `arn:aws:kms:us-east-1:123456789012:key/${mockKeyId}`;
|
|
447
|
+
|
|
448
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
449
|
+
Keys: [{ KeyId: mockKeyId }]
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
453
|
+
KeyMetadata: {
|
|
454
|
+
Arn: mockKeyArn,
|
|
455
|
+
KeyManager: 'CUSTOMER',
|
|
456
|
+
KeyState: 'Enabled'
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
461
|
+
expect(keyArn).toBe(mockKeyArn);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should return null when no AWS-managed keys found', async () => {
|
|
465
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
466
|
+
Keys: []
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
470
|
+
expect(keyArn).toBeNull();
|
|
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'
|
|
228
483
|
}
|
|
229
|
-
})
|
|
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();
|
|
230
496
|
});
|
|
231
497
|
|
|
232
|
-
it('should
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe('findAvailableElasticIP', () => {
|
|
538
|
+
it('should return available Elastic IP', async () => {
|
|
539
|
+
const mockElasticIP = {
|
|
540
|
+
AllocationId: 'eipalloc-12345',
|
|
541
|
+
PublicIp: '52.1.2.3'
|
|
237
542
|
};
|
|
238
543
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
544
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
545
|
+
Addresses: [mockElasticIP]
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
549
|
+
expect(eip).toEqual(mockElasticIP);
|
|
550
|
+
});
|
|
242
551
|
|
|
243
|
-
|
|
552
|
+
it('should return null when no available Elastic IPs', async () => {
|
|
553
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
554
|
+
Addresses: []
|
|
555
|
+
});
|
|
244
556
|
|
|
245
|
-
|
|
246
|
-
expect(
|
|
557
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
558
|
+
expect(eip).toBeNull();
|
|
247
559
|
});
|
|
248
560
|
|
|
249
|
-
it('should
|
|
250
|
-
|
|
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
|
+
};
|
|
251
568
|
|
|
252
|
-
|
|
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();
|
|
253
585
|
});
|
|
254
586
|
});
|
|
255
587
|
|
|
256
|
-
describe('
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
588
|
+
describe('findExistingNatGateway', () => {
|
|
589
|
+
const mockVpcId = 'vpc-12345678';
|
|
590
|
+
|
|
591
|
+
beforeEach(() => {
|
|
592
|
+
// Create a fresh discovery instance for each test
|
|
593
|
+
discovery = new AWSDiscovery('us-east-1');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should return NAT Gateway in public subnet', async () => {
|
|
597
|
+
const mockNatGateway = {
|
|
598
|
+
NatGatewayId: 'nat-12345678',
|
|
599
|
+
SubnetId: 'subnet-public-1',
|
|
600
|
+
State: 'available',
|
|
601
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
602
|
+
Tags: []
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
606
|
+
NatGateways: [mockNatGateway]
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Mock subnet lookup
|
|
610
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
611
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Mock route table - has IGW (public)
|
|
615
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
616
|
+
RouteTables: [{
|
|
617
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
618
|
+
Routes: [{ GatewayId: 'igw-12345' }]
|
|
619
|
+
}]
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
623
|
+
|
|
624
|
+
expect(result).toBeDefined();
|
|
625
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
626
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should detect NAT Gateway in private subnet', async () => {
|
|
630
|
+
const mockNatGateway = {
|
|
631
|
+
NatGatewayId: 'nat-12345678',
|
|
632
|
+
SubnetId: 'subnet-private-1',
|
|
633
|
+
State: 'available',
|
|
634
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
635
|
+
Tags: [
|
|
636
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
637
|
+
]
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
641
|
+
NatGateways: [mockNatGateway]
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
645
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Mock route table - no IGW (private)
|
|
649
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
650
|
+
RouteTables: [{
|
|
651
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
652
|
+
Routes: [{ GatewayId: 'local' }]
|
|
653
|
+
}]
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
657
|
+
|
|
658
|
+
expect(result).toBeDefined();
|
|
659
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
660
|
+
expect(result._isInPrivateSubnet).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('should skip non-Frigg NAT Gateway in private subnet', async () => {
|
|
664
|
+
const mockNatGateways = [
|
|
665
|
+
{
|
|
666
|
+
NatGatewayId: 'nat-other-12345',
|
|
667
|
+
SubnetId: 'subnet-private-1',
|
|
668
|
+
State: 'available',
|
|
669
|
+
Tags: [] // No Frigg tags
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
NatGatewayId: 'nat-good-12345',
|
|
673
|
+
SubnetId: 'subnet-public-1',
|
|
674
|
+
State: 'available',
|
|
675
|
+
Tags: []
|
|
676
|
+
}
|
|
677
|
+
];
|
|
678
|
+
|
|
679
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
680
|
+
NatGateways: mockNatGateways
|
|
681
|
+
});
|
|
260
682
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
683
|
+
// First call for subnet-private-1
|
|
684
|
+
ec2Mock.on(DescribeSubnetsCommand)
|
|
685
|
+
.resolvesOnce({
|
|
686
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
264
687
|
})
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Arn: mockKeyArn,
|
|
269
|
-
KeyManager: 'CUSTOMER',
|
|
270
|
-
KeyState: 'Enabled'
|
|
271
|
-
}
|
|
688
|
+
// Second call for subnet-public-1
|
|
689
|
+
.resolvesOnce({
|
|
690
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
272
691
|
});
|
|
273
692
|
|
|
274
|
-
|
|
693
|
+
// First call for private subnet route table
|
|
694
|
+
ec2Mock.on(DescribeRouteTablesCommand)
|
|
695
|
+
.resolvesOnce({
|
|
696
|
+
RouteTables: [{
|
|
697
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
698
|
+
Routes: [{ GatewayId: 'local' }] // Private
|
|
699
|
+
}]
|
|
700
|
+
})
|
|
701
|
+
// Second call for public subnet route table
|
|
702
|
+
.resolvesOnce({
|
|
703
|
+
RouteTables: [{
|
|
704
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
705
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
706
|
+
}]
|
|
707
|
+
});
|
|
275
708
|
|
|
276
|
-
const result = await discovery.
|
|
709
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
277
710
|
|
|
278
|
-
expect(result).
|
|
711
|
+
expect(result).toBeDefined();
|
|
712
|
+
expect(result.NatGatewayId).toBe('nat-good-12345');
|
|
713
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
279
714
|
});
|
|
280
715
|
|
|
281
|
-
it('should
|
|
282
|
-
|
|
283
|
-
|
|
716
|
+
it('should prioritize Frigg-managed NAT Gateways', async () => {
|
|
717
|
+
const mockNatGateways = [
|
|
718
|
+
{
|
|
719
|
+
NatGatewayId: 'nat-other-12345',
|
|
720
|
+
SubnetId: 'subnet-public-1',
|
|
721
|
+
State: 'available',
|
|
722
|
+
Tags: []
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
NatGatewayId: 'nat-frigg-12345',
|
|
726
|
+
SubnetId: 'subnet-public-2',
|
|
727
|
+
State: 'available',
|
|
728
|
+
Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }]
|
|
729
|
+
}
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
733
|
+
NatGateways: mockNatGateways
|
|
734
|
+
});
|
|
284
735
|
|
|
285
|
-
|
|
736
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
737
|
+
Subnets: [{ SubnetId: 'subnet-public-2', VpcId: mockVpcId }]
|
|
738
|
+
});
|
|
286
739
|
|
|
287
|
-
|
|
740
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
741
|
+
RouteTables: [{
|
|
742
|
+
Associations: [{ SubnetId: 'subnet-public-2' }],
|
|
743
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
744
|
+
}]
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
748
|
+
|
|
749
|
+
expect(result).toBeDefined();
|
|
750
|
+
expect(result.NatGatewayId).toBe('nat-frigg-12345');
|
|
751
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
288
752
|
});
|
|
289
753
|
|
|
290
|
-
it('should return
|
|
291
|
-
|
|
754
|
+
it('should return null when no NAT Gateways found', async () => {
|
|
755
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
756
|
+
NatGateways: []
|
|
757
|
+
});
|
|
292
758
|
|
|
293
|
-
const result = await discovery.
|
|
759
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
760
|
+
expect(result).toBeNull();
|
|
761
|
+
});
|
|
294
762
|
|
|
295
|
-
|
|
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([]);
|
|
296
952
|
});
|
|
297
953
|
});
|
|
298
954
|
|
|
@@ -300,48 +956,239 @@ describe('AWSDiscovery', () => {
|
|
|
300
956
|
it('should discover all AWS resources successfully', async () => {
|
|
301
957
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
302
958
|
const mockSubnets = [
|
|
303
|
-
{ SubnetId: 'subnet-1' },
|
|
959
|
+
{ SubnetId: 'subnet-1' },
|
|
304
960
|
{ SubnetId: 'subnet-2' }
|
|
305
961
|
];
|
|
962
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
306
963
|
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
307
964
|
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
308
965
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
966
|
+
const mockNatGateway = {
|
|
967
|
+
NatGatewayId: 'nat-12345678',
|
|
968
|
+
SubnetId: 'subnet-public-1',
|
|
969
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
970
|
+
_isInPrivateSubnet: false
|
|
971
|
+
};
|
|
309
972
|
|
|
310
973
|
// Mock all the discovery methods
|
|
311
974
|
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
312
975
|
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
976
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
313
977
|
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
314
978
|
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
315
979
|
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
980
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(mockNatGateway);
|
|
981
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
982
|
+
.mockResolvedValueOnce(true) // subnet-1 is private
|
|
983
|
+
.mockResolvedValueOnce(true); // subnet-2 is private
|
|
316
984
|
|
|
317
985
|
const result = await discovery.discoverResources();
|
|
318
986
|
|
|
319
|
-
expect(result).
|
|
987
|
+
expect(result).toMatchObject({
|
|
320
988
|
defaultVpcId: 'vpc-12345678',
|
|
321
989
|
defaultSecurityGroupId: 'sg-12345678',
|
|
322
990
|
privateSubnetId1: 'subnet-1',
|
|
323
991
|
privateSubnetId2: 'subnet-2',
|
|
992
|
+
publicSubnetId: 'subnet-public-1',
|
|
324
993
|
privateRouteTableId: 'rtb-12345678',
|
|
325
|
-
defaultKmsKeyId: mockKmsArn
|
|
994
|
+
defaultKmsKeyId: mockKmsArn,
|
|
995
|
+
existingNatGatewayId: 'nat-12345678',
|
|
996
|
+
existingElasticIpAllocationId: 'eipalloc-12345',
|
|
997
|
+
natGatewayInPrivateSubnet: false,
|
|
998
|
+
subnetConversionRequired: false,
|
|
999
|
+
privateSubnetsWithWrongRoutes: []
|
|
326
1000
|
});
|
|
327
1001
|
|
|
328
1002
|
// Verify all methods were called
|
|
329
1003
|
expect(discovery.findDefaultVpc).toHaveBeenCalled();
|
|
330
|
-
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678');
|
|
1004
|
+
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678', false);
|
|
1005
|
+
expect(discovery.findPublicSubnets).toHaveBeenCalledWith('vpc-12345678');
|
|
331
1006
|
expect(discovery.findDefaultSecurityGroup).toHaveBeenCalledWith('vpc-12345678');
|
|
332
1007
|
expect(discovery.findPrivateRouteTable).toHaveBeenCalledWith('vpc-12345678');
|
|
333
1008
|
expect(discovery.findDefaultKmsKey).toHaveBeenCalled();
|
|
1009
|
+
expect(discovery.findExistingNatGateway).toHaveBeenCalledWith('vpc-12345678');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('should detect subnet conversion requirements', async () => {
|
|
1013
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
1014
|
+
const mockSubnets = [
|
|
1015
|
+
{ SubnetId: 'subnet-1' },
|
|
1016
|
+
{ SubnetId: 'subnet-2' }
|
|
1017
|
+
];
|
|
1018
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
1019
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
1020
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
1021
|
+
|
|
1022
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1023
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1024
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
1025
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1026
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1027
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1028
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
1029
|
+
jest.spyOn(discovery, 'findAvailableElasticIP').mockResolvedValue(null);
|
|
1030
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
1031
|
+
.mockImplementation((subnetId) => {
|
|
1032
|
+
// subnet-1 is public, subnet-2 is private
|
|
1033
|
+
return Promise.resolve(subnetId === 'subnet-2');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const result = await discovery.discoverResources({ selfHeal: true });
|
|
1037
|
+
|
|
1038
|
+
expect(result).toMatchObject({
|
|
1039
|
+
defaultVpcId: 'vpc-12345678',
|
|
1040
|
+
privateSubnetId1: 'subnet-1',
|
|
1041
|
+
privateSubnetId2: 'subnet-2',
|
|
1042
|
+
publicSubnetId: 'subnet-public-1',
|
|
1043
|
+
subnetConversionRequired: true,
|
|
1044
|
+
privateSubnetsWithWrongRoutes: ['subnet-1']
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('should surface subnet analysis summary for diagnostic tooling', async () => {
|
|
1049
|
+
const mockVpc = { VpcId: 'vpc-987654321', CidrBlock: '10.0.0.0/16' };
|
|
1050
|
+
const mockSubnets = [
|
|
1051
|
+
{ SubnetId: 'subnet-public-a', AvailabilityZone: 'us-east-1a' },
|
|
1052
|
+
{ SubnetId: 'subnet-private-b', AvailabilityZone: 'us-east-1b' }
|
|
1053
|
+
];
|
|
1054
|
+
const mockSecurityGroup = { GroupId: 'sg-22222222' };
|
|
1055
|
+
const mockRouteTable = { RouteTableId: 'rtb-22222222' };
|
|
1056
|
+
|
|
1057
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1058
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1059
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue({ SubnetId: 'subnet-nat-home' });
|
|
1060
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1061
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1062
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1063
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue({
|
|
1064
|
+
NatGatewayId: 'nat-2222',
|
|
1065
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-2222' }],
|
|
1066
|
+
_isInPrivateSubnet: false
|
|
1067
|
+
});
|
|
1068
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
1069
|
+
.mockImplementation((subnetId) => subnetId === 'subnet-private-b');
|
|
1070
|
+
|
|
1071
|
+
const result = await discovery.discoverResources({ selfHeal: true });
|
|
1072
|
+
|
|
1073
|
+
expect(result.defaultVpcId).toBe('vpc-987654321');
|
|
1074
|
+
expect(result.subnetConversionRequired).toBe(true);
|
|
1075
|
+
expect(result.privateSubnetsWithWrongRoutes).toEqual(['subnet-public-a']);
|
|
1076
|
+
expect(result.privateSubnetId1).toBe('subnet-public-a');
|
|
1077
|
+
expect(result.privateSubnetId2).toBe('subnet-private-b');
|
|
1078
|
+
expect(result.existingNatGatewayId).toBe('nat-2222');
|
|
1079
|
+
expect(result.existingElasticIpAllocationId).toBe('eipalloc-2222');
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it('should handle selfHeal option', async () => {
|
|
1083
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
1084
|
+
const mockSubnets = [
|
|
1085
|
+
{ SubnetId: 'subnet-public-1' },
|
|
1086
|
+
{ SubnetId: 'subnet-public-2' }
|
|
1087
|
+
];
|
|
1088
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-3' };
|
|
1089
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
1090
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
1091
|
+
|
|
1092
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1093
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1094
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
1095
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1096
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1097
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1098
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
1099
|
+
jest.spyOn(discovery, 'findAvailableElasticIP').mockResolvedValue(null);
|
|
1100
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
1101
|
+
.mockResolvedValue(false); // All subnets are public
|
|
1102
|
+
|
|
1103
|
+
const result = await discovery.discoverResources({ selfHeal: true });
|
|
1104
|
+
|
|
1105
|
+
// Verify that findPrivateSubnets was called with autoConvert=true
|
|
1106
|
+
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678', true);
|
|
1107
|
+
|
|
1108
|
+
expect(result).toMatchObject({
|
|
1109
|
+
subnetConversionRequired: true,
|
|
1110
|
+
privateSubnetsWithWrongRoutes: ['subnet-public-1', 'subnet-public-2']
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('should reuse available Elastic IP when no NAT Gateway exists and no public subnet is found', async () => {
|
|
1115
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
1116
|
+
const mockSubnets = [
|
|
1117
|
+
{ SubnetId: 'subnet-a' },
|
|
1118
|
+
{ SubnetId: 'subnet-b' }
|
|
1119
|
+
];
|
|
1120
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
1121
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
1122
|
+
const mockElasticIp = { AllocationId: 'eipalloc-available' };
|
|
1123
|
+
|
|
1124
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1125
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1126
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(null);
|
|
1127
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1128
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1129
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1130
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
1131
|
+
const findAvailableElasticIPSpy = jest
|
|
1132
|
+
.spyOn(discovery, 'findAvailableElasticIP')
|
|
1133
|
+
.mockResolvedValue(mockElasticIp);
|
|
1134
|
+
jest.spyOn(discovery, 'isSubnetPrivate').mockResolvedValue(true);
|
|
1135
|
+
|
|
1136
|
+
const result = await discovery.discoverResources();
|
|
1137
|
+
|
|
1138
|
+
expect(result.publicSubnetId).toBeNull();
|
|
1139
|
+
expect(result.existingNatGatewayId).toBeNull();
|
|
1140
|
+
expect(result.existingElasticIpAllocationId).toBe('eipalloc-available');
|
|
1141
|
+
expect(findAvailableElasticIPSpy).toHaveBeenCalled();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it('should detect NAT Gateway in private subnet in discoverResources', async () => {
|
|
1145
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
1146
|
+
const mockSubnets = [
|
|
1147
|
+
{ SubnetId: 'subnet-1' },
|
|
1148
|
+
{ SubnetId: 'subnet-2' }
|
|
1149
|
+
];
|
|
1150
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
1151
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
1152
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
1153
|
+
const mockNatGateway = {
|
|
1154
|
+
NatGatewayId: 'nat-12345678',
|
|
1155
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
1156
|
+
_isInPrivateSubnet: true // NAT is in private subnet
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
1160
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1161
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
1162
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
1163
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
1164
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
1165
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(mockNatGateway);
|
|
1166
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
1167
|
+
.mockResolvedValueOnce(true)
|
|
1168
|
+
.mockResolvedValueOnce(true);
|
|
1169
|
+
|
|
1170
|
+
const result = await discovery.discoverResources();
|
|
1171
|
+
|
|
1172
|
+
expect(result).toMatchObject({
|
|
1173
|
+
defaultVpcId: 'vpc-12345678',
|
|
1174
|
+
existingNatGatewayId: 'nat-12345678',
|
|
1175
|
+
natGatewayInPrivateSubnet: true, // Should be true
|
|
1176
|
+
subnetConversionRequired: false,
|
|
1177
|
+
privateSubnetsWithWrongRoutes: []
|
|
1178
|
+
});
|
|
334
1179
|
});
|
|
335
1180
|
|
|
336
1181
|
it('should handle single subnet scenario', async () => {
|
|
337
1182
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
338
1183
|
const mockSubnets = [{ SubnetId: 'subnet-1' }]; // Only one subnet
|
|
1184
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
339
1185
|
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
340
1186
|
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
341
1187
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
342
1188
|
|
|
343
1189
|
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
344
1190
|
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
1191
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
345
1192
|
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
346
1193
|
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
347
1194
|
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
@@ -370,4 +1217,4 @@ describe('AWSDiscovery', () => {
|
|
|
370
1217
|
expect(customDiscovery.region).toBe('us-west-2');
|
|
371
1218
|
});
|
|
372
1219
|
});
|
|
373
|
-
});
|
|
1220
|
+
});
|