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

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 +695 -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,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
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * RunHealthCheckUseCase - Orchestrate Complete Stack Health Check
3
+ *
4
+ * Application Layer - Use Case
5
+ *
6
+ * Business logic for the "frigg doctor" command. Orchestrates multiple
7
+ * repositories and domain services to produce a comprehensive health report.
8
+ *
9
+ * Responsibilities:
10
+ * - Coordinate stack information retrieval
11
+ * - Detect drift at stack and resource level
12
+ * - Find orphaned resources
13
+ * - Analyze property mismatches
14
+ * - Calculate health score
15
+ * - Build comprehensive health report
16
+ */
17
+
18
+ const StackHealthReport = require('../../domain/entities/stack-health-report');
19
+ const Resource = require('../../domain/entities/resource');
20
+ const Issue = require('../../domain/entities/issue');
21
+ const ResourceState = require('../../domain/value-objects/resource-state');
22
+ const { getPropertyMutability } = require('../../domain/services/property-mutability-config');
23
+
24
+ class RunHealthCheckUseCase {
25
+ /**
26
+ * Create use case with required dependencies
27
+ *
28
+ * @param {Object} params
29
+ * @param {IStackRepository} params.stackRepository - Stack operations
30
+ * @param {IResourceDetector} params.resourceDetector - Resource discovery
31
+ * @param {MismatchAnalyzer} params.mismatchAnalyzer - Property drift analysis
32
+ * @param {HealthScoreCalculator} params.healthScoreCalculator - Health scoring
33
+ */
34
+ constructor({ stackRepository, resourceDetector, mismatchAnalyzer, healthScoreCalculator }) {
35
+ if (!stackRepository) {
36
+ throw new Error('stackRepository is required');
37
+ }
38
+ if (!resourceDetector) {
39
+ throw new Error('resourceDetector is required');
40
+ }
41
+ if (!mismatchAnalyzer) {
42
+ throw new Error('mismatchAnalyzer is required');
43
+ }
44
+ if (!healthScoreCalculator) {
45
+ throw new Error('healthScoreCalculator is required');
46
+ }
47
+
48
+ this.stackRepository = stackRepository;
49
+ this.resourceDetector = resourceDetector;
50
+ this.mismatchAnalyzer = mismatchAnalyzer;
51
+ this.healthScoreCalculator = healthScoreCalculator;
52
+ }
53
+
54
+ /**
55
+ * Execute complete health check for a stack
56
+ *
57
+ * @param {Object} params
58
+ * @param {StackIdentifier} params.stackIdentifier - Stack to check
59
+ * @param {Function} params.onProgress - Optional progress callback (step, message)
60
+ * @returns {Promise<StackHealthReport>} Comprehensive health report
61
+ */
62
+ async execute({ stackIdentifier, onProgress }) {
63
+ // Helper to call progress callback if provided
64
+ const progress = (step, message) => {
65
+ if (onProgress) {
66
+ onProgress(step, message);
67
+ }
68
+ };
69
+
70
+ // 1. Verify stack exists
71
+ progress('📋 Step 1/5:', 'Verifying stack exists...');
72
+ await this.stackRepository.getStack(stackIdentifier);
73
+
74
+ // 2. Detect stack-level drift
75
+ progress('🔍 Step 2/5:', 'Detecting stack drift...');
76
+ const driftDetection = await this.stackRepository.detectStackDrift(stackIdentifier);
77
+
78
+ // 3. Get all stack resources
79
+ progress('📊 Step 3/5:', 'Analyzing stack resources...');
80
+ const stackResources = await this.stackRepository.listResources(stackIdentifier);
81
+
82
+ // 4. Build resource entities with drift status
83
+ const resources = [];
84
+ const issues = [];
85
+
86
+ for (const stackResource of stackResources) {
87
+ let resourceState;
88
+
89
+ // Determine resource state
90
+ if (!stackResource.physicalId || stackResource.driftStatus === 'DELETED') {
91
+ // Missing resource (defined in template but doesn't exist in cloud)
92
+ resourceState = ResourceState.MISSING;
93
+
94
+ // Create issue for missing resource using factory method
95
+ issues.push(
96
+ Issue.missingResource({
97
+ resourceType: stackResource.resourceType,
98
+ resourceId: stackResource.logicalId,
99
+ description: `CloudFormation resource ${stackResource.logicalId} (${stackResource.resourceType}) is defined in the template but does not exist in the cloud.`,
100
+ })
101
+ );
102
+ } else if (stackResource.driftStatus === 'MODIFIED') {
103
+ // Drifted resource - get detailed drift information
104
+ resourceState = ResourceState.DRIFTED;
105
+
106
+ const resourceDrift = await this.stackRepository.getResourceDrift(
107
+ stackIdentifier,
108
+ stackResource.logicalId
109
+ );
110
+
111
+ // Analyze property mismatches using domain service
112
+ if (
113
+ resourceDrift.propertyDifferences &&
114
+ resourceDrift.propertyDifferences.length > 0
115
+ ) {
116
+ // Build property mutability map for this resource type
117
+ // AWS drift detection returns property paths, we need to provide mutability for each
118
+ const propertyMutabilityMap = {};
119
+ for (const propDiff of resourceDrift.propertyDifferences) {
120
+ const propertyPath = propDiff.PropertyPath.replace(/^\//, ''); // Remove leading slash
121
+ propertyMutabilityMap[propertyPath] = getPropertyMutability(
122
+ stackResource.resourceType,
123
+ propertyPath
124
+ );
125
+ }
126
+
127
+ const propertyMismatches = this.mismatchAnalyzer.analyze({
128
+ expected: resourceDrift.expectedProperties,
129
+ actual: resourceDrift.actualProperties,
130
+ propertyMutability: propertyMutabilityMap,
131
+ ignoreProperties: [],
132
+ });
133
+
134
+ // Create issue for each property mismatch using factory method
135
+ for (const mismatch of propertyMismatches) {
136
+ issues.push(
137
+ Issue.propertyMismatch({
138
+ resourceType: stackResource.resourceType,
139
+ resourceId: stackResource.physicalId,
140
+ mismatch,
141
+ })
142
+ );
143
+ }
144
+ }
145
+ } else {
146
+ // Resource is in sync
147
+ resourceState = ResourceState.IN_STACK;
148
+ }
149
+
150
+ // Create resource entity
151
+ // For missing resources, use placeholder physicalId since they don't exist in cloud
152
+ const resource = new Resource({
153
+ logicalId: stackResource.logicalId,
154
+ physicalId: stackResource.physicalId || `missing-${stackResource.logicalId}`,
155
+ resourceType: stackResource.resourceType,
156
+ state: resourceState,
157
+ });
158
+
159
+ resources.push(resource);
160
+ }
161
+
162
+ // 5. Find orphaned resources (exist in cloud but not in stack)
163
+ progress('🔎 Step 4/5:', 'Checking for orphaned resources...');
164
+ const orphanedResources = await this.resourceDetector.findOrphanedResources({
165
+ stackIdentifier,
166
+ stackResources,
167
+ });
168
+
169
+ for (const orphan of orphanedResources) {
170
+ // Create resource entity for orphan
171
+ // IMPORTANT: Include both properties AND tags for template comparison
172
+ // Tags are stored in properties.tags for logical ID mapping
173
+ const orphanResource = new Resource({
174
+ logicalId: null, // No logical ID (not in template)
175
+ physicalId: orphan.physicalId,
176
+ resourceType: orphan.resourceType,
177
+ state: ResourceState.ORPHANED,
178
+ properties: {
179
+ ...(orphan.properties || {}), // Include AWS properties
180
+ tags: orphan.tags || {}, // Include tags for logical ID matching
181
+ },
182
+ });
183
+
184
+ resources.push(orphanResource);
185
+
186
+ // Create issue for orphaned resource using factory method
187
+ issues.push(
188
+ Issue.orphanedResource({
189
+ resourceType: orphan.resourceType,
190
+ resourceId: orphan.physicalId,
191
+ description: `Resource ${orphan.physicalId} exists in the cloud but is not managed by CloudFormation stack ${stackIdentifier.stackName}.`,
192
+ })
193
+ );
194
+ }
195
+
196
+ // 6. Calculate health score using domain service
197
+ progress('🧮 Step 5/5:', 'Calculating health score...');
198
+ const healthScore = this.healthScoreCalculator.calculate({ resources, issues });
199
+
200
+ // 7. Build comprehensive health report (aggregate root)
201
+ const report = new StackHealthReport({
202
+ stackIdentifier,
203
+ healthScore,
204
+ resources,
205
+ issues,
206
+ timestamp: new Date(),
207
+ });
208
+
209
+ return report;
210
+ }
211
+ }
212
+
213
+ module.exports = RunHealthCheckUseCase;