@friggframework/devtools 2.0.0-next.44 → 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 -2074
  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,1150 @@
1
+ /**
2
+ * ImportTemplateGenerator Tests
3
+ *
4
+ * TDD tests for CloudFormation import template generation
5
+ * Domain Layer - Service Tests
6
+ *
7
+ * Responsibilities:
8
+ * - Generate CloudFormation template for import operation
9
+ * - Resolve template intrinsics (!Ref, !Sub, !GetAtt) with actual AWS values
10
+ * - Validate template matches current resource state
11
+ * - Merge resource definitions with existing template
12
+ */
13
+
14
+ const { ImportTemplateGenerator } = require('../import-template-generator');
15
+
16
+ describe('ImportTemplateGenerator', () => {
17
+ let generator;
18
+ let mockTemplateParser;
19
+ let mockResourceDetector;
20
+ let mockStackRepository;
21
+
22
+ beforeEach(() => {
23
+ // Mock template parser
24
+ mockTemplateParser = {
25
+ parseTemplate: jest.fn(),
26
+ };
27
+
28
+ // Mock resource detector (AWS state)
29
+ mockResourceDetector = {
30
+ getResourceDetails: jest.fn(),
31
+ };
32
+
33
+ // Mock stack repository (CloudFormation)
34
+ mockStackRepository = {
35
+ getTemplate: jest.fn(),
36
+ };
37
+
38
+ generator = new ImportTemplateGenerator({
39
+ templateParser: mockTemplateParser,
40
+ resourceDetector: mockResourceDetector,
41
+ stackRepository: mockStackRepository,
42
+ });
43
+ });
44
+
45
+ describe('generateImportTemplate', () => {
46
+ const stackIdentifier = {
47
+ stackName: 'test-stack',
48
+ region: 'us-east-1',
49
+ };
50
+
51
+ it('should generate template with resolved !Ref VpcCidr intrinsic', async () => {
52
+ // Arrange
53
+ const resourcesToImport = [
54
+ {
55
+ logicalId: 'FriggVPC',
56
+ physicalId: 'vpc-12345678',
57
+ resourceType: 'AWS::EC2::VPC',
58
+ },
59
+ ];
60
+
61
+ const buildTemplate = {
62
+ resources: {
63
+ FriggVPC: {
64
+ Type: 'AWS::EC2::VPC',
65
+ Properties: {
66
+ CidrBlock: { Ref: 'VpcCidr' }, // !Ref intrinsic
67
+ EnableDnsHostnames: true,
68
+ EnableDnsSupport: true,
69
+ },
70
+ },
71
+ },
72
+ };
73
+
74
+ const awsResourceDetails = {
75
+ properties: {
76
+ VpcId: 'vpc-12345678',
77
+ CidrBlock: '10.0.0.0/16', // Actual AWS value
78
+ },
79
+ };
80
+
81
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
82
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
83
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
84
+
85
+ // Act
86
+ const result = await generator.generateImportTemplate({
87
+ resourcesToImport,
88
+ buildTemplatePath: '/path/to/build-template.json',
89
+ stackIdentifier,
90
+ });
91
+
92
+ // Assert
93
+ expect(result.template.Resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
94
+ expect(result.template.Resources.FriggVPC.Properties.EnableDnsHostnames).toBe(true);
95
+ expect(result.resourceIdentifiers).toHaveLength(1);
96
+ expect(result.resourceIdentifiers[0]).toEqual({
97
+ ResourceType: 'AWS::EC2::VPC',
98
+ LogicalResourceId: 'FriggVPC',
99
+ ResourceIdentifier: { VpcId: 'vpc-12345678' },
100
+ });
101
+ });
102
+
103
+ it('should generate template with resolved !Sub ${AWS::StackName} intrinsic', async () => {
104
+ // Arrange
105
+ const resourcesToImport = [
106
+ {
107
+ logicalId: 'FriggVPC',
108
+ physicalId: 'vpc-12345678',
109
+ resourceType: 'AWS::EC2::VPC',
110
+ },
111
+ ];
112
+
113
+ const buildTemplate = {
114
+ resources: {
115
+ FriggVPC: {
116
+ Type: 'AWS::EC2::VPC',
117
+ Properties: {
118
+ CidrBlock: '10.0.0.0/16',
119
+ Tags: [
120
+ {
121
+ Key: 'Name',
122
+ Value: { 'Fn::Sub': '${AWS::StackName}-vpc' }, // !Sub intrinsic
123
+ },
124
+ ],
125
+ },
126
+ },
127
+ },
128
+ };
129
+
130
+ const awsResourceDetails = {
131
+ properties: {
132
+ VpcId: 'vpc-12345678',
133
+ CidrBlock: '10.0.0.0/16',
134
+ Tags: [
135
+ { Key: 'Name', Value: 'test-stack-vpc' },
136
+ ],
137
+ },
138
+ stackName: 'test-stack', // Used for !Sub resolution
139
+ };
140
+
141
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
142
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
143
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
144
+
145
+ // Act
146
+ const result = await generator.generateImportTemplate({
147
+ resourcesToImport,
148
+ buildTemplatePath: '/path/to/build-template.json',
149
+ stackIdentifier,
150
+ });
151
+
152
+ // Assert
153
+ expect(result.template.Resources.FriggVPC.Properties.Tags[0].Value).toBe(
154
+ 'test-stack-vpc'
155
+ );
156
+ });
157
+
158
+ it('should generate template with resolved !GetAtt intrinsic', async () => {
159
+ // Arrange
160
+ const resourcesToImport = [
161
+ {
162
+ logicalId: 'FriggPublicSubnet',
163
+ physicalId: 'subnet-public-123',
164
+ resourceType: 'AWS::EC2::Subnet',
165
+ },
166
+ ];
167
+
168
+ const buildTemplate = {
169
+ resources: {
170
+ FriggPublicSubnet: {
171
+ Type: 'AWS::EC2::Subnet',
172
+ Properties: {
173
+ VpcId: { 'Fn::GetAtt': ['FriggVPC', 'VpcId'] }, // !GetAtt intrinsic
174
+ CidrBlock: '10.0.1.0/24',
175
+ AvailabilityZone: { 'Fn::GetAtt': ['FriggVPC', 'AvailabilityZone'] },
176
+ },
177
+ },
178
+ },
179
+ };
180
+
181
+ const awsResourceDetails = {
182
+ properties: {
183
+ SubnetId: 'subnet-public-123',
184
+ VpcId: 'vpc-12345678', // Actual VPC ID from AWS
185
+ CidrBlock: '10.0.1.0/24',
186
+ AvailabilityZone: 'us-east-1a', // Actual AZ from AWS
187
+ },
188
+ };
189
+
190
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
191
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
192
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
193
+
194
+ // Act
195
+ const result = await generator.generateImportTemplate({
196
+ resourcesToImport,
197
+ buildTemplatePath: '/path/to/build-template.json',
198
+ stackIdentifier,
199
+ });
200
+
201
+ // Assert
202
+ expect(result.template.Resources.FriggPublicSubnet.Properties.VpcId).toBe(
203
+ 'vpc-12345678'
204
+ );
205
+ expect(result.template.Resources.FriggPublicSubnet.Properties.AvailabilityZone).toBe(
206
+ 'us-east-1a'
207
+ );
208
+ });
209
+
210
+ it('should resolve nested !Ref in array properties', async () => {
211
+ // Arrange
212
+ const resourcesToImport = [
213
+ {
214
+ logicalId: 'FriggLambdaSecurityGroup',
215
+ physicalId: 'sg-07c01370e830b6ad6',
216
+ resourceType: 'AWS::EC2::SecurityGroup',
217
+ },
218
+ ];
219
+
220
+ const buildTemplate = {
221
+ resources: {
222
+ FriggLambdaSecurityGroup: {
223
+ Type: 'AWS::EC2::SecurityGroup',
224
+ Properties: {
225
+ GroupDescription: 'Lambda security group',
226
+ VpcId: { Ref: 'FriggVPC' }, // !Ref intrinsic
227
+ SecurityGroupIngress: [
228
+ {
229
+ IpProtocol: 'tcp',
230
+ FromPort: 443,
231
+ ToPort: 443,
232
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
233
+ },
234
+ ],
235
+ },
236
+ },
237
+ },
238
+ };
239
+
240
+ const awsResourceDetails = {
241
+ properties: {
242
+ GroupId: 'sg-07c01370e830b6ad6',
243
+ VpcId: 'vpc-12345678',
244
+ GroupDescription: 'Lambda security group',
245
+ SecurityGroupIngress: [
246
+ {
247
+ IpProtocol: 'tcp',
248
+ FromPort: 443,
249
+ ToPort: 443,
250
+ SourceSecurityGroupId: 'sg-07c01370e830b6ad6',
251
+ },
252
+ ],
253
+ },
254
+ };
255
+
256
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
257
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
258
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
259
+
260
+ // Act
261
+ const result = await generator.generateImportTemplate({
262
+ resourcesToImport,
263
+ buildTemplatePath: '/path/to/build-template.json',
264
+ stackIdentifier,
265
+ });
266
+
267
+ // Assert
268
+ expect(result.template.Resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe(
269
+ 'vpc-12345678'
270
+ );
271
+ expect(
272
+ result.template.Resources.FriggLambdaSecurityGroup.Properties.SecurityGroupIngress[0]
273
+ .SourceSecurityGroupId
274
+ ).toBe('sg-07c01370e830b6ad6');
275
+ });
276
+
277
+ it('should merge resource definitions with existing CloudFormation template', async () => {
278
+ // Arrange
279
+ const resourcesToImport = [
280
+ {
281
+ logicalId: 'FriggVPC',
282
+ physicalId: 'vpc-12345678',
283
+ resourceType: 'AWS::EC2::VPC',
284
+ },
285
+ ];
286
+
287
+ const buildTemplate = {
288
+ resources: {
289
+ FriggVPC: {
290
+ Type: 'AWS::EC2::VPC',
291
+ Properties: {
292
+ CidrBlock: '10.0.0.0/16',
293
+ },
294
+ },
295
+ },
296
+ };
297
+
298
+ // Existing template already has other resources
299
+ const currentTemplate = {
300
+ Resources: {
301
+ MyLambdaFunction: {
302
+ Type: 'AWS::Lambda::Function',
303
+ Properties: {
304
+ FunctionName: 'my-function',
305
+ Runtime: 'nodejs18.x',
306
+ },
307
+ },
308
+ MyDynamoDBTable: {
309
+ Type: 'AWS::DynamoDB::Table',
310
+ Properties: {
311
+ TableName: 'my-table',
312
+ },
313
+ },
314
+ },
315
+ };
316
+
317
+ const awsResourceDetails = {
318
+ properties: {
319
+ VpcId: 'vpc-12345678',
320
+ CidrBlock: '10.0.0.0/16',
321
+ },
322
+ };
323
+
324
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
325
+ mockStackRepository.getTemplate.mockResolvedValue(currentTemplate);
326
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
327
+
328
+ // Act
329
+ const result = await generator.generateImportTemplate({
330
+ resourcesToImport,
331
+ buildTemplatePath: '/path/to/build-template.json',
332
+ stackIdentifier,
333
+ });
334
+
335
+ // Assert
336
+ expect(result.template.Resources.FriggVPC).toBeDefined();
337
+ expect(result.template.Resources.MyLambdaFunction).toBeDefined();
338
+ expect(result.template.Resources.MyDynamoDBTable).toBeDefined();
339
+ expect(Object.keys(result.template.Resources)).toHaveLength(3);
340
+ });
341
+
342
+ it('should throw error if logical ID not found in build template', async () => {
343
+ // Arrange
344
+ const resourcesToImport = [
345
+ {
346
+ logicalId: 'NonExistentResource',
347
+ physicalId: 'vpc-12345678',
348
+ resourceType: 'AWS::EC2::VPC',
349
+ },
350
+ ];
351
+
352
+ const buildTemplate = {
353
+ resources: {
354
+ FriggVPC: {
355
+ Type: 'AWS::EC2::VPC',
356
+ Properties: {
357
+ CidrBlock: '10.0.0.0/16',
358
+ },
359
+ },
360
+ },
361
+ };
362
+
363
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
364
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
365
+
366
+ // Act & Assert
367
+ await expect(
368
+ generator.generateImportTemplate({
369
+ resourcesToImport,
370
+ buildTemplatePath: '/path/to/build-template.json',
371
+ stackIdentifier,
372
+ })
373
+ ).rejects.toThrow(
374
+ 'Logical ID NonExistentResource not found in build template. Cannot generate import definition without template reference.'
375
+ );
376
+ });
377
+
378
+ it('should throw error if build template file does not exist', async () => {
379
+ // Arrange
380
+ const resourcesToImport = [
381
+ {
382
+ logicalId: 'FriggVPC',
383
+ physicalId: 'vpc-12345678',
384
+ resourceType: 'AWS::EC2::VPC',
385
+ },
386
+ ];
387
+
388
+ mockTemplateParser.parseTemplate.mockImplementation(() => {
389
+ throw new Error('ENOENT: no such file or directory');
390
+ });
391
+
392
+ // Act & Assert
393
+ await expect(
394
+ generator.generateImportTemplate({
395
+ resourcesToImport,
396
+ buildTemplatePath: '/path/to/nonexistent-template.json',
397
+ stackIdentifier,
398
+ })
399
+ ).rejects.toThrow('ENOENT: no such file or directory');
400
+ });
401
+
402
+ it('should generate correct resource identifiers for VPC, Subnet, SecurityGroup', async () => {
403
+ // Arrange
404
+ const resourcesToImport = [
405
+ {
406
+ logicalId: 'FriggVPC',
407
+ physicalId: 'vpc-12345678',
408
+ resourceType: 'AWS::EC2::VPC',
409
+ },
410
+ {
411
+ logicalId: 'FriggPrivateSubnet1',
412
+ physicalId: 'subnet-11111111',
413
+ resourceType: 'AWS::EC2::Subnet',
414
+ },
415
+ {
416
+ logicalId: 'FriggLambdaSecurityGroup',
417
+ physicalId: 'sg-07c01370e830b6ad6',
418
+ resourceType: 'AWS::EC2::SecurityGroup',
419
+ },
420
+ ];
421
+
422
+ const buildTemplate = {
423
+ resources: {
424
+ FriggVPC: {
425
+ Type: 'AWS::EC2::VPC',
426
+ Properties: { CidrBlock: '10.0.0.0/16' },
427
+ },
428
+ FriggPrivateSubnet1: {
429
+ Type: 'AWS::EC2::Subnet',
430
+ Properties: { CidrBlock: '10.0.1.0/24', VpcId: { Ref: 'FriggVPC' } },
431
+ },
432
+ FriggLambdaSecurityGroup: {
433
+ Type: 'AWS::EC2::SecurityGroup',
434
+ Properties: { GroupDescription: 'Lambda SG', VpcId: { Ref: 'FriggVPC' } },
435
+ },
436
+ },
437
+ };
438
+
439
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
440
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
441
+ mockResourceDetector.getResourceDetails
442
+ .mockResolvedValueOnce({
443
+ properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
444
+ })
445
+ .mockResolvedValueOnce({
446
+ properties: {
447
+ SubnetId: 'subnet-11111111',
448
+ CidrBlock: '10.0.1.0/24',
449
+ VpcId: 'vpc-12345678',
450
+ },
451
+ })
452
+ .mockResolvedValueOnce({
453
+ properties: {
454
+ GroupId: 'sg-07c01370e830b6ad6',
455
+ GroupDescription: 'Lambda SG',
456
+ VpcId: 'vpc-12345678',
457
+ },
458
+ });
459
+
460
+ // Act
461
+ const result = await generator.generateImportTemplate({
462
+ resourcesToImport,
463
+ buildTemplatePath: '/path/to/build-template.json',
464
+ stackIdentifier,
465
+ });
466
+
467
+ // Assert
468
+ expect(result.resourceIdentifiers).toHaveLength(3);
469
+ expect(result.resourceIdentifiers[0]).toEqual({
470
+ ResourceType: 'AWS::EC2::VPC',
471
+ LogicalResourceId: 'FriggVPC',
472
+ ResourceIdentifier: { VpcId: 'vpc-12345678' },
473
+ });
474
+ expect(result.resourceIdentifiers[1]).toEqual({
475
+ ResourceType: 'AWS::EC2::Subnet',
476
+ LogicalResourceId: 'FriggPrivateSubnet1',
477
+ ResourceIdentifier: { SubnetId: 'subnet-11111111' },
478
+ });
479
+ expect(result.resourceIdentifiers[2]).toEqual({
480
+ ResourceType: 'AWS::EC2::SecurityGroup',
481
+ LogicalResourceId: 'FriggLambdaSecurityGroup',
482
+ ResourceIdentifier: { Id: 'sg-07c01370e830b6ad6' },
483
+ });
484
+ });
485
+
486
+ it('should generate correct resource identifier for InternetGateway', async () => {
487
+ // Arrange
488
+ const resourcesToImport = [
489
+ {
490
+ logicalId: 'FriggInternetGateway',
491
+ physicalId: 'igw-0abc123def456',
492
+ resourceType: 'AWS::EC2::InternetGateway',
493
+ },
494
+ ];
495
+
496
+ const buildTemplate = {
497
+ resources: {
498
+ FriggInternetGateway: {
499
+ Type: 'AWS::EC2::InternetGateway',
500
+ Properties: {
501
+ Tags: [{ Key: 'Name', Value: 'Frigg IGW' }],
502
+ },
503
+ },
504
+ },
505
+ };
506
+
507
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
508
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
509
+ mockResourceDetector.getResourceDetails.mockResolvedValue({
510
+ properties: {
511
+ InternetGatewayId: 'igw-0abc123def456',
512
+ Tags: [{ Key: 'Name', Value: 'Frigg IGW' }],
513
+ },
514
+ });
515
+
516
+ // Act
517
+ const result = await generator.generateImportTemplate({
518
+ resourcesToImport,
519
+ buildTemplatePath: '/path/to/build-template.json',
520
+ stackIdentifier,
521
+ });
522
+
523
+ // Assert
524
+ expect(result.resourceIdentifiers[0]).toEqual({
525
+ ResourceType: 'AWS::EC2::InternetGateway',
526
+ LogicalResourceId: 'FriggInternetGateway',
527
+ ResourceIdentifier: { InternetGatewayId: 'igw-0abc123def456' },
528
+ });
529
+ });
530
+
531
+ it('should handle empty current template when stack does not exist yet', async () => {
532
+ // Arrange
533
+ const resourcesToImport = [
534
+ {
535
+ logicalId: 'FriggVPC',
536
+ physicalId: 'vpc-12345678',
537
+ resourceType: 'AWS::EC2::VPC',
538
+ },
539
+ ];
540
+
541
+ const buildTemplate = {
542
+ resources: {
543
+ FriggVPC: {
544
+ Type: 'AWS::EC2::VPC',
545
+ Properties: { CidrBlock: '10.0.0.0/16' },
546
+ },
547
+ },
548
+ };
549
+
550
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
551
+ // Stack doesn't exist yet
552
+ mockStackRepository.getTemplate.mockRejectedValue(new Error('Stack does not exist'));
553
+ mockResourceDetector.getResourceDetails.mockResolvedValue({
554
+ properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
555
+ });
556
+
557
+ // Act
558
+ const result = await generator.generateImportTemplate({
559
+ resourcesToImport,
560
+ buildTemplatePath: '/path/to/build-template.json',
561
+ stackIdentifier,
562
+ });
563
+
564
+ // Assert
565
+ expect(result.template.Resources.FriggVPC).toBeDefined();
566
+ expect(Object.keys(result.template.Resources)).toHaveLength(1);
567
+ });
568
+
569
+ it('should resolve complex nested intrinsics', async () => {
570
+ // Arrange
571
+ const resourcesToImport = [
572
+ {
573
+ logicalId: 'FriggPublicSubnet',
574
+ physicalId: 'subnet-public-123',
575
+ resourceType: 'AWS::EC2::Subnet',
576
+ },
577
+ ];
578
+
579
+ const buildTemplate = {
580
+ resources: {
581
+ FriggPublicSubnet: {
582
+ Type: 'AWS::EC2::Subnet',
583
+ Properties: {
584
+ VpcId: { Ref: 'FriggVPC' },
585
+ CidrBlock: '10.0.1.0/24',
586
+ Tags: [
587
+ {
588
+ Key: 'Name',
589
+ Value: { 'Fn::Sub': '${AWS::StackName}-public-subnet' },
590
+ },
591
+ {
592
+ Key: 'Type',
593
+ Value: 'public',
594
+ },
595
+ {
596
+ Key: 'VpcCidr',
597
+ Value: { 'Fn::GetAtt': ['FriggVPC', 'CidrBlock'] },
598
+ },
599
+ ],
600
+ },
601
+ },
602
+ },
603
+ };
604
+
605
+ const awsResourceDetails = {
606
+ properties: {
607
+ SubnetId: 'subnet-public-123',
608
+ VpcId: 'vpc-12345678',
609
+ CidrBlock: '10.0.1.0/24',
610
+ Tags: [
611
+ { Key: 'Name', Value: 'test-stack-public-subnet' },
612
+ { Key: 'Type', Value: 'public' },
613
+ { Key: 'VpcCidr', Value: '10.0.0.0/16' },
614
+ ],
615
+ },
616
+ stackName: 'test-stack',
617
+ };
618
+
619
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
620
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
621
+ mockResourceDetector.getResourceDetails.mockResolvedValue(awsResourceDetails);
622
+
623
+ // Act
624
+ const result = await generator.generateImportTemplate({
625
+ resourcesToImport,
626
+ buildTemplatePath: '/path/to/build-template.json',
627
+ stackIdentifier,
628
+ });
629
+
630
+ // Assert
631
+ const tags = result.template.Resources.FriggPublicSubnet.Properties.Tags;
632
+ expect(tags[0].Value).toBe('test-stack-public-subnet');
633
+ expect(tags[1].Value).toBe('public');
634
+ expect(tags[2].Value).toBe('10.0.0.0/16');
635
+ });
636
+
637
+ it('should handle multiple resources with different resource types', async () => {
638
+ // Arrange
639
+ const resourcesToImport = [
640
+ {
641
+ logicalId: 'FriggVPC',
642
+ physicalId: 'vpc-12345678',
643
+ resourceType: 'AWS::EC2::VPC',
644
+ },
645
+ {
646
+ logicalId: 'FriggRouteTable',
647
+ physicalId: 'rtb-0123456789',
648
+ resourceType: 'AWS::EC2::RouteTable',
649
+ },
650
+ {
651
+ logicalId: 'FriggVPCEndpoint',
652
+ physicalId: 'vpce-0987654321',
653
+ resourceType: 'AWS::EC2::VPCEndpoint',
654
+ },
655
+ ];
656
+
657
+ const buildTemplate = {
658
+ resources: {
659
+ FriggVPC: {
660
+ Type: 'AWS::EC2::VPC',
661
+ Properties: { CidrBlock: '10.0.0.0/16' },
662
+ },
663
+ FriggRouteTable: {
664
+ Type: 'AWS::EC2::RouteTable',
665
+ Properties: { VpcId: { Ref: 'FriggVPC' } },
666
+ },
667
+ FriggVPCEndpoint: {
668
+ Type: 'AWS::EC2::VPCEndpoint',
669
+ Properties: { VpcId: { Ref: 'FriggVPC' }, ServiceName: 's3' },
670
+ },
671
+ },
672
+ };
673
+
674
+ mockTemplateParser.parseTemplate.mockReturnValue(buildTemplate);
675
+ mockStackRepository.getTemplate.mockResolvedValue({ Resources: {} });
676
+ mockResourceDetector.getResourceDetails
677
+ .mockResolvedValueOnce({
678
+ properties: { VpcId: 'vpc-12345678', CidrBlock: '10.0.0.0/16' },
679
+ })
680
+ .mockResolvedValueOnce({
681
+ properties: { RouteTableId: 'rtb-0123456789', VpcId: 'vpc-12345678' },
682
+ })
683
+ .mockResolvedValueOnce({
684
+ properties: {
685
+ VpcEndpointId: 'vpce-0987654321',
686
+ VpcId: 'vpc-12345678',
687
+ ServiceName: 's3',
688
+ },
689
+ });
690
+
691
+ // Act
692
+ const result = await generator.generateImportTemplate({
693
+ resourcesToImport,
694
+ buildTemplatePath: '/path/to/build-template.json',
695
+ stackIdentifier,
696
+ });
697
+
698
+ // Assert
699
+ expect(result.resourceIdentifiers).toHaveLength(3);
700
+ expect(result.resourceIdentifiers[0].ResourceIdentifier).toEqual({
701
+ VpcId: 'vpc-12345678',
702
+ });
703
+ expect(result.resourceIdentifiers[1].ResourceIdentifier).toEqual({
704
+ RouteTableId: 'rtb-0123456789',
705
+ });
706
+ expect(result.resourceIdentifiers[2].ResourceIdentifier).toEqual({
707
+ VpcEndpointId: 'vpce-0987654321',
708
+ });
709
+ });
710
+ });
711
+
712
+ describe('_getResourceIdentifier', () => {
713
+ it('should return correct identifier for VPC', () => {
714
+ // Act
715
+ const result = generator._getResourceIdentifier('AWS::EC2::VPC', 'vpc-123');
716
+
717
+ // Assert
718
+ expect(result).toEqual({ VpcId: 'vpc-123' });
719
+ });
720
+
721
+ it('should return correct identifier for Subnet', () => {
722
+ // Act
723
+ const result = generator._getResourceIdentifier('AWS::EC2::Subnet', 'subnet-456');
724
+
725
+ // Assert
726
+ expect(result).toEqual({ SubnetId: 'subnet-456' });
727
+ });
728
+
729
+ it('should return correct identifier for SecurityGroup', () => {
730
+ // Act
731
+ const result = generator._getResourceIdentifier('AWS::EC2::SecurityGroup', 'sg-789');
732
+
733
+ // Assert
734
+ expect(result).toEqual({ Id: 'sg-789' });
735
+ });
736
+
737
+ it('should return correct identifier for InternetGateway', () => {
738
+ // Act
739
+ const result = generator._getResourceIdentifier('AWS::EC2::InternetGateway', 'igw-abc');
740
+
741
+ // Assert
742
+ expect(result).toEqual({ InternetGatewayId: 'igw-abc' });
743
+ });
744
+
745
+ it('should return correct identifier for NatGateway', () => {
746
+ // Act
747
+ const result = generator._getResourceIdentifier('AWS::EC2::NatGateway', 'nat-def');
748
+
749
+ // Assert
750
+ expect(result).toEqual({ NatGatewayId: 'nat-def' });
751
+ });
752
+
753
+ it('should return correct identifier for RouteTable', () => {
754
+ // Act
755
+ const result = generator._getResourceIdentifier('AWS::EC2::RouteTable', 'rtb-ghi');
756
+
757
+ // Assert
758
+ expect(result).toEqual({ RouteTableId: 'rtb-ghi' });
759
+ });
760
+
761
+ it('should return correct identifier for VPCEndpoint', () => {
762
+ // Act
763
+ const result = generator._getResourceIdentifier('AWS::EC2::VPCEndpoint', 'vpce-jkl');
764
+
765
+ // Assert
766
+ expect(result).toEqual({ VpcEndpointId: 'vpce-jkl' });
767
+ });
768
+
769
+ it('should return generic identifier for unknown resource type', () => {
770
+ // Act
771
+ const result = generator._getResourceIdentifier('AWS::Unknown::Type', 'unknown-123');
772
+
773
+ // Assert
774
+ expect(result).toEqual({ Id: 'unknown-123' });
775
+ });
776
+ });
777
+
778
+ describe('_resolveRef', () => {
779
+ it('should resolve VpcCidr to actual CIDR block', () => {
780
+ // Arrange
781
+ const awsResourceDetails = {
782
+ properties: {
783
+ CidrBlock: '10.0.0.0/16',
784
+ },
785
+ };
786
+
787
+ // Act
788
+ const result = generator._resolveRef('VpcCidr', awsResourceDetails, 'AWS::EC2::VPC');
789
+
790
+ // Assert
791
+ expect(result).toBe('10.0.0.0/16');
792
+ });
793
+
794
+ it('should resolve VpcId to actual VPC ID', () => {
795
+ // Arrange
796
+ const awsResourceDetails = {
797
+ properties: {
798
+ VpcId: 'vpc-12345678',
799
+ },
800
+ };
801
+
802
+ // Act
803
+ const result = generator._resolveRef('VpcId', awsResourceDetails, 'AWS::EC2::Subnet');
804
+
805
+ // Assert
806
+ expect(result).toBe('vpc-12345678');
807
+ });
808
+
809
+ it('should return property value from AWS details for unmapped refs', () => {
810
+ // Arrange
811
+ const awsResourceDetails = {
812
+ properties: {
813
+ CustomProperty: 'custom-value',
814
+ },
815
+ };
816
+
817
+ // Act
818
+ const result = generator._resolveRef(
819
+ 'CustomProperty',
820
+ awsResourceDetails,
821
+ 'AWS::Custom::Type'
822
+ );
823
+
824
+ // Assert
825
+ expect(result).toBe('custom-value');
826
+ });
827
+
828
+ it('should return ref name if not found in properties', () => {
829
+ // Arrange
830
+ const awsResourceDetails = {
831
+ properties: {},
832
+ };
833
+
834
+ // Act
835
+ const result = generator._resolveRef('UnknownRef', awsResourceDetails, 'AWS::EC2::VPC');
836
+
837
+ // Assert
838
+ expect(result).toBe('UnknownRef');
839
+ });
840
+ });
841
+
842
+ describe('_resolveSub', () => {
843
+ it('should resolve ${AWS::StackName} to actual stack name', () => {
844
+ // Arrange
845
+ const awsResourceDetails = {
846
+ stackName: 'test-stack',
847
+ };
848
+
849
+ // Act
850
+ const result = generator._resolveSub('${AWS::StackName}-vpc', awsResourceDetails);
851
+
852
+ // Assert
853
+ expect(result).toBe('test-stack-vpc');
854
+ });
855
+
856
+ it('should resolve multiple ${AWS::StackName} occurrences', () => {
857
+ // Arrange
858
+ const awsResourceDetails = {
859
+ stackName: 'my-stack',
860
+ };
861
+
862
+ // Act
863
+ const result = generator._resolveSub(
864
+ '${AWS::StackName}-${AWS::StackName}-resource',
865
+ awsResourceDetails
866
+ );
867
+
868
+ // Assert
869
+ expect(result).toBe('my-stack-my-stack-resource');
870
+ });
871
+
872
+ it('should resolve tag variables from AWS resource tags', () => {
873
+ // Arrange
874
+ const awsResourceDetails = {
875
+ stackName: 'my-stack',
876
+ tags: {
877
+ Environment: 'production',
878
+ Team: 'platform',
879
+ },
880
+ };
881
+
882
+ // Act
883
+ const result = generator._resolveSub(
884
+ '${AWS::StackName}-${Environment}-${Team}',
885
+ awsResourceDetails
886
+ );
887
+
888
+ // Assert
889
+ expect(result).toBe('my-stack-production-platform');
890
+ });
891
+
892
+ it('should return empty string if stack name missing', () => {
893
+ // Arrange
894
+ const awsResourceDetails = {};
895
+
896
+ // Act
897
+ const result = generator._resolveSub('${AWS::StackName}-vpc', awsResourceDetails);
898
+
899
+ // Assert
900
+ expect(result).toBe('-vpc');
901
+ });
902
+
903
+ it('should return non-string values as-is', () => {
904
+ // Arrange
905
+ const awsResourceDetails = {};
906
+
907
+ // Act
908
+ const result = generator._resolveSub({ some: 'object' }, awsResourceDetails);
909
+
910
+ // Assert
911
+ expect(result).toEqual({ some: 'object' });
912
+ });
913
+ });
914
+
915
+ describe('_resolveGetAtt', () => {
916
+ it('should resolve attribute from AWS resource properties', () => {
917
+ // Arrange
918
+ const awsResourceDetails = {
919
+ properties: {
920
+ VpcId: 'vpc-12345678',
921
+ CidrBlock: '10.0.0.0/16',
922
+ },
923
+ };
924
+
925
+ // Act
926
+ const result = generator._resolveGetAtt(['FriggVPC', 'VpcId'], awsResourceDetails);
927
+
928
+ // Assert
929
+ expect(result).toBe('vpc-12345678');
930
+ });
931
+
932
+ it('should return null if attribute not found', () => {
933
+ // Arrange
934
+ const awsResourceDetails = {
935
+ properties: {
936
+ VpcId: 'vpc-12345678',
937
+ },
938
+ };
939
+
940
+ // Act
941
+ const result = generator._resolveGetAtt(['FriggVPC', 'NonExistentAttribute'], awsResourceDetails);
942
+
943
+ // Assert
944
+ expect(result).toBeNull();
945
+ });
946
+ });
947
+
948
+ describe('_resolveValue', () => {
949
+ it('should resolve literal values unchanged', () => {
950
+ // Arrange
951
+ const awsResourceDetails = { properties: {} };
952
+
953
+ // Act
954
+ const result = generator._resolveValue('literal-value', awsResourceDetails, 'AWS::EC2::VPC');
955
+
956
+ // Assert
957
+ expect(result).toBe('literal-value');
958
+ });
959
+
960
+ it('should resolve nested object with intrinsics', () => {
961
+ // Arrange
962
+ const value = {
963
+ VpcId: { Ref: 'FriggVPC' },
964
+ CidrBlock: '10.0.1.0/24',
965
+ };
966
+ const awsResourceDetails = {
967
+ properties: {
968
+ VpcId: 'vpc-12345678',
969
+ },
970
+ };
971
+
972
+ // Act
973
+ const result = generator._resolveValue(value, awsResourceDetails, 'AWS::EC2::Subnet');
974
+
975
+ // Assert
976
+ expect(result.VpcId).toBe('vpc-12345678');
977
+ expect(result.CidrBlock).toBe('10.0.1.0/24');
978
+ });
979
+
980
+ it('should resolve array of values with intrinsics', () => {
981
+ // Arrange
982
+ const value = [
983
+ { Ref: 'Subnet1' },
984
+ { Ref: 'Subnet2' },
985
+ 'literal-subnet-id',
986
+ ];
987
+ const awsResourceDetails = {
988
+ properties: {
989
+ Subnet1: 'subnet-111',
990
+ Subnet2: 'subnet-222',
991
+ },
992
+ };
993
+
994
+ // Act
995
+ const result = generator._resolveValue(value, awsResourceDetails, 'AWS::Lambda::Function');
996
+
997
+ // Assert
998
+ expect(result).toEqual(['subnet-111', 'subnet-222', 'literal-subnet-id']);
999
+ });
1000
+ });
1001
+
1002
+ describe('_generateResourceDefinition', () => {
1003
+ it('should throw error if logical ID not in build template', () => {
1004
+ // Arrange
1005
+ const buildTemplate = {
1006
+ resources: {
1007
+ FriggVPC: { Type: 'AWS::EC2::VPC' },
1008
+ },
1009
+ };
1010
+ const awsResourceDetails = {
1011
+ properties: { VpcId: 'vpc-123' },
1012
+ };
1013
+
1014
+ // Act & Assert
1015
+ expect(() => {
1016
+ generator._generateResourceDefinition({
1017
+ logicalId: 'NonExistentResource',
1018
+ resourceType: 'AWS::EC2::VPC',
1019
+ physicalId: 'vpc-123',
1020
+ buildTemplate,
1021
+ awsResourceDetails,
1022
+ });
1023
+ }).toThrow(
1024
+ 'Logical ID NonExistentResource not found in build template. Cannot generate import definition without template reference.'
1025
+ );
1026
+ });
1027
+
1028
+ it('should generate resource definition with resolved properties', () => {
1029
+ // Arrange
1030
+ const buildTemplate = {
1031
+ resources: {
1032
+ FriggVPC: {
1033
+ Type: 'AWS::EC2::VPC',
1034
+ Properties: {
1035
+ CidrBlock: { Ref: 'VpcCidr' },
1036
+ EnableDnsHostnames: true,
1037
+ },
1038
+ },
1039
+ },
1040
+ };
1041
+ const awsResourceDetails = {
1042
+ properties: {
1043
+ VpcId: 'vpc-12345678',
1044
+ CidrBlock: '10.0.0.0/16',
1045
+ },
1046
+ };
1047
+
1048
+ // Act
1049
+ const result = generator._generateResourceDefinition({
1050
+ logicalId: 'FriggVPC',
1051
+ resourceType: 'AWS::EC2::VPC',
1052
+ physicalId: 'vpc-12345678',
1053
+ buildTemplate,
1054
+ awsResourceDetails,
1055
+ });
1056
+
1057
+ // Assert
1058
+ expect(result.Type).toBe('AWS::EC2::VPC');
1059
+ expect(result.Properties.CidrBlock).toBe('10.0.0.0/16');
1060
+ expect(result.Properties.EnableDnsHostnames).toBe(true);
1061
+ });
1062
+ });
1063
+
1064
+ describe('Resource Protection Policies', () => {
1065
+ it('should add DeletionPolicy: Retain to all imported resources', () => {
1066
+ // Arrange
1067
+ const buildTemplate = {
1068
+ resources: {
1069
+ FriggVPC: {
1070
+ Type: 'AWS::EC2::VPC',
1071
+ Properties: { CidrBlock: '10.0.0.0/16' },
1072
+ },
1073
+ },
1074
+ };
1075
+ const awsResourceDetails = {
1076
+ properties: { VpcId: 'vpc-123', CidrBlock: '10.0.0.0/16' },
1077
+ };
1078
+
1079
+ // Act
1080
+ const result = generator._generateResourceDefinition({
1081
+ logicalId: 'FriggVPC',
1082
+ resourceType: 'AWS::EC2::VPC',
1083
+ physicalId: 'vpc-123',
1084
+ buildTemplate,
1085
+ awsResourceDetails,
1086
+ });
1087
+
1088
+ // Assert
1089
+ expect(result.DeletionPolicy).toBe('Retain');
1090
+ });
1091
+
1092
+ it('should add UpdateReplacePolicy: Retain to all imported resources', () => {
1093
+ // Arrange
1094
+ const buildTemplate = {
1095
+ resources: {
1096
+ FriggVPC: {
1097
+ Type: 'AWS::EC2::VPC',
1098
+ Properties: { CidrBlock: '10.0.0.0/16' },
1099
+ },
1100
+ },
1101
+ };
1102
+ const awsResourceDetails = {
1103
+ properties: { VpcId: 'vpc-123', CidrBlock: '10.0.0.0/16' },
1104
+ };
1105
+
1106
+ // Act
1107
+ const result = generator._generateResourceDefinition({
1108
+ logicalId: 'FriggVPC',
1109
+ resourceType: 'AWS::EC2::VPC',
1110
+ physicalId: 'vpc-123',
1111
+ buildTemplate,
1112
+ awsResourceDetails,
1113
+ });
1114
+
1115
+ // Assert
1116
+ expect(result.UpdateReplacePolicy).toBe('Retain');
1117
+ });
1118
+
1119
+ it('should protect resources from deletion during stack updates', () => {
1120
+ // Arrange
1121
+ const buildTemplate = {
1122
+ resources: {
1123
+ FriggLambdaSecurityGroup: {
1124
+ Type: 'AWS::EC2::SecurityGroup',
1125
+ Properties: { GroupDescription: 'Lambda SG' },
1126
+ },
1127
+ },
1128
+ };
1129
+ const awsResourceDetails = {
1130
+ properties: { GroupId: 'sg-123', GroupDescription: 'Lambda SG' },
1131
+ };
1132
+
1133
+ // Act
1134
+ const result = generator._generateResourceDefinition({
1135
+ logicalId: 'FriggLambdaSecurityGroup',
1136
+ resourceType: 'AWS::EC2::SecurityGroup',
1137
+ physicalId: 'sg-123',
1138
+ buildTemplate,
1139
+ awsResourceDetails,
1140
+ });
1141
+
1142
+ // Assert: Both policies protect resources
1143
+ expect(result.DeletionPolicy).toBe('Retain');
1144
+ expect(result.UpdateReplacePolicy).toBe('Retain');
1145
+ // This prevents:
1146
+ // - Stack deletion from destroying the physical resource (DeletionPolicy)
1147
+ // - Stack update replacement from destroying the old physical resource (UpdateReplacePolicy)
1148
+ });
1149
+ });
1150
+ });