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

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 (165) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/CLAUDE.md +481 -0
  3. package/infrastructure/HEALTH.md +468 -0
  4. package/infrastructure/README.md +522 -0
  5. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  6. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  7. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  8. package/infrastructure/__tests__/template-generation.test.js +687 -0
  9. package/infrastructure/create-frigg-infrastructure.js +147 -0
  10. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  11. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  12. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +105 -0
  13. package/infrastructure/docs/deployment-instructions.md +268 -0
  14. package/infrastructure/docs/generate-iam-command.md +278 -0
  15. package/infrastructure/docs/iam-policy-templates.md +193 -0
  16. package/infrastructure/domains/database/aurora-builder.js +809 -0
  17. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  18. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  19. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  20. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  21. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  22. package/infrastructure/domains/database/migration-builder.js +701 -0
  23. package/infrastructure/domains/database/migration-builder.test.js +321 -0
  24. package/infrastructure/domains/database/migration-resolver.js +163 -0
  25. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  26. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  27. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  28. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  29. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  30. package/infrastructure/domains/health/application/ports/index.js +26 -0
  31. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  32. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  33. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  34. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  35. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  36. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  37. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  38. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  39. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  40. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  41. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  42. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  43. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  44. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  45. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  46. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  47. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  48. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  49. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  50. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  51. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  52. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  53. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  54. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  55. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  56. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  57. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  58. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  59. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  60. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  61. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  62. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  63. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  64. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  65. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  66. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  67. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  68. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  69. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  70. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  71. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  72. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  73. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  74. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  75. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  76. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  77. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  78. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  84. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  85. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  86. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  87. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  88. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  89. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  90. package/infrastructure/domains/integration/integration-builder.js +404 -0
  91. package/infrastructure/domains/integration/integration-builder.test.js +690 -0
  92. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  93. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  94. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  95. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  96. package/infrastructure/domains/networking/vpc-builder.js +2051 -0
  97. package/infrastructure/domains/networking/vpc-builder.test.js +1960 -0
  98. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  99. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  100. package/infrastructure/domains/networking/vpc-resolver.js +505 -0
  101. package/infrastructure/domains/networking/vpc-resolver.test.js +801 -0
  102. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  103. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  104. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  105. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  106. package/infrastructure/domains/security/iam-generator.js +816 -0
  107. package/infrastructure/domains/security/iam-generator.test.js +204 -0
  108. package/infrastructure/domains/security/kms-builder.js +415 -0
  109. package/infrastructure/domains/security/kms-builder.test.js +392 -0
  110. package/infrastructure/domains/security/kms-discovery.js +80 -0
  111. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  112. package/infrastructure/domains/security/kms-resolver.js +96 -0
  113. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  114. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +401 -0
  115. package/infrastructure/domains/security/templates/iam-policy-basic.json +218 -0
  116. package/infrastructure/domains/security/templates/iam-policy-full.json +288 -0
  117. package/infrastructure/domains/shared/base-builder.js +112 -0
  118. package/infrastructure/domains/shared/base-resolver.js +186 -0
  119. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  120. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  121. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  122. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  123. package/infrastructure/domains/shared/cloudformation-discovery.js +672 -0
  124. package/infrastructure/domains/shared/cloudformation-discovery.test.js +985 -0
  125. package/infrastructure/domains/shared/environment-builder.js +119 -0
  126. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  127. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +579 -0
  128. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +416 -0
  129. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  130. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  131. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  132. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  133. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  134. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  135. package/infrastructure/domains/shared/resource-discovery.js +233 -0
  136. package/infrastructure/domains/shared/resource-discovery.test.js +588 -0
  137. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  138. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  139. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  140. package/infrastructure/domains/shared/types/index.js +46 -0
  141. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  142. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  143. package/infrastructure/domains/shared/utilities/base-definition-factory.js +408 -0
  144. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  145. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +291 -0
  146. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  147. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  148. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +159 -0
  149. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +444 -0
  150. package/infrastructure/domains/shared/validation/env-validator.js +78 -0
  151. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  152. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  153. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  154. package/infrastructure/esbuild.config.js +53 -0
  155. package/infrastructure/index.js +4 -0
  156. package/infrastructure/infrastructure-composer.js +117 -0
  157. package/infrastructure/infrastructure-composer.test.js +1895 -0
  158. package/infrastructure/integration.test.js +383 -0
  159. package/infrastructure/scripts/build-prisma-layer.js +701 -0
  160. package/infrastructure/scripts/build-prisma-layer.test.js +170 -0
  161. package/infrastructure/scripts/build-time-discovery.js +238 -0
  162. package/infrastructure/scripts/build-time-discovery.test.js +379 -0
  163. package/infrastructure/scripts/run-discovery.js +110 -0
  164. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  165. package/package.json +8 -7
@@ -0,0 +1,985 @@
1
+ /**
2
+ * Tests for CloudFormation-based Resource Discovery
3
+ *
4
+ * Tests discovering resources from existing CloudFormation stacks
5
+ * before falling back to direct AWS API discovery.
6
+ */
7
+
8
+ const { CloudFormationDiscovery } = require('./cloudformation-discovery');
9
+
10
+ describe('CloudFormationDiscovery', () => {
11
+ let cfDiscovery;
12
+ let mockProvider;
13
+
14
+ beforeEach(() => {
15
+ mockProvider = {
16
+ describeStack: jest.fn(),
17
+ listStackResources: jest.fn(),
18
+ };
19
+ cfDiscovery = new CloudFormationDiscovery(mockProvider);
20
+ });
21
+
22
+ describe('discoverFromStack()', () => {
23
+ it('should return null when stack does not exist', async () => {
24
+ mockProvider.describeStack.mockRejectedValue(
25
+ new Error('Stack with id test-stack does not exist')
26
+ );
27
+
28
+ const result = await cfDiscovery.discoverFromStack('test-stack');
29
+
30
+ expect(result).toBeNull();
31
+ expect(mockProvider.describeStack).toHaveBeenCalledWith('test-stack');
32
+ });
33
+
34
+ it('should extract VPC resources from stack outputs', async () => {
35
+ const mockStack = {
36
+ StackName: 'test-stack',
37
+ Outputs: [
38
+ { OutputKey: 'VpcId', OutputValue: 'vpc-123' },
39
+ { OutputKey: 'PrivateSubnetIds', OutputValue: 'subnet-1,subnet-2' },
40
+ { OutputKey: 'PublicSubnetId', OutputValue: 'subnet-3' },
41
+ { OutputKey: 'SecurityGroupId', OutputValue: 'sg-123' },
42
+ ],
43
+ };
44
+
45
+ mockProvider.describeStack.mockResolvedValue(mockStack);
46
+ mockProvider.listStackResources.mockResolvedValue([]);
47
+
48
+ const result = await cfDiscovery.discoverFromStack('test-stack');
49
+
50
+ expect(result).toEqual({
51
+ fromCloudFormationStack: true,
52
+ stackName: 'test-stack',
53
+ defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId', not 'vpcId'
54
+ privateSubnetIds: ['subnet-1', 'subnet-2'],
55
+ publicSubnetId: 'subnet-3',
56
+ securityGroupId: 'sg-123',
57
+ });
58
+ });
59
+
60
+ it('should extract KMS key from stack outputs', async () => {
61
+ const mockStack = {
62
+ StackName: 'test-stack',
63
+ Outputs: [
64
+ { OutputKey: 'KMS_KEY_ARN', OutputValue: 'arn:aws:kms:us-east-1:123456789:key/abc' },
65
+ ],
66
+ };
67
+
68
+ mockProvider.describeStack.mockResolvedValue(mockStack);
69
+ mockProvider.listStackResources.mockResolvedValue([]);
70
+
71
+ const result = await cfDiscovery.discoverFromStack('test-stack');
72
+
73
+ expect(result).toEqual({
74
+ fromCloudFormationStack: true,
75
+ stackName: 'test-stack',
76
+ defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
77
+ });
78
+ });
79
+
80
+ it('should extract VPC subnets from stack resources', async () => {
81
+ const mockStack = { StackName: 'test-stack', Outputs: [] };
82
+ const mockResources = [
83
+ { LogicalResourceId: 'FriggPrivateSubnet1', PhysicalResourceId: 'subnet-priv-1', ResourceType: 'AWS::EC2::Subnet' },
84
+ { LogicalResourceId: 'FriggPrivateSubnet2', PhysicalResourceId: 'subnet-priv-2', ResourceType: 'AWS::EC2::Subnet' },
85
+ { LogicalResourceId: 'FriggPublicSubnet', PhysicalResourceId: 'subnet-pub-1', ResourceType: 'AWS::EC2::Subnet' },
86
+ { LogicalResourceId: 'FriggPublicSubnet2', PhysicalResourceId: 'subnet-pub-2', ResourceType: 'AWS::EC2::Subnet' },
87
+ ];
88
+
89
+ mockProvider.describeStack.mockResolvedValue(mockStack);
90
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
91
+
92
+ const result = await cfDiscovery.discoverFromStack('test-stack');
93
+
94
+ expect(result.privateSubnetId1).toBe('subnet-priv-1');
95
+ expect(result.privateSubnetId2).toBe('subnet-priv-2');
96
+ expect(result.publicSubnetId1).toBe('subnet-pub-1');
97
+ expect(result.publicSubnetId2).toBe('subnet-pub-2');
98
+ });
99
+
100
+ it('should extract route tables and VPC endpoints from stack resources', async () => {
101
+ const mockStack = { StackName: 'test-stack', Outputs: [] };
102
+ const mockResources = [
103
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
104
+ { LogicalResourceId: 'FriggVPCEndpointSecurityGroup', PhysicalResourceId: 'sg-vpce-123', ResourceType: 'AWS::EC2::SecurityGroup' },
105
+ { LogicalResourceId: 'FriggS3VPCEndpoint', PhysicalResourceId: 'vpce-s3-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
106
+ { LogicalResourceId: 'FriggDynamoDBVPCEndpoint', PhysicalResourceId: 'vpce-ddb-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
107
+ { LogicalResourceId: 'FriggKMSVPCEndpoint', PhysicalResourceId: 'vpce-kms-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
108
+ { LogicalResourceId: 'FriggSecretsManagerVPCEndpoint', PhysicalResourceId: 'vpce-sm-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
109
+ ];
110
+
111
+ mockProvider.describeStack.mockResolvedValue(mockStack);
112
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
113
+
114
+ const result = await cfDiscovery.discoverFromStack('test-stack');
115
+
116
+ expect(result.routeTableId).toBe('rtb-123');
117
+ expect(result.vpcEndpointSecurityGroupId).toBe('sg-vpce-123');
118
+ expect(result.s3VpcEndpointId).toBe('vpce-s3-123');
119
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-ddb-123');
120
+ expect(result.kmsVpcEndpointId).toBe('vpce-kms-123');
121
+ expect(result.secretsManagerVpcEndpointId).toBe('vpce-sm-123');
122
+ });
123
+
124
+ it('should extract Aurora cluster from stack resources', async () => {
125
+ const mockStack = {
126
+ StackName: 'test-stack',
127
+ Outputs: [],
128
+ };
129
+
130
+ const mockResources = [
131
+ {
132
+ LogicalResourceId: 'FriggAuroraCluster',
133
+ PhysicalResourceId: 'test-cluster',
134
+ ResourceType: 'AWS::RDS::DBCluster',
135
+ },
136
+ ];
137
+
138
+ mockProvider.describeStack.mockResolvedValue(mockStack);
139
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
140
+
141
+ const result = await cfDiscovery.discoverFromStack('test-stack');
142
+
143
+ expect(result).toEqual({
144
+ fromCloudFormationStack: true,
145
+ stackName: 'test-stack',
146
+ auroraClusterId: 'test-cluster',
147
+ existingLogicalIds: ['FriggAuroraCluster'],
148
+ });
149
+ });
150
+
151
+ it('should extract subnets from stack resources', async () => {
152
+ const mockStack = {
153
+ StackName: 'test-stack',
154
+ Outputs: [],
155
+ };
156
+
157
+ const mockResources = [
158
+ {
159
+ LogicalResourceId: 'FriggPrivateSubnet1',
160
+ PhysicalResourceId: 'subnet-private-1',
161
+ ResourceType: 'AWS::EC2::Subnet',
162
+ },
163
+ {
164
+ LogicalResourceId: 'FriggPrivateSubnet2',
165
+ PhysicalResourceId: 'subnet-private-2',
166
+ ResourceType: 'AWS::EC2::Subnet',
167
+ },
168
+ {
169
+ LogicalResourceId: 'FriggPublicSubnet',
170
+ PhysicalResourceId: 'subnet-public-1',
171
+ ResourceType: 'AWS::EC2::Subnet',
172
+ },
173
+ {
174
+ LogicalResourceId: 'FriggPublicSubnet2',
175
+ PhysicalResourceId: 'subnet-public-2',
176
+ ResourceType: 'AWS::EC2::Subnet',
177
+ },
178
+ ];
179
+
180
+ mockProvider.describeStack.mockResolvedValue(mockStack);
181
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
182
+
183
+ const result = await cfDiscovery.discoverFromStack('test-stack');
184
+
185
+ expect(result.privateSubnetId1).toBe('subnet-private-1');
186
+ expect(result.privateSubnetId2).toBe('subnet-private-2');
187
+ expect(result.publicSubnetId1).toBe('subnet-public-1');
188
+ expect(result.publicSubnetId2).toBe('subnet-public-2');
189
+ });
190
+
191
+ it('should extract S3 migration bucket from stack resources', async () => {
192
+ const mockStack = {
193
+ StackName: 'test-stack',
194
+ Outputs: [],
195
+ };
196
+
197
+ const mockResources = [
198
+ {
199
+ LogicalResourceId: 'FriggMigrationStatusBucket',
200
+ PhysicalResourceId: 'test-migration-bucket',
201
+ ResourceType: 'AWS::S3::Bucket',
202
+ },
203
+ ];
204
+
205
+ mockProvider.describeStack.mockResolvedValue(mockStack);
206
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
207
+
208
+ const result = await cfDiscovery.discoverFromStack('test-stack');
209
+
210
+ expect(result).toEqual({
211
+ fromCloudFormationStack: true,
212
+ stackName: 'test-stack',
213
+ migrationStatusBucket: 'test-migration-bucket',
214
+ existingLogicalIds: ['FriggMigrationStatusBucket'],
215
+ });
216
+ });
217
+
218
+ it('should extract SQS migration queue from stack resources', async () => {
219
+ const mockStack = {
220
+ StackName: 'test-stack',
221
+ Outputs: [],
222
+ };
223
+
224
+ const mockResources = [
225
+ {
226
+ LogicalResourceId: 'DbMigrationQueue',
227
+ PhysicalResourceId: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
228
+ ResourceType: 'AWS::SQS::Queue',
229
+ },
230
+ ];
231
+
232
+ mockProvider.describeStack.mockResolvedValue(mockStack);
233
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
234
+
235
+ const result = await cfDiscovery.discoverFromStack('test-stack');
236
+
237
+ expect(result).toEqual({
238
+ fromCloudFormationStack: true,
239
+ stackName: 'test-stack',
240
+ migrationQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
241
+ existingLogicalIds: ['DbMigrationQueue'],
242
+ });
243
+ });
244
+
245
+ it('should extract NAT Gateway from stack resources', async () => {
246
+ const mockStack = {
247
+ StackName: 'test-stack',
248
+ Outputs: [],
249
+ };
250
+
251
+ const mockResources = [
252
+ {
253
+ LogicalResourceId: 'FriggNatGateway',
254
+ PhysicalResourceId: 'nat-0123456789',
255
+ ResourceType: 'AWS::EC2::NatGateway',
256
+ },
257
+ ];
258
+
259
+ mockProvider.describeStack.mockResolvedValue(mockStack);
260
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
261
+
262
+ const result = await cfDiscovery.discoverFromStack('test-stack');
263
+
264
+ expect(result).toEqual({
265
+ fromCloudFormationStack: true,
266
+ stackName: 'test-stack',
267
+ natGatewayId: 'nat-0123456789',
268
+ existingLogicalIds: ['FriggNatGateway'],
269
+ });
270
+ });
271
+
272
+ it('should extract VPC directly from stack resources', async () => {
273
+ const mockStack = {
274
+ StackName: 'test-stack',
275
+ Outputs: [],
276
+ };
277
+
278
+ const mockResources = [
279
+ {
280
+ LogicalResourceId: 'FriggVPC',
281
+ PhysicalResourceId: 'vpc-037ec55fe87aec1e7',
282
+ ResourceType: 'AWS::EC2::VPC',
283
+ },
284
+ ];
285
+
286
+ mockProvider.describeStack.mockResolvedValue(mockStack);
287
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
288
+
289
+ const result = await cfDiscovery.discoverFromStack('test-stack');
290
+
291
+ expect(result).toEqual({
292
+ fromCloudFormationStack: true,
293
+ stackName: 'test-stack',
294
+ defaultVpcId: 'vpc-037ec55fe87aec1e7',
295
+ existingLogicalIds: ['FriggVPC'],
296
+ });
297
+ });
298
+
299
+ it('should combine outputs and resources correctly', async () => {
300
+ const mockStack = {
301
+ StackName: 'test-stack',
302
+ Outputs: [
303
+ { OutputKey: 'VpcId', OutputValue: 'vpc-123' },
304
+ { OutputKey: 'KMS_KEY_ARN', OutputValue: 'arn:aws:kms:us-east-1:123456789:key/abc' },
305
+ ],
306
+ };
307
+
308
+ const mockResources = [
309
+ {
310
+ LogicalResourceId: 'FriggAuroraCluster',
311
+ PhysicalResourceId: 'test-cluster',
312
+ ResourceType: 'AWS::RDS::DBCluster',
313
+ },
314
+ {
315
+ LogicalResourceId: 'FriggNatGateway',
316
+ PhysicalResourceId: 'nat-123',
317
+ ResourceType: 'AWS::EC2::NatGateway',
318
+ },
319
+ ];
320
+
321
+ mockProvider.describeStack.mockResolvedValue(mockStack);
322
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
323
+
324
+ const result = await cfDiscovery.discoverFromStack('test-stack');
325
+
326
+ expect(result).toEqual({
327
+ fromCloudFormationStack: true,
328
+ stackName: 'test-stack',
329
+ defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId'
330
+ defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
331
+ auroraClusterId: 'test-cluster',
332
+ natGatewayId: 'nat-123',
333
+ existingLogicalIds: ['FriggAuroraCluster', 'FriggNatGateway'],
334
+ });
335
+ });
336
+
337
+ it('should handle stack with no relevant resources', async () => {
338
+ const mockStack = {
339
+ StackName: 'test-stack',
340
+ Outputs: [],
341
+ };
342
+
343
+ mockProvider.describeStack.mockResolvedValue(mockStack);
344
+ mockProvider.listStackResources.mockResolvedValue([
345
+ {
346
+ LogicalResourceId: 'SomeOtherResource',
347
+ PhysicalResourceId: 'some-id',
348
+ ResourceType: 'AWS::Lambda::Function',
349
+ },
350
+ ]);
351
+
352
+ const result = await cfDiscovery.discoverFromStack('test-stack');
353
+
354
+ expect(result).toEqual({
355
+ fromCloudFormationStack: true,
356
+ stackName: 'test-stack',
357
+ });
358
+ });
359
+
360
+ it('should query EC2 for subnets when VPC found but no subnet resources in stack', async () => {
361
+ const mockStack = {
362
+ StackName: 'test-stack',
363
+ Outputs: [],
364
+ };
365
+
366
+ const mockResources = [
367
+ {
368
+ LogicalResourceId: 'FriggLambdaSecurityGroup',
369
+ PhysicalResourceId: 'sg-123',
370
+ ResourceType: 'AWS::EC2::SecurityGroup',
371
+ },
372
+ ];
373
+
374
+ const mockEC2Client = {
375
+ send: jest.fn(),
376
+ };
377
+
378
+ mockProvider.describeStack.mockResolvedValue(mockStack);
379
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
380
+ mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
381
+
382
+ // Mock security group query for VPC ID
383
+ mockEC2Client.send.mockResolvedValueOnce({
384
+ SecurityGroups: [{ VpcId: 'vpc-123' }],
385
+ });
386
+
387
+ // Mock subnet query
388
+ mockEC2Client.send.mockResolvedValueOnce({
389
+ Subnets: [
390
+ {
391
+ SubnetId: 'subnet-private-1',
392
+ MapPublicIpOnLaunch: false,
393
+ Tags: [
394
+ { Key: 'ManagedBy', Value: 'Frigg' },
395
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggPrivateSubnet1' },
396
+ ],
397
+ },
398
+ {
399
+ SubnetId: 'subnet-private-2',
400
+ MapPublicIpOnLaunch: false,
401
+ Tags: [
402
+ { Key: 'ManagedBy', Value: 'Frigg' },
403
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggPrivateSubnet2' },
404
+ ],
405
+ },
406
+ ],
407
+ });
408
+
409
+ const result = await cfDiscovery.discoverFromStack('test-stack');
410
+
411
+ expect(result.privateSubnetId1).toBe('subnet-private-1');
412
+ expect(result.privateSubnetId2).toBe('subnet-private-2');
413
+ expect(mockEC2Client.send).toHaveBeenCalledTimes(2);
414
+ });
415
+
416
+ it('should handle EC2 subnet query errors gracefully', async () => {
417
+ const mockStack = {
418
+ StackName: 'test-stack',
419
+ Outputs: [],
420
+ };
421
+
422
+ const mockResources = [
423
+ {
424
+ LogicalResourceId: 'FriggLambdaSecurityGroup',
425
+ PhysicalResourceId: 'sg-123',
426
+ ResourceType: 'AWS::EC2::SecurityGroup',
427
+ },
428
+ ];
429
+
430
+ const mockEC2Client = {
431
+ send: jest.fn(),
432
+ };
433
+
434
+ mockProvider.describeStack.mockResolvedValue(mockStack);
435
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
436
+ mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
437
+
438
+ // Mock security group query for VPC ID
439
+ mockEC2Client.send.mockResolvedValueOnce({
440
+ SecurityGroups: [{ VpcId: 'vpc-123' }],
441
+ });
442
+
443
+ // Mock subnet query failure
444
+ mockEC2Client.send.mockRejectedValueOnce(new Error('EC2 API Error'));
445
+
446
+ const result = await cfDiscovery.discoverFromStack('test-stack');
447
+
448
+ expect(result.defaultVpcId).toBe('vpc-123');
449
+ expect(result.privateSubnetId1).toBeUndefined();
450
+ });
451
+
452
+ it('should extract KMS key alias from stack resources and query for key ARN', async () => {
453
+ const mockStack = {
454
+ StackName: 'test-stack',
455
+ Outputs: [],
456
+ };
457
+
458
+ const mockResources = [
459
+ {
460
+ LogicalResourceId: 'FriggKMSKeyAlias',
461
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
462
+ ResourceType: 'AWS::KMS::Alias',
463
+ },
464
+ ];
465
+
466
+ mockProvider.describeStack.mockResolvedValue(mockStack);
467
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
468
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
469
+ KeyId: 'abc-123',
470
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
471
+ });
472
+
473
+ const result = await cfDiscovery.discoverFromStack('test-stack');
474
+
475
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
476
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
477
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
478
+ });
479
+
480
+ it('should query AWS API for KMS alias when serviceName and stage are provided', async () => {
481
+ const mockStack = {
482
+ StackName: 'test-stack',
483
+ Outputs: [],
484
+ };
485
+
486
+ const mockResources = [];
487
+
488
+ mockProvider.describeStack.mockResolvedValue(mockStack);
489
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
490
+ mockProvider.region = 'us-east-1';
491
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
492
+ KeyId: 'abc-123',
493
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
494
+ });
495
+
496
+ // Pass serviceName and stage to discover alias
497
+ cfDiscovery.serviceName = 'test-service';
498
+ cfDiscovery.stage = 'dev';
499
+
500
+ const result = await cfDiscovery.discoverFromStack('test-stack');
501
+
502
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
503
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
504
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
505
+ });
506
+
507
+ it('should handle KMS alias not found gracefully', async () => {
508
+ const mockStack = {
509
+ StackName: 'test-stack',
510
+ Outputs: [],
511
+ };
512
+
513
+ const mockResources = [];
514
+
515
+ mockProvider.describeStack.mockResolvedValue(mockStack);
516
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
517
+ mockProvider.region = 'us-east-1';
518
+ mockProvider.describeKmsKey = jest.fn().mockRejectedValue(
519
+ new Error('Alias/test-service-dev-frigg-kms is not found')
520
+ );
521
+
522
+ cfDiscovery.serviceName = 'test-service';
523
+ cfDiscovery.stage = 'dev';
524
+
525
+ const result = await cfDiscovery.discoverFromStack('test-stack');
526
+
527
+ expect(result.defaultKmsKeyId).toBeUndefined();
528
+ expect(result.kmsKeyAlias).toBeUndefined();
529
+ });
530
+
531
+ it('should prefer KMS key from stack resources over alias query', async () => {
532
+ const mockStack = {
533
+ StackName: 'test-stack',
534
+ Outputs: [],
535
+ };
536
+
537
+ const mockResources = [
538
+ {
539
+ LogicalResourceId: 'FriggKMSKey',
540
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
541
+ ResourceType: 'AWS::KMS::Key',
542
+ },
543
+ ];
544
+
545
+ mockProvider.describeStack.mockResolvedValue(mockStack);
546
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
547
+ mockProvider.describeKmsKey = jest.fn();
548
+
549
+ const result = await cfDiscovery.discoverFromStack('test-stack');
550
+
551
+ // Should use the key from stack resources, not query for alias
552
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
553
+ expect(mockProvider.describeKmsKey).not.toHaveBeenCalled();
554
+ });
555
+
556
+ it('should use KMS alias from stack resources even if key is also present', async () => {
557
+ const mockStack = {
558
+ StackName: 'test-stack',
559
+ Outputs: [],
560
+ };
561
+
562
+ const mockResources = [
563
+ {
564
+ LogicalResourceId: 'FriggKMSKey',
565
+ PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
566
+ ResourceType: 'AWS::KMS::Key',
567
+ },
568
+ {
569
+ LogicalResourceId: 'FriggKMSKeyAlias',
570
+ PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
571
+ ResourceType: 'AWS::KMS::Alias',
572
+ },
573
+ ];
574
+
575
+ mockProvider.describeStack.mockResolvedValue(mockStack);
576
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
577
+ mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
578
+ KeyId: 'xyz-789',
579
+ Arn: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
580
+ });
581
+
582
+ const result = await cfDiscovery.discoverFromStack('test-stack');
583
+
584
+ expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
585
+ expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
586
+ expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
587
+ });
588
+ });
589
+
590
+ describe('External VPC with routing infrastructure pattern', () => {
591
+ it('should discover routing resources when VPC is external', async () => {
592
+ // This tests the external VPC pattern: external VPC/subnets/KMS,
593
+ // but stack creates routing infrastructure (route table, NAT route, VPC endpoints)
594
+ const mockStack = {
595
+ StackName: 'create-frigg-app-production',
596
+ Outputs: [],
597
+ };
598
+
599
+ const mockResources = [
600
+ {
601
+ LogicalResourceId: 'FriggLambdaRouteTable',
602
+ PhysicalResourceId: 'rtb-0b83aca77ccde20a6',
603
+ ResourceType: 'AWS::EC2::RouteTable',
604
+ ResourceStatus: 'UPDATE_COMPLETE',
605
+ },
606
+ {
607
+ LogicalResourceId: 'FriggNATRoute',
608
+ PhysicalResourceId: 'rtb-0b83aca77ccde20a6|0.0.0.0/0',
609
+ ResourceType: 'AWS::EC2::Route',
610
+ ResourceStatus: 'UPDATE_COMPLETE',
611
+ },
612
+ {
613
+ LogicalResourceId: 'FriggSubnet1RouteAssociation',
614
+ PhysicalResourceId: 'rtbassoc-07245da0b447ca469',
615
+ ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
616
+ ResourceStatus: 'CREATE_COMPLETE',
617
+ },
618
+ {
619
+ LogicalResourceId: 'FriggSubnet2RouteAssociation',
620
+ PhysicalResourceId: 'rtbassoc-0806f9783c4ea181f',
621
+ ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
622
+ ResourceStatus: 'CREATE_COMPLETE',
623
+ },
624
+ {
625
+ LogicalResourceId: 'VPCEndpointS3',
626
+ PhysicalResourceId: 'vpce-0352ceac2124c14be',
627
+ ResourceType: 'AWS::EC2::VPCEndpoint',
628
+ ResourceStatus: 'CREATE_COMPLETE',
629
+ },
630
+ {
631
+ LogicalResourceId: 'VPCEndpointDynamoDB',
632
+ PhysicalResourceId: 'vpce-0b06c4f631199ea68',
633
+ ResourceType: 'AWS::EC2::VPCEndpoint',
634
+ ResourceStatus: 'CREATE_COMPLETE',
635
+ },
636
+ ];
637
+
638
+ mockProvider.describeStack.mockResolvedValue(mockStack);
639
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
640
+
641
+ const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
642
+
643
+ // Verify routing infrastructure was discovered
644
+ expect(result.routeTableId).toBe('rtb-0b83aca77ccde20a6');
645
+ expect(result.privateRouteTableId).toBe('rtb-0b83aca77ccde20a6');
646
+ expect(result.natRoute).toBe('rtb-0b83aca77ccde20a6|0.0.0.0/0');
647
+ expect(result.routeTableAssociations).toEqual([
648
+ 'rtbassoc-07245da0b447ca469',
649
+ 'rtbassoc-0806f9783c4ea181f',
650
+ ]);
651
+
652
+ // Verify VPC endpoints were discovered (both naming conventions)
653
+ expect(result.vpcEndpoints).toBeDefined();
654
+ expect(result.vpcEndpoints.s3).toBe('vpce-0352ceac2124c14be');
655
+ expect(result.vpcEndpoints.dynamodb).toBe('vpce-0b06c4f631199ea68');
656
+ expect(result.s3VpcEndpointId).toBe('vpce-0352ceac2124c14be');
657
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-0b06c4f631199ea68');
658
+
659
+ // Verify NO VPC/KMS resources (they're external)
660
+ expect(result.defaultVpcId).toBeUndefined();
661
+ expect(result.defaultKmsKeyId).toBeUndefined();
662
+ });
663
+
664
+ it('should work with legacy VPC endpoint naming (FriggS3VPCEndpoint)', async () => {
665
+ const mockStack = {
666
+ StackName: 'test-stack',
667
+ Outputs: [],
668
+ };
669
+
670
+ const mockResources = [
671
+ {
672
+ LogicalResourceId: 'FriggS3VPCEndpoint',
673
+ PhysicalResourceId: 'vpce-legacy-s3',
674
+ ResourceType: 'AWS::EC2::VPCEndpoint',
675
+ ResourceStatus: 'CREATE_COMPLETE',
676
+ },
677
+ {
678
+ LogicalResourceId: 'FriggDynamoDBVPCEndpoint',
679
+ PhysicalResourceId: 'vpce-legacy-ddb',
680
+ ResourceType: 'AWS::EC2::VPCEndpoint',
681
+ ResourceStatus: 'CREATE_COMPLETE',
682
+ },
683
+ ];
684
+
685
+ mockProvider.describeStack.mockResolvedValue(mockStack);
686
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
687
+
688
+ const result = await cfDiscovery.discoverFromStack('test-stack');
689
+
690
+ // Both naming conventions should work
691
+ expect(result.vpcEndpoints.s3).toBe('vpce-legacy-s3');
692
+ expect(result.vpcEndpoints.dynamodb).toBe('vpce-legacy-ddb');
693
+ expect(result.s3VpcEndpointId).toBe('vpce-legacy-s3');
694
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-legacy-ddb');
695
+ });
696
+
697
+ it('should extract FriggLambdaSecurityGroup from stack', async () => {
698
+ const mockStack = {
699
+ StackName: 'test-stack',
700
+ Outputs: [],
701
+ };
702
+
703
+ const mockResources = [
704
+ {
705
+ LogicalResourceId: 'FriggLambdaSecurityGroup',
706
+ PhysicalResourceId: 'sg-01002240c6a446202',
707
+ ResourceType: 'AWS::EC2::SecurityGroup',
708
+ ResourceStatus: 'UPDATE_COMPLETE',
709
+ },
710
+ {
711
+ LogicalResourceId: 'FriggLambdaRouteTable',
712
+ PhysicalResourceId: 'rtb-08af43bbf0775602d',
713
+ ResourceType: 'AWS::EC2::RouteTable',
714
+ ResourceStatus: 'UPDATE_COMPLETE',
715
+ },
716
+ ];
717
+
718
+ mockProvider.describeStack.mockResolvedValue(mockStack);
719
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
720
+
721
+ const result = await cfDiscovery.discoverFromStack('test-stack');
722
+
723
+ // Lambda security group should be extracted
724
+ expect(result.lambdaSecurityGroupId).toBe('sg-01002240c6a446202');
725
+ expect(result.defaultSecurityGroupId).toBe('sg-01002240c6a446202');
726
+ expect(result.existingLogicalIds).toContain('FriggLambdaSecurityGroup');
727
+ });
728
+
729
+ it('should support FriggPrivateRoute naming for NAT routes', async () => {
730
+ const mockStack = {
731
+ StackName: 'test-stack',
732
+ Outputs: [],
733
+ };
734
+
735
+ const mockResources = [
736
+ {
737
+ LogicalResourceId: 'FriggLambdaRouteTable',
738
+ PhysicalResourceId: 'rtb-123',
739
+ ResourceType: 'AWS::EC2::RouteTable',
740
+ ResourceStatus: 'UPDATE_COMPLETE',
741
+ },
742
+ {
743
+ LogicalResourceId: 'FriggPrivateRoute',
744
+ PhysicalResourceId: 'rtb-123|0.0.0.0/0',
745
+ ResourceType: 'AWS::EC2::Route',
746
+ ResourceStatus: 'UPDATE_COMPLETE',
747
+ },
748
+ ];
749
+
750
+ mockProvider.describeStack.mockResolvedValue(mockStack);
751
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
752
+
753
+ const result = await cfDiscovery.discoverFromStack('test-stack');
754
+
755
+ // Both FriggNATRoute and FriggPrivateRoute should be recognized
756
+ expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
757
+ expect(result.routeTableId).toBe('rtb-123');
758
+ });
759
+
760
+ it('should extract external references from route table without stackName error', async () => {
761
+ const mockStack = {
762
+ StackName: 'test-stack',
763
+ Outputs: [],
764
+ };
765
+
766
+ const mockResources = [
767
+ {
768
+ LogicalResourceId: 'FriggLambdaRouteTable',
769
+ PhysicalResourceId: 'rtb-real-id',
770
+ ResourceType: 'AWS::EC2::RouteTable',
771
+ ResourceStatus: 'UPDATE_COMPLETE',
772
+ },
773
+ ];
774
+
775
+ mockProvider.describeStack.mockResolvedValue(mockStack);
776
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
777
+
778
+ // Mock EC2 DescribeRouteTables to return route table with VPC info
779
+ mockProvider.getEC2Client = jest.fn().mockReturnValue({
780
+ send: jest.fn().mockResolvedValue({
781
+ RouteTables: [{
782
+ RouteTableId: 'rtb-real-id',
783
+ VpcId: 'vpc-extracted',
784
+ Routes: [
785
+ { NatGatewayId: 'nat-extracted', DestinationCidrBlock: '0.0.0.0/0' }
786
+ ],
787
+ Associations: [
788
+ { SubnetId: 'subnet-1' },
789
+ { SubnetId: 'subnet-2' }
790
+ ]
791
+ }]
792
+ })
793
+ });
794
+
795
+ const result = await cfDiscovery.discoverFromStack('test-stack');
796
+
797
+ // Should extract VPC, NAT, and subnets from route table
798
+ expect(result.defaultVpcId).toBe('vpc-extracted');
799
+ expect(result.existingNatGatewayId).toBe('nat-extracted');
800
+ expect(result.privateSubnetId1).toBe('subnet-1');
801
+ expect(result.privateSubnetId2).toBe('subnet-2');
802
+
803
+ // Should NOT throw 'stackName is not defined' error
804
+ expect(result).toBeDefined();
805
+ });
806
+ });
807
+
808
+ describe('existingLogicalIds tracking', () => {
809
+ it('should track OLD VPC endpoint logical IDs (VPCEndpointS3 pattern) for backwards compatibility', async () => {
810
+ // CRITICAL: Frontify production uses OLD naming convention
811
+ const mockStack = {
812
+ StackName: 'create-frigg-app-production',
813
+ Outputs: []
814
+ };
815
+
816
+ const mockResources = [
817
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
818
+ { LogicalResourceId: 'FriggNATRoute', PhysicalResourceId: 'rtb-123|0.0.0.0/0', ResourceType: 'AWS::EC2::Route' },
819
+ { LogicalResourceId: 'FriggSubnet1RouteAssociation', PhysicalResourceId: 'rtbassoc-1', ResourceType: 'AWS::EC2::SubnetRouteTableAssociation' },
820
+ { LogicalResourceId: 'FriggSubnet2RouteAssociation', PhysicalResourceId: 'rtbassoc-2', ResourceType: 'AWS::EC2::SubnetRouteTableAssociation' },
821
+ { LogicalResourceId: 'VPCEndpointS3', PhysicalResourceId: 'vpce-s3-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
822
+ { LogicalResourceId: 'VPCEndpointDynamoDB', PhysicalResourceId: 'vpce-ddb-123', ResourceType: 'AWS::EC2::VPCEndpoint' }
823
+ ];
824
+
825
+ mockProvider.describeStack.mockResolvedValue(mockStack);
826
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
827
+
828
+ const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
829
+
830
+ // CRITICAL: existingLogicalIds MUST contain old VPC endpoint names
831
+ expect(result.existingLogicalIds).toBeDefined();
832
+ expect(result.existingLogicalIds).toContain('FriggNATRoute');
833
+ expect(result.existingLogicalIds).toContain('FriggSubnet1RouteAssociation');
834
+ expect(result.existingLogicalIds).toContain('FriggSubnet2RouteAssociation');
835
+ expect(result.existingLogicalIds).toContain('VPCEndpointS3'); // OLD naming
836
+ expect(result.existingLogicalIds).toContain('VPCEndpointDynamoDB'); // OLD naming
837
+
838
+ // Should also have the flat discovery properties
839
+ expect(result.routeTableId).toBe('rtb-123');
840
+ expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
841
+ expect(result.s3VpcEndpointId).toBe('vpce-s3-123');
842
+ expect(result.dynamodbVpcEndpointId).toBe('vpce-ddb-123');
843
+ });
844
+
845
+ it('should track NEW VPC endpoint logical IDs (FriggS3VPCEndpoint pattern) for newer stacks', async () => {
846
+ const mockStack = {
847
+ StackName: 'test-stack',
848
+ Outputs: []
849
+ };
850
+
851
+ const mockResources = [
852
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-456', ResourceType: 'AWS::EC2::RouteTable' },
853
+ { LogicalResourceId: 'FriggPrivateRoute', PhysicalResourceId: 'rtb-456|0.0.0.0/0', ResourceType: 'AWS::EC2::Route' },
854
+ { LogicalResourceId: 'FriggS3VPCEndpoint', PhysicalResourceId: 'vpce-s3-456', ResourceType: 'AWS::EC2::VPCEndpoint' },
855
+ { LogicalResourceId: 'FriggDynamoDBVPCEndpoint', PhysicalResourceId: 'vpce-ddb-456', ResourceType: 'AWS::EC2::VPCEndpoint' },
856
+ { LogicalResourceId: 'FriggKMSVPCEndpoint', PhysicalResourceId: 'vpce-kms-456', ResourceType: 'AWS::EC2::VPCEndpoint' }
857
+ ];
858
+
859
+ mockProvider.describeStack.mockResolvedValue(mockStack);
860
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
861
+
862
+ const result = await cfDiscovery.discoverFromStack('test-stack');
863
+
864
+ // Should track NEW naming pattern in existingLogicalIds
865
+ expect(result.existingLogicalIds).toContain('FriggPrivateRoute');
866
+ expect(result.existingLogicalIds).toContain('FriggS3VPCEndpoint');
867
+ expect(result.existingLogicalIds).toContain('FriggDynamoDBVPCEndpoint');
868
+ expect(result.existingLogicalIds).toContain('FriggKMSVPCEndpoint');
869
+
870
+ // Should NOT contain old naming patterns
871
+ expect(result.existingLogicalIds).not.toContain('FriggNATRoute');
872
+ expect(result.existingLogicalIds).not.toContain('VPCEndpointS3');
873
+ });
874
+ });
875
+
876
+ describe('Subnet extraction from VPC query (OLD reliable approach)', () => {
877
+ it('should extract subnets by querying ALL subnets in VPC then filtering by route table', async () => {
878
+ // Tests the proven method from aws-discovery.js
879
+ // 1. Query ALL subnets in VPC using vpc-id filter (not association filter!)
880
+ // 2. Query route table by ID (RouteTableIds parameter, not Filters!)
881
+ // 3. Extract subnet IDs from route table's Associations array
882
+
883
+ const mockStack = {
884
+ StackName: 'test-stack',
885
+ Outputs: []
886
+ };
887
+
888
+ const mockResources = [
889
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
890
+ { LogicalResourceId: 'FriggVPC', PhysicalResourceId: 'vpc-456', ResourceType: 'AWS::EC2::VPC' }
891
+ ];
892
+
893
+ const sendMock = jest.fn();
894
+ sendMock
895
+ .mockResolvedValueOnce({
896
+ RouteTables: [{
897
+ RouteTableId: 'rtb-123',
898
+ VpcId: 'vpc-456',
899
+ Associations: [],
900
+ Routes: [{ NatGatewayId: 'nat-789', DestinationCidrBlock: '0.0.0.0/0' }]
901
+ }]
902
+ })
903
+ .mockResolvedValueOnce({ SecurityGroups: [{ GroupId: 'sg-default' }] })
904
+ .mockResolvedValueOnce({
905
+ Subnets: [
906
+ { SubnetId: 'subnet-aaa', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1a' },
907
+ { SubnetId: 'subnet-bbb', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1b' },
908
+ { SubnetId: 'subnet-ccc', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1c' }
909
+ ]
910
+ })
911
+ .mockResolvedValueOnce({
912
+ RouteTables: [{
913
+ RouteTableId: 'rtb-123',
914
+ Associations: [
915
+ { RouteTableAssociationId: 'rtbassoc-111', SubnetId: 'subnet-aaa' },
916
+ { RouteTableAssociationId: 'rtbassoc-222', SubnetId: 'subnet-bbb' }
917
+ ]
918
+ }]
919
+ });
920
+
921
+ const mockEC2Client = { send: sendMock };
922
+
923
+ mockProvider.describeStack.mockResolvedValue(mockStack);
924
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
925
+ mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
926
+
927
+ const result = await cfDiscovery.discoverFromStack('test-stack');
928
+
929
+ // Should have extracted subnets using VPC query approach
930
+ expect(result.privateSubnetId1).toBe('subnet-aaa');
931
+ expect(result.privateSubnetId2).toBe('subnet-bbb');
932
+ });
933
+
934
+ it('should handle VPC with only 1 associated subnet (use second as fallback)', async () => {
935
+ const mockStack = {
936
+ StackName: 'test-stack',
937
+ Outputs: []
938
+ };
939
+
940
+ const mockResources = [
941
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
942
+ { LogicalResourceId: 'FriggVPC', PhysicalResourceId: 'vpc-456', ResourceType: 'AWS::EC2::VPC' }
943
+ ];
944
+
945
+ const sendMock = jest.fn();
946
+ sendMock
947
+ .mockResolvedValueOnce({
948
+ RouteTables: [{
949
+ RouteTableId: 'rtb-123',
950
+ VpcId: 'vpc-456',
951
+ Associations: [],
952
+ Routes: [{ NatGatewayId: 'nat-789', DestinationCidrBlock: '0.0.0.0/0' }]
953
+ }]
954
+ })
955
+ .mockResolvedValueOnce({ SecurityGroups: [{ GroupId: 'sg-default' }] })
956
+ .mockResolvedValueOnce({
957
+ Subnets: [
958
+ { SubnetId: 'subnet-aaa', VpcId: 'vpc-456' },
959
+ { SubnetId: 'subnet-bbb', VpcId: 'vpc-456' }
960
+ ]
961
+ })
962
+ .mockResolvedValueOnce({
963
+ RouteTables: [{
964
+ RouteTableId: 'rtb-123',
965
+ Associations: [
966
+ { RouteTableAssociationId: 'rtbassoc-111', SubnetId: 'subnet-aaa' }
967
+ ]
968
+ }]
969
+ });
970
+
971
+ const mockEC2Client = { send: sendMock };
972
+
973
+ mockProvider.describeStack.mockResolvedValue(mockStack);
974
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
975
+ mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
976
+
977
+ const result = await cfDiscovery.discoverFromStack('test-stack');
978
+
979
+ // Should use first from route table, second from fallback
980
+ expect(result.privateSubnetId1).toBe('subnet-aaa');
981
+ expect(result.privateSubnetId2).toBe('subnet-bbb');
982
+ });
983
+ });
984
+ });
985
+