@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,580 @@
1
+ /**
2
+ * Tests for AWSStackRepository Adapter
3
+ *
4
+ * Tests CloudFormation API integration using mocked AWS SDK clients
5
+ */
6
+
7
+ const AWSStackRepository = require('./aws-stack-repository');
8
+ const StackIdentifier = require('../../domain/value-objects/stack-identifier');
9
+
10
+ // Mock AWS SDK
11
+ jest.mock('@aws-sdk/client-cloudformation', () => {
12
+ return {
13
+ CloudFormationClient: jest.fn(),
14
+ DescribeStacksCommand: jest.fn(),
15
+ ListStackResourcesCommand: jest.fn(),
16
+ DescribeStackResourcesCommand: jest.fn(),
17
+ DescribeStackResourceCommand: jest.fn(),
18
+ GetTemplateCommand: jest.fn(),
19
+ DetectStackDriftCommand: jest.fn(),
20
+ DescribeStackDriftDetectionStatusCommand: jest.fn(),
21
+ DescribeStackResourceDriftsCommand: jest.fn(),
22
+ };
23
+ });
24
+
25
+ describe('AWSStackRepository', () => {
26
+ let repository;
27
+ let mockSend;
28
+ let mockClient;
29
+
30
+ beforeEach(() => {
31
+ // Reset mocks
32
+ jest.clearAllMocks();
33
+
34
+ // Create mock client with send method
35
+ mockSend = jest.fn();
36
+ mockClient = {
37
+ send: mockSend,
38
+ };
39
+
40
+ // Mock CloudFormationClient constructor
41
+ const { CloudFormationClient } = require('@aws-sdk/client-cloudformation');
42
+ CloudFormationClient.mockImplementation(() => mockClient);
43
+
44
+ repository = new AWSStackRepository();
45
+ });
46
+
47
+ describe('getStack', () => {
48
+ it('should get stack information', async () => {
49
+ const identifier = new StackIdentifier({
50
+ stackName: 'my-app-prod',
51
+ region: 'us-east-1',
52
+ accountId: '123456789012',
53
+ });
54
+
55
+ const mockStack = {
56
+ StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
57
+ StackName: 'my-app-prod',
58
+ StackStatus: 'UPDATE_COMPLETE',
59
+ CreationTime: new Date('2024-01-01T00:00:00Z'),
60
+ LastUpdatedTime: new Date('2024-01-15T00:00:00Z'),
61
+ Parameters: [
62
+ { ParameterKey: 'Environment', ParameterValue: 'production' },
63
+ ],
64
+ Outputs: [
65
+ { OutputKey: 'VpcId', OutputValue: 'vpc-123' },
66
+ ],
67
+ Tags: [
68
+ { Key: 'Team', Value: 'platform' },
69
+ ],
70
+ };
71
+
72
+ mockSend.mockResolvedValue({
73
+ Stacks: [mockStack],
74
+ });
75
+
76
+ const stack = await repository.getStack(identifier);
77
+
78
+ expect(stack).toEqual({
79
+ stackName: 'my-app-prod',
80
+ region: 'us-east-1',
81
+ accountId: '123456789012',
82
+ stackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
83
+ status: 'UPDATE_COMPLETE',
84
+ creationTime: new Date('2024-01-01T00:00:00Z'),
85
+ lastUpdatedTime: new Date('2024-01-15T00:00:00Z'),
86
+ parameters: { Environment: 'production' },
87
+ outputs: { VpcId: 'vpc-123' },
88
+ tags: { Team: 'platform' },
89
+ });
90
+
91
+ expect(mockSend).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it('should throw error if stack does not exist', async () => {
95
+ const identifier = new StackIdentifier({
96
+ stackName: 'non-existent',
97
+ region: 'us-east-1',
98
+ });
99
+
100
+ mockSend.mockRejectedValue({
101
+ name: 'ValidationError',
102
+ message: 'Stack does not exist',
103
+ });
104
+
105
+ await expect(repository.getStack(identifier)).rejects.toThrow(
106
+ 'Stack non-existent does not exist in region us-east-1'
107
+ );
108
+ });
109
+
110
+ it('should handle stacks without parameters', async () => {
111
+ const identifier = new StackIdentifier({
112
+ stackName: 'simple-stack',
113
+ region: 'us-east-1',
114
+ });
115
+
116
+ mockSend.mockResolvedValue({
117
+ Stacks: [
118
+ {
119
+ StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/simple-stack/guid',
120
+ StackName: 'simple-stack',
121
+ StackStatus: 'CREATE_COMPLETE',
122
+ CreationTime: new Date('2024-01-01T00:00:00Z'),
123
+ },
124
+ ],
125
+ });
126
+
127
+ const stack = await repository.getStack(identifier);
128
+
129
+ expect(stack.parameters).toEqual({});
130
+ expect(stack.outputs).toEqual({});
131
+ expect(stack.tags).toEqual({});
132
+ });
133
+ });
134
+
135
+ describe('listResources', () => {
136
+ it('should list all stack resources', async () => {
137
+ const identifier = new StackIdentifier({
138
+ stackName: 'my-app-prod',
139
+ region: 'us-east-1',
140
+ });
141
+
142
+ mockSend.mockResolvedValue({
143
+ StackResourceSummaries: [
144
+ {
145
+ LogicalResourceId: 'MyVPC',
146
+ PhysicalResourceId: 'vpc-123',
147
+ ResourceType: 'AWS::EC2::VPC',
148
+ ResourceStatus: 'CREATE_COMPLETE',
149
+ LastUpdatedTimestamp: new Date('2024-01-15T00:00:00Z'),
150
+ DriftInformation: { StackResourceDriftStatus: 'IN_SYNC' },
151
+ },
152
+ {
153
+ LogicalResourceId: 'MySubnet',
154
+ PhysicalResourceId: 'subnet-456',
155
+ ResourceType: 'AWS::EC2::Subnet',
156
+ ResourceStatus: 'CREATE_COMPLETE',
157
+ LastUpdatedTimestamp: new Date('2024-01-15T00:00:00Z'),
158
+ DriftInformation: { StackResourceDriftStatus: 'NOT_CHECKED' },
159
+ },
160
+ ],
161
+ });
162
+
163
+ const resources = await repository.listResources(identifier);
164
+
165
+ expect(resources).toHaveLength(2);
166
+ expect(resources[0]).toEqual({
167
+ logicalId: 'MyVPC',
168
+ physicalId: 'vpc-123',
169
+ resourceType: 'AWS::EC2::VPC',
170
+ status: 'CREATE_COMPLETE',
171
+ lastUpdatedTime: new Date('2024-01-15T00:00:00Z'),
172
+ driftStatus: 'IN_SYNC',
173
+ });
174
+
175
+ expect(resources[1].driftStatus).toBe('NOT_CHECKED');
176
+ });
177
+
178
+ it('should handle pagination for large stacks', async () => {
179
+ const identifier = new StackIdentifier({
180
+ stackName: 'large-stack',
181
+ region: 'us-east-1',
182
+ });
183
+
184
+ // First page
185
+ mockSend.mockResolvedValueOnce({
186
+ StackResourceSummaries: [
187
+ {
188
+ LogicalResourceId: 'Resource1',
189
+ PhysicalResourceId: 'res-1',
190
+ ResourceType: 'AWS::EC2::VPC',
191
+ ResourceStatus: 'CREATE_COMPLETE',
192
+ LastUpdatedTimestamp: new Date('2024-01-15T00:00:00Z'),
193
+ DriftInformation: { StackResourceDriftStatus: 'IN_SYNC' },
194
+ },
195
+ ],
196
+ NextToken: 'token-123',
197
+ });
198
+
199
+ // Second page
200
+ mockSend.mockResolvedValueOnce({
201
+ StackResourceSummaries: [
202
+ {
203
+ LogicalResourceId: 'Resource2',
204
+ PhysicalResourceId: 'res-2',
205
+ ResourceType: 'AWS::EC2::Subnet',
206
+ ResourceStatus: 'CREATE_COMPLETE',
207
+ LastUpdatedTimestamp: new Date('2024-01-15T00:00:00Z'),
208
+ DriftInformation: { StackResourceDriftStatus: 'IN_SYNC' },
209
+ },
210
+ ],
211
+ });
212
+
213
+ const resources = await repository.listResources(identifier);
214
+
215
+ expect(resources).toHaveLength(2);
216
+ expect(mockSend).toHaveBeenCalledTimes(2);
217
+ });
218
+
219
+ it('should throw error if stack does not exist', async () => {
220
+ const identifier = new StackIdentifier({
221
+ stackName: 'non-existent',
222
+ region: 'us-east-1',
223
+ });
224
+
225
+ const error = new Error('Stack does not exist');
226
+ error.name = 'ValidationError';
227
+ mockSend.mockRejectedValue(error);
228
+
229
+ await expect(repository.listResources(identifier)).rejects.toThrow('Stack does not exist');
230
+ });
231
+ });
232
+
233
+ describe('getResource', () => {
234
+ it('should get resource details', async () => {
235
+ const identifier = new StackIdentifier({
236
+ stackName: 'my-app-prod',
237
+ region: 'us-east-1',
238
+ });
239
+
240
+ mockSend.mockResolvedValue({
241
+ StackResourceDetail: {
242
+ LogicalResourceId: 'MyVPC',
243
+ PhysicalResourceId: 'vpc-123',
244
+ ResourceType: 'AWS::EC2::VPC',
245
+ ResourceStatus: 'CREATE_COMPLETE',
246
+ Metadata: '{"Description": "Main VPC"}',
247
+ },
248
+ });
249
+
250
+ const resource = await repository.getResource(identifier, 'MyVPC');
251
+
252
+ expect(resource).toEqual({
253
+ logicalId: 'MyVPC',
254
+ physicalId: 'vpc-123',
255
+ resourceType: 'AWS::EC2::VPC',
256
+ status: 'CREATE_COMPLETE',
257
+ properties: {},
258
+ metadata: { Description: 'Main VPC' },
259
+ });
260
+ });
261
+
262
+ it('should handle resource without metadata', async () => {
263
+ const identifier = new StackIdentifier({
264
+ stackName: 'my-app-prod',
265
+ region: 'us-east-1',
266
+ });
267
+
268
+ mockSend.mockResolvedValue({
269
+ StackResourceDetail: {
270
+ LogicalResourceId: 'MyVPC',
271
+ PhysicalResourceId: 'vpc-123',
272
+ ResourceType: 'AWS::EC2::VPC',
273
+ ResourceStatus: 'CREATE_COMPLETE',
274
+ },
275
+ });
276
+
277
+ const resource = await repository.getResource(identifier, 'MyVPC');
278
+
279
+ expect(resource.metadata).toEqual({});
280
+ });
281
+
282
+ it('should throw error if resource does not exist', async () => {
283
+ const identifier = new StackIdentifier({
284
+ stackName: 'my-app-prod',
285
+ region: 'us-east-1',
286
+ });
287
+
288
+ const error = new Error('Resource does not exist');
289
+ error.name = 'ValidationError';
290
+ mockSend.mockRejectedValue(error);
291
+
292
+ await expect(repository.getResource(identifier, 'NonExistent')).rejects.toThrow('Resource does not exist');
293
+ });
294
+ });
295
+
296
+ describe('getTemplate', () => {
297
+ it('should get CloudFormation template', async () => {
298
+ const identifier = new StackIdentifier({
299
+ stackName: 'my-app-prod',
300
+ region: 'us-east-1',
301
+ });
302
+
303
+ const mockTemplate = {
304
+ Resources: {
305
+ MyVPC: {
306
+ Type: 'AWS::EC2::VPC',
307
+ Properties: {
308
+ CidrBlock: '10.0.0.0/16',
309
+ },
310
+ },
311
+ },
312
+ };
313
+
314
+ mockSend.mockResolvedValue({
315
+ TemplateBody: JSON.stringify(mockTemplate),
316
+ });
317
+
318
+ const template = await repository.getTemplate(identifier);
319
+
320
+ expect(template).toEqual(mockTemplate);
321
+ });
322
+
323
+ it('should handle YAML templates', async () => {
324
+ const identifier = new StackIdentifier({
325
+ stackName: 'my-app-prod',
326
+ region: 'us-east-1',
327
+ });
328
+
329
+ const yamlTemplate = `
330
+ Resources:
331
+ MyVPC:
332
+ Type: AWS::EC2::VPC
333
+ Properties:
334
+ CidrBlock: 10.0.0.0/16
335
+ `;
336
+
337
+ mockSend.mockResolvedValue({
338
+ TemplateBody: yamlTemplate,
339
+ });
340
+
341
+ const template = await repository.getTemplate(identifier);
342
+
343
+ expect(template).toHaveProperty('Resources');
344
+ expect(template.Resources).toHaveProperty('MyVPC');
345
+ });
346
+ });
347
+
348
+ describe('exists', () => {
349
+ it('should return true if stack exists', async () => {
350
+ const identifier = new StackIdentifier({
351
+ stackName: 'my-app-prod',
352
+ region: 'us-east-1',
353
+ });
354
+
355
+ mockSend.mockResolvedValue({
356
+ Stacks: [{
357
+ StackName: 'my-app-prod',
358
+ StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/my-app-prod/guid',
359
+ StackStatus: 'UPDATE_COMPLETE',
360
+ CreationTime: new Date('2024-01-01T00:00:00Z'),
361
+ }],
362
+ });
363
+
364
+ const exists = await repository.exists(identifier);
365
+
366
+ expect(exists).toBe(true);
367
+ });
368
+
369
+ it('should return false if stack does not exist', async () => {
370
+ const identifier = new StackIdentifier({
371
+ stackName: 'non-existent',
372
+ region: 'us-east-1',
373
+ });
374
+
375
+ const error = new Error('Stack does not exist');
376
+ error.name = 'ValidationError';
377
+ mockSend.mockRejectedValue(error);
378
+
379
+ const exists = await repository.exists(identifier);
380
+
381
+ expect(exists).toBe(false);
382
+ });
383
+
384
+ it('should return false if stack is deleted', async () => {
385
+ const identifier = new StackIdentifier({
386
+ stackName: 'deleted-stack',
387
+ region: 'us-east-1',
388
+ });
389
+
390
+ mockSend.mockResolvedValue({
391
+ Stacks: [{
392
+ StackName: 'deleted-stack',
393
+ StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/deleted-stack/guid',
394
+ StackStatus: 'DELETE_COMPLETE',
395
+ CreationTime: new Date('2024-01-01T00:00:00Z'),
396
+ }],
397
+ });
398
+
399
+ const exists = await repository.exists(identifier);
400
+
401
+ expect(exists).toBe(false);
402
+ });
403
+ });
404
+
405
+ describe('detectStackDrift', () => {
406
+ it('should detect stack drift', async () => {
407
+ const identifier = new StackIdentifier({
408
+ stackName: 'my-app-prod',
409
+ region: 'us-east-1',
410
+ });
411
+
412
+ // Mock drift detection initiation
413
+ mockSend.mockResolvedValueOnce({
414
+ StackDriftDetectionId: 'drift-detection-123',
415
+ });
416
+
417
+ // Mock drift detection status polling
418
+ mockSend.mockResolvedValueOnce({
419
+ DetectionStatus: 'DETECTION_COMPLETE',
420
+ StackDriftStatus: 'DRIFTED',
421
+ DriftedStackResourceCount: 2,
422
+ Timestamp: new Date('2024-01-15T10:00:00Z'),
423
+ });
424
+
425
+ const result = await repository.detectStackDrift(identifier);
426
+
427
+ expect(result).toEqual({
428
+ stackDriftStatus: 'DRIFTED',
429
+ driftedResourceCount: 2,
430
+ detectionTime: new Date('2024-01-15T10:00:00Z'),
431
+ });
432
+
433
+ expect(mockSend).toHaveBeenCalledTimes(2);
434
+ });
435
+
436
+ it('should handle in-sync stacks', async () => {
437
+ const identifier = new StackIdentifier({
438
+ stackName: 'my-app-prod',
439
+ region: 'us-east-1',
440
+ });
441
+
442
+ mockSend.mockResolvedValueOnce({
443
+ StackDriftDetectionId: 'drift-detection-123',
444
+ });
445
+
446
+ mockSend.mockResolvedValueOnce({
447
+ DetectionStatus: 'DETECTION_COMPLETE',
448
+ StackDriftStatus: 'IN_SYNC',
449
+ DriftedStackResourceCount: 0,
450
+ Timestamp: new Date('2024-01-15T10:00:00Z'),
451
+ });
452
+
453
+ const result = await repository.detectStackDrift(identifier);
454
+
455
+ expect(result.stackDriftStatus).toBe('IN_SYNC');
456
+ expect(result.driftedResourceCount).toBe(0);
457
+ });
458
+
459
+ it('should poll until detection completes', async () => {
460
+ const identifier = new StackIdentifier({
461
+ stackName: 'my-app-prod',
462
+ region: 'us-east-1',
463
+ });
464
+
465
+ mockSend.mockResolvedValueOnce({
466
+ StackDriftDetectionId: 'drift-detection-123',
467
+ });
468
+
469
+ // First poll: in progress
470
+ mockSend.mockResolvedValueOnce({
471
+ DetectionStatus: 'DETECTION_IN_PROGRESS',
472
+ });
473
+
474
+ // Second poll: complete
475
+ mockSend.mockResolvedValueOnce({
476
+ DetectionStatus: 'DETECTION_COMPLETE',
477
+ StackDriftStatus: 'IN_SYNC',
478
+ DriftedStackResourceCount: 0,
479
+ Timestamp: new Date('2024-01-15T10:00:00Z'),
480
+ });
481
+
482
+ const result = await repository.detectStackDrift(identifier);
483
+
484
+ expect(result.stackDriftStatus).toBe('IN_SYNC');
485
+ expect(mockSend).toHaveBeenCalledTimes(3);
486
+ });
487
+ });
488
+
489
+ describe('getResourceDrift', () => {
490
+ it('should get resource drift details', async () => {
491
+ const identifier = new StackIdentifier({
492
+ stackName: 'my-app-prod',
493
+ region: 'us-east-1',
494
+ });
495
+
496
+ mockSend.mockResolvedValue({
497
+ StackResourceDrifts: [
498
+ {
499
+ LogicalResourceId: 'MyVPC',
500
+ StackResourceDriftStatus: 'MODIFIED',
501
+ ExpectedProperties: JSON.stringify({
502
+ CidrBlock: '10.0.0.0/16',
503
+ EnableDnsSupport: true,
504
+ }),
505
+ ActualProperties: JSON.stringify({
506
+ CidrBlock: '10.0.0.0/16',
507
+ EnableDnsSupport: false,
508
+ }),
509
+ PropertyDifferences: [
510
+ {
511
+ PropertyPath: '/Properties/EnableDnsSupport',
512
+ ExpectedValue: 'true',
513
+ ActualValue: 'false',
514
+ DifferenceType: 'NOT_EQUAL',
515
+ },
516
+ ],
517
+ },
518
+ ],
519
+ });
520
+
521
+ const drift = await repository.getResourceDrift(identifier, 'MyVPC');
522
+
523
+ expect(drift).toEqual({
524
+ driftStatus: 'MODIFIED',
525
+ expectedProperties: {
526
+ CidrBlock: '10.0.0.0/16',
527
+ EnableDnsSupport: true,
528
+ },
529
+ actualProperties: {
530
+ CidrBlock: '10.0.0.0/16',
531
+ EnableDnsSupport: false,
532
+ },
533
+ propertyDifferences: [
534
+ {
535
+ PropertyPath: '/Properties/EnableDnsSupport',
536
+ ExpectedValue: 'true',
537
+ ActualValue: 'false',
538
+ DifferenceType: 'NOT_EQUAL',
539
+ },
540
+ ],
541
+ });
542
+ });
543
+
544
+ it('should handle resources in sync', async () => {
545
+ const identifier = new StackIdentifier({
546
+ stackName: 'my-app-prod',
547
+ region: 'us-east-1',
548
+ });
549
+
550
+ mockSend.mockResolvedValue({
551
+ StackResourceDrifts: [
552
+ {
553
+ LogicalResourceId: 'MyVPC',
554
+ StackResourceDriftStatus: 'IN_SYNC',
555
+ ExpectedProperties: JSON.stringify({ CidrBlock: '10.0.0.0/16' }),
556
+ ActualProperties: JSON.stringify({ CidrBlock: '10.0.0.0/16' }),
557
+ PropertyDifferences: [],
558
+ },
559
+ ],
560
+ });
561
+
562
+ const drift = await repository.getResourceDrift(identifier, 'MyVPC');
563
+
564
+ expect(drift.driftStatus).toBe('IN_SYNC');
565
+ expect(drift.propertyDifferences).toEqual([]);
566
+ });
567
+ });
568
+
569
+ describe('constructor', () => {
570
+ it('should create instance with default region', () => {
571
+ const repo = new AWSStackRepository();
572
+ expect(repo).toBeInstanceOf(AWSStackRepository);
573
+ });
574
+
575
+ it('should create instance with custom region', () => {
576
+ const repo = new AWSStackRepository({ region: 'eu-west-1' });
577
+ expect(repo).toBeInstanceOf(AWSStackRepository);
578
+ });
579
+ });
580
+ });