@friggframework/devtools 2.0.0--canary.474.82ba370.0 → 2.0.0--canary.474.efd7936.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/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +7 -4
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +28 -4
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +165 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +400 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +13 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +29 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +386 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/package.json +6 -6
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MismatchAnalyzer Domain Service
|
|
3
|
+
*
|
|
4
|
+
* Analyzes differences between expected (CloudFormation template) and actual
|
|
5
|
+
* (cloud resource) property values to detect drift.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Deep object comparison
|
|
9
|
+
* - Array comparison (order-sensitive)
|
|
10
|
+
* - Primitive value comparison
|
|
11
|
+
* - Type mismatch detection
|
|
12
|
+
* - Property mutability tracking
|
|
13
|
+
* - Ignore specific properties
|
|
14
|
+
* - Nested property path tracking
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const PropertyMismatch = require('../entities/property-mismatch');
|
|
18
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
19
|
+
|
|
20
|
+
class MismatchAnalyzer {
|
|
21
|
+
/**
|
|
22
|
+
* Analyze differences between expected and actual property values
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} params
|
|
25
|
+
* @param {Object} params.expected - Expected properties from CloudFormation template
|
|
26
|
+
* @param {Object} params.actual - Actual properties from cloud resource
|
|
27
|
+
* @param {Object} params.propertyMutability - Map of property paths to PropertyMutability instances
|
|
28
|
+
* @param {string[]} [params.ignoreProperties=[]] - Property paths to ignore
|
|
29
|
+
* @returns {PropertyMismatch[]} Array of detected mismatches
|
|
30
|
+
*/
|
|
31
|
+
analyze({ expected, actual, propertyMutability, ignoreProperties = [] }) {
|
|
32
|
+
const mismatches = [];
|
|
33
|
+
|
|
34
|
+
// Recursively compare objects
|
|
35
|
+
this._compareObjects({
|
|
36
|
+
expected,
|
|
37
|
+
actual,
|
|
38
|
+
propertyMutability,
|
|
39
|
+
ignoreProperties,
|
|
40
|
+
currentPath: '',
|
|
41
|
+
mismatches,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return mismatches;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively compare two objects
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
_compareObjects({
|
|
53
|
+
expected,
|
|
54
|
+
actual,
|
|
55
|
+
propertyMutability,
|
|
56
|
+
ignoreProperties,
|
|
57
|
+
currentPath,
|
|
58
|
+
mismatches,
|
|
59
|
+
}) {
|
|
60
|
+
// Get all unique property keys from both objects
|
|
61
|
+
const allKeys = new Set([
|
|
62
|
+
...Object.keys(expected || {}),
|
|
63
|
+
...Object.keys(actual || {}),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
for (const key of allKeys) {
|
|
67
|
+
const propertyPath = currentPath ? `${currentPath}.${key}` : key;
|
|
68
|
+
|
|
69
|
+
// Skip ignored properties
|
|
70
|
+
if (ignoreProperties.includes(propertyPath)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const expectedValue = expected?.[key];
|
|
75
|
+
const actualValue = actual?.[key];
|
|
76
|
+
|
|
77
|
+
// Check if values are different
|
|
78
|
+
if (!this._areValuesEqual(expectedValue, actualValue)) {
|
|
79
|
+
// Check if both are objects (and not arrays or null)
|
|
80
|
+
if (
|
|
81
|
+
this._isPlainObject(expectedValue) &&
|
|
82
|
+
this._isPlainObject(actualValue)
|
|
83
|
+
) {
|
|
84
|
+
// Recursively compare nested objects
|
|
85
|
+
this._compareObjects({
|
|
86
|
+
expected: expectedValue,
|
|
87
|
+
actual: actualValue,
|
|
88
|
+
propertyMutability,
|
|
89
|
+
ignoreProperties,
|
|
90
|
+
currentPath: propertyPath,
|
|
91
|
+
mismatches,
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
// Create a mismatch for this property
|
|
95
|
+
const mutability =
|
|
96
|
+
propertyMutability[propertyPath] || PropertyMutability.MUTABLE;
|
|
97
|
+
|
|
98
|
+
const mismatch = new PropertyMismatch({
|
|
99
|
+
propertyPath,
|
|
100
|
+
expectedValue,
|
|
101
|
+
actualValue,
|
|
102
|
+
mutability,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
mismatches.push(mismatch);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if two values are equal
|
|
113
|
+
*
|
|
114
|
+
* @private
|
|
115
|
+
* @param {*} value1
|
|
116
|
+
* @param {*} value2
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
_areValuesEqual(value1, value2) {
|
|
120
|
+
// Handle null/undefined equivalence
|
|
121
|
+
if (this._isNullish(value1) && this._isNullish(value2)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle different types
|
|
126
|
+
if (typeof value1 !== typeof value2) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle primitives
|
|
131
|
+
if (
|
|
132
|
+
typeof value1 === 'string' ||
|
|
133
|
+
typeof value1 === 'number' ||
|
|
134
|
+
typeof value1 === 'boolean'
|
|
135
|
+
) {
|
|
136
|
+
return value1 === value2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle arrays
|
|
140
|
+
if (Array.isArray(value1) && Array.isArray(value2)) {
|
|
141
|
+
return this._areArraysEqual(value1, value2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle plain objects
|
|
145
|
+
if (this._isPlainObject(value1) && this._isPlainObject(value2)) {
|
|
146
|
+
return this._areObjectsEqual(value1, value2);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle dates
|
|
150
|
+
if (value1 instanceof Date && value2 instanceof Date) {
|
|
151
|
+
return value1.getTime() === value2.getTime();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fallback: strict equality
|
|
155
|
+
return value1 === value2;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if value is null or undefined
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
* @param {*} value
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
_isNullish(value) {
|
|
166
|
+
return value === null || value === undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if value is a plain object (not array, not null, not Date, etc.)
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
* @param {*} value
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
_isPlainObject(value) {
|
|
177
|
+
return (
|
|
178
|
+
typeof value === 'object' &&
|
|
179
|
+
value !== null &&
|
|
180
|
+
!Array.isArray(value) &&
|
|
181
|
+
!(value instanceof Date) &&
|
|
182
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Deep equality check for arrays (order-sensitive)
|
|
188
|
+
*
|
|
189
|
+
* @private
|
|
190
|
+
* @param {Array} arr1
|
|
191
|
+
* @param {Array} arr2
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
_areArraysEqual(arr1, arr2) {
|
|
195
|
+
if (arr1.length !== arr2.length) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < arr1.length; i++) {
|
|
200
|
+
if (!this._areValuesEqual(arr1[i], arr2[i])) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Deep equality check for plain objects
|
|
210
|
+
*
|
|
211
|
+
* @private
|
|
212
|
+
* @param {Object} obj1
|
|
213
|
+
* @param {Object} obj2
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
_areObjectsEqual(obj1, obj2) {
|
|
217
|
+
const keys1 = Object.keys(obj1);
|
|
218
|
+
const keys2 = Object.keys(obj2);
|
|
219
|
+
|
|
220
|
+
if (keys1.length !== keys2.length) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const key of keys1) {
|
|
225
|
+
if (!this._areValuesEqual(obj1[key], obj2[key])) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = MismatchAnalyzer;
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for MismatchAnalyzer Domain Service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const MismatchAnalyzer = require('./mismatch-analyzer');
|
|
6
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
7
|
+
|
|
8
|
+
describe('MismatchAnalyzer', () => {
|
|
9
|
+
let analyzer;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
analyzer = new MismatchAnalyzer();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('primitive value comparison', () => {
|
|
16
|
+
it('should detect no mismatch for identical strings', () => {
|
|
17
|
+
const mismatches = analyzer.analyze({
|
|
18
|
+
expected: { name: 'my-vpc' },
|
|
19
|
+
actual: { name: 'my-vpc' },
|
|
20
|
+
propertyMutability: { name: PropertyMutability.MUTABLE },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(mismatches).toHaveLength(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should detect mismatch for different strings', () => {
|
|
27
|
+
const mismatches = analyzer.analyze({
|
|
28
|
+
expected: { name: 'my-vpc-v1' },
|
|
29
|
+
actual: { name: 'my-vpc-v2' },
|
|
30
|
+
propertyMutability: { name: PropertyMutability.MUTABLE },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(mismatches).toHaveLength(1);
|
|
34
|
+
expect(mismatches[0].propertyPath).toBe('name');
|
|
35
|
+
expect(mismatches[0].expectedValue).toBe('my-vpc-v1');
|
|
36
|
+
expect(mismatches[0].actualValue).toBe('my-vpc-v2');
|
|
37
|
+
expect(mismatches[0].mutability.value).toBe('MUTABLE');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect mismatch for different numbers', () => {
|
|
41
|
+
const mismatches = analyzer.analyze({
|
|
42
|
+
expected: { maxSize: 10 },
|
|
43
|
+
actual: { maxSize: 20 },
|
|
44
|
+
propertyMutability: { maxSize: PropertyMutability.MUTABLE },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(mismatches).toHaveLength(1);
|
|
48
|
+
expect(mismatches[0].propertyPath).toBe('maxSize');
|
|
49
|
+
expect(mismatches[0].expectedValue).toBe(10);
|
|
50
|
+
expect(mismatches[0].actualValue).toBe(20);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should detect mismatch for different booleans', () => {
|
|
54
|
+
const mismatches = analyzer.analyze({
|
|
55
|
+
expected: { enableDnsSupport: true },
|
|
56
|
+
actual: { enableDnsSupport: false },
|
|
57
|
+
propertyMutability: { enableDnsSupport: PropertyMutability.MUTABLE },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(mismatches).toHaveLength(1);
|
|
61
|
+
expect(mismatches[0].propertyPath).toBe('enableDnsSupport');
|
|
62
|
+
expect(mismatches[0].expectedValue).toBe(true);
|
|
63
|
+
expect(mismatches[0].actualValue).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('null and undefined handling', () => {
|
|
68
|
+
it('should detect no mismatch for both null', () => {
|
|
69
|
+
const mismatches = analyzer.analyze({
|
|
70
|
+
expected: { description: null },
|
|
71
|
+
actual: { description: null },
|
|
72
|
+
propertyMutability: { description: PropertyMutability.MUTABLE },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(mismatches).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should detect mismatch for null vs value', () => {
|
|
79
|
+
const mismatches = analyzer.analyze({
|
|
80
|
+
expected: { description: null },
|
|
81
|
+
actual: { description: 'Production VPC' },
|
|
82
|
+
propertyMutability: { description: PropertyMutability.MUTABLE },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(mismatches).toHaveLength(1);
|
|
86
|
+
expect(mismatches[0].propertyPath).toBe('description');
|
|
87
|
+
expect(mismatches[0].expectedValue).toBeNull();
|
|
88
|
+
expect(mismatches[0].actualValue).toBe('Production VPC');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle undefined as equivalent to null', () => {
|
|
92
|
+
const mismatches = analyzer.analyze({
|
|
93
|
+
expected: { description: undefined },
|
|
94
|
+
actual: { description: null },
|
|
95
|
+
propertyMutability: { description: PropertyMutability.MUTABLE },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(mismatches).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('nested object comparison', () => {
|
|
103
|
+
it('should detect no mismatch for identical nested objects', () => {
|
|
104
|
+
const mismatches = analyzer.analyze({
|
|
105
|
+
expected: {
|
|
106
|
+
tags: {
|
|
107
|
+
Environment: 'production',
|
|
108
|
+
Team: 'platform',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
actual: {
|
|
112
|
+
tags: {
|
|
113
|
+
Environment: 'production',
|
|
114
|
+
Team: 'platform',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
propertyMutability: { tags: PropertyMutability.MUTABLE },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(mismatches).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should detect mismatch in nested object property', () => {
|
|
124
|
+
const mismatches = analyzer.analyze({
|
|
125
|
+
expected: {
|
|
126
|
+
tags: {
|
|
127
|
+
Environment: 'production',
|
|
128
|
+
Team: 'platform',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
actual: {
|
|
132
|
+
tags: {
|
|
133
|
+
Environment: 'staging',
|
|
134
|
+
Team: 'platform',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
propertyMutability: { 'tags.Environment': PropertyMutability.MUTABLE },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(mismatches).toHaveLength(1);
|
|
141
|
+
expect(mismatches[0].propertyPath).toBe('tags.Environment');
|
|
142
|
+
expect(mismatches[0].expectedValue).toBe('production');
|
|
143
|
+
expect(mismatches[0].actualValue).toBe('staging');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should detect mismatch when nested property is missing', () => {
|
|
147
|
+
const mismatches = analyzer.analyze({
|
|
148
|
+
expected: {
|
|
149
|
+
tags: {
|
|
150
|
+
Environment: 'production',
|
|
151
|
+
Team: 'platform',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
actual: {
|
|
155
|
+
tags: {
|
|
156
|
+
Environment: 'production',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
propertyMutability: { 'tags.Team': PropertyMutability.MUTABLE },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(mismatches).toHaveLength(1);
|
|
163
|
+
expect(mismatches[0].propertyPath).toBe('tags.Team');
|
|
164
|
+
expect(mismatches[0].expectedValue).toBe('platform');
|
|
165
|
+
expect(mismatches[0].actualValue).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should detect mismatch when nested property is added', () => {
|
|
169
|
+
const mismatches = analyzer.analyze({
|
|
170
|
+
expected: {
|
|
171
|
+
tags: {
|
|
172
|
+
Environment: 'production',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
actual: {
|
|
176
|
+
tags: {
|
|
177
|
+
Environment: 'production',
|
|
178
|
+
Team: 'platform',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
propertyMutability: { 'tags.Team': PropertyMutability.MUTABLE },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(mismatches).toHaveLength(1);
|
|
185
|
+
expect(mismatches[0].propertyPath).toBe('tags.Team');
|
|
186
|
+
expect(mismatches[0].expectedValue).toBeUndefined();
|
|
187
|
+
expect(mismatches[0].actualValue).toBe('platform');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('array comparison', () => {
|
|
192
|
+
it('should detect no mismatch for identical arrays', () => {
|
|
193
|
+
const mismatches = analyzer.analyze({
|
|
194
|
+
expected: { subnets: ['subnet-1', 'subnet-2'] },
|
|
195
|
+
actual: { subnets: ['subnet-1', 'subnet-2'] },
|
|
196
|
+
propertyMutability: { subnets: PropertyMutability.MUTABLE },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(mismatches).toHaveLength(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should detect mismatch for different array values', () => {
|
|
203
|
+
const mismatches = analyzer.analyze({
|
|
204
|
+
expected: { subnets: ['subnet-1', 'subnet-2'] },
|
|
205
|
+
actual: { subnets: ['subnet-1', 'subnet-3'] },
|
|
206
|
+
propertyMutability: { subnets: PropertyMutability.MUTABLE },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(mismatches).toHaveLength(1);
|
|
210
|
+
expect(mismatches[0].propertyPath).toBe('subnets');
|
|
211
|
+
expect(mismatches[0].expectedValue).toEqual(['subnet-1', 'subnet-2']);
|
|
212
|
+
expect(mismatches[0].actualValue).toEqual(['subnet-1', 'subnet-3']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should detect mismatch for different array lengths', () => {
|
|
216
|
+
const mismatches = analyzer.analyze({
|
|
217
|
+
expected: { subnets: ['subnet-1', 'subnet-2'] },
|
|
218
|
+
actual: { subnets: ['subnet-1'] },
|
|
219
|
+
propertyMutability: { subnets: PropertyMutability.MUTABLE },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(mismatches).toHaveLength(1);
|
|
223
|
+
expect(mismatches[0].propertyPath).toBe('subnets');
|
|
224
|
+
expect(mismatches[0].expectedValue).toEqual(['subnet-1', 'subnet-2']);
|
|
225
|
+
expect(mismatches[0].actualValue).toEqual(['subnet-1']);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should detect mismatch for different array order (order-sensitive)', () => {
|
|
229
|
+
const mismatches = analyzer.analyze({
|
|
230
|
+
expected: { subnets: ['subnet-1', 'subnet-2'] },
|
|
231
|
+
actual: { subnets: ['subnet-2', 'subnet-1'] },
|
|
232
|
+
propertyMutability: { subnets: PropertyMutability.MUTABLE },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(mismatches).toHaveLength(1);
|
|
236
|
+
expect(mismatches[0].propertyPath).toBe('subnets');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle arrays of objects', () => {
|
|
240
|
+
const mismatches = analyzer.analyze({
|
|
241
|
+
expected: {
|
|
242
|
+
tags: [
|
|
243
|
+
{ Key: 'Environment', Value: 'production' },
|
|
244
|
+
{ Key: 'Team', Value: 'platform' },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
actual: {
|
|
248
|
+
tags: [
|
|
249
|
+
{ Key: 'Environment', Value: 'staging' },
|
|
250
|
+
{ Key: 'Team', Value: 'platform' },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
propertyMutability: { tags: PropertyMutability.MUTABLE },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(mismatches).toHaveLength(1);
|
|
257
|
+
expect(mismatches[0].propertyPath).toBe('tags');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('type mismatch', () => {
|
|
262
|
+
it('should detect type mismatch (string vs number)', () => {
|
|
263
|
+
const mismatches = analyzer.analyze({
|
|
264
|
+
expected: { port: '8080' },
|
|
265
|
+
actual: { port: 8080 },
|
|
266
|
+
propertyMutability: { port: PropertyMutability.MUTABLE },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(mismatches).toHaveLength(1);
|
|
270
|
+
expect(mismatches[0].propertyPath).toBe('port');
|
|
271
|
+
expect(mismatches[0].expectedValue).toBe('8080');
|
|
272
|
+
expect(mismatches[0].actualValue).toBe(8080);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should detect type mismatch (object vs array)', () => {
|
|
276
|
+
const mismatches = analyzer.analyze({
|
|
277
|
+
expected: { data: {} },
|
|
278
|
+
actual: { data: [] },
|
|
279
|
+
propertyMutability: { data: PropertyMutability.MUTABLE },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(mismatches).toHaveLength(1);
|
|
283
|
+
expect(mismatches[0].propertyPath).toBe('data');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('property mutability', () => {
|
|
288
|
+
it('should use MUTABLE mutability by default', () => {
|
|
289
|
+
const mismatches = analyzer.analyze({
|
|
290
|
+
expected: { name: 'vpc-v1' },
|
|
291
|
+
actual: { name: 'vpc-v2' },
|
|
292
|
+
propertyMutability: {}, // No mutability specified
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(mismatches).toHaveLength(1);
|
|
296
|
+
expect(mismatches[0].mutability.value).toBe('MUTABLE');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should apply IMMUTABLE mutability', () => {
|
|
300
|
+
const mismatches = analyzer.analyze({
|
|
301
|
+
expected: { bucketName: 'bucket-v1' },
|
|
302
|
+
actual: { bucketName: 'bucket-v2' },
|
|
303
|
+
propertyMutability: { bucketName: PropertyMutability.IMMUTABLE },
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(mismatches).toHaveLength(1);
|
|
307
|
+
expect(mismatches[0].mutability.value).toBe('IMMUTABLE');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should apply CONDITIONAL mutability', () => {
|
|
311
|
+
const mismatches = analyzer.analyze({
|
|
312
|
+
expected: { engineVersion: '13.7' },
|
|
313
|
+
actual: { engineVersion: '13.8' },
|
|
314
|
+
propertyMutability: { engineVersion: PropertyMutability.CONDITIONAL },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(mismatches).toHaveLength(1);
|
|
318
|
+
expect(mismatches[0].mutability.value).toBe('CONDITIONAL');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('multiple mismatches', () => {
|
|
323
|
+
it('should detect multiple mismatches in same object', () => {
|
|
324
|
+
const mismatches = analyzer.analyze({
|
|
325
|
+
expected: {
|
|
326
|
+
name: 'vpc-v1',
|
|
327
|
+
cidr: '10.0.0.0/16',
|
|
328
|
+
enableDnsSupport: true,
|
|
329
|
+
},
|
|
330
|
+
actual: {
|
|
331
|
+
name: 'vpc-v2',
|
|
332
|
+
cidr: '10.1.0.0/16',
|
|
333
|
+
enableDnsSupport: true,
|
|
334
|
+
},
|
|
335
|
+
propertyMutability: {
|
|
336
|
+
name: PropertyMutability.MUTABLE,
|
|
337
|
+
cidr: PropertyMutability.IMMUTABLE,
|
|
338
|
+
enableDnsSupport: PropertyMutability.MUTABLE,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(mismatches).toHaveLength(2);
|
|
343
|
+
|
|
344
|
+
const nameMismatch = mismatches.find((m) => m.propertyPath === 'name');
|
|
345
|
+
expect(nameMismatch).toBeDefined();
|
|
346
|
+
expect(nameMismatch.mutability.value).toBe('MUTABLE');
|
|
347
|
+
|
|
348
|
+
const cidrMismatch = mismatches.find((m) => m.propertyPath === 'cidr');
|
|
349
|
+
expect(cidrMismatch).toBeDefined();
|
|
350
|
+
expect(cidrMismatch.mutability.value).toBe('IMMUTABLE');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('ignore properties', () => {
|
|
355
|
+
it('should ignore specified properties', () => {
|
|
356
|
+
const mismatches = analyzer.analyze({
|
|
357
|
+
expected: {
|
|
358
|
+
name: 'my-vpc',
|
|
359
|
+
lastModified: '2024-01-01',
|
|
360
|
+
},
|
|
361
|
+
actual: {
|
|
362
|
+
name: 'my-vpc',
|
|
363
|
+
lastModified: '2024-01-15',
|
|
364
|
+
},
|
|
365
|
+
propertyMutability: { name: PropertyMutability.MUTABLE },
|
|
366
|
+
ignoreProperties: ['lastModified'],
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(mismatches).toHaveLength(0);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should ignore nested properties', () => {
|
|
373
|
+
const mismatches = analyzer.analyze({
|
|
374
|
+
expected: {
|
|
375
|
+
config: {
|
|
376
|
+
name: 'production',
|
|
377
|
+
timestamp: '2024-01-01',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
actual: {
|
|
381
|
+
config: {
|
|
382
|
+
name: 'production',
|
|
383
|
+
timestamp: '2024-01-15',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
propertyMutability: { 'config.name': PropertyMutability.MUTABLE },
|
|
387
|
+
ignoreProperties: ['config.timestamp'],
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(mismatches).toHaveLength(0);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('empty objects', () => {
|
|
395
|
+
it('should detect no mismatch for both empty objects', () => {
|
|
396
|
+
const mismatches = analyzer.analyze({
|
|
397
|
+
expected: {},
|
|
398
|
+
actual: {},
|
|
399
|
+
propertyMutability: {},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(mismatches).toHaveLength(0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should detect mismatch when expected is empty but actual has properties', () => {
|
|
406
|
+
const mismatches = analyzer.analyze({
|
|
407
|
+
expected: {},
|
|
408
|
+
actual: { name: 'my-vpc' },
|
|
409
|
+
propertyMutability: { name: PropertyMutability.MUTABLE },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(mismatches).toHaveLength(1);
|
|
413
|
+
expect(mismatches[0].propertyPath).toBe('name');
|
|
414
|
+
expect(mismatches[0].expectedValue).toBeUndefined();
|
|
415
|
+
expect(mismatches[0].actualValue).toBe('my-vpc');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should detect mismatch when actual is empty but expected has properties', () => {
|
|
419
|
+
const mismatches = analyzer.analyze({
|
|
420
|
+
expected: { name: 'my-vpc' },
|
|
421
|
+
actual: {},
|
|
422
|
+
propertyMutability: { name: PropertyMutability.MUTABLE },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(mismatches).toHaveLength(1);
|
|
426
|
+
expect(mismatches[0].propertyPath).toBe('name');
|
|
427
|
+
expect(mismatches[0].expectedValue).toBe('my-vpc');
|
|
428
|
+
expect(mismatches[0].actualValue).toBeUndefined();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
@@ -152,6 +152,19 @@ class StackIdentifier {
|
|
|
152
152
|
return `${this.stackName} (${this.region})`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Serialize to JSON
|
|
157
|
+
*
|
|
158
|
+
* @returns {Object}
|
|
159
|
+
*/
|
|
160
|
+
toJSON() {
|
|
161
|
+
return {
|
|
162
|
+
stackName: this.stackName,
|
|
163
|
+
region: this.region,
|
|
164
|
+
accountId: this.accountId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
/**
|
|
156
169
|
* Create StackIdentifier from ARN
|
|
157
170
|
*
|