@friggframework/devtools 2.0.0--canary.474.4793186.0 → 2.0.0--canary.474.82fd52e.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/application/use-cases/reconcile-properties-use-case.js +146 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +229 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +180 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- 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/package.json +6 -6
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepairViaImportUseCase - Import Orphaned Resources into CloudFormation Stack
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Use Case
|
|
5
|
+
*
|
|
6
|
+
* Business logic for the "frigg repair --import" command. Orchestrates resource
|
|
7
|
+
* import operations to fix orphaned resources by bringing them under CloudFormation management.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Validate resources can be imported
|
|
11
|
+
* - Retrieve resource details from cloud
|
|
12
|
+
* - Generate CloudFormation template snippets
|
|
13
|
+
* - Execute import operations (single or batch)
|
|
14
|
+
* - Track import operation status
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class RepairViaImportUseCase {
|
|
18
|
+
/**
|
|
19
|
+
* Create use case with required dependencies
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} params
|
|
22
|
+
* @param {IResourceImporter} params.resourceImporter - Resource import operations
|
|
23
|
+
* @param {IResourceDetector} params.resourceDetector - Resource discovery and details
|
|
24
|
+
*/
|
|
25
|
+
constructor({ resourceImporter, resourceDetector }) {
|
|
26
|
+
if (!resourceImporter) {
|
|
27
|
+
throw new Error('resourceImporter is required');
|
|
28
|
+
}
|
|
29
|
+
if (!resourceDetector) {
|
|
30
|
+
throw new Error('resourceDetector is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.resourceImporter = resourceImporter;
|
|
34
|
+
this.resourceDetector = resourceDetector;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Import a single orphaned resource into a CloudFormation stack
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} params
|
|
41
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
42
|
+
* @param {string} params.logicalId - Desired logical ID for resource in template
|
|
43
|
+
* @param {string} params.physicalId - Physical ID of resource in cloud
|
|
44
|
+
* @param {string} params.resourceType - CloudFormation resource type
|
|
45
|
+
* @returns {Promise<Object>} Import result
|
|
46
|
+
*/
|
|
47
|
+
async importSingleResource({ stackIdentifier, logicalId, physicalId, resourceType }) {
|
|
48
|
+
// 1. Validate resource can be imported
|
|
49
|
+
const validation = await this.resourceImporter.validateImport({
|
|
50
|
+
resourceType,
|
|
51
|
+
physicalId,
|
|
52
|
+
region: stackIdentifier.region,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!validation.canImport) {
|
|
56
|
+
throw new Error(validation.reason);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Get detailed resource properties from cloud
|
|
60
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
61
|
+
resourceType,
|
|
62
|
+
physicalId,
|
|
63
|
+
region: stackIdentifier.region,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 3. Execute import operation
|
|
67
|
+
const importResult = await this.resourceImporter.importResource({
|
|
68
|
+
stackIdentifier,
|
|
69
|
+
logicalId,
|
|
70
|
+
resourceType,
|
|
71
|
+
physicalId,
|
|
72
|
+
properties: resourceDetails.properties,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 4. Return result with warnings if any
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
operationId: importResult.operationId,
|
|
79
|
+
status: importResult.status,
|
|
80
|
+
message: importResult.message,
|
|
81
|
+
warnings: validation.warnings || [],
|
|
82
|
+
resource: {
|
|
83
|
+
logicalId,
|
|
84
|
+
physicalId,
|
|
85
|
+
resourceType,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Import multiple orphaned resources into a stack in batch
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} params
|
|
94
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
95
|
+
* @param {Array<Object>} params.resources - Resources to import
|
|
96
|
+
* @param {string} params.resources[].logicalId - Logical ID
|
|
97
|
+
* @param {string} params.resources[].physicalId - Physical ID
|
|
98
|
+
* @param {string} params.resources[].resourceType - Resource type
|
|
99
|
+
* @returns {Promise<Object>} Batch import result
|
|
100
|
+
*/
|
|
101
|
+
async importMultipleResources({ stackIdentifier, resources }) {
|
|
102
|
+
const validationErrors = [];
|
|
103
|
+
const validResources = [];
|
|
104
|
+
|
|
105
|
+
// 1. Validate all resources first
|
|
106
|
+
for (const resource of resources) {
|
|
107
|
+
try {
|
|
108
|
+
const validation = await this.resourceImporter.validateImport({
|
|
109
|
+
resourceType: resource.resourceType,
|
|
110
|
+
physicalId: resource.physicalId,
|
|
111
|
+
region: stackIdentifier.region,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!validation.canImport) {
|
|
115
|
+
validationErrors.push({
|
|
116
|
+
logicalId: resource.logicalId,
|
|
117
|
+
physicalId: resource.physicalId,
|
|
118
|
+
reason: validation.reason,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get resource details
|
|
124
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
125
|
+
resourceType: resource.resourceType,
|
|
126
|
+
physicalId: resource.physicalId,
|
|
127
|
+
region: stackIdentifier.region,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
validResources.push({
|
|
131
|
+
logicalId: resource.logicalId,
|
|
132
|
+
resourceType: resource.resourceType,
|
|
133
|
+
physicalId: resource.physicalId,
|
|
134
|
+
properties: resourceDetails.properties,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
validationErrors.push({
|
|
138
|
+
logicalId: resource.logicalId,
|
|
139
|
+
physicalId: resource.physicalId,
|
|
140
|
+
reason: error.message,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. If ANY validation failed, fail the entire batch (all-or-nothing approach)
|
|
146
|
+
if (validationErrors.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
importedCount: 0,
|
|
150
|
+
failedCount: validationErrors.length,
|
|
151
|
+
validationErrors,
|
|
152
|
+
message: `${validationErrors.length} resource(s) failed validation - batch import aborted`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. All validations passed - import resources in batch
|
|
157
|
+
if (validResources.length > 0) {
|
|
158
|
+
const importResult = await this.resourceImporter.importMultipleResources({
|
|
159
|
+
stackIdentifier,
|
|
160
|
+
resources: validResources,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
importedCount: importResult.importedCount,
|
|
166
|
+
failedCount: importResult.failedCount,
|
|
167
|
+
operationId: importResult.operationId,
|
|
168
|
+
status: importResult.status,
|
|
169
|
+
message: importResult.message,
|
|
170
|
+
details: importResult.details,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. No resources provided
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
importedCount: 0,
|
|
178
|
+
failedCount: 0,
|
|
179
|
+
message: 'No resources provided for import',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get status of an ongoing import operation
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} params
|
|
187
|
+
* @param {string} params.operationId - CloudFormation change set ID
|
|
188
|
+
* @returns {Promise<Object>} Operation status
|
|
189
|
+
*/
|
|
190
|
+
async getImportStatus({ operationId }) {
|
|
191
|
+
return await this.resourceImporter.getImportStatus(operationId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Preview what template changes will be made for an import
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} params
|
|
198
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
199
|
+
* @param {string} params.logicalId - Desired logical ID
|
|
200
|
+
* @param {string} params.physicalId - Physical resource ID
|
|
201
|
+
* @param {string} params.resourceType - Resource type
|
|
202
|
+
* @returns {Promise<Object>} Preview with template snippet
|
|
203
|
+
*/
|
|
204
|
+
async previewImport({ stackIdentifier, logicalId, physicalId, resourceType }) {
|
|
205
|
+
// Get resource details from cloud
|
|
206
|
+
const resourceDetails = await this.resourceDetector.getResourceDetails({
|
|
207
|
+
resourceType,
|
|
208
|
+
physicalId,
|
|
209
|
+
region: stackIdentifier.region,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Generate template snippet
|
|
213
|
+
const templateSnippet = await this.resourceImporter.generateTemplateSnippet({
|
|
214
|
+
logicalId,
|
|
215
|
+
resourceType,
|
|
216
|
+
properties: resourceDetails.properties,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
logicalId,
|
|
221
|
+
physicalId,
|
|
222
|
+
resourceType,
|
|
223
|
+
templateSnippet,
|
|
224
|
+
properties: resourceDetails.properties,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = RepairViaImportUseCase;
|
package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RepairViaImportUseCase
|
|
3
|
+
*
|
|
4
|
+
* Use case for importing orphaned resources into CloudFormation stack
|
|
5
|
+
* (frigg repair --import command)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const RepairViaImportUseCase = require('./repair-via-import-use-case');
|
|
9
|
+
const StackIdentifier = require('../../domain/value-objects/stack-identifier');
|
|
10
|
+
|
|
11
|
+
describe('RepairViaImportUseCase', () => {
|
|
12
|
+
let useCase;
|
|
13
|
+
let mockResourceImporter;
|
|
14
|
+
let mockResourceDetector;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Mock repositories
|
|
18
|
+
mockResourceImporter = {
|
|
19
|
+
validateImport: jest.fn(),
|
|
20
|
+
importResource: jest.fn(),
|
|
21
|
+
importMultipleResources: jest.fn(),
|
|
22
|
+
getImportStatus: jest.fn(),
|
|
23
|
+
generateTemplateSnippet: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
mockResourceDetector = {
|
|
27
|
+
getResourceDetails: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
useCase = new RepairViaImportUseCase({
|
|
31
|
+
resourceImporter: mockResourceImporter,
|
|
32
|
+
resourceDetector: mockResourceDetector,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('importSingleResource', () => {
|
|
37
|
+
it('should import a single orphaned resource', async () => {
|
|
38
|
+
const stackIdentifier = new StackIdentifier({
|
|
39
|
+
stackName: 'my-app-prod',
|
|
40
|
+
region: 'us-east-1',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const resourceToImport = {
|
|
44
|
+
logicalId: 'OrphanedDBCluster',
|
|
45
|
+
physicalId: 'my-orphan-cluster',
|
|
46
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Mock validation
|
|
50
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
51
|
+
canImport: true,
|
|
52
|
+
reason: null,
|
|
53
|
+
warnings: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Mock resource details retrieval
|
|
57
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
58
|
+
physicalId: 'my-orphan-cluster',
|
|
59
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
60
|
+
properties: {
|
|
61
|
+
Engine: 'aurora-postgresql',
|
|
62
|
+
EngineVersion: '13.7',
|
|
63
|
+
MasterUsername: 'admin',
|
|
64
|
+
},
|
|
65
|
+
tags: [
|
|
66
|
+
{ Key: 'frigg:stack', Value: 'my-app-prod' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Mock import operation
|
|
71
|
+
mockResourceImporter.importResource.mockResolvedValue({
|
|
72
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz',
|
|
73
|
+
status: 'IN_PROGRESS',
|
|
74
|
+
message: 'Resource import initiated',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await useCase.importSingleResource({
|
|
78
|
+
stackIdentifier,
|
|
79
|
+
logicalId: resourceToImport.logicalId,
|
|
80
|
+
physicalId: resourceToImport.physicalId,
|
|
81
|
+
resourceType: resourceToImport.resourceType,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
expect(result.operationId).toBeDefined();
|
|
86
|
+
expect(result.status).toBe('IN_PROGRESS');
|
|
87
|
+
expect(mockResourceImporter.validateImport).toHaveBeenCalledWith({
|
|
88
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
89
|
+
physicalId: 'my-orphan-cluster',
|
|
90
|
+
region: 'us-east-1',
|
|
91
|
+
});
|
|
92
|
+
expect(mockResourceImporter.importResource).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should fail if resource cannot be imported', async () => {
|
|
96
|
+
const stackIdentifier = new StackIdentifier({
|
|
97
|
+
stackName: 'my-app-prod',
|
|
98
|
+
region: 'us-east-1',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Mock validation failure
|
|
102
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
103
|
+
canImport: false,
|
|
104
|
+
reason: 'Resource type AWS::Lambda::Function does not support import',
|
|
105
|
+
warnings: [],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
useCase.importSingleResource({
|
|
110
|
+
stackIdentifier,
|
|
111
|
+
logicalId: 'MyFunction',
|
|
112
|
+
physicalId: 'my-function',
|
|
113
|
+
resourceType: 'AWS::Lambda::Function',
|
|
114
|
+
})
|
|
115
|
+
).rejects.toThrow('Resource type AWS::Lambda::Function does not support import');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should include warnings in result', async () => {
|
|
119
|
+
const stackIdentifier = new StackIdentifier({
|
|
120
|
+
stackName: 'my-app-prod',
|
|
121
|
+
region: 'us-east-1',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Mock validation with warnings
|
|
125
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
126
|
+
canImport: true,
|
|
127
|
+
reason: null,
|
|
128
|
+
warnings: ['Resource has manual configuration changes that may be lost'],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
132
|
+
physicalId: 'vpc-123',
|
|
133
|
+
resourceType: 'AWS::EC2::VPC',
|
|
134
|
+
properties: {
|
|
135
|
+
CidrBlock: '10.0.0.0/16',
|
|
136
|
+
},
|
|
137
|
+
tags: [],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
mockResourceImporter.importResource.mockResolvedValue({
|
|
141
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz',
|
|
142
|
+
status: 'IN_PROGRESS',
|
|
143
|
+
message: 'Resource import initiated',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await useCase.importSingleResource({
|
|
147
|
+
stackIdentifier,
|
|
148
|
+
logicalId: 'MyVPC',
|
|
149
|
+
physicalId: 'vpc-123',
|
|
150
|
+
resourceType: 'AWS::EC2::VPC',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(result.success).toBe(true);
|
|
154
|
+
expect(result.warnings).toHaveLength(1);
|
|
155
|
+
expect(result.warnings[0]).toContain('manual configuration changes');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('importMultipleResources', () => {
|
|
160
|
+
it('should import multiple orphaned resources in batch', async () => {
|
|
161
|
+
const stackIdentifier = new StackIdentifier({
|
|
162
|
+
stackName: 'my-app-prod',
|
|
163
|
+
region: 'us-east-1',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const resourcesToImport = [
|
|
167
|
+
{
|
|
168
|
+
logicalId: 'OrphanedVPC',
|
|
169
|
+
physicalId: 'vpc-123',
|
|
170
|
+
resourceType: 'AWS::EC2::VPC',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
logicalId: 'OrphanedSubnet',
|
|
174
|
+
physicalId: 'subnet-456',
|
|
175
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// Mock validation for both resources
|
|
180
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
181
|
+
canImport: true,
|
|
182
|
+
reason: null,
|
|
183
|
+
warnings: [],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Mock resource details
|
|
187
|
+
mockResourceDetector.getResourceDetails
|
|
188
|
+
.mockResolvedValueOnce({
|
|
189
|
+
physicalId: 'vpc-123',
|
|
190
|
+
resourceType: 'AWS::EC2::VPC',
|
|
191
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
192
|
+
tags: [],
|
|
193
|
+
})
|
|
194
|
+
.mockResolvedValueOnce({
|
|
195
|
+
physicalId: 'subnet-456',
|
|
196
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
197
|
+
properties: { CidrBlock: '10.0.1.0/24' },
|
|
198
|
+
tags: [],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Mock batch import
|
|
202
|
+
mockResourceImporter.importMultipleResources.mockResolvedValue({
|
|
203
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/batch-import-xyz',
|
|
204
|
+
status: 'IN_PROGRESS',
|
|
205
|
+
importedCount: 2,
|
|
206
|
+
failedCount: 0,
|
|
207
|
+
message: 'Batch import initiated',
|
|
208
|
+
details: [
|
|
209
|
+
{ logicalId: 'OrphanedVPC', status: 'IN_PROGRESS' },
|
|
210
|
+
{ logicalId: 'OrphanedSubnet', status: 'IN_PROGRESS' },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await useCase.importMultipleResources({
|
|
215
|
+
stackIdentifier,
|
|
216
|
+
resources: resourcesToImport,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.success).toBe(true);
|
|
220
|
+
expect(result.importedCount).toBe(2);
|
|
221
|
+
expect(result.failedCount).toBe(0);
|
|
222
|
+
expect(mockResourceImporter.importMultipleResources).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle partial failures in batch import', async () => {
|
|
226
|
+
const stackIdentifier = new StackIdentifier({
|
|
227
|
+
stackName: 'my-app-prod',
|
|
228
|
+
region: 'us-east-1',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const resourcesToImport = [
|
|
232
|
+
{
|
|
233
|
+
logicalId: 'OrphanedVPC',
|
|
234
|
+
physicalId: 'vpc-123',
|
|
235
|
+
resourceType: 'AWS::EC2::VPC',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
logicalId: 'InvalidResource',
|
|
239
|
+
physicalId: 'invalid-123',
|
|
240
|
+
resourceType: 'AWS::Lambda::Function',
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// Mock validation - first passes, second fails
|
|
245
|
+
mockResourceImporter.validateImport
|
|
246
|
+
.mockResolvedValueOnce({
|
|
247
|
+
canImport: true,
|
|
248
|
+
reason: null,
|
|
249
|
+
warnings: [],
|
|
250
|
+
})
|
|
251
|
+
.mockResolvedValueOnce({
|
|
252
|
+
canImport: false,
|
|
253
|
+
reason: 'Resource does not exist',
|
|
254
|
+
warnings: [],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Mock resource details for first resource only
|
|
258
|
+
mockResourceDetector.getResourceDetails.mockResolvedValueOnce({
|
|
259
|
+
physicalId: 'vpc-123',
|
|
260
|
+
resourceType: 'AWS::EC2::VPC',
|
|
261
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
262
|
+
tags: [],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await useCase.importMultipleResources({
|
|
266
|
+
stackIdentifier,
|
|
267
|
+
resources: resourcesToImport,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result.success).toBe(false); // Overall failure due to partial failure
|
|
271
|
+
expect(result.importedCount).toBe(0); // No resources actually imported yet
|
|
272
|
+
expect(result.failedCount).toBe(1);
|
|
273
|
+
expect(result.validationErrors).toHaveLength(1);
|
|
274
|
+
expect(result.validationErrors[0].logicalId).toBe('InvalidResource');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('getImportStatus', () => {
|
|
279
|
+
it('should get status of import operation', async () => {
|
|
280
|
+
const operationId = 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz';
|
|
281
|
+
|
|
282
|
+
mockResourceImporter.getImportStatus.mockResolvedValue({
|
|
283
|
+
operationId,
|
|
284
|
+
status: 'COMPLETE',
|
|
285
|
+
progress: 100,
|
|
286
|
+
message: 'Resource import completed successfully',
|
|
287
|
+
completedTime: new Date('2024-01-15T12:00:00Z'),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const status = await useCase.getImportStatus({ operationId });
|
|
291
|
+
|
|
292
|
+
expect(status.status).toBe('COMPLETE');
|
|
293
|
+
expect(status.progress).toBe(100);
|
|
294
|
+
expect(mockResourceImporter.getImportStatus).toHaveBeenCalledWith(operationId);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return in-progress status for ongoing import', async () => {
|
|
298
|
+
const operationId = 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz';
|
|
299
|
+
|
|
300
|
+
mockResourceImporter.getImportStatus.mockResolvedValue({
|
|
301
|
+
operationId,
|
|
302
|
+
status: 'IN_PROGRESS',
|
|
303
|
+
progress: 50,
|
|
304
|
+
message: 'Importing resources...',
|
|
305
|
+
completedTime: null,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const status = await useCase.getImportStatus({ operationId });
|
|
309
|
+
|
|
310
|
+
expect(status.status).toBe('IN_PROGRESS');
|
|
311
|
+
expect(status.progress).toBe(50);
|
|
312
|
+
expect(status.completedTime).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('previewImport', () => {
|
|
317
|
+
it('should preview template changes for import', async () => {
|
|
318
|
+
const stackIdentifier = new StackIdentifier({
|
|
319
|
+
stackName: 'my-app-prod',
|
|
320
|
+
region: 'us-east-1',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Mock resource details
|
|
324
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
325
|
+
physicalId: 'my-orphan-cluster',
|
|
326
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
327
|
+
properties: {
|
|
328
|
+
Engine: 'aurora-postgresql',
|
|
329
|
+
EngineVersion: '13.7',
|
|
330
|
+
},
|
|
331
|
+
tags: [],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Mock template snippet generation
|
|
335
|
+
mockResourceImporter.generateTemplateSnippet.mockResolvedValue({
|
|
336
|
+
OrphanedDBCluster: {
|
|
337
|
+
Type: 'AWS::RDS::DBCluster',
|
|
338
|
+
Properties: {
|
|
339
|
+
Engine: 'aurora-postgresql',
|
|
340
|
+
EngineVersion: '13.7',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const preview = await useCase.previewImport({
|
|
346
|
+
stackIdentifier,
|
|
347
|
+
logicalId: 'OrphanedDBCluster',
|
|
348
|
+
physicalId: 'my-orphan-cluster',
|
|
349
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(preview.logicalId).toBe('OrphanedDBCluster');
|
|
353
|
+
expect(preview.physicalId).toBe('my-orphan-cluster');
|
|
354
|
+
expect(preview.templateSnippet).toBeDefined();
|
|
355
|
+
expect(preview.templateSnippet.OrphanedDBCluster.Type).toBe('AWS::RDS::DBCluster');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('constructor', () => {
|
|
360
|
+
it('should require resourceImporter', () => {
|
|
361
|
+
expect(() => {
|
|
362
|
+
new RepairViaImportUseCase({
|
|
363
|
+
resourceDetector: mockResourceDetector,
|
|
364
|
+
});
|
|
365
|
+
}).toThrow('resourceImporter is required');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should require resourceDetector', () => {
|
|
369
|
+
expect(() => {
|
|
370
|
+
new RepairViaImportUseCase({
|
|
371
|
+
resourceImporter: mockResourceImporter,
|
|
372
|
+
});
|
|
373
|
+
}).toThrow('resourceDetector is required');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|