@friggframework/devtools 2.0.0--canary.474.082077e.0 → 2.0.0--canary.474.884529c.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.
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +397 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +461 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/package.json +6 -6
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AWSResourceImporter Adapter
|
|
3
|
+
*
|
|
4
|
+
* Tests CloudFormation resource import operations using mocked AWS SDK
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const AWSResourceImporter = require('./aws-resource-importer');
|
|
8
|
+
const StackIdentifier = require('../../domain/value-objects/stack-identifier');
|
|
9
|
+
|
|
10
|
+
// Mock AWS SDK
|
|
11
|
+
jest.mock('@aws-sdk/client-cloudformation', () => ({
|
|
12
|
+
CloudFormationClient: jest.fn(),
|
|
13
|
+
CreateChangeSetCommand: jest.fn(),
|
|
14
|
+
DescribeChangeSetCommand: jest.fn(),
|
|
15
|
+
ExecuteChangeSetCommand: jest.fn(),
|
|
16
|
+
GetTemplateCommand: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('AWSResourceImporter', () => {
|
|
20
|
+
let importer;
|
|
21
|
+
let mockSend;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
|
|
26
|
+
mockSend = jest.fn();
|
|
27
|
+
const { CloudFormationClient } = require('@aws-sdk/client-cloudformation');
|
|
28
|
+
CloudFormationClient.mockImplementation(() => ({ send: mockSend }));
|
|
29
|
+
|
|
30
|
+
importer = new AWSResourceImporter({ region: 'us-east-1' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('supportsImport', () => {
|
|
34
|
+
it('should return true for VPC', async () => {
|
|
35
|
+
const supports = await importer.supportsImport('AWS::EC2::VPC');
|
|
36
|
+
expect(supports).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return true for RDS DBCluster', async () => {
|
|
40
|
+
const supports = await importer.supportsImport('AWS::RDS::DBCluster');
|
|
41
|
+
expect(supports).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return true for KMS Key', async () => {
|
|
45
|
+
const supports = await importer.supportsImport('AWS::KMS::Key');
|
|
46
|
+
expect(supports).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return false for unsupported type', async () => {
|
|
50
|
+
const supports = await importer.supportsImport('AWS::Lambda::Function');
|
|
51
|
+
expect(supports).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getIdentifierProperty', () => {
|
|
56
|
+
it('should return VpcId for VPC', async () => {
|
|
57
|
+
const prop = await importer.getIdentifierProperty('AWS::EC2::VPC');
|
|
58
|
+
expect(prop).toBe('VpcId');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return SubnetId for Subnet', async () => {
|
|
62
|
+
const prop = await importer.getIdentifierProperty('AWS::EC2::Subnet');
|
|
63
|
+
expect(prop).toBe('SubnetId');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return DBClusterIdentifier for RDS DBCluster', async () => {
|
|
67
|
+
const prop = await importer.getIdentifierProperty('AWS::RDS::DBCluster');
|
|
68
|
+
expect(prop).toBe('DBClusterIdentifier');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw error for unsupported type', async () => {
|
|
72
|
+
await expect(
|
|
73
|
+
importer.getIdentifierProperty('AWS::Lambda::Function')
|
|
74
|
+
).rejects.toThrow('Resource type AWS::Lambda::Function does not support import');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('validateImport', () => {
|
|
79
|
+
it('should validate VPC import', async () => {
|
|
80
|
+
const result = await importer.validateImport({
|
|
81
|
+
resourceType: 'AWS::EC2::VPC',
|
|
82
|
+
physicalId: 'vpc-123',
|
|
83
|
+
region: 'us-east-1',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
canImport: true,
|
|
88
|
+
reason: '',
|
|
89
|
+
warnings: [],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should fail validation for unsupported type', async () => {
|
|
94
|
+
const result = await importer.validateImport({
|
|
95
|
+
resourceType: 'AWS::Lambda::Function',
|
|
96
|
+
physicalId: 'my-function',
|
|
97
|
+
region: 'us-east-1',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.canImport).toBe(false);
|
|
101
|
+
expect(result.reason).toContain('not supported');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should warn about missing required properties', async () => {
|
|
105
|
+
const result = await importer.validateImport({
|
|
106
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
107
|
+
physicalId: 'my-cluster',
|
|
108
|
+
region: 'us-east-1',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result.canImport).toBe(true);
|
|
112
|
+
expect(result.warnings).toContain(
|
|
113
|
+
'Ensure DBCluster has required properties (Engine, MasterUsername, etc.)'
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('importResource', () => {
|
|
119
|
+
it('should import a single VPC resource', async () => {
|
|
120
|
+
const stackIdentifier = new StackIdentifier({
|
|
121
|
+
stackName: 'my-app-prod',
|
|
122
|
+
region: 'us-east-1',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Mock CreateChangeSet
|
|
126
|
+
mockSend.mockResolvedValueOnce({
|
|
127
|
+
Id: 'changeset-123',
|
|
128
|
+
StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Mock ExecuteChangeSet
|
|
132
|
+
mockSend.mockResolvedValueOnce({});
|
|
133
|
+
|
|
134
|
+
const result = await importer.importResource({
|
|
135
|
+
stackIdentifier,
|
|
136
|
+
logicalId: 'ImportedVPC',
|
|
137
|
+
resourceType: 'AWS::EC2::VPC',
|
|
138
|
+
physicalId: 'vpc-123',
|
|
139
|
+
properties: {
|
|
140
|
+
CidrBlock: '10.0.0.0/16',
|
|
141
|
+
EnableDnsSupport: true,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
operationId: 'changeset-123',
|
|
147
|
+
status: 'IN_PROGRESS',
|
|
148
|
+
message: 'Resource import initiated via change set changeset-123',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(mockSend).toHaveBeenCalledTimes(2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should throw error for unsupported resource type', async () => {
|
|
155
|
+
const stackIdentifier = new StackIdentifier({
|
|
156
|
+
stackName: 'my-app-prod',
|
|
157
|
+
region: 'us-east-1',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
importer.importResource({
|
|
162
|
+
stackIdentifier,
|
|
163
|
+
logicalId: 'MyFunction',
|
|
164
|
+
resourceType: 'AWS::Lambda::Function',
|
|
165
|
+
physicalId: 'my-function',
|
|
166
|
+
properties: {},
|
|
167
|
+
})
|
|
168
|
+
).rejects.toThrow('Resource type AWS::Lambda::Function does not support import');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle import failure', async () => {
|
|
172
|
+
const stackIdentifier = new StackIdentifier({
|
|
173
|
+
stackName: 'my-app-prod',
|
|
174
|
+
region: 'us-east-1',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const error = new Error('Resource already exists in stack');
|
|
178
|
+
error.name = 'AlreadyExistsException';
|
|
179
|
+
mockSend.mockRejectedValue(error);
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
importer.importResource({
|
|
183
|
+
stackIdentifier,
|
|
184
|
+
logicalId: 'ExistingVPC',
|
|
185
|
+
resourceType: 'AWS::EC2::VPC',
|
|
186
|
+
physicalId: 'vpc-123',
|
|
187
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
188
|
+
})
|
|
189
|
+
).rejects.toThrow('Resource already exists in stack');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('importMultipleResources', () => {
|
|
194
|
+
it('should import multiple resources in single operation', async () => {
|
|
195
|
+
const stackIdentifier = new StackIdentifier({
|
|
196
|
+
stackName: 'my-app-prod',
|
|
197
|
+
region: 'us-east-1',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const resources = [
|
|
201
|
+
{
|
|
202
|
+
logicalId: 'ImportedVPC',
|
|
203
|
+
resourceType: 'AWS::EC2::VPC',
|
|
204
|
+
physicalId: 'vpc-123',
|
|
205
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
logicalId: 'ImportedSubnet',
|
|
209
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
210
|
+
physicalId: 'subnet-456',
|
|
211
|
+
properties: { VpcId: 'vpc-123', CidrBlock: '10.0.1.0/24' },
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
// Mock CreateChangeSet
|
|
216
|
+
mockSend.mockResolvedValueOnce({
|
|
217
|
+
Id: 'changeset-multi-123',
|
|
218
|
+
StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Mock ExecuteChangeSet
|
|
222
|
+
mockSend.mockResolvedValueOnce({});
|
|
223
|
+
|
|
224
|
+
const result = await importer.importMultipleResources({
|
|
225
|
+
stackIdentifier,
|
|
226
|
+
resources,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual({
|
|
230
|
+
operationId: 'changeset-multi-123',
|
|
231
|
+
status: 'IN_PROGRESS',
|
|
232
|
+
importedCount: 2,
|
|
233
|
+
failedCount: 0,
|
|
234
|
+
message: 'Import operation initiated for 2 resources',
|
|
235
|
+
details: [],
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should filter out unsupported resource types', async () => {
|
|
240
|
+
const stackIdentifier = new StackIdentifier({
|
|
241
|
+
stackName: 'my-app-prod',
|
|
242
|
+
region: 'us-east-1',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const resources = [
|
|
246
|
+
{
|
|
247
|
+
logicalId: 'ImportedVPC',
|
|
248
|
+
resourceType: 'AWS::EC2::VPC',
|
|
249
|
+
physicalId: 'vpc-123',
|
|
250
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
logicalId: 'UnsupportedFunction',
|
|
254
|
+
resourceType: 'AWS::Lambda::Function',
|
|
255
|
+
physicalId: 'my-function',
|
|
256
|
+
properties: {},
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// Mock CreateChangeSet (only for VPC)
|
|
261
|
+
mockSend.mockResolvedValueOnce({
|
|
262
|
+
Id: 'changeset-filtered-123',
|
|
263
|
+
StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Mock ExecuteChangeSet
|
|
267
|
+
mockSend.mockResolvedValueOnce({});
|
|
268
|
+
|
|
269
|
+
const result = await importer.importMultipleResources({
|
|
270
|
+
stackIdentifier,
|
|
271
|
+
resources,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(result.importedCount).toBe(1);
|
|
275
|
+
expect(result.failedCount).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('getImportStatus', () => {
|
|
280
|
+
it('should get status of in-progress import', async () => {
|
|
281
|
+
mockSend.mockResolvedValue({
|
|
282
|
+
Status: 'CREATE_PENDING',
|
|
283
|
+
ExecutionStatus: 'AVAILABLE',
|
|
284
|
+
StatusReason: 'Change set created',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const status = await importer.getImportStatus('changeset-123');
|
|
288
|
+
|
|
289
|
+
expect(status).toEqual({
|
|
290
|
+
operationId: 'changeset-123',
|
|
291
|
+
status: 'IN_PROGRESS',
|
|
292
|
+
progress: 25,
|
|
293
|
+
message: 'Change set created',
|
|
294
|
+
completedTime: null,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should get status of completed import', async () => {
|
|
299
|
+
const completedTime = new Date('2024-01-15T10:30:00Z');
|
|
300
|
+
|
|
301
|
+
mockSend.mockResolvedValue({
|
|
302
|
+
Status: 'CREATE_COMPLETE',
|
|
303
|
+
ExecutionStatus: 'EXECUTE_COMPLETE',
|
|
304
|
+
StatusReason: 'Import completed successfully',
|
|
305
|
+
CreationTime: completedTime,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const status = await importer.getImportStatus('changeset-123');
|
|
309
|
+
|
|
310
|
+
expect(status.status).toBe('COMPLETE');
|
|
311
|
+
expect(status.progress).toBe(100);
|
|
312
|
+
expect(status.completedTime).toEqual(completedTime);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should get status of failed import', async () => {
|
|
316
|
+
mockSend.mockResolvedValue({
|
|
317
|
+
Status: 'FAILED',
|
|
318
|
+
ExecutionStatus: 'EXECUTE_FAILED',
|
|
319
|
+
StatusReason: 'Resource already exists',
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const status = await importer.getImportStatus('changeset-123');
|
|
323
|
+
|
|
324
|
+
expect(status.status).toBe('FAILED');
|
|
325
|
+
expect(status.message).toContain('already exists');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('generateTemplateSnippet', () => {
|
|
330
|
+
it('should generate VPC template snippet', async () => {
|
|
331
|
+
const snippet = await importer.generateTemplateSnippet({
|
|
332
|
+
logicalId: 'ImportedVPC',
|
|
333
|
+
resourceType: 'AWS::EC2::VPC',
|
|
334
|
+
properties: {
|
|
335
|
+
CidrBlock: '10.0.0.0/16',
|
|
336
|
+
EnableDnsSupport: true,
|
|
337
|
+
EnableDnsHostnames: true,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(snippet).toEqual({
|
|
342
|
+
ImportedVPC: {
|
|
343
|
+
Type: 'AWS::EC2::VPC',
|
|
344
|
+
Properties: {
|
|
345
|
+
CidrBlock: '10.0.0.0/16',
|
|
346
|
+
EnableDnsSupport: true,
|
|
347
|
+
EnableDnsHostnames: true,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should generate RDS DBCluster template snippet', async () => {
|
|
354
|
+
const snippet = await importer.generateTemplateSnippet({
|
|
355
|
+
logicalId: 'ImportedCluster',
|
|
356
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
357
|
+
properties: {
|
|
358
|
+
Engine: 'aurora-postgresql',
|
|
359
|
+
EngineVersion: '13.7',
|
|
360
|
+
MasterUsername: 'admin',
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(snippet).toEqual({
|
|
365
|
+
ImportedCluster: {
|
|
366
|
+
Type: 'AWS::RDS::DBCluster',
|
|
367
|
+
Properties: {
|
|
368
|
+
Engine: 'aurora-postgresql',
|
|
369
|
+
EngineVersion: '13.7',
|
|
370
|
+
MasterUsername: 'admin',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should throw error for unsupported resource type', async () => {
|
|
377
|
+
await expect(
|
|
378
|
+
importer.generateTemplateSnippet({
|
|
379
|
+
logicalId: 'MyFunction',
|
|
380
|
+
resourceType: 'AWS::Lambda::Function',
|
|
381
|
+
properties: {},
|
|
382
|
+
})
|
|
383
|
+
).rejects.toThrow('Resource type AWS::Lambda::Function does not support import');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('constructor', () => {
|
|
388
|
+
it('should create instance with default region', () => {
|
|
389
|
+
const imp = new AWSResourceImporter();
|
|
390
|
+
expect(imp).toBeInstanceOf(AWSResourceImporter);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should create instance with custom region', () => {
|
|
394
|
+
const imp = new AWSResourceImporter({ region: 'eu-west-1' });
|
|
395
|
+
expect(imp).toBeInstanceOf(AWSResourceImporter);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.474.
|
|
4
|
+
"version": "2.0.0--canary.474.884529c.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@babel/eslint-parser": "^7.18.9",
|
|
12
12
|
"@babel/parser": "^7.25.3",
|
|
13
13
|
"@babel/traverse": "^7.25.3",
|
|
14
|
-
"@friggframework/schemas": "2.0.0--canary.474.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.474.
|
|
14
|
+
"@friggframework/schemas": "2.0.0--canary.474.884529c.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.474.884529c.0",
|
|
16
16
|
"@hapi/boom": "^10.0.1",
|
|
17
17
|
"@inquirer/prompts": "^5.3.8",
|
|
18
18
|
"axios": "^1.7.2",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"serverless-http": "^2.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@friggframework/eslint-config": "2.0.0--canary.474.
|
|
38
|
-
"@friggframework/prettier-config": "2.0.0--canary.474.
|
|
37
|
+
"@friggframework/eslint-config": "2.0.0--canary.474.884529c.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.474.884529c.0",
|
|
39
39
|
"aws-sdk-client-mock": "^4.1.0",
|
|
40
40
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
41
41
|
"jest": "^30.1.3",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "884529c77537c39c284673e5e31433048c1251ec"
|
|
74
74
|
}
|