@friggframework/devtools 2.0.0-next.61 → 2.0.0-next.62

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 (305) hide show
  1. package/package.json +14 -7
  2. package/.eslintrc.json +0 -3
  3. package/CHANGELOG.md +0 -132
  4. package/infrastructure/ARCHITECTURE.md +0 -487
  5. package/infrastructure/CLAUDE.md +0 -481
  6. package/infrastructure/HEALTH.md +0 -468
  7. package/infrastructure/README.md +0 -522
  8. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +0 -391
  9. package/infrastructure/__tests__/helpers/test-utils.js +0 -277
  10. package/infrastructure/__tests__/postgres-config.test.js +0 -914
  11. package/infrastructure/__tests__/template-generation.test.js +0 -687
  12. package/infrastructure/create-frigg-infrastructure.js +0 -147
  13. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +0 -630
  14. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +0 -1317
  15. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +0 -105
  16. package/infrastructure/docs/deployment-instructions.md +0 -268
  17. package/infrastructure/docs/generate-iam-command.md +0 -278
  18. package/infrastructure/docs/iam-policy-templates.md +0 -193
  19. package/infrastructure/domains/database/aurora-builder.js +0 -809
  20. package/infrastructure/domains/database/aurora-builder.test.js +0 -950
  21. package/infrastructure/domains/database/aurora-discovery.js +0 -87
  22. package/infrastructure/domains/database/aurora-discovery.test.js +0 -188
  23. package/infrastructure/domains/database/aurora-resolver.js +0 -210
  24. package/infrastructure/domains/database/aurora-resolver.test.js +0 -347
  25. package/infrastructure/domains/database/migration-builder.js +0 -701
  26. package/infrastructure/domains/database/migration-builder.test.js +0 -321
  27. package/infrastructure/domains/database/migration-resolver.js +0 -163
  28. package/infrastructure/domains/database/migration-resolver.test.js +0 -337
  29. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +0 -164
  30. package/infrastructure/domains/health/application/ports/IResourceDetector.js +0 -129
  31. package/infrastructure/domains/health/application/ports/IResourceImporter.js +0 -142
  32. package/infrastructure/domains/health/application/ports/IStackRepository.js +0 -131
  33. package/infrastructure/domains/health/application/ports/index.js +0 -26
  34. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +0 -679
  35. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +0 -167
  36. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +0 -1130
  37. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +0 -221
  38. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +0 -152
  39. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +0 -343
  40. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +0 -535
  41. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +0 -376
  42. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +0 -213
  43. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +0 -441
  44. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +0 -267
  45. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +0 -324
  46. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +0 -386
  47. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +0 -1419
  48. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +0 -391
  49. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +0 -551
  50. package/infrastructure/domains/health/domain/entities/issue.js +0 -299
  51. package/infrastructure/domains/health/domain/entities/issue.test.js +0 -528
  52. package/infrastructure/domains/health/domain/entities/property-mismatch.js +0 -108
  53. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +0 -275
  54. package/infrastructure/domains/health/domain/entities/resource.js +0 -159
  55. package/infrastructure/domains/health/domain/entities/resource.test.js +0 -432
  56. package/infrastructure/domains/health/domain/entities/stack-health-report.js +0 -306
  57. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +0 -601
  58. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +0 -380
  59. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +0 -971
  60. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +0 -1150
  61. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +0 -672
  62. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +0 -496
  63. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +0 -419
  64. package/infrastructure/domains/health/domain/services/health-score-calculator.js +0 -248
  65. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +0 -504
  66. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +0 -195
  67. package/infrastructure/domains/health/domain/services/import-template-generator.js +0 -435
  68. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +0 -345
  69. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +0 -234
  70. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +0 -431
  71. package/infrastructure/domains/health/domain/services/property-mutability-config.js +0 -382
  72. package/infrastructure/domains/health/domain/services/template-parser.js +0 -245
  73. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +0 -192
  74. package/infrastructure/domains/health/domain/value-objects/health-score.js +0 -138
  75. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +0 -267
  76. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +0 -161
  77. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +0 -198
  78. package/infrastructure/domains/health/domain/value-objects/resource-state.js +0 -167
  79. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +0 -196
  80. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +0 -192
  81. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +0 -262
  82. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +0 -312
  83. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +0 -367
  84. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +0 -432
  85. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +0 -784
  86. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +0 -1133
  87. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +0 -565
  88. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +0 -554
  89. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +0 -318
  90. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +0 -398
  91. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +0 -777
  92. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +0 -580
  93. package/infrastructure/domains/integration/integration-builder.js +0 -404
  94. package/infrastructure/domains/integration/integration-builder.test.js +0 -690
  95. package/infrastructure/domains/integration/integration-resolver.js +0 -170
  96. package/infrastructure/domains/integration/integration-resolver.test.js +0 -369
  97. package/infrastructure/domains/integration/websocket-builder.js +0 -69
  98. package/infrastructure/domains/integration/websocket-builder.test.js +0 -195
  99. package/infrastructure/domains/networking/vpc-builder.js +0 -2051
  100. package/infrastructure/domains/networking/vpc-builder.test.js +0 -1960
  101. package/infrastructure/domains/networking/vpc-discovery.js +0 -177
  102. package/infrastructure/domains/networking/vpc-discovery.test.js +0 -350
  103. package/infrastructure/domains/networking/vpc-resolver.js +0 -505
  104. package/infrastructure/domains/networking/vpc-resolver.test.js +0 -801
  105. package/infrastructure/domains/parameters/ssm-builder.js +0 -79
  106. package/infrastructure/domains/parameters/ssm-builder.test.js +0 -189
  107. package/infrastructure/domains/parameters/ssm-discovery.js +0 -84
  108. package/infrastructure/domains/parameters/ssm-discovery.test.js +0 -210
  109. package/infrastructure/domains/security/iam-generator.js +0 -816
  110. package/infrastructure/domains/security/iam-generator.test.js +0 -204
  111. package/infrastructure/domains/security/kms-builder.js +0 -415
  112. package/infrastructure/domains/security/kms-builder.test.js +0 -392
  113. package/infrastructure/domains/security/kms-discovery.js +0 -80
  114. package/infrastructure/domains/security/kms-discovery.test.js +0 -177
  115. package/infrastructure/domains/security/kms-resolver.js +0 -96
  116. package/infrastructure/domains/security/kms-resolver.test.js +0 -216
  117. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +0 -401
  118. package/infrastructure/domains/security/templates/iam-policy-basic.json +0 -218
  119. package/infrastructure/domains/security/templates/iam-policy-full.json +0 -288
  120. package/infrastructure/domains/shared/base-builder.js +0 -112
  121. package/infrastructure/domains/shared/base-resolver.js +0 -186
  122. package/infrastructure/domains/shared/base-resolver.test.js +0 -305
  123. package/infrastructure/domains/shared/builder-orchestrator.js +0 -212
  124. package/infrastructure/domains/shared/builder-orchestrator.test.js +0 -213
  125. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +0 -334
  126. package/infrastructure/domains/shared/cloudformation-discovery.js +0 -672
  127. package/infrastructure/domains/shared/cloudformation-discovery.test.js +0 -985
  128. package/infrastructure/domains/shared/environment-builder.js +0 -119
  129. package/infrastructure/domains/shared/environment-builder.test.js +0 -247
  130. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +0 -579
  131. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +0 -416
  132. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +0 -93
  133. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +0 -136
  134. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +0 -82
  135. package/infrastructure/domains/shared/providers/provider-factory.js +0 -108
  136. package/infrastructure/domains/shared/providers/provider-factory.test.js +0 -170
  137. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +0 -306
  138. package/infrastructure/domains/shared/resource-discovery.js +0 -233
  139. package/infrastructure/domains/shared/resource-discovery.test.js +0 -588
  140. package/infrastructure/domains/shared/types/app-definition.js +0 -205
  141. package/infrastructure/domains/shared/types/discovery-result.js +0 -106
  142. package/infrastructure/domains/shared/types/discovery-result.test.js +0 -258
  143. package/infrastructure/domains/shared/types/index.js +0 -46
  144. package/infrastructure/domains/shared/types/resource-ownership.js +0 -108
  145. package/infrastructure/domains/shared/types/resource-ownership.test.js +0 -101
  146. package/infrastructure/domains/shared/utilities/base-definition-factory.js +0 -394
  147. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +0 -338
  148. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +0 -291
  149. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +0 -134
  150. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +0 -268
  151. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +0 -159
  152. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +0 -444
  153. package/infrastructure/domains/shared/validation/env-validator.js +0 -78
  154. package/infrastructure/domains/shared/validation/env-validator.test.js +0 -173
  155. package/infrastructure/domains/shared/validation/plugin-validator.js +0 -187
  156. package/infrastructure/domains/shared/validation/plugin-validator.test.js +0 -323
  157. package/infrastructure/esbuild.config.js +0 -53
  158. package/infrastructure/index.js +0 -4
  159. package/infrastructure/infrastructure-composer.js +0 -117
  160. package/infrastructure/infrastructure-composer.test.js +0 -1895
  161. package/infrastructure/integration.test.js +0 -383
  162. package/infrastructure/scripts/build-prisma-layer.js +0 -701
  163. package/infrastructure/scripts/build-prisma-layer.test.js +0 -170
  164. package/infrastructure/scripts/build-time-discovery.js +0 -238
  165. package/infrastructure/scripts/build-time-discovery.test.js +0 -379
  166. package/infrastructure/scripts/run-discovery.js +0 -110
  167. package/infrastructure/scripts/verify-prisma-layer.js +0 -72
  168. package/layers/prisma/.build-complete +0 -3
  169. package/layers/prisma/nodejs/package.json +0 -8
  170. package/management-ui/.eslintrc.js +0 -22
  171. package/management-ui/components.json +0 -21
  172. package/management-ui/docs/phase2-integration-guide.md +0 -320
  173. package/management-ui/index.html +0 -13
  174. package/management-ui/package.json +0 -76
  175. package/management-ui/packages/devtools/frigg-cli/ui-command/index.js +0 -302
  176. package/management-ui/postcss.config.js +0 -6
  177. package/management-ui/server/api/backend.js +0 -256
  178. package/management-ui/server/api/cli.js +0 -315
  179. package/management-ui/server/api/codegen.js +0 -663
  180. package/management-ui/server/api/connections.js +0 -857
  181. package/management-ui/server/api/discovery.js +0 -185
  182. package/management-ui/server/api/environment/index.js +0 -1
  183. package/management-ui/server/api/environment/router.js +0 -378
  184. package/management-ui/server/api/environment.js +0 -328
  185. package/management-ui/server/api/integrations.js +0 -876
  186. package/management-ui/server/api/logs.js +0 -248
  187. package/management-ui/server/api/monitoring.js +0 -282
  188. package/management-ui/server/api/open-ide.js +0 -31
  189. package/management-ui/server/api/project.js +0 -1029
  190. package/management-ui/server/api/users/sessions.js +0 -371
  191. package/management-ui/server/api/users/simulation.js +0 -254
  192. package/management-ui/server/api/users.js +0 -362
  193. package/management-ui/server/api-contract.md +0 -275
  194. package/management-ui/server/index.js +0 -873
  195. package/management-ui/server/middleware/errorHandler.js +0 -93
  196. package/management-ui/server/middleware/security.js +0 -32
  197. package/management-ui/server/processManager.js +0 -296
  198. package/management-ui/server/server.js +0 -346
  199. package/management-ui/server/services/aws-monitor.js +0 -413
  200. package/management-ui/server/services/npm-registry.js +0 -347
  201. package/management-ui/server/services/template-engine.js +0 -538
  202. package/management-ui/server/utils/cliIntegration.js +0 -220
  203. package/management-ui/server/utils/environment/auditLogger.js +0 -471
  204. package/management-ui/server/utils/environment/awsParameterStore.js +0 -275
  205. package/management-ui/server/utils/environment/encryption.js +0 -278
  206. package/management-ui/server/utils/environment/envFileManager.js +0 -286
  207. package/management-ui/server/utils/import-commonjs.js +0 -28
  208. package/management-ui/server/utils/response.js +0 -83
  209. package/management-ui/server/websocket/handler.js +0 -325
  210. package/management-ui/src/App.jsx +0 -25
  211. package/management-ui/src/assets/FriggLogo.svg +0 -1
  212. package/management-ui/src/components/AppRouter.jsx +0 -65
  213. package/management-ui/src/components/Button.jsx +0 -70
  214. package/management-ui/src/components/Card.jsx +0 -97
  215. package/management-ui/src/components/EnvironmentCompare.jsx +0 -400
  216. package/management-ui/src/components/EnvironmentEditor.jsx +0 -372
  217. package/management-ui/src/components/EnvironmentImportExport.jsx +0 -469
  218. package/management-ui/src/components/EnvironmentSchema.jsx +0 -491
  219. package/management-ui/src/components/EnvironmentSecurity.jsx +0 -463
  220. package/management-ui/src/components/ErrorBoundary.jsx +0 -73
  221. package/management-ui/src/components/IntegrationCard.jsx +0 -481
  222. package/management-ui/src/components/IntegrationCardEnhanced.jsx +0 -770
  223. package/management-ui/src/components/IntegrationExplorer.jsx +0 -379
  224. package/management-ui/src/components/IntegrationStatus.jsx +0 -336
  225. package/management-ui/src/components/Layout.jsx +0 -716
  226. package/management-ui/src/components/LoadingSpinner.jsx +0 -113
  227. package/management-ui/src/components/RepositoryPicker.jsx +0 -248
  228. package/management-ui/src/components/SessionMonitor.jsx +0 -350
  229. package/management-ui/src/components/StatusBadge.jsx +0 -208
  230. package/management-ui/src/components/UserContextSwitcher.jsx +0 -212
  231. package/management-ui/src/components/UserSimulation.jsx +0 -327
  232. package/management-ui/src/components/Welcome.jsx +0 -434
  233. package/management-ui/src/components/codegen/APIEndpointGenerator.jsx +0 -637
  234. package/management-ui/src/components/codegen/APIModuleSelector.jsx +0 -227
  235. package/management-ui/src/components/codegen/CodeGenerationWizard.jsx +0 -247
  236. package/management-ui/src/components/codegen/CodePreviewEditor.jsx +0 -316
  237. package/management-ui/src/components/codegen/DynamicModuleForm.jsx +0 -271
  238. package/management-ui/src/components/codegen/FormBuilder.jsx +0 -737
  239. package/management-ui/src/components/codegen/IntegrationGenerator.jsx +0 -855
  240. package/management-ui/src/components/codegen/ProjectScaffoldWizard.jsx +0 -797
  241. package/management-ui/src/components/codegen/SchemaBuilder.jsx +0 -303
  242. package/management-ui/src/components/codegen/TemplateSelector.jsx +0 -586
  243. package/management-ui/src/components/codegen/index.js +0 -10
  244. package/management-ui/src/components/connections/ConnectionConfigForm.jsx +0 -362
  245. package/management-ui/src/components/connections/ConnectionHealthMonitor.jsx +0 -182
  246. package/management-ui/src/components/connections/ConnectionTester.jsx +0 -200
  247. package/management-ui/src/components/connections/EntityRelationshipMapper.jsx +0 -292
  248. package/management-ui/src/components/connections/OAuthFlow.jsx +0 -204
  249. package/management-ui/src/components/connections/index.js +0 -5
  250. package/management-ui/src/components/index.js +0 -21
  251. package/management-ui/src/components/monitoring/APIGatewayMetrics.jsx +0 -222
  252. package/management-ui/src/components/monitoring/LambdaMetrics.jsx +0 -169
  253. package/management-ui/src/components/monitoring/MetricsChart.jsx +0 -197
  254. package/management-ui/src/components/monitoring/MonitoringDashboard.jsx +0 -393
  255. package/management-ui/src/components/monitoring/SQSMetrics.jsx +0 -246
  256. package/management-ui/src/components/monitoring/index.js +0 -6
  257. package/management-ui/src/components/monitoring/monitoring.css +0 -218
  258. package/management-ui/src/components/theme-provider.jsx +0 -52
  259. package/management-ui/src/components/theme-toggle.jsx +0 -39
  260. package/management-ui/src/components/ui/badge.tsx +0 -36
  261. package/management-ui/src/components/ui/button.test.jsx +0 -56
  262. package/management-ui/src/components/ui/button.tsx +0 -57
  263. package/management-ui/src/components/ui/card.tsx +0 -76
  264. package/management-ui/src/components/ui/dropdown-menu.tsx +0 -199
  265. package/management-ui/src/components/ui/select.tsx +0 -157
  266. package/management-ui/src/components/ui/skeleton.jsx +0 -15
  267. package/management-ui/src/hooks/useFrigg.jsx +0 -387
  268. package/management-ui/src/hooks/useSocket.jsx +0 -58
  269. package/management-ui/src/index.css +0 -193
  270. package/management-ui/src/lib/utils.ts +0 -6
  271. package/management-ui/src/main.jsx +0 -10
  272. package/management-ui/src/pages/CodeGeneration.jsx +0 -14
  273. package/management-ui/src/pages/Connections.jsx +0 -252
  274. package/management-ui/src/pages/ConnectionsEnhanced.jsx +0 -633
  275. package/management-ui/src/pages/Dashboard.jsx +0 -311
  276. package/management-ui/src/pages/Environment.jsx +0 -314
  277. package/management-ui/src/pages/IntegrationConfigure.jsx +0 -669
  278. package/management-ui/src/pages/IntegrationDiscovery.jsx +0 -567
  279. package/management-ui/src/pages/IntegrationTest.jsx +0 -742
  280. package/management-ui/src/pages/Integrations.jsx +0 -253
  281. package/management-ui/src/pages/Monitoring.jsx +0 -17
  282. package/management-ui/src/pages/Simulation.jsx +0 -155
  283. package/management-ui/src/pages/Users.jsx +0 -492
  284. package/management-ui/src/services/api.js +0 -41
  285. package/management-ui/src/services/apiModuleService.js +0 -193
  286. package/management-ui/src/services/websocket-handlers.js +0 -120
  287. package/management-ui/src/test/api/project.test.js +0 -273
  288. package/management-ui/src/test/components/Welcome.test.jsx +0 -378
  289. package/management-ui/src/test/mocks/server.js +0 -178
  290. package/management-ui/src/test/setup.js +0 -61
  291. package/management-ui/src/test/utils/test-utils.jsx +0 -134
  292. package/management-ui/src/utils/repository.js +0 -98
  293. package/management-ui/src/utils/repository.test.js +0 -118
  294. package/management-ui/src/workflows/phase2-integration-workflows.js +0 -884
  295. package/management-ui/tailwind.config.js +0 -63
  296. package/management-ui/tsconfig.json +0 -37
  297. package/management-ui/tsconfig.node.json +0 -10
  298. package/management-ui/vite.config.js +0 -26
  299. package/management-ui/vitest.config.js +0 -38
  300. package/test/auther-definition-method-tester.js +0 -45
  301. package/test/index.js +0 -9
  302. package/test/integration-validator.js +0 -2
  303. package/test/mock-api-readme.md +0 -102
  304. package/test/mock-api.js +0 -284
  305. package/test/mock-integration.js +0 -78
@@ -1,1960 +0,0 @@
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 add stack-managed security group back to template to prevent deletion', async () => {
402
- const appDefinition = {
403
- vpc: { enable: true },
404
- };
405
-
406
- const discoveredResources = {
407
- fromCloudFormationStack: true,
408
- stackName: 'test-stack',
409
- existingLogicalIds: ['FriggLambdaSecurityGroup'],
410
- defaultVpcId: 'vpc-123',
411
- privateSubnetId1: 'subnet-1',
412
- privateSubnetId2: 'subnet-2',
413
- lambdaSecurityGroupId: 'sg-existing-stack', // Existing in stack
414
- };
415
-
416
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
417
-
418
- // CRITICAL: Must RE-ADD stack-managed SG to template or CloudFormation will DELETE it
419
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
420
- expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
421
- expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-123');
422
- expect(result.resources.FriggLambdaSecurityGroup.Properties.GroupDescription).toBeDefined();
423
-
424
- // Should use Ref in Lambda config (not recreating)
425
- expect(result.vpcConfig.securityGroupIds).toContainEqual({ Ref: 'FriggLambdaSecurityGroup' });
426
- });
427
-
428
- it('should add stack-managed subnets back to template to prevent deletion', async () => {
429
- const appDefinition = {
430
- vpc: { enable: true },
431
- };
432
-
433
- const discoveredResources = {
434
- fromCloudFormationStack: true,
435
- stackName: 'test-stack',
436
- existingLogicalIds: ['FriggPrivateSubnet1', 'FriggPrivateSubnet2'],
437
- defaultVpcId: 'vpc-123',
438
- // Subnets exist in stack with specific IDs
439
- privateSubnetId1: 'subnet-existing-1',
440
- privateSubnetId2: 'subnet-existing-2',
441
- };
442
-
443
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
444
-
445
- // CRITICAL: Must RE-ADD stack-managed subnets to template or CloudFormation will DELETE them
446
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
447
- expect(result.resources.FriggPrivateSubnet1.Type).toBe('AWS::EC2::Subnet');
448
- expect(result.resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-123');
449
-
450
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
451
- expect(result.resources.FriggPrivateSubnet2.Type).toBe('AWS::EC2::Subnet');
452
- expect(result.resources.FriggPrivateSubnet2.Properties.VpcId).toBe('vpc-123');
453
-
454
- // Should use Refs (not external IDs)
455
- expect(result.vpcConfig.subnetIds).toEqual([
456
- { Ref: 'FriggPrivateSubnet1' },
457
- { Ref: 'FriggPrivateSubnet2' }
458
- ]);
459
- });
460
-
461
- it('should add stack-managed VPC endpoints back to template to prevent deletion', async () => {
462
- const appDefinition = {
463
- vpc: { enable: true },
464
- encryption: { fieldLevelEncryptionMethod: 'kms' },
465
- };
466
-
467
- // Structured discovery from CloudFormation
468
- const discoveredResources = {
469
- fromCloudFormationStack: true,
470
- stackName: 'test-stack',
471
- existingLogicalIds: [
472
- 'FriggLambdaSecurityGroup',
473
- 'FriggLambdaRouteTable',
474
- 'FriggS3VPCEndpoint',
475
- 'FriggDynamoDBVPCEndpoint',
476
- 'FriggKMSVPCEndpoint',
477
- 'FriggSecretsManagerVPCEndpoint',
478
- 'FriggSQSVPCEndpoint'
479
- ],
480
- defaultVpcId: 'vpc-123',
481
- privateSubnetId1: 'subnet-1',
482
- privateSubnetId2: 'subnet-2',
483
- routeTableId: 'rtb-123',
484
- lambdaSecurityGroupId: 'sg-123',
485
- // VPC endpoints discovered in stack
486
- s3VpcEndpointId: 'vpce-s3-stack',
487
- dynamodbVpcEndpointId: 'vpce-ddb-stack',
488
- kmsVpcEndpointId: 'vpce-kms-stack',
489
- secretsManagerVpcEndpointId: 'vpce-sm-stack',
490
- sqsVpcEndpointId: 'vpce-sqs-stack',
491
- };
492
-
493
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
494
-
495
- // CRITICAL: Must RE-ADD stack-managed endpoints to template or CloudFormation will DELETE them
496
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
497
- expect(result.resources.FriggS3VPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
498
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
499
-
500
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
501
- expect(result.resources.FriggDynamoDBVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
502
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
503
-
504
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
505
- expect(result.resources.FriggKMSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
506
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
507
-
508
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
509
- expect(result.resources.FriggSecretsManagerVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
510
- expect(result.resources.FriggSecretsManagerVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
511
-
512
- expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
513
- expect(result.resources.FriggSQSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
514
- expect(result.resources.FriggSQSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
515
-
516
- // Should create VPC Endpoint Security Group for interface endpoints
517
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
518
- expect(result.resources.FriggVPCEndpointSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
519
- });
520
-
521
- it('should create VPC endpoints when discovered from AWS but not stack', async () => {
522
- const appDefinition = {
523
- vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
524
- encryption: { fieldLevelEncryptionMethod: 'kms' },
525
- };
526
- const discoveredResources = {
527
- defaultVpcId: 'vpc-123',
528
- privateSubnetId1: 'subnet-1',
529
- privateSubnetId2: 'subnet-2',
530
- // No VPC endpoints in stack (would be strings)
531
- // existingEndpoints will be passed as empty
532
- };
533
-
534
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
535
-
536
- // Should create CloudFormation resources (not in stack)
537
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
538
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
539
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
540
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
541
- expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
542
- });
543
-
544
- it('should skip VPC endpoints when disabled', async () => {
545
- const appDefinition = {
546
- vpc: {
547
- enable: true,
548
- management: 'discover',
549
- enableVPCEndpoints: false,
550
- },
551
- };
552
-
553
- const discoveredResources = {
554
- defaultVpcId: 'vpc-123',
555
- privateSubnetId1: 'subnet-1',
556
- privateSubnetId2: 'subnet-2',
557
- };
558
-
559
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
560
-
561
- expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
562
- });
563
-
564
- it('should create route table associations when VPC endpoints exist but no NAT Gateway', async () => {
565
- const appDefinition = {
566
- vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
567
- };
568
- const discoveredResources = {
569
- defaultVpcId: 'vpc-123',
570
- privateSubnetId1: 'subnet-1',
571
- privateSubnetId2: 'subnet-2',
572
- // No NAT Gateway, so associations won't be created by NAT Gateway routing
573
- };
574
-
575
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
576
-
577
- // Route table should be created for VPC endpoints
578
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
579
- expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
580
-
581
- // Subnet associations should be created (healing)
582
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
583
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
584
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toBe('subnet-1');
585
-
586
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
587
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toBe('subnet-2');
588
- });
589
-
590
- it('should include IAM permissions for VPC operations', async () => {
591
- const appDefinition = {
592
- vpc: {
593
- enable: true,
594
- },
595
- };
596
-
597
- const discoveredResources = {
598
- defaultVpcId: 'vpc-123',
599
- privateSubnetId1: 'subnet-priv1',
600
- privateSubnetId2: 'subnet-priv2',
601
- };
602
-
603
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
604
-
605
- const vpcPermissions = result.iamStatements.find(stmt =>
606
- stmt.Action.includes('ec2:CreateNetworkInterface')
607
- );
608
-
609
- expect(vpcPermissions).toBeDefined();
610
- expect(vpcPermissions.Action).toContain('ec2:DescribeNetworkInterfaces');
611
- expect(vpcPermissions.Action).toContain('ec2:DeleteNetworkInterface');
612
- });
613
- });
614
-
615
- describe('build() - create-new mode', () => {
616
- it('should create complete VPC infrastructure', async () => {
617
- const appDefinition = {
618
- vpc: {
619
- enable: true,
620
- management: 'create-new',
621
- },
622
- };
623
-
624
- const result = await vpcBuilder.build(appDefinition, {});
625
-
626
- expect(result.resources.FriggVPC).toBeDefined();
627
- expect(result.resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
628
- expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
629
- });
630
-
631
- it('should create private and public subnets', async () => {
632
- const appDefinition = {
633
- vpc: {
634
- enable: true,
635
- management: 'create-new',
636
- },
637
- };
638
-
639
- const result = await vpcBuilder.build(appDefinition, {});
640
-
641
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
642
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
643
- expect(result.resources.FriggPublicSubnet).toBeDefined();
644
- });
645
-
646
- it('should create security group for Lambda functions', async () => {
647
- const appDefinition = {
648
- vpc: {
649
- enable: true,
650
- management: 'create-new',
651
- },
652
- };
653
-
654
- const result = await vpcBuilder.build(appDefinition, {});
655
-
656
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
657
- expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
658
- });
659
-
660
- it('should use CloudFormation references for new resources', async () => {
661
- const appDefinition = {
662
- vpc: {
663
- enable: true,
664
- management: 'create-new',
665
- },
666
- };
667
-
668
- const result = await vpcBuilder.build(appDefinition, {});
669
-
670
- expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
671
- expect(result.vpcConfig.subnetIds).toContainEqual({ Ref: 'FriggPrivateSubnet1' });
672
- expect(result.vpcConfig.subnetIds).toContainEqual({ Ref: 'FriggPrivateSubnet2' });
673
- });
674
-
675
- it('should use custom CIDR block if provided', async () => {
676
- const appDefinition = {
677
- vpc: {
678
- enable: true,
679
- management: 'create-new',
680
- cidrBlock: '192.168.0.0/16',
681
- },
682
- };
683
-
684
- const result = await vpcBuilder.build(appDefinition, {});
685
-
686
- expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('192.168.0.0/16');
687
- });
688
- });
689
-
690
- describe('build() - use-existing mode', () => {
691
- it('should use provided VPC and subnet IDs', async () => {
692
- const appDefinition = {
693
- vpc: {
694
- enable: true,
695
- management: 'use-existing',
696
- vpcId: 'vpc-custom',
697
- subnets: {
698
- ids: ['subnet-a', 'subnet-b'],
699
- },
700
- securityGroupIds: ['sg-custom'],
701
- },
702
- };
703
-
704
- const result = await vpcBuilder.build(appDefinition, {});
705
-
706
- expect(result.vpcConfig.subnetIds).toEqual(['subnet-a', 'subnet-b']);
707
- expect(result.vpcConfig.securityGroupIds).toEqual(['sg-custom']);
708
- });
709
-
710
- it('should not create VPC resources in use-existing mode', async () => {
711
- const appDefinition = {
712
- vpc: {
713
- enable: true,
714
- management: 'use-existing',
715
- vpcId: 'vpc-custom',
716
- subnets: {
717
- ids: ['subnet-a', 'subnet-b'],
718
- },
719
- },
720
- };
721
-
722
- const result = await vpcBuilder.build(appDefinition, {});
723
-
724
- expect(result.resources.FriggVPC).toBeUndefined();
725
- expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
726
- });
727
- });
728
-
729
- describe('getDependencies()', () => {
730
- it('should have no dependencies', () => {
731
- const deps = vpcBuilder.getDependencies();
732
-
733
- expect(deps).toEqual([]);
734
- });
735
- });
736
-
737
- describe('getName()', () => {
738
- it('should return VpcBuilder', () => {
739
- expect(vpcBuilder.getName()).toBe('VpcBuilder');
740
- });
741
- });
742
-
743
- describe('NAT Gateway handling', () => {
744
- it('should create NAT gateway when management is createAndManage', async () => {
745
- const appDefinition = {
746
- vpc: {
747
- enable: true,
748
- management: 'discover',
749
- natGateway: {
750
- management: 'createAndManage',
751
- },
752
- selfHeal: true,
753
- },
754
- };
755
-
756
- const discoveredResources = {
757
- defaultVpcId: 'vpc-123',
758
- publicSubnetId: 'subnet-public',
759
- };
760
-
761
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
762
-
763
- expect(result.resources.FriggNATGateway).toBeDefined();
764
- expect(result.resources.FriggNATGateway.Type).toBe('AWS::EC2::NatGateway');
765
- });
766
-
767
- it('should create route table associations for private subnets with NAT Gateway', async () => {
768
- const appDefinition = {
769
- vpc: {
770
- enable: true,
771
- management: 'discover',
772
- subnets: { management: 'create' },
773
- natGateway: {
774
- management: 'createAndManage',
775
- },
776
- selfHeal: true,
777
- },
778
- };
779
-
780
- const discoveredResources = {
781
- defaultVpcId: 'vpc-123',
782
- publicSubnetId: 'subnet-public',
783
- };
784
-
785
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
786
-
787
- // Verify route table is created
788
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
789
- expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
790
-
791
- // Verify route to NAT Gateway
792
- expect(result.resources.FriggPrivateRoute).toBeDefined();
793
- expect(result.resources.FriggPrivateRoute.Properties.NatGatewayId).toEqual({ Ref: 'FriggNATGateway' });
794
-
795
- // Verify subnet route table associations
796
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
797
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
798
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toEqual({ Ref: 'FriggPrivateSubnet1' });
799
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.RouteTableId).toEqual({ Ref: 'FriggLambdaRouteTable' });
800
-
801
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
802
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
803
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toEqual({ Ref: 'FriggPrivateSubnet2' });
804
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.RouteTableId).toEqual({ Ref: 'FriggLambdaRouteTable' });
805
- });
806
-
807
- it('should add UpdateReplacePolicy to force association recreation on updates', async () => {
808
- const appDefinition = {
809
- vpc: {
810
- enable: true,
811
- management: 'discover',
812
- subnets: { management: 'discover' },
813
- natGateway: { management: 'discover' },
814
- },
815
- };
816
-
817
- const discoveredResources = {
818
- vpcId: 'vpc-123',
819
- privateSubnetId1: 'subnet-existing-1',
820
- privateSubnetId2: 'subnet-existing-2',
821
- natGatewayId: 'nat-existing',
822
- routeTableId: 'rtb-old',
823
- existingLogicalIds: ['FriggSubnet1RouteAssociation', 'FriggSubnet2RouteAssociation'],
824
- };
825
-
826
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
827
-
828
- // Verify associations have UpdateReplacePolicy to force recreation
829
- expect(result.resources.FriggSubnet1RouteAssociation.UpdateReplacePolicy).toBe('Delete');
830
- expect(result.resources.FriggSubnet2RouteAssociation.UpdateReplacePolicy).toBe('Delete');
831
-
832
- // This forces CloudFormation to delete old associations and create new ones
833
- // instead of trying to update them in-place (which doesn't work)
834
- });
835
-
836
- it('should not create NAT when existing NAT is properly placed', async () => {
837
- const appDefinition = {
838
- vpc: {
839
- enable: true,
840
- natGateway: {
841
- management: 'createAndManage',
842
- },
843
- selfHeal: true,
844
- },
845
- };
846
-
847
- const discoveredResources = {
848
- defaultVpcId: 'vpc-123',
849
- publicSubnetId: 'subnet-public',
850
- existingNatGatewayId: 'nat-good',
851
- natGatewayInPrivateSubnet: false,
852
- };
853
-
854
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
855
-
856
- expect(result.resources.FriggNATGateway).toBeUndefined();
857
- });
858
-
859
- it('should create new NAT when existing is in private subnet', async () => {
860
- const appDefinition = {
861
- vpc: {
862
- enable: true,
863
- natGateway: {
864
- management: 'createAndManage',
865
- },
866
- selfHeal: true,
867
- },
868
- };
869
-
870
- const discoveredResources = {
871
- defaultVpcId: 'vpc-123',
872
- publicSubnetId: 'subnet-public',
873
- existingNatGatewayId: 'nat-misplaced',
874
- natGatewayInPrivateSubnet: true, // WRONG placement
875
- };
876
-
877
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
878
-
879
- expect(result.resources.FriggNATGateway).toBeDefined();
880
- });
881
-
882
- it('should create new NAT Gateway when existing is in private subnet', async () => {
883
- const appDefinition = {
884
- vpc: {
885
- enable: true,
886
- natGateway: {
887
- management: 'createAndManage',
888
- },
889
- selfHeal: false,
890
- },
891
- };
892
-
893
- const discoveredResources = {
894
- defaultVpcId: 'vpc-123',
895
- privateSubnetId1: 'subnet-priv1',
896
- privateSubnetId2: 'subnet-priv2',
897
- publicSubnetId: 'subnet-public',
898
- existingNatGatewayId: 'nat-misplaced',
899
- natGatewayInPrivateSubnet: true,
900
- };
901
-
902
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
903
-
904
- // Should create new NAT Gateway instead of using the misplaced one
905
- expect(result.resources.FriggNATGateway).toBeDefined();
906
- expect(result.resources.FriggNATGateway.Type).toBe('AWS::EC2::NatGateway');
907
- });
908
-
909
- it('should reuse existing elastic IP allocation', async () => {
910
- const appDefinition = {
911
- vpc: {
912
- enable: true,
913
- natGateway: {
914
- management: 'createAndManage',
915
- },
916
- selfHeal: true,
917
- },
918
- };
919
-
920
- const discoveredResources = {
921
- defaultVpcId: 'vpc-123',
922
- publicSubnetId: 'subnet-public',
923
- existingElasticIpAllocationId: 'eipalloc-123',
924
- };
925
-
926
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
927
-
928
- if (result.resources.FriggNATGateway) {
929
- // When reusing existing EIP, it should be a CloudFormation reference
930
- expect(result.resources.FriggNATGateway.Properties.AllocationId).toEqual(
931
- { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] }
932
- );
933
- }
934
- });
935
- });
936
-
937
- describe('VPC Endpoints', () => {
938
- it('should create KMS endpoint when KMS encryption is enabled', async () => {
939
- const appDefinition = {
940
- vpc: {
941
- enable: true,
942
- management: 'create-new',
943
- },
944
- encryption: {
945
- fieldLevelEncryptionMethod: 'kms',
946
- },
947
- };
948
-
949
- const discoveredResources = {};
950
-
951
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
952
-
953
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
954
- });
955
-
956
- it('should create Secrets Manager endpoint when enabled', async () => {
957
- const appDefinition = {
958
- vpc: {
959
- enable: true,
960
- management: 'create-new',
961
- },
962
- secretsManager: {
963
- enable: true,
964
- },
965
- };
966
-
967
- const discoveredResources = {};
968
-
969
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
970
-
971
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
972
- });
973
-
974
- it('should not create KMS endpoint when encryption is AES', async () => {
975
- const appDefinition = {
976
- vpc: {
977
- enable: true,
978
- management: 'create-new',
979
- },
980
- encryption: {
981
- fieldLevelEncryptionMethod: 'aes',
982
- },
983
- };
984
-
985
- const discoveredResources = {};
986
-
987
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
988
-
989
- expect(result.resources.FriggKMSVPCEndpoint).toBeUndefined();
990
- });
991
- });
992
-
993
- describe('Self-healing', () => {
994
- it('should create missing subnets when selfHeal is enabled', async () => {
995
- const appDefinition = {
996
- vpc: {
997
- enable: true,
998
- management: 'discover',
999
- selfHeal: true,
1000
- },
1001
- };
1002
-
1003
- const discoveredResources = {
1004
- defaultVpcId: 'vpc-123',
1005
- privateSubnetId1: null,
1006
- privateSubnetId2: null,
1007
- };
1008
-
1009
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1010
-
1011
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1012
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1013
- });
1014
-
1015
- it('should throw error for missing subnets without selfHeal', async () => {
1016
- const appDefinition = {
1017
- vpc: {
1018
- enable: true,
1019
- management: 'discover',
1020
- subnets: { management: 'discover' },
1021
- selfHeal: false,
1022
- },
1023
- };
1024
-
1025
- const discoveredResources = {
1026
- defaultVpcId: 'vpc-123',
1027
- privateSubnetId1: null,
1028
- privateSubnetId2: null,
1029
- };
1030
-
1031
- await expect(vpcBuilder.build(appDefinition, discoveredResources)).rejects.toThrow(
1032
- 'No subnets discovered'
1033
- );
1034
- });
1035
- });
1036
-
1037
- describe('Management Mode (Simplified API)', () => {
1038
- it('should reuse stack VPC when managementMode=managed + vpcIsolation=isolated AND stack has VPC', async () => {
1039
- const appDefinition = {
1040
- managementMode: 'managed',
1041
- vpcIsolation: 'isolated',
1042
- vpc: {
1043
- enable: true,
1044
- management: 'create-new', // Should be IGNORED
1045
- },
1046
- };
1047
-
1048
- // CloudFormation stack has VPC (from previous deployment of this stage)
1049
- const discoveredResources = {
1050
- defaultVpcId: 'vpc-stack-dev', // CloudFormation discovery sets this
1051
- privateSubnetId1: 'subnet-private-1',
1052
- privateSubnetId2: 'subnet-private-2',
1053
- publicSubnetId1: 'subnet-public-1',
1054
- publicSubnetId2: 'subnet-public-2',
1055
- };
1056
-
1057
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
1058
-
1059
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1060
-
1061
- // Should warn about ignored options
1062
- expect(consoleLogSpy).toHaveBeenCalledWith(
1063
- expect.stringContaining("managementMode='managed' ignoring")
1064
- );
1065
-
1066
- // Should log reusing stack VPC
1067
- expect(consoleLogSpy).toHaveBeenCalledWith(
1068
- expect.stringContaining("stack has VPC, reusing")
1069
- );
1070
-
1071
- // Should keep VPC definition in template (CloudFormation idempotency)
1072
- // Even though VPC exists, we include the definition - CF won't recreate it
1073
- expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1074
- expect(result.resources.FriggVPC).toBeDefined();
1075
- expect(result.resources.FriggVPC.Type).toBe('AWS::EC2::VPC');
1076
-
1077
- // Should keep subnet definitions in template and use Refs
1078
- expect(result.vpcConfig.subnetIds).toEqual([
1079
- { Ref: 'FriggPrivateSubnet1' },
1080
- { Ref: 'FriggPrivateSubnet2' }
1081
- ]);
1082
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1083
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1084
-
1085
- consoleLogSpy.mockRestore();
1086
- });
1087
-
1088
- it('should create new VPC when managementMode=managed + vpcIsolation=isolated AND stack has NO VPC', async () => {
1089
- const appDefinition = {
1090
- managementMode: 'managed',
1091
- vpcIsolation: 'isolated',
1092
- vpc: {
1093
- enable: true,
1094
- management: 'discover', // Should be IGNORED
1095
- },
1096
- };
1097
-
1098
- // No VPC in CloudFormation stack (fresh deployment)
1099
- // Default VPC might exist in AWS, but not stack-managed
1100
- const discoveredResources = {
1101
- // No defaultVpcId means no VPC in CloudFormation stack
1102
- };
1103
-
1104
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
1105
-
1106
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1107
-
1108
- // Should warn about ignored options
1109
- expect(consoleLogSpy).toHaveBeenCalledWith(
1110
- expect.stringContaining("managementMode='managed' ignoring")
1111
- );
1112
-
1113
- // Should log creating new VPC
1114
- expect(consoleLogSpy).toHaveBeenCalledWith(
1115
- expect.stringContaining("no stack VPC, creating new")
1116
- );
1117
-
1118
- // Should create new isolated VPC
1119
- expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1120
- expect(result.resources.FriggVPC).toBeDefined();
1121
-
1122
- // Subnets should use CloudFormation Fn::Cidr
1123
- expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
1124
- 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1125
- });
1126
-
1127
- consoleLogSpy.mockRestore();
1128
- });
1129
-
1130
- it('should use managementMode=managed with vpcIsolation=shared to discover VPC', async () => {
1131
- const appDefinition = {
1132
- managementMode: 'managed',
1133
- vpcIsolation: 'shared',
1134
- vpc: {
1135
- enable: true,
1136
- subnets: { management: 'use-existing' }, // Should be IGNORED
1137
- },
1138
- };
1139
-
1140
- const discoveredResources = {
1141
- defaultVpcId: 'vpc-existing',
1142
- };
1143
-
1144
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
1145
-
1146
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1147
-
1148
- // Should warn about ignored options
1149
- expect(consoleLogSpy).toHaveBeenCalledWith(
1150
- expect.stringContaining("ignoring")
1151
- );
1152
-
1153
- // Should discover existing VPC
1154
- expect(result.vpcId).toBe('vpc-existing');
1155
- expect(result.resources.FriggVPC).toBeUndefined();
1156
-
1157
- // Should create new stage-specific subnets
1158
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1159
-
1160
- consoleLogSpy.mockRestore();
1161
- });
1162
-
1163
- it('should default to discover mode for backwards compatibility', async () => {
1164
- const appDefinition = {
1165
- // No managementMode specified
1166
- vpc: {
1167
- enable: true,
1168
- management: 'create-new', // Should be RESPECTED
1169
- },
1170
- };
1171
-
1172
- const result = await vpcBuilder.build(appDefinition, {});
1173
-
1174
- // Should respect legacy vpc.management
1175
- expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1176
- expect(result.resources.FriggVPC).toBeDefined();
1177
- });
1178
- });
1179
-
1180
- describe('VPC Sharing Control', () => {
1181
- it('should share VPC across stages when shareAcrossStages is true (default)', async () => {
1182
- const appDefinition = {
1183
- vpc: {
1184
- enable: true,
1185
- shareAcrossStages: true, // Explicit opt-in to sharing
1186
- },
1187
- };
1188
-
1189
- const discoveredResources = {
1190
- defaultVpcId: 'vpc-shared',
1191
- natGatewayId: 'nat-shared',
1192
- };
1193
-
1194
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1195
-
1196
- // Should use discovered VPC (not create new one)
1197
- expect(result.vpcId).toBe('vpc-shared');
1198
- expect(result.resources.FriggVPC).toBeUndefined();
1199
-
1200
- // Should create stage-specific subnets for isolation
1201
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1202
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1203
-
1204
- // Should reuse discovered NAT Gateway
1205
- expect(result.resources.FriggNATGateway).toBeUndefined();
1206
- });
1207
-
1208
- it('should create isolated VPC when shareAcrossStages is false', async () => {
1209
- const appDefinition = {
1210
- vpc: {
1211
- enable: true,
1212
- shareAcrossStages: false, // Explicit opt-out of sharing
1213
- },
1214
- };
1215
-
1216
- const discoveredResources = {
1217
- defaultVpcId: 'vpc-shared',
1218
- natGatewayId: 'nat-shared',
1219
- };
1220
-
1221
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1222
-
1223
- // Should create new VPC (ignore discovered resources)
1224
- expect(result.vpcId).toEqual({ Ref: 'FriggVPC' });
1225
- expect(result.resources.FriggVPC).toBeDefined();
1226
- expect(result.resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
1227
-
1228
- // Should create stage-specific subnets with Fn::Cidr (dynamic from VPC CIDR)
1229
- expect(result.resources.FriggPrivateSubnet1).toBeDefined();
1230
- expect(result.resources.FriggPrivateSubnet2).toBeDefined();
1231
-
1232
- // Subnets should use CloudFormation Fn::Cidr, NOT hardcoded 172.31.x.x
1233
- expect(result.resources.FriggPrivateSubnet1.Properties.CidrBlock).toEqual({
1234
- 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1235
- });
1236
- expect(result.resources.FriggPrivateSubnet2.Properties.CidrBlock).toEqual({
1237
- 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1238
- });
1239
-
1240
- // Should create new NAT Gateway
1241
- expect(result.resources.FriggNATGateway).toBeDefined();
1242
- });
1243
-
1244
- it('should default to shared VPC when shareAcrossStages is not specified', async () => {
1245
- const appDefinition = {
1246
- vpc: {
1247
- enable: true,
1248
- // shareAcrossStages not specified - should default to true
1249
- },
1250
- };
1251
-
1252
- const discoveredResources = {
1253
- defaultVpcId: 'vpc-discovered',
1254
- };
1255
-
1256
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1257
-
1258
- // Should use discovered VPC by default (backwards compatibility)
1259
- expect(result.vpcId).toBe('vpc-discovered');
1260
- expect(result.resources.FriggVPC).toBeUndefined();
1261
- });
1262
- });
1263
-
1264
- describe('generateSubnetCidrs()', () => {
1265
- it('should use CloudFormation Fn::Cidr for create-new mode', () => {
1266
- const cidrs = vpcBuilder.generateSubnetCidrs('create-new', {});
1267
-
1268
- expect(cidrs.private1).toEqual({
1269
- 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1270
- });
1271
- expect(cidrs.private2).toEqual({
1272
- 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1273
- });
1274
- expect(cidrs.public1).toEqual({
1275
- 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1276
- });
1277
- expect(cidrs.public2).toEqual({
1278
- 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }]
1279
- });
1280
- });
1281
-
1282
- it('should use default static CIDRs when no existing subnets in VPC', () => {
1283
- const discoveredResources = {
1284
- subnets: []
1285
- };
1286
-
1287
- const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1288
-
1289
- expect(cidrs.private1).toBe('172.31.240.0/24');
1290
- expect(cidrs.private2).toBe('172.31.241.0/24');
1291
- expect(cidrs.public1).toBe('172.31.250.0/24');
1292
- expect(cidrs.public2).toBe('172.31.251.0/24');
1293
- });
1294
-
1295
- it('should avoid CIDR conflicts with existing subnets', () => {
1296
- const discoveredResources = {
1297
- subnets: [
1298
- { CidrBlock: '172.31.240.0/24' }, // Conflicts with default private1
1299
- { CidrBlock: '172.31.241.0/24' }, // Conflicts with default private2
1300
- { CidrBlock: '172.31.0.0/20' }, // Default VPC subnet
1301
- { CidrBlock: '172.31.16.0/20' }, // Default VPC subnet
1302
- ]
1303
- };
1304
-
1305
- const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1306
-
1307
- // Should skip 240 and 241 (already taken), use 242-243 for private, 250-251 for public
1308
- expect(cidrs.private1).toBe('172.31.242.0/24');
1309
- expect(cidrs.private2).toBe('172.31.243.0/24');
1310
- expect(cidrs.public1).toBe('172.31.250.0/24'); // Public range starts at 250
1311
- expect(cidrs.public2).toBe('172.31.251.0/24');
1312
- });
1313
-
1314
- it('should find first available CIDR blocks when some in range are taken', () => {
1315
- const discoveredResources = {
1316
- subnets: [
1317
- { CidrBlock: '172.31.240.0/24' },
1318
- { CidrBlock: '172.31.242.0/24' },
1319
- { CidrBlock: '172.31.244.0/24' },
1320
- ]
1321
- };
1322
-
1323
- const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1324
-
1325
- // Should use 241, 243 for private (filling gaps), 250, 251 for public
1326
- expect(cidrs.private1).toBe('172.31.241.0/24');
1327
- expect(cidrs.private2).toBe('172.31.243.0/24');
1328
- expect(cidrs.public1).toBe('172.31.250.0/24'); // Public range starts at 250
1329
- expect(cidrs.public2).toBe('172.31.251.0/24');
1330
- });
1331
-
1332
- it('should handle missing discoveredResources gracefully', () => {
1333
- const cidrs = vpcBuilder.generateSubnetCidrs('discover', null);
1334
-
1335
- // Should fallback to default CIDRs
1336
- expect(cidrs.private1).toBe('172.31.240.0/24');
1337
- expect(cidrs.private2).toBe('172.31.241.0/24');
1338
- });
1339
-
1340
- it('should handle discoveredResources without subnets array', () => {
1341
- const discoveredResources = { vpcId: 'vpc-123' };
1342
-
1343
- const cidrs = vpcBuilder.generateSubnetCidrs('discover', discoveredResources);
1344
-
1345
- // Should fallback to default CIDRs
1346
- expect(cidrs.private1).toBe('172.31.240.0/24');
1347
- expect(cidrs.private2).toBe('172.31.241.0/24');
1348
- });
1349
- });
1350
-
1351
- describe('Outputs', () => {
1352
- it.skip('should generate VPC ID output', async () => {
1353
- const appDefinition = {
1354
- vpc: {
1355
- enable: true,
1356
- management: 'create-new',
1357
- },
1358
- };
1359
-
1360
- const result = await vpcBuilder.build(appDefinition, {});
1361
-
1362
- expect(result.outputs.VpcId).toBeDefined();
1363
- });
1364
-
1365
- it.skip('should generate subnet outputs', async () => {
1366
- const appDefinition = {
1367
- vpc: {
1368
- enable: true,
1369
- management: 'create-new',
1370
- },
1371
- };
1372
-
1373
- const result = await vpcBuilder.build(appDefinition, {});
1374
-
1375
- expect(result.outputs.PrivateSubnet1Id).toBeDefined();
1376
- expect(result.outputs.PrivateSubnet2Id).toBeDefined();
1377
- });
1378
- });
1379
-
1380
- describe('External VPC with stack-managed routing infrastructure pattern', () => {
1381
- it('should correctly handle external VPC with NEW logical IDs (FriggPrivateRoute pattern)', async () => {
1382
- // This pattern occurs when VPC/subnets/NAT are external but routing (route tables,
1383
- // VPC endpoints, security groups) are managed by CloudFormation stack
1384
- // This tests the NEWER naming convention
1385
- const appDefinition = {
1386
- vpc: { enable: true },
1387
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1388
- database: {
1389
- dynamodb: { enable: true } // Enable DynamoDB to create DynamoDB VPC endpoint
1390
- }
1391
- };
1392
-
1393
- // Discovery results from real-world production scenario (newer stack)
1394
- const discoveredResources = {
1395
- fromCloudFormationStack: true,
1396
- stackName: 'test-production-stack',
1397
- existingLogicalIds: [
1398
- 'FriggLambdaSecurityGroup',
1399
- 'FriggLambdaRouteTable',
1400
- 'FriggPrivateRoute', // NEW naming
1401
- 'FriggPrivateSubnet1RouteTableAssociation', // NEW naming
1402
- 'FriggPrivateSubnet2RouteTableAssociation', // NEW naming
1403
- 'FriggS3VPCEndpoint', // NEW naming
1404
- 'FriggDynamoDBVPCEndpoint', // NEW naming
1405
- 'FriggKMSVPCEndpoint' // NEW naming
1406
- ],
1407
- // Stack resources (from CloudFormation)
1408
- lambdaSecurityGroupId: 'sg-01002240c6a446202',
1409
- routeTableId: 'rtb-08af43bbf0775602d',
1410
- s3VpcEndpointId: 'vpce-0d1ecb2c53ce9b4b8',
1411
- dynamodbVpcEndpointId: 'vpce-0fb749b207f1020b0',
1412
- kmsVpcEndpointId: 'vpce-0e38c25155b86de22',
1413
- // External resources (discovered via queries)
1414
- defaultVpcId: 'vpc-0cd17c0e06cb28b28',
1415
- privateSubnetId1: 'subnet-034f6562dbbc16348',
1416
- privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
1417
- existingNatGatewayId: 'nat-022660c36a47e2d79'
1418
- };
1419
-
1420
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1421
-
1422
- // === ASSERTIONS: Template Structure ===
1423
-
1424
- // 1. VPC should be external (not in template)
1425
- expect(result.resources.FriggVPC).toBeUndefined();
1426
- expect(result.vpcId).toBe('vpc-0cd17c0e06cb28b28');
1427
-
1428
- // 2. Security Group MUST be in template (stack-managed)
1429
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
1430
- expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
1431
- expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
1432
-
1433
- // 3. Subnets should be external (use hardcoded IDs, not in template)
1434
- expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
1435
- expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
1436
- expect(result.vpcConfig.subnetIds).toEqual([
1437
- 'subnet-034f6562dbbc16348',
1438
- 'subnet-0b8be2b82aeb5cdec'
1439
- ]);
1440
-
1441
- // 4. NAT Gateway should be external (not in template)
1442
- expect(result.resources.FriggNATGateway).toBeUndefined();
1443
- expect(result.resources.FriggNATGatewayEIP).toBeUndefined();
1444
- expect(result.natGatewayId).toBe('nat-022660c36a47e2d79');
1445
-
1446
- // 5. Route table MUST be in template (stack-managed)
1447
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1448
- expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
1449
-
1450
- // 6. Route table associations MUST be in template
1451
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
1452
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
1453
-
1454
- // 7. VPC Endpoints MUST be in template (stack-managed, prevents deletion)
1455
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
1456
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1457
-
1458
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
1459
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
1460
-
1461
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
1462
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
1463
-
1464
- // 8. VPC Endpoint Security Group needed for interface endpoints
1465
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1466
-
1467
- // === ASSERTIONS: Resource Count ===
1468
- const resourceKeys = Object.keys(result.resources);
1469
- const friggResources = resourceKeys.filter(k => k.startsWith('Frigg') || k.startsWith('VPC'));
1470
-
1471
- // Should have routing infrastructure + endpoints + security groups
1472
- // NOT full VPC (no FriggVPC, FriggPrivateSubnet1/2, FriggNATGateway)
1473
- expect(friggResources).toContain('FriggLambdaSecurityGroup');
1474
- expect(friggResources).toContain('FriggLambdaRouteTable');
1475
- expect(friggResources).toContain('FriggS3VPCEndpoint');
1476
- expect(friggResources).toContain('FriggDynamoDBVPCEndpoint');
1477
- expect(friggResources).toContain('FriggKMSVPCEndpoint');
1478
- expect(friggResources).not.toContain('FriggVPC');
1479
- expect(friggResources).not.toContain('FriggPrivateSubnet1');
1480
- expect(friggResources).not.toContain('FriggNATGateway');
1481
- });
1482
-
1483
- it('should use OLD logical IDs for backwards compatibility (FriggNATRoute, VPCEndpointS3 pattern)', async () => {
1484
- // CRITICAL TEST: Real Frontify production stack uses OLD naming convention
1485
- // Stack currently has: FriggNATRoute, VPCEndpointS3, VPCEndpointDynamoDB
1486
- // We MUST use these same logical IDs to avoid AlreadyExists errors
1487
- const appDefinition = {
1488
- vpc: {
1489
- enable: true,
1490
- ownership: {
1491
- securityGroup: 'external'
1492
- },
1493
- external: {
1494
- securityGroupIds: ['sg-0c5e0d0e4a2f5efcf']
1495
- }
1496
- },
1497
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1498
- database: {
1499
- dynamodb: { enable: true }
1500
- }
1501
- };
1502
-
1503
- // Discovery results matching ACTUAL Frontify production stack
1504
- const discoveredResources = {
1505
- fromCloudFormationStack: true,
1506
- stackName: 'create-frigg-app-production',
1507
- existingLogicalIds: [
1508
- 'FriggLambdaRouteTable',
1509
- 'FriggNATRoute', // OLD naming
1510
- 'FriggSubnet1RouteAssociation', // OLD naming
1511
- 'FriggSubnet2RouteAssociation', // OLD naming
1512
- 'VPCEndpointS3', // OLD naming
1513
- 'VPCEndpointDynamoDB' // OLD naming
1514
- ],
1515
- // Structured discovery (what resolver needs)
1516
- _structured: {
1517
- stackManaged: [
1518
- { logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d', resourceType: 'AWS::EC2::RouteTable' },
1519
- { logicalId: 'FriggNATRoute', physicalId: 'rtb-08af43bbf0775602d|0.0.0.0/0', resourceType: 'AWS::EC2::Route' },
1520
- { logicalId: 'VPCEndpointS3', physicalId: 'vpce-0352ceac2124c14be', resourceType: 'AWS::EC2::VPCEndpoint' },
1521
- { logicalId: 'VPCEndpointDynamoDB', physicalId: 'vpce-0b06c4f631199ea68', resourceType: 'AWS::EC2::VPCEndpoint' }
1522
- ],
1523
- external: [
1524
- { physicalId: 'vpc-01cd124575c683a17', resourceType: 'AWS::EC2::VPC' },
1525
- { physicalId: 'sg-0c5e0d0e4a2f5efcf', resourceType: 'AWS::EC2::SecurityGroup' },
1526
- { physicalId: 'subnet-0bbca02e9981df72c', resourceType: 'AWS::EC2::Subnet' },
1527
- { physicalId: 'subnet-005f7092b91efaaeb', resourceType: 'AWS::EC2::Subnet' },
1528
- { physicalId: 'nat-05a536cbe7056325f', resourceType: 'AWS::EC2::NatGateway' }
1529
- ]
1530
- },
1531
- // Flat discovery (for backwards compatibility)
1532
- routeTableId: 'rtb-08af43bbf0775602d',
1533
- natRoute: 'rtb-08af43bbf0775602d|0.0.0.0/0',
1534
- s3VpcEndpointId: 'vpce-0352ceac2124c14be',
1535
- dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
1536
- // External resources
1537
- defaultVpcId: 'vpc-01cd124575c683a17',
1538
- defaultSecurityGroupId: 'sg-0c5e0d0e4a2f5efcf',
1539
- privateSubnetId1: 'subnet-0bbca02e9981df72c',
1540
- privateSubnetId2: 'subnet-005f7092b91efaaeb',
1541
- existingNatGatewayId: 'nat-05a536cbe7056325f'
1542
- };
1543
-
1544
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1545
-
1546
- // CRITICAL: Must use OLD logical IDs to match existing stack
1547
- expect(result.resources.FriggNATRoute).toBeDefined();
1548
- expect(result.resources.FriggNATRoute.Type).toBe('AWS::EC2::Route');
1549
- expect(result.resources.FriggPrivateRoute).toBeUndefined(); // Should NOT create new ID
1550
-
1551
- expect(result.resources.FriggSubnet1RouteAssociation).toBeDefined();
1552
- expect(result.resources.FriggSubnet2RouteAssociation).toBeDefined();
1553
- expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeUndefined();
1554
- expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeUndefined();
1555
-
1556
- expect(result.resources.VPCEndpointS3).toBeDefined();
1557
- expect(result.resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
1558
- expect(result.resources.FriggS3VPCEndpoint).toBeUndefined(); // Should NOT create new ID
1559
-
1560
- expect(result.resources.VPCEndpointDynamoDB).toBeDefined();
1561
- expect(result.resources.VPCEndpointDynamoDB.Type).toBe('AWS::EC2::VPCEndpoint');
1562
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined(); // Should NOT create new ID
1563
-
1564
- // Route table should still be created
1565
- expect(result.resources.FriggLambdaRouteTable).toBeDefined();
1566
- });
1567
-
1568
- it('should convert OLD logical IDs to structured discovery stackManaged array', () => {
1569
- // TDD test: Verify that VPCEndpointS3 in existingLogicalIds gets added to stackManaged
1570
- const flatDiscovery = {
1571
- fromCloudFormationStack: true,
1572
- stackName: 'create-frigg-app-production',
1573
- existingLogicalIds: [
1574
- 'VPCEndpointS3', // OLD naming
1575
- 'VPCEndpointDynamoDB', // OLD naming
1576
- 'FriggNATRoute' // OLD naming
1577
- ],
1578
- s3VpcEndpointId: 'vpce-0352ceac2124c14be',
1579
- dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
1580
- natRoute: 'rtb-xxx|0.0.0.0/0'
1581
- };
1582
-
1583
- const structured = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1584
-
1585
- // CRITICAL: Old logical IDs should be in stackManaged array
1586
- expect(structured.stackManaged).toContainEqual(
1587
- expect.objectContaining({
1588
- logicalId: 'VPCEndpointS3',
1589
- physicalId: 'vpce-0352ceac2124c14be',
1590
- resourceType: 'AWS::EC2::VPCEndpoint'
1591
- })
1592
- );
1593
- expect(structured.stackManaged).toContainEqual(
1594
- expect.objectContaining({
1595
- logicalId: 'VPCEndpointDynamoDB',
1596
- physicalId: 'vpce-0b06c4f631199ea68',
1597
- resourceType: 'AWS::EC2::VPCEndpoint'
1598
- })
1599
- );
1600
- expect(structured.stackManaged).toContainEqual(
1601
- expect.objectContaining({
1602
- logicalId: 'FriggNATRoute',
1603
- physicalId: 'rtb-xxx|0.0.0.0/0',
1604
- resourceType: 'AWS::EC2::Route'
1605
- })
1606
- );
1607
- });
1608
- });
1609
-
1610
- describe('convertFlatDiscoveryToStructured - Direct Properties', () => {
1611
- it('should copy flat discovery properties to structured discovery for resolver access', () => {
1612
- const flatDiscovery = {
1613
- fromCloudFormationStack: true,
1614
- defaultVpcId: 'vpc-123',
1615
- defaultSecurityGroupId: 'sg-default-456',
1616
- lambdaSecurityGroupId: 'sg-lambda-789',
1617
- privateSubnetId1: 'subnet-1',
1618
- privateSubnetId2: 'subnet-2',
1619
- natGatewayId: 'nat-123'
1620
- };
1621
-
1622
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1623
-
1624
- // Direct properties should be copied for resolver access
1625
- expect(result.defaultVpcId).toBe('vpc-123');
1626
- expect(result.defaultSecurityGroupId).toBe('sg-default-456');
1627
- expect(result.lambdaSecurityGroupId).toBe('sg-lambda-789');
1628
- expect(result.privateSubnetId1).toBe('subnet-1');
1629
- expect(result.privateSubnetId2).toBe('subnet-2');
1630
- expect(result.natGatewayId).toBe('nat-123');
1631
- });
1632
- });
1633
-
1634
- describe('VPC Endpoint Security Group with External Lambda SG', () => {
1635
- it('should use external Lambda SG ID (not Ref) for VPC endpoint SG when Lambda SG is external', async () => {
1636
- const appDefinition = {
1637
- vpc: {
1638
- enable: true,
1639
- enableVPCEndpoints: true,
1640
- ownership: {
1641
- securityGroup: 'external' // External Lambda SG
1642
- }
1643
- },
1644
- encryption: { fieldLevelEncryptionMethod: 'kms' }
1645
- };
1646
- const discoveredResources = {
1647
- fromCloudFormationStack: true,
1648
- defaultVpcId: 'vpc-123',
1649
- defaultSecurityGroupId: 'sg-default-456', // Default VPC SG
1650
- lambdaSecurityGroupId: 'sg-stack-789', // Stack-managed SG (will be ignored)
1651
- privateSubnetId1: 'subnet-1',
1652
- privateSubnetId2: 'subnet-2',
1653
- natGatewayId: 'nat-123',
1654
- existingLogicalIds: ['FriggS3VPCEndpoint', 'FriggKMSVPCEndpoint'],
1655
- s3VpcEndpointId: 'vpce-s3-stack',
1656
- kmsVpcEndpointId: 'vpce-kms-stack'
1657
- };
1658
-
1659
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1660
-
1661
- // VPC Endpoint SG should be created
1662
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1663
-
1664
- // CRITICAL: Should use external Lambda SG ID directly, NOT a CloudFormation Ref
1665
- const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
1666
- expect(ingressRule.SourceSecurityGroupId).toBe('sg-default-456'); // Direct ID, not { Ref: 'FriggLambdaSecurityGroup' }
1667
- expect(typeof ingressRule.SourceSecurityGroupId).toBe('string');
1668
-
1669
- // Verify FriggLambdaSecurityGroup is NOT in the template
1670
- expect(result.resources.FriggLambdaSecurityGroup).toBeUndefined();
1671
- });
1672
-
1673
- it('should use CloudFormation Ref when Lambda SG is stack-managed', async () => {
1674
- const appDefinition = {
1675
- vpc: {
1676
- enable: true,
1677
- enableVPCEndpoints: true,
1678
- ownership: {
1679
- securityGroup: 'stack' // Stack-managed Lambda SG
1680
- }
1681
- },
1682
- encryption: { fieldLevelEncryptionMethod: 'kms' }
1683
- };
1684
- const discoveredResources = {
1685
- defaultVpcId: 'vpc-123',
1686
- privateSubnetId1: 'subnet-1',
1687
- privateSubnetId2: 'subnet-2'
1688
- };
1689
-
1690
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1691
-
1692
- // VPC Endpoint SG should be created
1693
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1694
-
1695
- // Should use CloudFormation Ref when Lambda SG is in stack
1696
- const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
1697
- expect(ingressRule.SourceSecurityGroupId).toEqual({ Ref: 'FriggLambdaSecurityGroup' });
1698
-
1699
- // Verify FriggLambdaSecurityGroup IS in the template
1700
- expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
1701
- });
1702
- });
1703
-
1704
- describe('convertFlatDiscoveryToStructured - VPC Endpoints from CloudFormation', () => {
1705
- it('should add VPC endpoints to stackManaged when in existingLogicalIds', () => {
1706
- const flatDiscovery = {
1707
- fromCloudFormationStack: true,
1708
- stackName: 'test-stack',
1709
- existingLogicalIds: [
1710
- 'FriggS3VPCEndpoint',
1711
- 'FriggDynamoDBVPCEndpoint',
1712
- 'FriggKMSVPCEndpoint'
1713
- ],
1714
- s3VpcEndpointId: 'vpce-s3-stack',
1715
- dynamodbVpcEndpointId: 'vpce-ddb-stack',
1716
- kmsVpcEndpointId: 'vpce-kms-stack'
1717
- };
1718
-
1719
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1720
-
1721
- // VPC endpoints should be in stackManaged (not external)
1722
- expect(result.stackManaged).toContainEqual(
1723
- expect.objectContaining({
1724
- logicalId: 'FriggS3VPCEndpoint',
1725
- physicalId: 'vpce-s3-stack',
1726
- resourceType: 'AWS::EC2::VPCEndpoint'
1727
- })
1728
- );
1729
- expect(result.stackManaged).toContainEqual(
1730
- expect.objectContaining({
1731
- logicalId: 'FriggDynamoDBVPCEndpoint',
1732
- physicalId: 'vpce-ddb-stack',
1733
- resourceType: 'AWS::EC2::VPCEndpoint'
1734
- })
1735
- );
1736
- expect(result.stackManaged).toContainEqual(
1737
- expect.objectContaining({
1738
- logicalId: 'FriggKMSVPCEndpoint',
1739
- physicalId: 'vpce-kms-stack',
1740
- resourceType: 'AWS::EC2::VPCEndpoint'
1741
- })
1742
- );
1743
-
1744
- // Should NOT be in external array
1745
- expect(result.external.some(r => r.physicalId === 'vpce-s3-stack')).toBe(false);
1746
- expect(result.external.some(r => r.physicalId === 'vpce-ddb-stack')).toBe(false);
1747
- expect(result.external.some(r => r.physicalId === 'vpce-kms-stack')).toBe(false);
1748
- });
1749
-
1750
- it('should add VPC endpoints to external when NOT in existingLogicalIds', () => {
1751
- const flatDiscovery = {
1752
- fromCloudFormationStack: false, // AWS API discovery
1753
- s3VpcEndpointId: 'vpce-s3-external',
1754
- dynamodbVpcEndpointId: 'vpce-ddb-external'
1755
- };
1756
-
1757
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1758
-
1759
- // Should be in external (AWS discovery)
1760
- expect(result.external).toContainEqual(
1761
- expect.objectContaining({
1762
- physicalId: 'vpce-s3-external',
1763
- resourceType: 'AWS::EC2::VPCEndpoint',
1764
- source: 'aws-discovery'
1765
- })
1766
- );
1767
-
1768
- // Should NOT be in stackManaged
1769
- expect(result.stackManaged.some(r => r.physicalId === 'vpce-s3-external')).toBe(false);
1770
- });
1771
-
1772
- it('should preserve existing VPC endpoints and only create missing ones', async () => {
1773
- const appDefinition = {
1774
- vpc: { enable: true },
1775
- encryption: { fieldLevelEncryptionMethod: 'kms' },
1776
- };
1777
-
1778
- const discoveredResources = {
1779
- fromCloudFormationStack: true,
1780
- stackName: 'test-stack',
1781
- existingLogicalIds: [
1782
- 'FriggS3VPCEndpoint', // In stack
1783
- 'FriggDynamoDBVPCEndpoint', // In stack
1784
- 'FriggKMSVPCEndpoint' // In stack
1785
- // SecretsManager and SQS NOT in stack (were deleted)
1786
- ],
1787
- defaultVpcId: 'vpc-123',
1788
- privateSubnetId1: 'subnet-1',
1789
- privateSubnetId2: 'subnet-2',
1790
- lambdaSecurityGroupId: 'sg-123',
1791
- routeTableId: 'rtb-123',
1792
- // Endpoints in stack
1793
- s3VpcEndpointId: 'vpce-s3-existing',
1794
- dynamodbVpcEndpointId: 'vpce-ddb-existing',
1795
- kmsVpcEndpointId: 'vpce-kms-existing'
1796
- // secretsManagerVpcEndpointId and sqsVpcEndpointId NOT present
1797
- };
1798
-
1799
- const result = await vpcBuilder.build(appDefinition, discoveredResources);
1800
-
1801
- // Existing endpoints MUST be in template (re-added)
1802
- expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
1803
- expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
1804
-
1805
- expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
1806
- expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcId).toBe('vpc-123');
1807
-
1808
- expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
1809
- expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcId).toBe('vpc-123');
1810
-
1811
- // Missing endpoints should also be created
1812
- expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
1813
- expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
1814
-
1815
- // VPC Endpoint Security Group should be created
1816
- expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
1817
- });
1818
- });
1819
-
1820
- describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
1821
- it('should add VPC from CloudFormation query to external array', () => {
1822
- const flatDiscovery = {
1823
- fromCloudFormationStack: true,
1824
- stackName: 'test-stack',
1825
- existingLogicalIds: ['FriggLambdaRouteTable', 'FriggLambdaSecurityGroup'],
1826
- // VPC ID was extracted from security group query (NOT a stack resource)
1827
- defaultVpcId: 'vpc-extracted-from-sg',
1828
- lambdaSecurityGroupId: 'sg-123',
1829
- routeTableId: 'rtb-123'
1830
- };
1831
-
1832
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1833
-
1834
- // VPC should be in external array (discovered via query, not in stack)
1835
- const vpcExternal = result.external.find(r => r.resourceType === 'AWS::EC2::VPC');
1836
- expect(vpcExternal).toBeDefined();
1837
- expect(vpcExternal.physicalId).toBe('vpc-extracted-from-sg');
1838
- expect(vpcExternal.source).toBe('cloudformation-query');
1839
-
1840
- // Security group SHOULD be in stackManaged (is in stack)
1841
- const sgStack = result.stackManaged.find(r => r.logicalId === 'FriggLambdaSecurityGroup');
1842
- expect(sgStack).toBeDefined();
1843
- expect(sgStack.physicalId).toBe('sg-123');
1844
- });
1845
-
1846
- it('should add subnets from route table associations to external array', () => {
1847
- const flatDiscovery = {
1848
- fromCloudFormationStack: true,
1849
- stackName: 'test-stack',
1850
- existingLogicalIds: ['FriggLambdaRouteTable'],
1851
- routeTableId: 'rtb-123',
1852
- // Subnets extracted from route table associations (NOT stack resources)
1853
- privateSubnetId1: 'subnet-1',
1854
- privateSubnetId2: 'subnet-2'
1855
- };
1856
-
1857
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1858
-
1859
- // Subnets should be in external array
1860
- const subnet1 = result.external.find(r => r.physicalId === 'subnet-1');
1861
- const subnet2 = result.external.find(r => r.physicalId === 'subnet-2');
1862
-
1863
- expect(subnet1).toBeDefined();
1864
- expect(subnet1.resourceType).toBe('AWS::EC2::Subnet');
1865
- expect(subnet1.source).toBe('cloudformation-query');
1866
-
1867
- expect(subnet2).toBeDefined();
1868
- expect(subnet2.resourceType).toBe('AWS::EC2::Subnet');
1869
- expect(subnet2.source).toBe('cloudformation-query');
1870
- });
1871
-
1872
- it('should add NAT Gateway from route table queries to external array', () => {
1873
- const flatDiscovery = {
1874
- fromCloudFormationStack: true,
1875
- stackName: 'test-stack',
1876
- existingLogicalIds: ['FriggLambdaRouteTable', 'FriggPrivateRoute'],
1877
- routeTableId: 'rtb-123',
1878
- // NAT Gateway extracted from route table routes (NOT a stack resource)
1879
- existingNatGatewayId: 'nat-extracted'
1880
- };
1881
-
1882
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1883
-
1884
- // NAT should be in external array
1885
- const natExternal = result.external.find(r => r.resourceType === 'AWS::EC2::NatGateway');
1886
- expect(natExternal).toBeDefined();
1887
- expect(natExternal.physicalId).toBe('nat-extracted');
1888
- expect(natExternal.source).toBe('cloudformation-query');
1889
- });
1890
-
1891
- it('should NOT add resources to external if they are in stack', () => {
1892
- const flatDiscovery = {
1893
- fromCloudFormationStack: true,
1894
- stackName: 'test-stack',
1895
- existingLogicalIds: ['FriggVPC', 'FriggPrivateSubnet1'],
1896
- // These ARE in the stack
1897
- defaultVpcId: 'vpc-in-stack',
1898
- privateSubnetId1: 'subnet-in-stack'
1899
- };
1900
-
1901
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1902
-
1903
- // Should be in stackManaged, NOT external
1904
- expect(result.stackManaged.some(r => r.logicalId === 'FriggVPC')).toBe(true);
1905
- expect(result.stackManaged.some(r => r.logicalId === 'FriggPrivateSubnet1')).toBe(true);
1906
-
1907
- // Should NOT be in external
1908
- expect(result.external.some(r => r.physicalId === 'vpc-in-stack')).toBe(false);
1909
- expect(result.external.some(r => r.physicalId === 'subnet-in-stack')).toBe(false);
1910
- });
1911
-
1912
- it('should handle external VPC pattern: stack resources + queried external references', () => {
1913
- const flatDiscovery = {
1914
- fromCloudFormationStack: true,
1915
- stackName: 'test-production-stack',
1916
- existingLogicalIds: [
1917
- 'FriggLambdaSecurityGroup',
1918
- 'FriggLambdaRouteTable',
1919
- 'FriggPrivateRoute',
1920
- 'FriggPrivateSubnet1RouteTableAssociation',
1921
- 'FriggPrivateSubnet2RouteTableAssociation',
1922
- 'FriggS3VPCEndpoint',
1923
- 'FriggDynamoDBVPCEndpoint',
1924
- 'FriggKMSVPCEndpoint'
1925
- ],
1926
- // Stack resources
1927
- lambdaSecurityGroupId: 'sg-stack-123',
1928
- routeTableId: 'rtb-stack-456',
1929
- s3VpcEndpointId: 'vpce-s3-stack',
1930
- // External resources (discovered via queries)
1931
- defaultVpcId: 'vpc-external-123',
1932
- privateSubnetId1: 'subnet-external-1',
1933
- privateSubnetId2: 'subnet-external-2',
1934
- existingNatGatewayId: 'nat-external-789'
1935
- };
1936
-
1937
- const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
1938
-
1939
- // Stack resources should be in stackManaged
1940
- expect(result.stackManaged).toEqual(
1941
- expect.arrayContaining([
1942
- expect.objectContaining({ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack-123' }),
1943
- expect.objectContaining({ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-stack-456' }),
1944
- expect.objectContaining({ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack' })
1945
- ])
1946
- );
1947
-
1948
- // External resources should be in external array
1949
- expect(result.external).toEqual(
1950
- expect.arrayContaining([
1951
- expect.objectContaining({ physicalId: 'vpc-external-123', resourceType: 'AWS::EC2::VPC', source: 'cloudformation-query' }),
1952
- expect.objectContaining({ physicalId: 'subnet-external-1', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
1953
- expect.objectContaining({ physicalId: 'subnet-external-2', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
1954
- expect.objectContaining({ physicalId: 'nat-external-789', resourceType: 'AWS::EC2::NatGateway', source: 'cloudformation-query' })
1955
- ])
1956
- );
1957
- });
1958
- });
1959
- });
1960
-