@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46

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.
Files changed (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +633 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Tests for Issue Entity
3
+ */
4
+
5
+ const Issue = require('./issue');
6
+ const ResourceState = require('../value-objects/resource-state');
7
+ const PropertyMismatch = require('./property-mismatch');
8
+ const PropertyMutability = require('../value-objects/property-mutability');
9
+
10
+ describe('Issue', () => {
11
+ describe('constructor', () => {
12
+ it('should create an orphaned resource issue', () => {
13
+ const issue = new Issue({
14
+ type: 'ORPHANED_RESOURCE',
15
+ severity: 'critical',
16
+ resourceType: 'AWS::RDS::DBCluster',
17
+ resourceId: 'my-app-prod-aurora',
18
+ description: 'Aurora cluster exists in AWS but not managed by CloudFormation',
19
+ resolution: 'Import resource into CloudFormation stack',
20
+ canAutoFix: true,
21
+ });
22
+
23
+ expect(issue.type).toBe('ORPHANED_RESOURCE');
24
+ expect(issue.severity).toBe('critical');
25
+ expect(issue.resourceType).toBe('AWS::RDS::DBCluster');
26
+ expect(issue.resourceId).toBe('my-app-prod-aurora');
27
+ expect(issue.description).toBe('Aurora cluster exists in AWS but not managed by CloudFormation');
28
+ expect(issue.resolution).toBe('Import resource into CloudFormation stack');
29
+ expect(issue.canAutoFix).toBe(true);
30
+ });
31
+
32
+ it('should create a missing resource issue', () => {
33
+ const issue = new Issue({
34
+ type: 'MISSING_RESOURCE',
35
+ severity: 'critical',
36
+ resourceType: 'AWS::KMS::Key',
37
+ resourceId: 'FriggKMSKey',
38
+ description: 'KMS key defined in stack but does not exist in AWS',
39
+ resolution: 'Verify resource was not manually deleted',
40
+ canAutoFix: false,
41
+ });
42
+
43
+ expect(issue.type).toBe('MISSING_RESOURCE');
44
+ expect(issue.severity).toBe('critical');
45
+ });
46
+
47
+ it('should create a property mismatch issue', () => {
48
+ const mismatch = new PropertyMismatch({
49
+ propertyPath: 'Properties.Tags',
50
+ expectedValue: [{ Key: 'Environment', Value: 'production' }],
51
+ actualValue: [{ Key: 'Env', Value: 'prod' }],
52
+ mutability: PropertyMutability.MUTABLE,
53
+ });
54
+
55
+ const issue = new Issue({
56
+ type: 'PROPERTY_MISMATCH',
57
+ severity: 'warning',
58
+ resourceType: 'AWS::EC2::VPC',
59
+ resourceId: 'vpc-0abc123',
60
+ description: 'VPC tags differ from expected configuration',
61
+ resolution: 'Update VPC tags to match desired state',
62
+ canAutoFix: true,
63
+ propertyMismatch: mismatch,
64
+ });
65
+
66
+ expect(issue.type).toBe('PROPERTY_MISMATCH');
67
+ expect(issue.propertyMismatch).toBe(mismatch);
68
+ expect(issue.canAutoFix).toBe(true);
69
+ });
70
+
71
+ it('should require type', () => {
72
+ expect(() => {
73
+ new Issue({
74
+ severity: 'critical',
75
+ resourceType: 'AWS::S3::Bucket',
76
+ resourceId: 'my-bucket',
77
+ description: 'Test',
78
+ resolution: 'Fix it',
79
+ });
80
+ }).toThrow('type is required');
81
+ });
82
+
83
+ it('should require severity', () => {
84
+ expect(() => {
85
+ new Issue({
86
+ type: 'ORPHANED_RESOURCE',
87
+ resourceType: 'AWS::S3::Bucket',
88
+ resourceId: 'my-bucket',
89
+ description: 'Test',
90
+ resolution: 'Fix it',
91
+ });
92
+ }).toThrow('severity is required');
93
+ });
94
+
95
+ it('should require resourceType', () => {
96
+ expect(() => {
97
+ new Issue({
98
+ type: 'ORPHANED_RESOURCE',
99
+ severity: 'critical',
100
+ resourceId: 'my-bucket',
101
+ description: 'Test',
102
+ resolution: 'Fix it',
103
+ });
104
+ }).toThrow('resourceType is required');
105
+ });
106
+
107
+ it('should require resourceId', () => {
108
+ expect(() => {
109
+ new Issue({
110
+ type: 'ORPHANED_RESOURCE',
111
+ severity: 'critical',
112
+ resourceType: 'AWS::S3::Bucket',
113
+ description: 'Test',
114
+ resolution: 'Fix it',
115
+ });
116
+ }).toThrow('resourceId is required');
117
+ });
118
+
119
+ it('should require description', () => {
120
+ expect(() => {
121
+ new Issue({
122
+ type: 'ORPHANED_RESOURCE',
123
+ severity: 'critical',
124
+ resourceType: 'AWS::S3::Bucket',
125
+ resourceId: 'my-bucket',
126
+ resolution: 'Fix it',
127
+ });
128
+ }).toThrow('description is required');
129
+ });
130
+
131
+ it('should validate issue type', () => {
132
+ expect(() => {
133
+ new Issue({
134
+ type: 'INVALID_TYPE',
135
+ severity: 'critical',
136
+ resourceType: 'AWS::S3::Bucket',
137
+ resourceId: 'my-bucket',
138
+ description: 'Test',
139
+ resolution: 'Fix it',
140
+ });
141
+ }).toThrow('Invalid issue type: INVALID_TYPE');
142
+ });
143
+
144
+ it('should validate severity', () => {
145
+ expect(() => {
146
+ new Issue({
147
+ type: 'ORPHANED_RESOURCE',
148
+ severity: 'invalid',
149
+ resourceType: 'AWS::S3::Bucket',
150
+ resourceId: 'my-bucket',
151
+ description: 'Test',
152
+ resolution: 'Fix it',
153
+ });
154
+ }).toThrow('Invalid severity: invalid');
155
+ });
156
+
157
+ it('should default canAutoFix to false', () => {
158
+ const issue = new Issue({
159
+ type: 'ORPHANED_RESOURCE',
160
+ severity: 'critical',
161
+ resourceType: 'AWS::S3::Bucket',
162
+ resourceId: 'my-bucket',
163
+ description: 'Test',
164
+ resolution: 'Fix it',
165
+ });
166
+
167
+ expect(issue.canAutoFix).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe('type checks', () => {
172
+ it('should check if issue is orphaned resource', () => {
173
+ const issue = new Issue({
174
+ type: 'ORPHANED_RESOURCE',
175
+ severity: 'critical',
176
+ resourceType: 'AWS::RDS::DBCluster',
177
+ resourceId: 'my-cluster',
178
+ description: 'Test',
179
+ resolution: 'Import',
180
+ });
181
+
182
+ expect(issue.isOrphanedResource()).toBe(true);
183
+ expect(issue.isMissingResource()).toBe(false);
184
+ expect(issue.isPropertyMismatch()).toBe(false);
185
+ expect(issue.isDrifted()).toBe(false);
186
+ });
187
+
188
+ it('should check if issue is missing resource', () => {
189
+ const issue = new Issue({
190
+ type: 'MISSING_RESOURCE',
191
+ severity: 'critical',
192
+ resourceType: 'AWS::KMS::Key',
193
+ resourceId: 'FriggKMSKey',
194
+ description: 'Test',
195
+ resolution: 'Verify deletion',
196
+ });
197
+
198
+ expect(issue.isOrphanedResource()).toBe(false);
199
+ expect(issue.isMissingResource()).toBe(true);
200
+ expect(issue.isPropertyMismatch()).toBe(false);
201
+ expect(issue.isDrifted()).toBe(false);
202
+ });
203
+
204
+ it('should check if issue is property mismatch', () => {
205
+ const issue = new Issue({
206
+ type: 'PROPERTY_MISMATCH',
207
+ severity: 'warning',
208
+ resourceType: 'AWS::EC2::VPC',
209
+ resourceId: 'vpc-123',
210
+ description: 'Test',
211
+ resolution: 'Update',
212
+ });
213
+
214
+ expect(issue.isOrphanedResource()).toBe(false);
215
+ expect(issue.isMissingResource()).toBe(false);
216
+ expect(issue.isPropertyMismatch()).toBe(true);
217
+ expect(issue.isDrifted()).toBe(false);
218
+ });
219
+
220
+ it('should check if issue is drifted', () => {
221
+ const issue = new Issue({
222
+ type: 'DRIFTED_RESOURCE',
223
+ severity: 'warning',
224
+ resourceType: 'AWS::S3::Bucket',
225
+ resourceId: 'my-bucket',
226
+ description: 'Test',
227
+ resolution: 'Reconcile',
228
+ });
229
+
230
+ expect(issue.isOrphanedResource()).toBe(false);
231
+ expect(issue.isMissingResource()).toBe(false);
232
+ expect(issue.isPropertyMismatch()).toBe(false);
233
+ expect(issue.isDrifted()).toBe(true);
234
+ });
235
+ });
236
+
237
+ describe('severity checks', () => {
238
+ it('should check if issue is critical', () => {
239
+ const issue = new Issue({
240
+ type: 'ORPHANED_RESOURCE',
241
+ severity: 'critical',
242
+ resourceType: 'AWS::RDS::DBCluster',
243
+ resourceId: 'my-cluster',
244
+ description: 'Test',
245
+ resolution: 'Import',
246
+ });
247
+
248
+ expect(issue.isCritical()).toBe(true);
249
+ expect(issue.isWarning()).toBe(false);
250
+ expect(issue.isInfo()).toBe(false);
251
+ });
252
+
253
+ it('should check if issue is warning', () => {
254
+ const issue = new Issue({
255
+ type: 'PROPERTY_MISMATCH',
256
+ severity: 'warning',
257
+ resourceType: 'AWS::EC2::VPC',
258
+ resourceId: 'vpc-123',
259
+ description: 'Test',
260
+ resolution: 'Update',
261
+ });
262
+
263
+ expect(issue.isCritical()).toBe(false);
264
+ expect(issue.isWarning()).toBe(true);
265
+ expect(issue.isInfo()).toBe(false);
266
+ });
267
+
268
+ it('should check if issue is info', () => {
269
+ const issue = new Issue({
270
+ type: 'MISSING_TAG',
271
+ severity: 'info',
272
+ resourceType: 'AWS::Lambda::Function',
273
+ resourceId: 'my-function',
274
+ description: 'Test',
275
+ resolution: 'Add tag',
276
+ });
277
+
278
+ expect(issue.isCritical()).toBe(false);
279
+ expect(issue.isWarning()).toBe(false);
280
+ expect(issue.isInfo()).toBe(true);
281
+ });
282
+ });
283
+
284
+ describe('toString', () => {
285
+ it('should return string representation', () => {
286
+ const issue = new Issue({
287
+ type: 'ORPHANED_RESOURCE',
288
+ severity: 'critical',
289
+ resourceType: 'AWS::RDS::DBCluster',
290
+ resourceId: 'my-app-prod-aurora',
291
+ description: 'Aurora cluster exists in AWS but not managed by CloudFormation',
292
+ resolution: 'Import resource into CloudFormation stack',
293
+ });
294
+
295
+ const str = issue.toString();
296
+ expect(str).toContain('ORPHANED_RESOURCE');
297
+ expect(str).toContain('critical');
298
+ expect(str).toContain('AWS::RDS::DBCluster');
299
+ expect(str).toContain('my-app-prod-aurora');
300
+ });
301
+ });
302
+
303
+ describe('toJSON', () => {
304
+ it('should serialize to JSON', () => {
305
+ const issue = new Issue({
306
+ type: 'ORPHANED_RESOURCE',
307
+ severity: 'critical',
308
+ resourceType: 'AWS::RDS::DBCluster',
309
+ resourceId: 'my-app-prod-aurora',
310
+ description: 'Aurora cluster exists in AWS but not managed by CloudFormation',
311
+ resolution: 'Import resource into CloudFormation stack',
312
+ canAutoFix: true,
313
+ });
314
+
315
+ const json = issue.toJSON();
316
+
317
+ expect(json).toEqual({
318
+ type: 'ORPHANED_RESOURCE',
319
+ severity: 'critical',
320
+ resourceType: 'AWS::RDS::DBCluster',
321
+ resourceId: 'my-app-prod-aurora',
322
+ description: 'Aurora cluster exists in AWS but not managed by CloudFormation',
323
+ resolution: 'Import resource into CloudFormation stack',
324
+ canAutoFix: true,
325
+ propertyMismatch: null,
326
+ });
327
+ });
328
+
329
+ it('should include property mismatch in JSON', () => {
330
+ const mismatch = new PropertyMismatch({
331
+ propertyPath: 'Properties.Tags',
332
+ expectedValue: [{ Key: 'Env', Value: 'prod' }],
333
+ actualValue: [{ Key: 'Env', Value: 'dev' }],
334
+ mutability: PropertyMutability.MUTABLE,
335
+ });
336
+
337
+ const issue = new Issue({
338
+ type: 'PROPERTY_MISMATCH',
339
+ severity: 'warning',
340
+ resourceType: 'AWS::EC2::VPC',
341
+ resourceId: 'vpc-123',
342
+ description: 'VPC tags differ',
343
+ resolution: 'Update tags',
344
+ canAutoFix: true,
345
+ propertyMismatch: mismatch,
346
+ });
347
+
348
+ const json = issue.toJSON();
349
+
350
+ expect(json.propertyMismatch).toBeDefined();
351
+ expect(json.propertyMismatch.propertyPath).toBe('Properties.Tags');
352
+ });
353
+ });
354
+
355
+ describe('static factory methods', () => {
356
+ it('should create orphaned resource issue', () => {
357
+ const issue = Issue.orphanedResource({
358
+ resourceType: 'AWS::RDS::DBCluster',
359
+ resourceId: 'my-cluster',
360
+ description: 'Cluster not in stack',
361
+ });
362
+
363
+ expect(issue.type).toBe('ORPHANED_RESOURCE');
364
+ expect(issue.severity).toBe('critical');
365
+ expect(issue.canAutoFix).toBe(true);
366
+ });
367
+
368
+ it('should create missing resource issue', () => {
369
+ const issue = Issue.missingResource({
370
+ resourceType: 'AWS::KMS::Key',
371
+ resourceId: 'FriggKMSKey',
372
+ description: 'Key not in AWS',
373
+ });
374
+
375
+ expect(issue.type).toBe('MISSING_RESOURCE');
376
+ expect(issue.severity).toBe('critical');
377
+ expect(issue.canAutoFix).toBe(false);
378
+ });
379
+
380
+ it('should create property mismatch issue', () => {
381
+ const mismatch = new PropertyMismatch({
382
+ propertyPath: 'Properties.Tags',
383
+ expectedValue: ['tag1'],
384
+ actualValue: ['tag2'],
385
+ mutability: PropertyMutability.MUTABLE,
386
+ });
387
+
388
+ const issue = Issue.propertyMismatch({
389
+ resourceType: 'AWS::EC2::VPC',
390
+ resourceId: 'vpc-123',
391
+ mismatch,
392
+ });
393
+
394
+ expect(issue.type).toBe('PROPERTY_MISMATCH');
395
+ expect(issue.severity).toBe('warning'); // Default for mutable
396
+ expect(issue.propertyMismatch).toBe(mismatch);
397
+ });
398
+
399
+ it('should create property mismatch with critical severity for immutable', () => {
400
+ const mismatch = new PropertyMismatch({
401
+ propertyPath: 'Properties.BucketName',
402
+ expectedValue: 'bucket-v2',
403
+ actualValue: 'bucket-v1',
404
+ mutability: PropertyMutability.IMMUTABLE,
405
+ });
406
+
407
+ const issue = Issue.propertyMismatch({
408
+ resourceType: 'AWS::S3::Bucket',
409
+ resourceId: 'my-bucket',
410
+ mismatch,
411
+ });
412
+
413
+ expect(issue.severity).toBe('critical'); // Critical for immutable
414
+ expect(issue.canAutoFix).toBe(false); // Can't auto-fix immutable
415
+ });
416
+ });
417
+
418
+ describe('_formatValue', () => {
419
+ it('should format primitive values', () => {
420
+ expect(Issue._formatValue('test')).toBe('test');
421
+ expect(Issue._formatValue(123)).toBe('123');
422
+ expect(Issue._formatValue(true)).toBe('true');
423
+ expect(Issue._formatValue(null)).toBe('null');
424
+ expect(Issue._formatValue(undefined)).toBe('undefined');
425
+ });
426
+
427
+ it('should format simple arrays', () => {
428
+ expect(Issue._formatValue(['a', 'b', 'c'])).toBe('["a","b","c"]');
429
+ expect(Issue._formatValue([1, 2, 3])).toBe('[1,2,3]');
430
+ });
431
+
432
+ it('should format arrays of objects (like Tags)', () => {
433
+ const tags = [
434
+ { Key: 'Name', Value: 'test' },
435
+ { Key: 'Environment', Value: 'prod' },
436
+ ];
437
+ const result = Issue._formatValue(tags);
438
+ expect(result).toContain('Key');
439
+ expect(result).toContain('Name');
440
+ expect(result).toContain('test');
441
+ });
442
+
443
+ it('should truncate long arrays of objects', () => {
444
+ const manyTags = [
445
+ { Key: 'Tag1', Value: 'val1' },
446
+ { Key: 'Tag2', Value: 'val2' },
447
+ { Key: 'Tag3', Value: 'val3' },
448
+ { Key: 'Tag4', Value: 'val4' },
449
+ ];
450
+ const result = Issue._formatValue(manyTags);
451
+ expect(result).toContain('4 total');
452
+ expect(result).toContain('...');
453
+ });
454
+
455
+ it('should format small objects', () => {
456
+ const obj = { a: 1, b: 2 };
457
+ expect(Issue._formatValue(obj)).toBe('{"a":1,"b":2}');
458
+ });
459
+
460
+ it('should truncate large objects', () => {
461
+ const largeObj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
462
+ const result = Issue._formatValue(largeObj);
463
+ expect(result).toContain('5 keys total');
464
+ expect(result).toContain('...');
465
+ });
466
+
467
+ it('should format empty collections', () => {
468
+ expect(Issue._formatValue([])).toBe('[]');
469
+ expect(Issue._formatValue({})).toBe('{}');
470
+ });
471
+ });
472
+
473
+ describe('propertyMismatch with formatted values', () => {
474
+ it('should format Tags arrays correctly in description', () => {
475
+ const mismatch = new PropertyMismatch({
476
+ propertyPath: 'Properties.Tags',
477
+ expectedValue: [
478
+ { Key: 'Name', Value: 'test' },
479
+ { Key: 'Environment', Value: 'prod' },
480
+ ],
481
+ actualValue: [
482
+ { Key: 'Name', Value: 'test' },
483
+ { Key: 'Environment', Value: 'prod' },
484
+ { Key: 'ManagedBy', Value: 'Frigg' },
485
+ ],
486
+ mutability: PropertyMutability.MUTABLE,
487
+ });
488
+
489
+ const issue = Issue.propertyMismatch({
490
+ resourceType: 'AWS::EC2::Subnet',
491
+ resourceId: 'subnet-123',
492
+ mismatch,
493
+ });
494
+
495
+ // Should not contain [object Object]
496
+ expect(issue.description).not.toContain('[object Object]');
497
+ // Should contain JSON representation
498
+ expect(issue.description).toContain('Key');
499
+ expect(issue.description).toContain('Name');
500
+ });
501
+
502
+ it('should handle complex nested objects', () => {
503
+ const mismatch = new PropertyMismatch({
504
+ propertyPath: 'Properties.VpcConfig',
505
+ expectedValue: {
506
+ SubnetIds: ['subnet-1', 'subnet-2'],
507
+ SecurityGroupIds: ['sg-1'],
508
+ },
509
+ actualValue: {
510
+ SubnetIds: ['subnet-3', 'subnet-4'],
511
+ SecurityGroupIds: ['sg-2'],
512
+ },
513
+ mutability: PropertyMutability.MUTABLE,
514
+ });
515
+
516
+ const issue = Issue.propertyMismatch({
517
+ resourceType: 'AWS::Lambda::Function',
518
+ resourceId: 'my-function',
519
+ mismatch,
520
+ });
521
+
522
+ // Should not contain [object Object]
523
+ expect(issue.description).not.toContain('[object Object]');
524
+ // Should contain structured representation
525
+ expect(issue.description).toContain('SubnetIds');
526
+ });
527
+ });
528
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * PropertyMismatch Entity
3
+ *
4
+ * Represents a difference between expected and actual property values
5
+ * for a CloudFormation resource.
6
+ */
7
+
8
+ const PropertyMutability = require('../value-objects/property-mutability');
9
+
10
+ class PropertyMismatch {
11
+ /**
12
+ * Create a new PropertyMismatch
13
+ *
14
+ * @param {Object} params
15
+ * @param {string} params.propertyPath - Path to the property (e.g., 'Properties.BucketName')
16
+ * @param {*} params.expectedValue - Expected property value
17
+ * @param {*} params.actualValue - Actual property value
18
+ * @param {PropertyMutability} params.mutability - Property mutability
19
+ */
20
+ constructor({ propertyPath, expectedValue, actualValue, mutability }) {
21
+ // Validate required fields
22
+ if (!propertyPath) {
23
+ throw new Error('propertyPath is required');
24
+ }
25
+
26
+ // Note: expectedValue and actualValue can be undefined (for missing properties)
27
+ // They can also be null (explicit null value)
28
+ // Only check if they're provided in the params object at all
29
+ if (!('expectedValue' in arguments[0])) {
30
+ throw new Error('expectedValue must be provided (can be null or undefined)');
31
+ }
32
+
33
+ if (!('actualValue' in arguments[0])) {
34
+ throw new Error('actualValue must be provided (can be null or undefined)');
35
+ }
36
+
37
+ if (!mutability) {
38
+ throw new Error('mutability is required');
39
+ }
40
+
41
+ if (!(mutability instanceof PropertyMutability)) {
42
+ throw new Error('mutability must be a PropertyMutability instance');
43
+ }
44
+
45
+ this.propertyPath = propertyPath;
46
+ this.expectedValue = expectedValue;
47
+ this.actualValue = actualValue;
48
+ this.mutability = mutability;
49
+ }
50
+
51
+ /**
52
+ * Check if fixing this mismatch requires resource replacement
53
+ *
54
+ * @returns {boolean}
55
+ */
56
+ requiresReplacement() {
57
+ return this.mutability.requiresReplacement();
58
+ }
59
+
60
+ /**
61
+ * Check if this mismatch can be automatically fixed
62
+ *
63
+ * @returns {boolean}
64
+ */
65
+ canAutoFix() {
66
+ return this.mutability.canChange();
67
+ }
68
+
69
+ /**
70
+ * Get severity level of this mismatch
71
+ *
72
+ * @returns {'critical' | 'warning'}
73
+ */
74
+ getSeverity() {
75
+ return this.mutability.isImmutable() ? 'critical' : 'warning';
76
+ }
77
+
78
+ /**
79
+ * Get string representation
80
+ *
81
+ * @returns {string}
82
+ */
83
+ toString() {
84
+ const expectedStr = this.expectedValue === null ? 'null' : this.expectedValue;
85
+ const actualStr = this.actualValue === null ? 'null' : this.actualValue;
86
+
87
+ return `PropertyMismatch: ${this.propertyPath} (expected: ${expectedStr}, actual: ${actualStr}, mutability: ${this.mutability.toString()})`;
88
+ }
89
+
90
+ /**
91
+ * Serialize to JSON
92
+ *
93
+ * @returns {Object}
94
+ */
95
+ toJSON() {
96
+ return {
97
+ propertyPath: this.propertyPath,
98
+ expectedValue: this.expectedValue,
99
+ actualValue: this.actualValue,
100
+ mutability: this.mutability.toString(),
101
+ severity: this.getSeverity(),
102
+ canAutoFix: this.canAutoFix(),
103
+ requiresReplacement: this.requiresReplacement(),
104
+ };
105
+ }
106
+ }
107
+
108
+ module.exports = PropertyMismatch;