@friggframework/devtools 2.0.0--canary.474.884529c.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/package.json +6 -6
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReconcilePropertiesUseCase - Reconcile Property Drift
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Use Case
|
|
5
|
+
*
|
|
6
|
+
* Business logic for the "frigg repair --reconcile" command. Orchestrates property
|
|
7
|
+
* drift reconciliation to fix mutable property mismatches between CloudFormation
|
|
8
|
+
* template and actual cloud resources.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Validate properties can be reconciled
|
|
12
|
+
* - Preview reconciliation impact
|
|
13
|
+
* - Execute reconciliation (template mode or resource mode)
|
|
14
|
+
* - Handle batch reconciliations
|
|
15
|
+
* - Skip immutable properties
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ReconcilePropertiesUseCase {
|
|
19
|
+
/**
|
|
20
|
+
* Create use case with required dependencies
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {IPropertyReconciler} params.propertyReconciler - Property reconciliation operations
|
|
24
|
+
*/
|
|
25
|
+
constructor({ propertyReconciler }) {
|
|
26
|
+
if (!propertyReconciler) {
|
|
27
|
+
throw new Error('propertyReconciler is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.propertyReconciler = propertyReconciler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile a single property mismatch
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} params
|
|
37
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
38
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
39
|
+
* @param {PropertyMismatch} params.mismatch - Property mismatch to reconcile
|
|
40
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
41
|
+
* @returns {Promise<Object>} Reconciliation result
|
|
42
|
+
*/
|
|
43
|
+
async reconcileSingleProperty({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
44
|
+
// 1. Check if property can be reconciled
|
|
45
|
+
const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
|
|
46
|
+
|
|
47
|
+
if (!canReconcile) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Property ${mismatch.propertyPath} cannot be reconciled automatically (immutable property requires replacement)`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Execute reconciliation
|
|
54
|
+
const result = await this.propertyReconciler.reconcileProperty({
|
|
55
|
+
stackIdentifier,
|
|
56
|
+
logicalId,
|
|
57
|
+
mismatch,
|
|
58
|
+
mode,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reconcile multiple property mismatches for a resource
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} params
|
|
68
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
69
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
70
|
+
* @param {PropertyMismatch[]} params.mismatches - Property mismatches to reconcile
|
|
71
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
72
|
+
* @returns {Promise<Object>} Batch reconciliation result
|
|
73
|
+
*/
|
|
74
|
+
async reconcileMultipleProperties({
|
|
75
|
+
stackIdentifier,
|
|
76
|
+
logicalId,
|
|
77
|
+
mismatches,
|
|
78
|
+
mode = 'template',
|
|
79
|
+
}) {
|
|
80
|
+
// 1. Filter out immutable properties (cannot be reconciled)
|
|
81
|
+
const reconcilableProperties = [];
|
|
82
|
+
const skippedProperties = [];
|
|
83
|
+
|
|
84
|
+
for (const mismatch of mismatches) {
|
|
85
|
+
const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
|
|
86
|
+
|
|
87
|
+
if (canReconcile) {
|
|
88
|
+
reconcilableProperties.push(mismatch);
|
|
89
|
+
} else {
|
|
90
|
+
skippedProperties.push(mismatch);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. If no properties can be reconciled, return early
|
|
95
|
+
if (reconcilableProperties.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
reconciledCount: 0,
|
|
98
|
+
failedCount: 0,
|
|
99
|
+
skippedCount: skippedProperties.length,
|
|
100
|
+
reconcilableCount: 0,
|
|
101
|
+
message: `All ${mismatches.length} property mismatch(es) require resource replacement (immutable)`,
|
|
102
|
+
results: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Reconcile the reconcilable properties
|
|
107
|
+
const batchResult = await this.propertyReconciler.reconcileMultipleProperties({
|
|
108
|
+
stackIdentifier,
|
|
109
|
+
logicalId,
|
|
110
|
+
mismatches: reconcilableProperties,
|
|
111
|
+
mode,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 4. Return combined result
|
|
115
|
+
return {
|
|
116
|
+
reconciledCount: batchResult.reconciledCount,
|
|
117
|
+
failedCount: batchResult.failedCount,
|
|
118
|
+
skippedCount: skippedProperties.length,
|
|
119
|
+
reconcilableCount: reconcilableProperties.length,
|
|
120
|
+
message: batchResult.message,
|
|
121
|
+
results: batchResult.results,
|
|
122
|
+
skippedProperties: skippedProperties.length > 0 ? skippedProperties : undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Preview reconciliation without applying changes
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} params
|
|
130
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
131
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
132
|
+
* @param {PropertyMismatch} params.mismatch - Property mismatch to preview
|
|
133
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
134
|
+
* @returns {Promise<Object>} Preview result
|
|
135
|
+
*/
|
|
136
|
+
async previewReconciliation({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
137
|
+
return await this.propertyReconciler.previewReconciliation({
|
|
138
|
+
stackIdentifier,
|
|
139
|
+
logicalId,
|
|
140
|
+
mismatch,
|
|
141
|
+
mode,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = ReconcilePropertiesUseCase;
|
package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ReconcilePropertiesUseCase
|
|
3
|
+
*
|
|
4
|
+
* Use case for reconciling property drift between CloudFormation template
|
|
5
|
+
* and actual cloud resources (frigg repair --reconcile command)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ReconcilePropertiesUseCase = require('./reconcile-properties-use-case');
|
|
9
|
+
const StackIdentifier = require('../../domain/value-objects/stack-identifier');
|
|
10
|
+
const PropertyMismatch = require('../../domain/entities/property-mismatch');
|
|
11
|
+
const PropertyMutability = require('../../domain/value-objects/property-mutability');
|
|
12
|
+
|
|
13
|
+
describe('ReconcilePropertiesUseCase', () => {
|
|
14
|
+
let useCase;
|
|
15
|
+
let mockPropertyReconciler;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Mock property reconciler
|
|
19
|
+
mockPropertyReconciler = {
|
|
20
|
+
canReconcile: jest.fn(),
|
|
21
|
+
reconcileProperty: jest.fn(),
|
|
22
|
+
reconcileMultipleProperties: jest.fn(),
|
|
23
|
+
previewReconciliation: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useCase = new ReconcilePropertiesUseCase({
|
|
27
|
+
propertyReconciler: mockPropertyReconciler,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('reconcileSingleProperty', () => {
|
|
32
|
+
it('should reconcile a single mutable property mismatch', async () => {
|
|
33
|
+
const stackIdentifier = new StackIdentifier({
|
|
34
|
+
stackName: 'my-app-prod',
|
|
35
|
+
region: 'us-east-1',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const mismatch = new PropertyMismatch({
|
|
39
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
40
|
+
expectedValue: true,
|
|
41
|
+
actualValue: false,
|
|
42
|
+
mutability: PropertyMutability.MUTABLE,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Mock reconciler can handle this mismatch
|
|
46
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
47
|
+
|
|
48
|
+
// Mock reconciliation result
|
|
49
|
+
mockPropertyReconciler.reconcileProperty.mockResolvedValue({
|
|
50
|
+
success: true,
|
|
51
|
+
mode: 'template',
|
|
52
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
53
|
+
oldValue: true,
|
|
54
|
+
newValue: false,
|
|
55
|
+
message: 'Template updated to match actual resource state',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await useCase.reconcileSingleProperty({
|
|
59
|
+
stackIdentifier,
|
|
60
|
+
logicalId: 'MyVPC',
|
|
61
|
+
mismatch,
|
|
62
|
+
mode: 'template',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
expect(result.mode).toBe('template');
|
|
67
|
+
expect(result.propertyPath).toBe('Properties.EnableDnsSupport');
|
|
68
|
+
expect(mockPropertyReconciler.canReconcile).toHaveBeenCalledWith(mismatch);
|
|
69
|
+
expect(mockPropertyReconciler.reconcileProperty).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should fail if property cannot be reconciled', async () => {
|
|
73
|
+
const stackIdentifier = new StackIdentifier({
|
|
74
|
+
stackName: 'my-app-prod',
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mismatch = new PropertyMismatch({
|
|
79
|
+
propertyPath: 'Properties.CidrBlock',
|
|
80
|
+
expectedValue: '10.0.0.0/16',
|
|
81
|
+
actualValue: '10.1.0.0/16',
|
|
82
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Mock reconciler cannot handle immutable property
|
|
86
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(false);
|
|
87
|
+
|
|
88
|
+
await expect(
|
|
89
|
+
useCase.reconcileSingleProperty({
|
|
90
|
+
stackIdentifier,
|
|
91
|
+
logicalId: 'MyVPC',
|
|
92
|
+
mismatch,
|
|
93
|
+
mode: 'template',
|
|
94
|
+
})
|
|
95
|
+
).rejects.toThrow('Property Properties.CidrBlock cannot be reconciled automatically');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use resource mode when specified', async () => {
|
|
99
|
+
const stackIdentifier = new StackIdentifier({
|
|
100
|
+
stackName: 'my-app-prod',
|
|
101
|
+
region: 'us-east-1',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mismatch = new PropertyMismatch({
|
|
105
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
106
|
+
expectedValue: true,
|
|
107
|
+
actualValue: false,
|
|
108
|
+
mutability: PropertyMutability.MUTABLE,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
112
|
+
|
|
113
|
+
mockPropertyReconciler.reconcileProperty.mockResolvedValue({
|
|
114
|
+
success: true,
|
|
115
|
+
mode: 'resource',
|
|
116
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
117
|
+
oldValue: false,
|
|
118
|
+
newValue: true,
|
|
119
|
+
message: 'Resource updated to match template definition',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await useCase.reconcileSingleProperty({
|
|
123
|
+
stackIdentifier,
|
|
124
|
+
logicalId: 'MyVPC',
|
|
125
|
+
mismatch,
|
|
126
|
+
mode: 'resource',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
expect(result.mode).toBe('resource');
|
|
131
|
+
expect(mockPropertyReconciler.reconcileProperty).toHaveBeenCalledWith({
|
|
132
|
+
stackIdentifier,
|
|
133
|
+
logicalId: 'MyVPC',
|
|
134
|
+
mismatch,
|
|
135
|
+
mode: 'resource',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('reconcileMultipleProperties', () => {
|
|
141
|
+
it('should reconcile multiple property mismatches for a resource', async () => {
|
|
142
|
+
const stackIdentifier = new StackIdentifier({
|
|
143
|
+
stackName: 'my-app-prod',
|
|
144
|
+
region: 'us-east-1',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const mismatches = [
|
|
148
|
+
new PropertyMismatch({
|
|
149
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
150
|
+
expectedValue: true,
|
|
151
|
+
actualValue: false,
|
|
152
|
+
mutability: PropertyMutability.MUTABLE,
|
|
153
|
+
}),
|
|
154
|
+
new PropertyMismatch({
|
|
155
|
+
propertyPath: 'Properties.EnableDnsHostnames',
|
|
156
|
+
expectedValue: true,
|
|
157
|
+
actualValue: false,
|
|
158
|
+
mutability: PropertyMutability.MUTABLE,
|
|
159
|
+
}),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Mock all mismatches can be reconciled
|
|
163
|
+
mockPropertyReconciler.canReconcile.mockResolvedValue(true);
|
|
164
|
+
|
|
165
|
+
// Mock batch reconciliation
|
|
166
|
+
mockPropertyReconciler.reconcileMultipleProperties.mockResolvedValue({
|
|
167
|
+
reconciledCount: 2,
|
|
168
|
+
failedCount: 0,
|
|
169
|
+
results: [
|
|
170
|
+
{
|
|
171
|
+
success: true,
|
|
172
|
+
mode: 'template',
|
|
173
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
174
|
+
oldValue: true,
|
|
175
|
+
newValue: false,
|
|
176
|
+
message: 'Template updated',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
success: true,
|
|
180
|
+
mode: 'template',
|
|
181
|
+
propertyPath: 'Properties.EnableDnsHostnames',
|
|
182
|
+
oldValue: true,
|
|
183
|
+
newValue: false,
|
|
184
|
+
message: 'Template updated',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
message: 'Reconciled 2 of 2 properties',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await useCase.reconcileMultipleProperties({
|
|
191
|
+
stackIdentifier,
|
|
192
|
+
logicalId: 'MyVPC',
|
|
193
|
+
mismatches,
|
|
194
|
+
mode: 'template',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.reconciledCount).toBe(2);
|
|
198
|
+
expect(result.failedCount).toBe(0);
|
|
199
|
+
expect(result.results).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should skip immutable properties and reconcile mutable ones', async () => {
|
|
203
|
+
const stackIdentifier = new StackIdentifier({
|
|
204
|
+
stackName: 'my-app-prod',
|
|
205
|
+
region: 'us-east-1',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const mismatches = [
|
|
209
|
+
new PropertyMismatch({
|
|
210
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
211
|
+
expectedValue: true,
|
|
212
|
+
actualValue: false,
|
|
213
|
+
mutability: PropertyMutability.MUTABLE,
|
|
214
|
+
}),
|
|
215
|
+
new PropertyMismatch({
|
|
216
|
+
propertyPath: 'Properties.CidrBlock',
|
|
217
|
+
expectedValue: '10.0.0.0/16',
|
|
218
|
+
actualValue: '10.1.0.0/16',
|
|
219
|
+
mutability: PropertyMutability.IMMUTABLE,
|
|
220
|
+
}),
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// Mock first can be reconciled, second cannot
|
|
224
|
+
mockPropertyReconciler.canReconcile
|
|
225
|
+
.mockResolvedValueOnce(true)
|
|
226
|
+
.mockResolvedValueOnce(false);
|
|
227
|
+
|
|
228
|
+
// Mock reconciliation of the mutable property
|
|
229
|
+
mockPropertyReconciler.reconcileMultipleProperties.mockResolvedValue({
|
|
230
|
+
reconciledCount: 1,
|
|
231
|
+
failedCount: 0,
|
|
232
|
+
results: [
|
|
233
|
+
{
|
|
234
|
+
success: true,
|
|
235
|
+
mode: 'template',
|
|
236
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
237
|
+
oldValue: true,
|
|
238
|
+
newValue: false,
|
|
239
|
+
message: 'Template updated',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
message: 'Reconciled 1 of 1 properties',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await useCase.reconcileMultipleProperties({
|
|
246
|
+
stackIdentifier,
|
|
247
|
+
logicalId: 'MyVPC',
|
|
248
|
+
mismatches,
|
|
249
|
+
mode: 'template',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.reconciledCount).toBe(1); // One mutable property reconciled
|
|
253
|
+
expect(result.skippedCount).toBe(1); // One immutable property skipped
|
|
254
|
+
expect(result.reconcilableCount).toBe(1);
|
|
255
|
+
expect(result.results).toHaveLength(1);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('previewReconciliation', () => {
|
|
260
|
+
it('should preview reconciliation changes', async () => {
|
|
261
|
+
const stackIdentifier = new StackIdentifier({
|
|
262
|
+
stackName: 'my-app-prod',
|
|
263
|
+
region: 'us-east-1',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const mismatch = new PropertyMismatch({
|
|
267
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
268
|
+
expectedValue: true,
|
|
269
|
+
actualValue: false,
|
|
270
|
+
mutability: PropertyMutability.MUTABLE,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Mock preview
|
|
274
|
+
mockPropertyReconciler.previewReconciliation.mockResolvedValue({
|
|
275
|
+
canReconcile: true,
|
|
276
|
+
mode: 'template',
|
|
277
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
278
|
+
currentValue: true,
|
|
279
|
+
proposedValue: false,
|
|
280
|
+
impact: 'Will update CloudFormation template to match actual resource state',
|
|
281
|
+
warnings: [],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const preview = await useCase.previewReconciliation({
|
|
285
|
+
stackIdentifier,
|
|
286
|
+
logicalId: 'MyVPC',
|
|
287
|
+
mismatch,
|
|
288
|
+
mode: 'template',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(preview.canReconcile).toBe(true);
|
|
292
|
+
expect(preview.mode).toBe('template');
|
|
293
|
+
expect(preview.impact).toContain('CloudFormation template');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should include warnings for risky reconciliations', async () => {
|
|
297
|
+
const stackIdentifier = new StackIdentifier({
|
|
298
|
+
stackName: 'my-app-prod',
|
|
299
|
+
region: 'us-east-1',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const mismatch = new PropertyMismatch({
|
|
303
|
+
propertyPath: 'Properties.EngineVersion',
|
|
304
|
+
expectedValue: '13.7',
|
|
305
|
+
actualValue: '13.8',
|
|
306
|
+
mutability: PropertyMutability.CONDITIONAL,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Mock preview with warnings
|
|
310
|
+
mockPropertyReconciler.previewReconciliation.mockResolvedValue({
|
|
311
|
+
canReconcile: true,
|
|
312
|
+
mode: 'resource',
|
|
313
|
+
propertyPath: 'Properties.EngineVersion',
|
|
314
|
+
currentValue: '13.7',
|
|
315
|
+
proposedValue: '13.8',
|
|
316
|
+
impact: 'Will update database engine version - may cause downtime',
|
|
317
|
+
warnings: [
|
|
318
|
+
'Engine version downgrade may not be supported',
|
|
319
|
+
'Downtime may occur during version change',
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const preview = await useCase.previewReconciliation({
|
|
324
|
+
stackIdentifier,
|
|
325
|
+
logicalId: 'MyDBCluster',
|
|
326
|
+
mismatch,
|
|
327
|
+
mode: 'resource',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(preview.canReconcile).toBe(true);
|
|
331
|
+
expect(preview.warnings).toHaveLength(2);
|
|
332
|
+
expect(preview.warnings[0]).toContain('Engine version');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('constructor', () => {
|
|
337
|
+
it('should require propertyReconciler', () => {
|
|
338
|
+
expect(() => {
|
|
339
|
+
new ReconcilePropertiesUseCase({});
|
|
340
|
+
}).toThrow('propertyReconciler is required');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|