@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,1262 @@
1
+ /**
2
+ * Tests for VPC Builder
3
+ *
4
+ * Tests VPC infrastructure building with various management modes
5
+ */
6
+
7
+ const { VpcBuilder } = require('./vpc-builder');
8
+ const { ValidationResult } = require('../shared/base-builder');
9
+
10
+ describe('VpcBuilder', () => {
11
+ let vpcBuilder;
12
+
13
+ beforeEach(() => {
14
+ vpcBuilder = new VpcBuilder();
15
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
16
+ });
17
+
18
+ afterEach(() => {
19
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
20
+ });
21
+
22
+ describe('shouldExecute()', () => {
23
+ it('should return true when VPC is enabled', () => {
24
+ const appDefinition = {
25
+ vpc: { enable: true },
26
+ };
27
+
28
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(true);
29
+ });
30
+
31
+ it('should return false when VPC is disabled', () => {
32
+ const appDefinition = {
33
+ vpc: { enable: false },
34
+ };
35
+
36
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
37
+ });
38
+
39
+ it('should return false when VPC is not defined', () => {
40
+ const appDefinition = {};
41
+
42
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
43
+ });
44
+
45
+ it('should return false when FRIGG_SKIP_AWS_DISCOVERY is set (local mode)', () => {
46
+ process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
47
+ const appDefinition = {
48
+ vpc: { enable: true },
49
+ };
50
+
51
+ expect(vpcBuilder.shouldExecute(appDefinition)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('validate()', () => {
56
+ it('should pass validation for valid discover mode config', () => {
57
+ const appDefinition = {
58
+ vpc: {
59
+ enable: true,
60
+ management: 'discover',
61
+ },
62
+ };
63
+
64
+ const result = vpcBuilder.validate(appDefinition);
65
+
66
+ expect(result).toBeInstanceOf(ValidationResult);
67
+ expect(result.valid).toBe(true);
68
+ expect(result.errors).toEqual([]);
69
+ });
70
+
71
+ it('should pass validation for valid create-new mode config', () => {
72
+ const appDefinition = {
73
+ vpc: {
74
+ enable: true,
75
+ management: 'create-new',
76
+ },
77
+ };
78
+
79
+ const result = vpcBuilder.validate(appDefinition);
80
+
81
+ expect(result.valid).toBe(true);
82
+ });
83
+
84
+ it('should pass validation for valid use-existing mode with vpcId', () => {
85
+ const appDefinition = {
86
+ vpc: {
87
+ enable: true,
88
+ management: 'use-existing',
89
+ vpcId: 'vpc-123456',
90
+ securityGroupIds: ['sg-123'],
91
+ },
92
+ };
93
+
94
+ const result = vpcBuilder.validate(appDefinition);
95
+
96
+ expect(result.valid).toBe(true);
97
+ });
98
+
99
+ it('should error if VPC configuration is missing', () => {
100
+ const appDefinition = {};
101
+
102
+ const result = vpcBuilder.validate(appDefinition);
103
+
104
+ expect(result.valid).toBe(false);
105
+ expect(result.errors).toContain('VPC configuration is missing');
106
+ });
107
+
108
+ it('should error for invalid management mode', () => {
109
+ const appDefinition = {
110
+ vpc: {
111
+ enable: true,
112
+ management: 'invalid-mode',
113
+ },
114
+ };
115
+
116
+ const result = vpcBuilder.validate(appDefinition);
117
+
118
+ expect(result.valid).toBe(false);
119
+ expect(result.errors.some(err => err.includes('Invalid vpc.management'))).toBe(true);
120
+ });
121
+
122
+ it('should error when use-existing mode without vpcId', () => {
123
+ const appDefinition = {
124
+ vpc: {
125
+ enable: true,
126
+ management: 'use-existing',
127
+ },
128
+ };
129
+
130
+ const result = vpcBuilder.validate(appDefinition);
131
+
132
+ expect(result.valid).toBe(false);
133
+ expect(result.errors).toContain(
134
+ 'vpc.vpcId is required when management="use-existing"'
135
+ );
136
+ });
137
+
138
+ it('should warn when use-existing mode without security groups', () => {
139
+ const appDefinition = {
140
+ vpc: {
141
+ enable: true,
142
+ management: 'use-existing',
143
+ vpcId: 'vpc-123',
144
+ },
145
+ };
146
+
147
+ const result = vpcBuilder.validate(appDefinition);
148
+
149
+ expect(result.warnings.some(warn => warn.includes('securityGroupIds not provided'))).toBe(true);
150
+ });
151
+
152
+ it('should error for invalid CIDR block format', () => {
153
+ const appDefinition = {
154
+ vpc: {
155
+ enable: true,
156
+ cidrBlock: 'invalid-cidr',
157
+ },
158
+ };
159
+
160
+ const result = vpcBuilder.validate(appDefinition);
161
+
162
+ expect(result.valid).toBe(false);
163
+ expect(result.errors.some(err => err.includes('Invalid CIDR block format'))).toBe(true);
164
+ });
165
+
166
+ it('should accept valid CIDR block formats', () => {
167
+ const validCidrs = ['10.0.0.0/16', '172.31.0.0/16', '192.168.0.0/24'];
168
+
169
+ validCidrs.forEach(cidr => {
170
+ const appDefinition = {
171
+ vpc: {
172
+ enable: true,
173
+ cidrBlock: cidr,
174
+ },
175
+ };
176
+
177
+ const result = vpcBuilder.validate(appDefinition);
178
+ expect(result.valid).toBe(true);
179
+ });
180
+ });
181
+
182
+ it('should error when use-existing subnets without subnet IDs', () => {
183
+ const appDefinition = {
184
+ vpc: {
185
+ enable: true,
186
+ subnets: {
187
+ management: 'use-existing',
188
+ },
189
+ },
190
+ };
191
+
192
+ const result = vpcBuilder.validate(appDefinition);
193
+
194
+ expect(result.valid).toBe(false);
195
+ expect(result.errors.some(err => err.includes('At least 2 subnet IDs required'))).toBe(true);
196
+ });
197
+
198
+ it('should error when use-existing subnets with only 1 subnet', () => {
199
+ const appDefinition = {
200
+ vpc: {
201
+ enable: true,
202
+ subnets: {
203
+ management: 'use-existing',
204
+ ids: ['subnet-1'],
205
+ },
206
+ },
207
+ };
208
+
209
+ const result = vpcBuilder.validate(appDefinition);
210
+
211
+ expect(result.valid).toBe(false);
212
+ });
213
+
214
+ it('should pass when use-existing subnets with 2+ subnets', () => {
215
+ const appDefinition = {
216
+ vpc: {
217
+ enable: true,
218
+ subnets: {
219
+ management: 'use-existing',
220
+ ids: ['subnet-1', 'subnet-2'],
221
+ },
222
+ },
223
+ };
224
+
225
+ const result = vpcBuilder.validate(appDefinition);
226
+
227
+ expect(result.valid).toBe(true);
228
+ });
229
+
230
+ it('should default to discover mode when management not specified', () => {
231
+ const appDefinition = {
232
+ vpc: {
233
+ enable: true,
234
+ },
235
+ };
236
+
237
+ const result = vpcBuilder.validate(appDefinition);
238
+
239
+ expect(result.valid).toBe(true);
240
+ });
241
+ });
242
+
243
+ describe('build() - discover mode', () => {
244
+ it('should reuse stack-managed subnets when discovered from CloudFormation', async () => {
245
+ const appDefinition = {
246
+ vpc: { enable: true },
247
+ };
248
+
249
+ const discoveredResources = {
250
+ defaultVpcId: 'vpc-discovered',
251
+ privateSubnetId1: 'subnet-stack-private-1',
252
+ privateSubnetId2: 'subnet-stack-private-2',
253
+ publicSubnetId1: 'subnet-stack-public-1',
254
+ publicSubnetId2: 'subnet-stack-public-2',
255
+ };
256
+
257
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
258
+
259
+ // Should use discovered VPC
260
+ expect(result.vpcId).toBe('vpc-discovered');
261
+
262
+ // Should reuse stack-managed subnets (not create new ones)
263
+ expect(result.vpcConfig.subnetIds).toEqual([
264
+ 'subnet-stack-private-1',
265
+ 'subnet-stack-private-2',
266
+ ]);
267
+
268
+ // Should NOT create new subnet resources
269
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
270
+ expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
271
+ });
272
+
273
+ it('should use discovered VPC but create stage-specific subnets when no stack subnets exist', async () => {
274
+ const appDefinition = {
275
+ vpc: {
276
+ enable: true,
277
+ management: 'discover',
278
+ },
279
+ };
280
+
281
+ const discoveredResources = {
282
+ defaultVpcId: 'vpc-discovered',
283
+ // No stack-managed subnets, so create new ones for stage isolation
284
+ defaultSecurityGroupId: 'sg-discovered',
285
+ };
286
+
287
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
288
+
289
+ // Should create new stage-specific subnets for isolation (prevent route table conflicts)
290
+ expect(result.vpcConfig.subnetIds).toEqual([
291
+ { Ref: 'FriggPrivateSubnet1' },
292
+ { Ref: 'FriggPrivateSubnet2' },
293
+ ]);
294
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
295
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
296
+ // In discover mode, we create FriggLambdaSecurityGroup in the discovered VPC
297
+ expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
298
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
299
+ expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-discovered');
300
+ });
301
+
302
+ it('should allow sharing discovered subnets when explicitly configured', async () => {
303
+ const appDefinition = {
304
+ vpc: {
305
+ enable: true,
306
+ management: 'discover',
307
+ subnets: {
308
+ management: 'discover', // Explicitly opt-in to subnet sharing
309
+ },
310
+ },
311
+ };
312
+
313
+ const discoveredResources = {
314
+ defaultVpcId: 'vpc-discovered',
315
+ privateSubnetId1: 'subnet-shared-1',
316
+ privateSubnetId2: 'subnet-shared-2',
317
+ };
318
+
319
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
320
+
321
+ // OLD BEHAVIOR: When explicitly set to 'discover', reuse discovered subnets
322
+ expect(result.vpcConfig.subnetIds).toEqual(['subnet-shared-1', 'subnet-shared-2']);
323
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
324
+ });
325
+
326
+ it('should create VPC endpoints in discover mode with selfHeal when none exist', async () => {
327
+ const appDefinition = {
328
+ vpc: {
329
+ enable: true,
330
+ management: 'discover',
331
+ selfHeal: true,
332
+ },
333
+ };
334
+
335
+ const discoveredResources = {
336
+ defaultVpcId: 'vpc-discovered',
337
+ privateSubnetId1: 'subnet-private1',
338
+ privateSubnetId2: 'subnet-private2',
339
+ existingNatGatewayId: 'nat-123',
340
+ // No VPC endpoints discovered
341
+ };
342
+
343
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
344
+
345
+ // With selfHeal enabled and no VPC endpoints found, they should be created
346
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
347
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
348
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
349
+ });
350
+
351
+ it('should NOT create VPC endpoints in discover mode when they already exist', async () => {
352
+ const appDefinition = {
353
+ vpc: {
354
+ enable: true,
355
+ management: 'discover',
356
+ selfHeal: true,
357
+ },
358
+ };
359
+
360
+ const discoveredResources = {
361
+ defaultVpcId: 'vpc-discovered',
362
+ privateSubnetId1: 'subnet-private1',
363
+ privateSubnetId2: 'subnet-private2',
364
+ existingNatGatewayId: 'nat-123',
365
+ // VPC endpoints already exist
366
+ s3VpcEndpointId: 'vpce-s3-123',
367
+ dynamodbVpcEndpointId: 'vpce-dynamodb-456',
368
+ };
369
+
370
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
371
+
372
+ // With existing VPC endpoints discovered, they should NOT be recreated
373
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
374
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined();
375
+ });
376
+
377
+ it('should create VPC endpoints with selfHeal when missing', async () => {
378
+ const appDefinition = {
379
+ vpc: {
380
+ enable: true,
381
+ management: 'discover',
382
+ selfHeal: true,
383
+ },
384
+ };
385
+
386
+ const discoveredResources = {
387
+ defaultVpcId: 'vpc-123',
388
+ privateSubnetId1: 'subnet-1',
389
+ privateSubnetId2: 'subnet-2',
390
+ defaultSecurityGroupId: 'sg-123',
391
+ // No VPC endpoints discovered - selfHeal should create them
392
+ };
393
+
394
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
395
+
396
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
397
+ expect(result.resources.FriggS3VPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
398
+ expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
399
+ });
400
+
401
+ it('should reuse stack-managed VPC endpoints without creating CloudFormation resources', async () => {
402
+ const appDefinition = {
403
+ vpc: { enable: true, enableVPCEndpoints: true },
404
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
405
+ database: { postgres: { enable: true } },
406
+ };
407
+ const discoveredResources = {
408
+ defaultVpcId: 'vpc-123',
409
+ privateSubnetId1: 'subnet-1',
410
+ privateSubnetId2: 'subnet-2',
411
+ // VPC endpoints from CloudFormation stack (string IDs)
412
+ s3VpcEndpointId: 'vpce-s3-stack',
413
+ dynamoDbVpcEndpointId: 'vpce-ddb-stack',
414
+ kmsVpcEndpointId: 'vpce-kms-stack',
415
+ secretsManagerVpcEndpointId: 'vpce-sm-stack',
416
+ sqsVpcEndpointId: 'vpce-sqs-stack',
417
+ };
418
+
419
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
420
+
421
+ // Should NOT create CloudFormation resources (reuse stack endpoints)
422
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
423
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined();
424
+ expect(result.resources.FriggKMSVPCEndpoint).toBeUndefined();
425
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeUndefined();
426
+ expect(result.resources.FriggSQSVPCEndpoint).toBeUndefined();
427
+
428
+ // Should still NOT create VPC Endpoint Security Group
429
+ expect(result.resources.FriggVPCEndpointSecurityGroup).toBeUndefined();
430
+ });
431
+
432
+ it('should create VPC endpoints when discovered from AWS but not stack', async () => {
433
+ const appDefinition = {
434
+ vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
435
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
436
+ };
437
+ const discoveredResources = {
438
+ defaultVpcId: 'vpc-123',
439
+ privateSubnetId1: 'subnet-1',
440
+ privateSubnetId2: 'subnet-2',
441
+ // No VPC endpoints in stack (would be strings)
442
+ // existingEndpoints will be passed as empty
443
+ };
444
+
445
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
446
+
447
+ // Should create CloudFormation resources (not in stack)
448
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
449
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
450
+ expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
451
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
452
+ expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
453
+ });
454
+
455
+ it('should skip VPC endpoints when disabled', async () => {
456
+ const appDefinition = {
457
+ vpc: {
458
+ enable: true,
459
+ management: 'discover',
460
+ enableVPCEndpoints: false,
461
+ },
462
+ };
463
+
464
+ const discoveredResources = {
465
+ defaultVpcId: 'vpc-123',
466
+ privateSubnetId1: 'subnet-1',
467
+ privateSubnetId2: 'subnet-2',
468
+ };
469
+
470
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
471
+
472
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
473
+ });
474
+
475
+ it('should create route table associations when VPC endpoints exist but no NAT Gateway', async () => {
476
+ const appDefinition = {
477
+ vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
478
+ };
479
+ const discoveredResources = {
480
+ defaultVpcId: 'vpc-123',
481
+ privateSubnetId1: 'subnet-1',
482
+ privateSubnetId2: 'subnet-2',
483
+ // No NAT Gateway, so associations won't be created by NAT Gateway routing
484
+ };
485
+
486
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
487
+
488
+ // Route table should be created for VPC endpoints
489
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
490
+ expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
491
+
492
+ // Subnet associations should be created (healing)
493
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
494
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
495
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toBe('subnet-1');
496
+
497
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
498
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toBe('subnet-2');
499
+ });
500
+
501
+ it('should include IAM permissions for VPC operations', async () => {
502
+ const appDefinition = {
503
+ vpc: {
504
+ enable: true,
505
+ },
506
+ };
507
+
508
+ const discoveredResources = {
509
+ defaultVpcId: 'vpc-123',
510
+ privateSubnetId1: 'subnet-priv1',
511
+ privateSubnetId2: 'subnet-priv2',
512
+ };
513
+
514
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
515
+
516
+ const vpcPermissions = result.iamStatements.find(stmt =>
517
+ stmt.Action.includes('ec2:CreateNetworkInterface')
518
+ );
519
+
520
+ expect(vpcPermissions).toBeDefined();
521
+ expect(vpcPermissions.Action).toContain('ec2:DescribeNetworkInterfaces');
522
+ expect(vpcPermissions.Action).toContain('ec2:DeleteNetworkInterface');
523
+ });
524
+ });
525
+
526
+ describe('build() - create-new mode', () => {
527
+ it('should create complete VPC infrastructure', async () => {
528
+ const appDefinition = {
529
+ vpc: {
530
+ enable: true,
531
+ management: 'create-new',
532
+ },
533
+ };
534
+
535
+ const result = await vpcBuilder.build(appDefinition, {});
536
+
537
+ expect(result.resources.FriggVPC).toBeDefined();
538
+ expect(result.resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
539
+ expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
540
+ });
541
+
542
+ it('should create private and public subnets', async () => {
543
+ const appDefinition = {
544
+ vpc: {
545
+ enable: true,
546
+ management: 'create-new',
547
+ },
548
+ };
549
+
550
+ const result = await vpcBuilder.build(appDefinition, {});
551
+
552
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
553
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
554
+ expect(result.resources.FriggPublicSubnet).toBeDefined();
555
+ });
556
+
557
+ it('should create security group for Lambda functions', async () => {
558
+ const appDefinition = {
559
+ vpc: {
560
+ enable: true,
561
+ management: 'create-new',
562
+ },
563
+ };
564
+
565
+ const result = await vpcBuilder.build(appDefinition, {});
566
+
567
+ expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
568
+ expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
569
+ });
570
+
571
+ it('should use CloudFormation references for new resources', async () => {
572
+ const appDefinition = {
573
+ vpc: {
574
+ enable: true,
575
+ management: 'create-new',
576
+ },
577
+ };
578
+
579
+ const result = await vpcBuilder.build(appDefinition, {});
580
+
581
+ expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
582
+ expect(result.vpcConfig.subnetIds).toContainEqual({ Ref: 'FriggPrivateSubnet1' });
583
+ expect(result.vpcConfig.subnetIds).toContainEqual({ Ref: 'FriggPrivateSubnet2' });
584
+ });
585
+
586
+ it('should use custom CIDR block if provided', async () => {
587
+ const appDefinition = {
588
+ vpc: {
589
+ enable: true,
590
+ management: 'create-new',
591
+ cidrBlock: '192.168.0.0/16',
592
+ },
593
+ };
594
+
595
+ const result = await vpcBuilder.build(appDefinition, {});
596
+
597
+ expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('192.168.0.0/16');
598
+ });
599
+ });
600
+
601
+ describe('build() - use-existing mode', () => {
602
+ it('should use provided VPC and subnet IDs', async () => {
603
+ const appDefinition = {
604
+ vpc: {
605
+ enable: true,
606
+ management: 'use-existing',
607
+ vpcId: 'vpc-custom',
608
+ subnets: {
609
+ ids: ['subnet-a', 'subnet-b'],
610
+ },
611
+ securityGroupIds: ['sg-custom'],
612
+ },
613
+ };
614
+
615
+ const result = await vpcBuilder.build(appDefinition, {});
616
+
617
+ expect(result.vpcConfig.subnetIds).toEqual(['subnet-a', 'subnet-b']);
618
+ expect(result.vpcConfig.securityGroupIds).toEqual(['sg-custom']);
619
+ });
620
+
621
+ it('should not create VPC resources in use-existing mode', async () => {
622
+ const appDefinition = {
623
+ vpc: {
624
+ enable: true,
625
+ management: 'use-existing',
626
+ vpcId: 'vpc-custom',
627
+ subnets: {
628
+ ids: ['subnet-a', 'subnet-b'],
629
+ },
630
+ },
631
+ };
632
+
633
+ const result = await vpcBuilder.build(appDefinition, {});
634
+
635
+ expect(result.resources.FriggVPC).toBeUndefined();
636
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
637
+ });
638
+ });
639
+
640
+ describe('getDependencies()', () => {
641
+ it('should have no dependencies', () => {
642
+ const deps = vpcBuilder.getDependencies();
643
+
644
+ expect(deps).toEqual([]);
645
+ });
646
+ });
647
+
648
+ describe('getName()', () => {
649
+ it('should return VpcBuilder', () => {
650
+ expect(vpcBuilder.getName()).toBe('VpcBuilder');
651
+ });
652
+ });
653
+
654
+ describe('NAT Gateway handling', () => {
655
+ it('should create NAT gateway when management is createAndManage', async () => {
656
+ const appDefinition = {
657
+ vpc: {
658
+ enable: true,
659
+ management: 'discover',
660
+ natGateway: {
661
+ management: 'createAndManage',
662
+ },
663
+ selfHeal: true,
664
+ },
665
+ };
666
+
667
+ const discoveredResources = {
668
+ defaultVpcId: 'vpc-123',
669
+ publicSubnetId: 'subnet-public',
670
+ };
671
+
672
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
673
+
674
+ expect(result.resources.FriggNATGateway).toBeDefined();
675
+ expect(result.resources.FriggNATGateway.Type).toBe('AWS::EC2::NatGateway');
676
+ });
677
+
678
+ it('should create route table associations for private subnets with NAT Gateway', async () => {
679
+ const appDefinition = {
680
+ vpc: {
681
+ enable: true,
682
+ management: 'discover',
683
+ subnets: { management: 'create' },
684
+ natGateway: {
685
+ management: 'createAndManage',
686
+ },
687
+ selfHeal: true,
688
+ },
689
+ };
690
+
691
+ const discoveredResources = {
692
+ defaultVpcId: 'vpc-123',
693
+ publicSubnetId: 'subnet-public',
694
+ };
695
+
696
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
697
+
698
+ // Verify route table is created
699
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
700
+ expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
701
+
702
+ // Verify route to NAT Gateway
703
+ expect(result.resources.FriggPrivateRoute).toBeDefined();
704
+ expect(result.resources.FriggPrivateRoute.Properties.NatGatewayId).toEqual({ Ref: 'FriggNATGateway' });
705
+
706
+ // Verify subnet route table associations
707
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
708
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
709
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toEqual({ Ref: 'FriggPrivateSubnet1' });
710
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.RouteTableId).toEqual({ Ref: 'FriggLambdaRouteTable' });
711
+
712
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
713
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
714
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toEqual({ Ref: 'FriggPrivateSubnet2' });
715
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.RouteTableId).toEqual({ Ref: 'FriggLambdaRouteTable' });
716
+ });
717
+
718
+ it('should not create NAT when existing NAT is properly placed', async () => {
719
+ const appDefinition = {
720
+ vpc: {
721
+ enable: true,
722
+ natGateway: {
723
+ management: 'createAndManage',
724
+ },
725
+ selfHeal: true,
726
+ },
727
+ };
728
+
729
+ const discoveredResources = {
730
+ defaultVpcId: 'vpc-123',
731
+ publicSubnetId: 'subnet-public',
732
+ existingNatGatewayId: 'nat-good',
733
+ natGatewayInPrivateSubnet: false,
734
+ };
735
+
736
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
737
+
738
+ expect(result.resources.FriggNATGateway).toBeUndefined();
739
+ });
740
+
741
+ it('should create new NAT when existing is in private subnet', async () => {
742
+ const appDefinition = {
743
+ vpc: {
744
+ enable: true,
745
+ natGateway: {
746
+ management: 'createAndManage',
747
+ },
748
+ selfHeal: true,
749
+ },
750
+ };
751
+
752
+ const discoveredResources = {
753
+ defaultVpcId: 'vpc-123',
754
+ publicSubnetId: 'subnet-public',
755
+ existingNatGatewayId: 'nat-misplaced',
756
+ natGatewayInPrivateSubnet: true, // WRONG placement
757
+ };
758
+
759
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
760
+
761
+ expect(result.resources.FriggNATGateway).toBeDefined();
762
+ });
763
+
764
+ it('should create new NAT Gateway when existing is in private subnet', async () => {
765
+ const appDefinition = {
766
+ vpc: {
767
+ enable: true,
768
+ natGateway: {
769
+ management: 'createAndManage',
770
+ },
771
+ selfHeal: false,
772
+ },
773
+ };
774
+
775
+ const discoveredResources = {
776
+ defaultVpcId: 'vpc-123',
777
+ privateSubnetId1: 'subnet-priv1',
778
+ privateSubnetId2: 'subnet-priv2',
779
+ publicSubnetId: 'subnet-public',
780
+ existingNatGatewayId: 'nat-misplaced',
781
+ natGatewayInPrivateSubnet: true,
782
+ };
783
+
784
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
785
+
786
+ // Should create new NAT Gateway instead of using the misplaced one
787
+ expect(result.resources.FriggNATGateway).toBeDefined();
788
+ expect(result.resources.FriggNATGateway.Type).toBe('AWS::EC2::NatGateway');
789
+ });
790
+
791
+ it('should reuse existing elastic IP allocation', async () => {
792
+ const appDefinition = {
793
+ vpc: {
794
+ enable: true,
795
+ natGateway: {
796
+ management: 'createAndManage',
797
+ },
798
+ selfHeal: true,
799
+ },
800
+ };
801
+
802
+ const discoveredResources = {
803
+ defaultVpcId: 'vpc-123',
804
+ publicSubnetId: 'subnet-public',
805
+ existingElasticIpAllocationId: 'eipalloc-123',
806
+ };
807
+
808
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
809
+
810
+ if (result.resources.FriggNATGateway) {
811
+ // When reusing existing EIP, it should be a CloudFormation reference
812
+ expect(result.resources.FriggNATGateway.Properties.AllocationId).toEqual(
813
+ { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] }
814
+ );
815
+ }
816
+ });
817
+ });
818
+
819
+ describe('VPC Endpoints', () => {
820
+ it('should create KMS endpoint when KMS encryption is enabled', async () => {
821
+ const appDefinition = {
822
+ vpc: {
823
+ enable: true,
824
+ management: 'create-new',
825
+ },
826
+ encryption: {
827
+ fieldLevelEncryptionMethod: 'kms',
828
+ },
829
+ };
830
+
831
+ const discoveredResources = {};
832
+
833
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
834
+
835
+ expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
836
+ });
837
+
838
+ it('should create Secrets Manager endpoint when enabled', async () => {
839
+ const appDefinition = {
840
+ vpc: {
841
+ enable: true,
842
+ management: 'create-new',
843
+ },
844
+ secretsManager: {
845
+ enable: true,
846
+ },
847
+ };
848
+
849
+ const discoveredResources = {};
850
+
851
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
852
+
853
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
854
+ });
855
+
856
+ it('should not create KMS endpoint when encryption is AES', async () => {
857
+ const appDefinition = {
858
+ vpc: {
859
+ enable: true,
860
+ management: 'create-new',
861
+ },
862
+ encryption: {
863
+ fieldLevelEncryptionMethod: 'aes',
864
+ },
865
+ };
866
+
867
+ const discoveredResources = {};
868
+
869
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
870
+
871
+ expect(result.resources.FriggKMSVPCEndpoint).toBeUndefined();
872
+ });
873
+ });
874
+
875
+ describe('Self-healing', () => {
876
+ it('should create missing subnets when selfHeal is enabled', async () => {
877
+ const appDefinition = {
878
+ vpc: {
879
+ enable: true,
880
+ management: 'discover',
881
+ selfHeal: true,
882
+ },
883
+ };
884
+
885
+ const discoveredResources = {
886
+ defaultVpcId: 'vpc-123',
887
+ privateSubnetId1: null,
888
+ privateSubnetId2: null,
889
+ };
890
+
891
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
892
+
893
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
894
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
895
+ });
896
+
897
+ it('should throw error for missing subnets without selfHeal', async () => {
898
+ const appDefinition = {
899
+ vpc: {
900
+ enable: true,
901
+ management: 'discover',
902
+ subnets: { management: 'discover' },
903
+ selfHeal: false,
904
+ },
905
+ };
906
+
907
+ const discoveredResources = {
908
+ defaultVpcId: 'vpc-123',
909
+ privateSubnetId1: null,
910
+ privateSubnetId2: null,
911
+ };
912
+
913
+ await expect(vpcBuilder.build(appDefinition, discoveredResources)).rejects.toThrow(
914
+ 'No subnets discovered'
915
+ );
916
+ });
917
+ });
918
+
919
+ describe('Management Mode (Simplified API)', () => {
920
+ it('should reuse stack VPC when managementMode=managed + vpcIsolation=isolated AND stack has VPC', async () => {
921
+ const appDefinition = {
922
+ managementMode: 'managed',
923
+ vpcIsolation: 'isolated',
924
+ vpc: {
925
+ enable: true,
926
+ management: 'create-new', // Should be IGNORED
927
+ },
928
+ };
929
+
930
+ // CloudFormation stack has VPC (from previous deployment of this stage)
931
+ const discoveredResources = {
932
+ defaultVpcId: 'vpc-stack-dev', // CloudFormation discovery sets this
933
+ privateSubnetId1: 'subnet-private-1',
934
+ privateSubnetId2: 'subnet-private-2',
935
+ publicSubnetId1: 'subnet-public-1',
936
+ publicSubnetId2: 'subnet-public-2',
937
+ };
938
+
939
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
940
+
941
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
942
+
943
+ // Should warn about ignored options
944
+ expect(consoleLogSpy).toHaveBeenCalledWith(
945
+ expect.stringContaining("managementMode='managed' ignoring")
946
+ );
947
+
948
+ // Should log reusing stack VPC
949
+ expect(consoleLogSpy).toHaveBeenCalledWith(
950
+ expect.stringContaining("stack has VPC, reusing")
951
+ );
952
+
953
+ // Should keep VPC definition in template (CloudFormation idempotency)
954
+ // Even though VPC exists, we include the definition - CF won't recreate it
955
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
956
+ expect(result.resources.FriggVPC).toBeDefined();
957
+ expect(result.resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
958
+
959
+ // Should keep subnet definitions in template and use Refs
960
+ expect(result.vpcConfig.subnetIds).toEqual([
961
+ { Ref: 'FriggPrivateSubnet1' },
962
+ { Ref: 'FriggPrivateSubnet2' }
963
+ ]);
964
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
965
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
966
+
967
+ consoleLogSpy.mockRestore();
968
+ });
969
+
970
+ it('should create new VPC when managementMode=managed + vpcIsolation=isolated AND stack has NO VPC', async () => {
971
+ const appDefinition = {
972
+ managementMode: 'managed',
973
+ vpcIsolation: 'isolated',
974
+ vpc: {
975
+ enable: true,
976
+ management: 'discover', // Should be IGNORED
977
+ },
978
+ };
979
+
980
+ // No VPC in CloudFormation stack (fresh deployment)
981
+ // Default VPC might exist in AWS, but not stack-managed
982
+ const discoveredResources = {
983
+ // No defaultVpcId means no VPC in CloudFormation stack
984
+ };
985
+
986
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
987
+
988
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
989
+
990
+ // Should warn about ignored options
991
+ expect(consoleLogSpy).toHaveBeenCalledWith(
992
+ expect.stringContaining("managementMode='managed' ignoring")
993
+ );
994
+
995
+ // Should log creating new VPC
996
+ expect(consoleLogSpy).toHaveBeenCalledWith(
997
+ expect.stringContaining("no stack VPC, creating new")
998
+ );
999
+
1000
+ // Should create new isolated VPC
1001
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1002
+ expect(result.resources.FriggVPC).toBeDefined();
1003
+
1004
+ // Subnets should use CloudFormation Fn::Cidr
1005
+ expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
1006
+ 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1007
+ });
1008
+
1009
+ consoleLogSpy.mockRestore();
1010
+ });
1011
+
1012
+ it('should use managementMode=managed with vpcIsolation=shared to discover VPC', async () => {
1013
+ const appDefinition = {
1014
+ managementMode: 'managed',
1015
+ vpcIsolation: 'shared',
1016
+ vpc: {
1017
+ enable: true,
1018
+ subnets: { management: 'use-existing' }, // Should be IGNORED
1019
+ },
1020
+ };
1021
+
1022
+ const discoveredResources = {
1023
+ defaultVpcId: 'vpc-existing',
1024
+ };
1025
+
1026
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
1027
+
1028
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1029
+
1030
+ // Should warn about ignored options
1031
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1032
+ expect.stringContaining("ignoring")
1033
+ );
1034
+
1035
+ // Should discover existing VPC
1036
+ expect(result.vpcId).toBe('vpc-existing');
1037
+ expect(result.resources.FriggVPC).toBeUndefined();
1038
+
1039
+ // Should create new stage-specific subnets
1040
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1041
+
1042
+ consoleLogSpy.mockRestore();
1043
+ });
1044
+
1045
+ it('should default to discover mode for backwards compatibility', async () => {
1046
+ const appDefinition = {
1047
+ // No managementMode specified
1048
+ vpc: {
1049
+ enable: true,
1050
+ management: 'create-new', // Should be RESPECTED
1051
+ },
1052
+ };
1053
+
1054
+ const result = await vpcBuilder.build(appDefinition, {});
1055
+
1056
+ // Should respect legacy vpc.management
1057
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1058
+ expect(result.resources.FriggVPC).toBeDefined();
1059
+ });
1060
+ });
1061
+
1062
+ describe('VPC Sharing Control', () => {
1063
+ it('should share VPC across stages when shareAcrossStages is true (default)', async () => {
1064
+ const appDefinition = {
1065
+ vpc: {
1066
+ enable: true,
1067
+ shareAcrossStages: true, // Explicit opt-in to sharing
1068
+ },
1069
+ };
1070
+
1071
+ const discoveredResources = {
1072
+ defaultVpcId: 'vpc-shared',
1073
+ natGatewayId: 'nat-shared',
1074
+ };
1075
+
1076
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1077
+
1078
+ // Should use discovered VPC (not create new one)
1079
+ expect(result.vpcId).toBe('vpc-shared');
1080
+ expect(result.resources.FriggVPC).toBeUndefined();
1081
+
1082
+ // Should create stage-specific subnets for isolation
1083
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1084
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1085
+
1086
+ // Should reuse discovered NAT Gateway
1087
+ expect(result.resources.FriggNATGateway).toBeUndefined();
1088
+ });
1089
+
1090
+ it('should create isolated VPC when shareAcrossStages is false', async () => {
1091
+ const appDefinition = {
1092
+ vpc: {
1093
+ enable: true,
1094
+ shareAcrossStages: false, // Explicit opt-out of sharing
1095
+ },
1096
+ };
1097
+
1098
+ const discoveredResources = {
1099
+ defaultVpcId: 'vpc-shared',
1100
+ natGatewayId: 'nat-shared',
1101
+ };
1102
+
1103
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1104
+
1105
+ // Should create new VPC (ignore discovered resources)
1106
+ expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1107
+ expect(result.resources.FriggVPC).toBeDefined();
1108
+ expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
1109
+
1110
+ // Should create stage-specific subnets with Fn::Cidr (dynamic from VPC CIDR)
1111
+ expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1112
+ expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1113
+
1114
+ // Subnets should use CloudFormation Fn::Cidr, NOT hardcoded 172.31.x.x
1115
+ expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
1116
+ 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1117
+ });
1118
+ expect(result.resources.FriggPrivateSubnet2.Properties.CidrBlock).toEqual({
1119
+ 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1120
+ });
1121
+
1122
+ // Should create new NAT Gateway
1123
+ expect(result.resources.FriggNATGateway).toBeDefined();
1124
+ });
1125
+
1126
+ it('should default to shared VPC when shareAcrossStages is not specified', async () => {
1127
+ const appDefinition = {
1128
+ vpc: {
1129
+ enable: true,
1130
+ // shareAcrossStages not specified - should default to true
1131
+ },
1132
+ };
1133
+
1134
+ const discoveredResources = {
1135
+ defaultVpcId: 'vpc-discovered',
1136
+ };
1137
+
1138
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
1139
+
1140
+ // Should use discovered VPC by default (backwards compatibility)
1141
+ expect(result.vpcId).toBe('vpc-discovered');
1142
+ expect(result.resources.FriggVPC).toBeUndefined();
1143
+ });
1144
+ });
1145
+
1146
+ describe('generateSubnetCidrs()', () => {
1147
+ it('should use CloudFormation Fn::Cidr for create-new mode', () => {
1148
+ const cidrs = vpcBuilder.generateSubnetCidrs('create-new', {});
1149
+
1150
+ expect(cidrs.private1).toEqual({
1151
+ 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1152
+ });
1153
+ expect(cidrs.private2).toEqual({
1154
+ 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1155
+ });
1156
+ expect(cidrs.public1).toEqual({
1157
+ 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1158
+ });
1159
+ expect(cidrs.public2).toEqual({
1160
+ 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1161
+ });
1162
+ });
1163
+
1164
+ it('should use default static CIDRs when no existing subnets in VPC', () => {
1165
+ const discoveredResources = {
1166
+ subnets: []
1167
+ };
1168
+
1169
+ const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1170
+
1171
+ expect(cidrs.private1).toBe('172.31.240.0/24');
1172
+ expect(cidrs.private2).toBe('172.31.241.0/24');
1173
+ expect(cidrs.public1).toBe('172.31.250.0/24');
1174
+ expect(cidrs.public2).toBe('172.31.251.0/24');
1175
+ });
1176
+
1177
+ it('should avoid CIDR conflicts with existing subnets', () => {
1178
+ const discoveredResources = {
1179
+ subnets: [
1180
+ { CidrBlock: '172.31.240.0/24' }, // Conflicts with default private1
1181
+ { CidrBlock: '172.31.241.0/24' }, // Conflicts with default private2
1182
+ { CidrBlock: '172.31.0.0/20' }, // Default VPC subnet
1183
+ { CidrBlock: '172.31.16.0/20' }, // Default VPC subnet
1184
+ ]
1185
+ };
1186
+
1187
+ const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1188
+
1189
+ // Should skip 240 and 241 (already taken), use 242-243 for private, 250-251 for public
1190
+ expect(cidrs.private1).toBe('172.31.242.0/24');
1191
+ expect(cidrs.private2).toBe('172.31.243.0/24');
1192
+ expect(cidrs.public1).toBe('172.31.250.0/24'); // Public range starts at 250
1193
+ expect(cidrs.public2).toBe('172.31.251.0/24');
1194
+ });
1195
+
1196
+ it('should find first available CIDR blocks when some in range are taken', () => {
1197
+ const discoveredResources = {
1198
+ subnets: [
1199
+ { CidrBlock: '172.31.240.0/24' },
1200
+ { CidrBlock: '172.31.242.0/24' },
1201
+ { CidrBlock: '172.31.244.0/24' },
1202
+ ]
1203
+ };
1204
+
1205
+ const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1206
+
1207
+ // Should use 241, 243 for private (filling gaps), 250, 251 for public
1208
+ expect(cidrs.private1).toBe('172.31.241.0/24');
1209
+ expect(cidrs.private2).toBe('172.31.243.0/24');
1210
+ expect(cidrs.public1).toBe('172.31.250.0/24'); // Public range starts at 250
1211
+ expect(cidrs.public2).toBe('172.31.251.0/24');
1212
+ });
1213
+
1214
+ it('should handle missing discoveredResources gracefully', () => {
1215
+ const cidrs = vpcBuilder.generateSubnetCidrs('discover', null);
1216
+
1217
+ // Should fallback to default CIDRs
1218
+ expect(cidrs.private1).toBe('172.31.240.0/24');
1219
+ expect(cidrs.private2).toBe('172.31.241.0/24');
1220
+ });
1221
+
1222
+ it('should handle discoveredResources without subnets array', () => {
1223
+ const discoveredResources = { vpcId: 'vpc-123' };
1224
+
1225
+ const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1226
+
1227
+ // Should fallback to default CIDRs
1228
+ expect(cidrs.private1).toBe('172.31.240.0/24');
1229
+ expect(cidrs.private2).toBe('172.31.241.0/24');
1230
+ });
1231
+ });
1232
+
1233
+ describe('Outputs', () => {
1234
+ it.skip('should generate VPC ID output', async () => {
1235
+ const appDefinition = {
1236
+ vpc: {
1237
+ enable: true,
1238
+ management: 'create-new',
1239
+ },
1240
+ };
1241
+
1242
+ const result = await vpcBuilder.build(appDefinition, {});
1243
+
1244
+ expect(result.outputs.VpcId).toBeDefined();
1245
+ });
1246
+
1247
+ it.skip('should generate subnet outputs', async () => {
1248
+ const appDefinition = {
1249
+ vpc: {
1250
+ enable: true,
1251
+ management: 'create-new',
1252
+ },
1253
+ };
1254
+
1255
+ const result = await vpcBuilder.build(appDefinition, {});
1256
+
1257
+ expect(result.outputs.PrivateSubnet1Id).toBeDefined();
1258
+ expect(result.outputs.PrivateSubnet2Id).toBeDefined();
1259
+ });
1260
+ });
1261
+ });
1262
+