@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +695 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * TDD Test for Orphan Detection with CloudFormation Tags
3
+ *
4
+ * BUG DISCOVERED: Resources can have aws:cloudformation:stack-name tags
5
+ * but NOT actually be in the CloudFormation stack. This happens when:
6
+ * 1. Resources are manually created and tagged with CloudFormation tags
7
+ * 2. Resources are removed from stack but tags remain
8
+ * 3. Resources are imported but import fails/reverts
9
+ *
10
+ * Real-world example from quo-integrations-dev:
11
+ * - 2 VPCs both tagged with aws:cloudformation:stack-name=quo-integrations-dev
12
+ * - CloudFormation stack has 0 VPCs in it (verified via DescribeStackResources)
13
+ * - Both VPCs are orphans despite having CloudFormation tags
14
+ *
15
+ * SOLUTION: Don't trust CloudFormation tags. Instead:
16
+ * 1. Get actual stack resources from CloudFormation (we already have this!)
17
+ * 2. Build Set of physical IDs that are IN the stack
18
+ * 3. Compare discovered resources against this Set
19
+ * 4. If resource has CFN tag but physical ID not in Set = ORPHAN
20
+ */
21
+
22
+ const AWSResourceDetector = require('../aws-resource-detector');
23
+ const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
24
+
25
+ // Mock AWS SDK
26
+ jest.mock('@aws-sdk/client-ec2', () => ({
27
+ EC2Client: jest.fn(),
28
+ DescribeVpcsCommand: jest.fn(),
29
+ DescribeSubnetsCommand: jest.fn(),
30
+ DescribeSecurityGroupsCommand: jest.fn(),
31
+ DescribeRouteTablesCommand: jest.fn(),
32
+ }));
33
+
34
+ jest.mock('@aws-sdk/client-rds', () => ({
35
+ RDSClient: jest.fn(),
36
+ DescribeDBClustersCommand: jest.fn(),
37
+ }));
38
+
39
+ jest.mock('@aws-sdk/client-kms', () => ({
40
+ KMSClient: jest.fn(),
41
+ ListKeysCommand: jest.fn(),
42
+ DescribeKeyCommand: jest.fn(),
43
+ ListAliasesCommand: jest.fn(),
44
+ }));
45
+
46
+ describe('Orphan Detection with CloudFormation-Tagged Resources (TDD)', () => {
47
+ let detector;
48
+ let mockEC2Send;
49
+ let mockRDSSend;
50
+ let mockKMSSend;
51
+
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+
55
+ // Mock EC2 client
56
+ mockEC2Send = jest.fn();
57
+ const { EC2Client } = require('@aws-sdk/client-ec2');
58
+ EC2Client.mockImplementation(() => ({ send: mockEC2Send }));
59
+
60
+ // Mock RDS client - return empty arrays by default
61
+ mockRDSSend = jest.fn().mockResolvedValue({ DBClusters: [] });
62
+ const { RDSClient } = require('@aws-sdk/client-rds');
63
+ RDSClient.mockImplementation(() => ({ send: mockRDSSend }));
64
+
65
+ // Mock KMS client - return empty arrays by default
66
+ mockKMSSend = jest.fn().mockResolvedValue({ Keys: [] });
67
+ const { KMSClient } = require('@aws-sdk/client-kms');
68
+ KMSClient.mockImplementation(() => ({ send: mockKMSSend }));
69
+
70
+ detector = new AWSResourceDetector({ region: 'us-east-1' });
71
+ });
72
+
73
+ describe('Bug: Resources with CloudFormation tags but not in stack', () => {
74
+ test('should detect VPCs with CloudFormation tags that are NOT in the actual stack', async () => {
75
+ // Real-world scenario from quo-integrations-dev
76
+ const stackIdentifier = new StackIdentifier({
77
+ stackName: 'quo-integrations-dev',
78
+ region: 'us-east-1',
79
+ });
80
+
81
+ // CloudFormation stack has 0 VPCs (stack uses existing VPC, doesn't manage it)
82
+ const stackResources = [
83
+ {
84
+ logicalId: 'MyLambda',
85
+ physicalId: 'quo-integrations-dev-lambda',
86
+ resourceType: 'AWS::Lambda::Function',
87
+ },
88
+ // NO VPCs in the stack!
89
+ ];
90
+
91
+ // Mock EC2 returns 2 VPCs both with CloudFormation tags
92
+ mockEC2Send.mockResolvedValue({
93
+ Vpcs: [
94
+ {
95
+ // VPC #1: Has CloudFormation tag for this stack BUT not in stack = ORPHAN
96
+ VpcId: 'vpc-0eadd96976d29ede7',
97
+ CidrBlock: '10.0.0.0/16',
98
+ State: 'available',
99
+ IsDefault: false,
100
+ Tags: [
101
+ {
102
+ Key: 'aws:cloudformation:stack-name',
103
+ Value: 'quo-integrations-dev',
104
+ },
105
+ { Key: 'aws:cloudformation:stack-id', Value: 'arn:aws:cloudformation:...' },
106
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
107
+ { Key: 'Name', Value: 'quo-integrations-dev-vpc' },
108
+ { Key: 'ManagedBy', Value: 'Frigg' },
109
+ ],
110
+ },
111
+ {
112
+ // VPC #2: Has CloudFormation tag for this stack BUT not in stack = ORPHAN
113
+ VpcId: 'vpc-0e2351eac99adcb83',
114
+ CidrBlock: '10.1.0.0/16',
115
+ State: 'available',
116
+ IsDefault: false,
117
+ Tags: [
118
+ {
119
+ Key: 'aws:cloudformation:stack-name',
120
+ Value: 'quo-integrations-dev',
121
+ },
122
+ { Key: 'aws:cloudformation:stack-id', Value: 'arn:aws:cloudformation:...' },
123
+ { Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
124
+ { Key: 'Name', Value: 'quo-integrations-dev-vpc' },
125
+ { Key: 'ManagedBy', Value: 'Frigg' },
126
+ ],
127
+ },
128
+ {
129
+ // VPC #3: From different stack - NOT orphaned
130
+ VpcId: 'vpc-other-stack',
131
+ CidrBlock: '10.2.0.0/16',
132
+ State: 'available',
133
+ Tags: [
134
+ {
135
+ Key: 'aws:cloudformation:stack-name',
136
+ Value: 'quo-integrations-prod',
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ });
142
+
143
+ // Act
144
+ const orphans = await detector.findOrphanedResources({
145
+ stackIdentifier,
146
+ stackResources,
147
+ });
148
+
149
+ // Assert
150
+ // Should find BOTH VPCs as orphans despite having CloudFormation tags
151
+ expect(orphans).toHaveLength(2);
152
+
153
+ // Both orphaned VPCs should be identified
154
+ const orphanIds = orphans.map((o) => o.physicalId).sort();
155
+ expect(orphanIds).toEqual(['vpc-0e2351eac99adcb83', 'vpc-0eadd96976d29ede7']);
156
+
157
+ // Each orphan should have proper reason
158
+ for (const orphan of orphans) {
159
+ expect(orphan.isOrphaned).toBe(true);
160
+ expect(orphan.reason).toContain(
161
+ 'has CloudFormation tag for stack quo-integrations-dev but is not actually managed by the stack'
162
+ );
163
+ }
164
+ });
165
+
166
+ test('should NOT flag resources that are actually in the stack even with CloudFormation tags', async () => {
167
+ const stackIdentifier = new StackIdentifier({
168
+ stackName: 'my-app-prod',
169
+ region: 'us-east-1',
170
+ });
171
+
172
+ // Stack HAS a VPC in CloudFormation
173
+ const stackResources = [
174
+ {
175
+ logicalId: 'MyVPC',
176
+ physicalId: 'vpc-in-stack',
177
+ resourceType: 'AWS::EC2::VPC',
178
+ },
179
+ ];
180
+
181
+ mockEC2Send.mockResolvedValue({
182
+ Vpcs: [
183
+ {
184
+ // VPC is in the stack AND has CloudFormation tag - NOT orphaned
185
+ VpcId: 'vpc-in-stack',
186
+ CidrBlock: '10.0.0.0/16',
187
+ State: 'available',
188
+ Tags: [
189
+ { Key: 'aws:cloudformation:stack-name', Value: 'my-app-prod' },
190
+ ],
191
+ },
192
+ ],
193
+ });
194
+
195
+ const orphans = await detector.findOrphanedResources({
196
+ stackIdentifier,
197
+ stackResources,
198
+ });
199
+
200
+ // Should find NO orphans - VPC is actually in the stack
201
+ expect(orphans).toEqual([]);
202
+ });
203
+
204
+ test('should detect mixed scenario: some resources in stack, some orphaned with same tags', async () => {
205
+ const stackIdentifier = new StackIdentifier({
206
+ stackName: 'test-stack',
207
+ region: 'us-east-1',
208
+ });
209
+
210
+ // Stack has 2 subnets
211
+ const stackResources = [
212
+ {
213
+ logicalId: 'Subnet1',
214
+ physicalId: 'subnet-in-stack-1',
215
+ resourceType: 'AWS::EC2::Subnet',
216
+ },
217
+ {
218
+ logicalId: 'Subnet2',
219
+ physicalId: 'subnet-in-stack-2',
220
+ resourceType: 'AWS::EC2::Subnet',
221
+ },
222
+ ];
223
+
224
+ mockEC2Send.mockResolvedValue({
225
+ Subnets: [
226
+ {
227
+ // Subnet #1: In stack - NOT orphaned
228
+ SubnetId: 'subnet-in-stack-1',
229
+ VpcId: 'vpc-123',
230
+ CidrBlock: '10.0.1.0/24',
231
+ AvailabilityZone: 'us-east-1a',
232
+ State: 'available',
233
+ Tags: [
234
+ { Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
235
+ ],
236
+ },
237
+ {
238
+ // Subnet #2: In stack - NOT orphaned
239
+ SubnetId: 'subnet-in-stack-2',
240
+ VpcId: 'vpc-123',
241
+ CidrBlock: '10.0.2.0/24',
242
+ AvailabilityZone: 'us-east-1b',
243
+ State: 'available',
244
+ Tags: [
245
+ { Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
246
+ ],
247
+ },
248
+ {
249
+ // Subnet #3: Has CloudFormation tag BUT not in stack - IS ORPHANED
250
+ SubnetId: 'subnet-orphan',
251
+ VpcId: 'vpc-123',
252
+ CidrBlock: '10.0.3.0/24',
253
+ AvailabilityZone: 'us-east-1c',
254
+ State: 'available',
255
+ Tags: [
256
+ { Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
257
+ { Key: 'Note', Value: 'Manually created and tagged' },
258
+ ],
259
+ },
260
+ ],
261
+ });
262
+
263
+ const orphans = await detector.findOrphanedResources({
264
+ stackIdentifier,
265
+ stackResources,
266
+ });
267
+
268
+ // Should find 1 orphan (subnet-orphan)
269
+ expect(orphans).toHaveLength(1);
270
+ expect(orphans[0].physicalId).toBe('subnet-orphan');
271
+ });
272
+
273
+ test('should handle stack with no resources of checked types', async () => {
274
+ const stackIdentifier = new StackIdentifier({
275
+ stackName: 'lambda-only-stack',
276
+ region: 'us-east-1',
277
+ });
278
+
279
+ // Stack only has Lambda functions (no VPCs, no subnets)
280
+ const stackResources = [
281
+ {
282
+ logicalId: 'MyFunction',
283
+ physicalId: 'my-function',
284
+ resourceType: 'AWS::Lambda::Function',
285
+ },
286
+ ];
287
+
288
+ // Region has VPCs with CloudFormation tags for this stack
289
+ mockEC2Send.mockResolvedValue({
290
+ Vpcs: [
291
+ {
292
+ VpcId: 'vpc-orphan',
293
+ CidrBlock: '10.0.0.0/16',
294
+ State: 'available',
295
+ Tags: [
296
+ { Key: 'aws:cloudformation:stack-name', Value: 'lambda-only-stack' },
297
+ ],
298
+ },
299
+ ],
300
+ });
301
+
302
+ const orphans = await detector.findOrphanedResources({
303
+ stackIdentifier,
304
+ stackResources,
305
+ });
306
+
307
+ // Should find 1 orphan VPC (stack doesn't manage any VPCs)
308
+ expect(orphans).toHaveLength(1);
309
+ expect(orphans[0].physicalId).toBe('vpc-orphan');
310
+ });
311
+ });
312
+ });
@@ -0,0 +1,367 @@
1
+ /**
2
+ * TDD Test for Orphan Detection with Multiple Stacks
3
+ *
4
+ * Bug: findOrphanedResources marks ALL resources in region as orphaned,
5
+ * including resources from other CloudFormation stacks and default AWS resources.
6
+ *
7
+ * Expected Behavior:
8
+ * - Only detect resources with frigg:stack tag matching target stack
9
+ * - Exclude resources managed by CloudFormation (aws:cloudformation:stack-name tag)
10
+ * - Exclude default AWS resources (default VPC, AWS-managed KMS keys)
11
+ * - Only check resource types that exist in stack template
12
+ */
13
+
14
+ const AWSResourceDetector = require('../aws-resource-detector');
15
+ const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
16
+
17
+ // Mock AWS SDK
18
+ jest.mock('@aws-sdk/client-ec2', () => ({
19
+ EC2Client: jest.fn(),
20
+ DescribeVpcsCommand: jest.fn(),
21
+ DescribeSubnetsCommand: jest.fn(),
22
+ DescribeSecurityGroupsCommand: jest.fn(),
23
+ DescribeRouteTablesCommand: jest.fn(),
24
+ }));
25
+
26
+ jest.mock('@aws-sdk/client-rds', () => ({
27
+ RDSClient: jest.fn(),
28
+ DescribeDBClustersCommand: jest.fn(),
29
+ }));
30
+
31
+ jest.mock('@aws-sdk/client-kms', () => ({
32
+ KMSClient: jest.fn(),
33
+ ListKeysCommand: jest.fn(),
34
+ DescribeKeyCommand: jest.fn(),
35
+ ListAliasesCommand: jest.fn(),
36
+ }));
37
+
38
+ jest.mock('@aws-sdk/client-cloudformation', () => ({
39
+ CloudFormationClient: jest.fn(),
40
+ DescribeStackResourcesCommand: jest.fn(),
41
+ }));
42
+
43
+ describe('Orphan Detection with Multiple Stacks (TDD)', () => {
44
+ let detector;
45
+ let mockEC2Send;
46
+ let mockRDSSend;
47
+ let mockKMSSend;
48
+ let mockCFSend;
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+
53
+ // Mock EC2 client
54
+ mockEC2Send = jest.fn();
55
+ const { EC2Client } = require('@aws-sdk/client-ec2');
56
+ EC2Client.mockImplementation(() => ({ send: mockEC2Send }));
57
+
58
+ // Mock RDS client - return empty arrays by default
59
+ mockRDSSend = jest.fn().mockResolvedValue({ DBClusters: [] });
60
+ const { RDSClient } = require('@aws-sdk/client-rds');
61
+ RDSClient.mockImplementation(() => ({ send: mockRDSSend }));
62
+
63
+ // Mock KMS client - return empty arrays by default
64
+ mockKMSSend = jest.fn().mockResolvedValue({ Keys: [] });
65
+ const { KMSClient } = require('@aws-sdk/client-kms');
66
+ KMSClient.mockImplementation(() => ({ send: mockKMSSend }));
67
+
68
+ // Mock CloudFormation client
69
+ mockCFSend = jest.fn();
70
+ const { CloudFormationClient } = require('@aws-sdk/client-cloudformation');
71
+ CloudFormationClient.mockImplementation(() => ({ send: mockCFSend }));
72
+
73
+ detector = new AWSResourceDetector({ region: 'us-east-1' });
74
+ });
75
+
76
+ describe('Scenario: Multiple stacks and default resources in same region', () => {
77
+ test('should only detect orphans with frigg:stack tag matching target stack', async () => {
78
+ const stackIdentifier = new StackIdentifier({
79
+ stackName: 'quo-integrations-dev',
80
+ region: 'us-east-1',
81
+ });
82
+
83
+ // Stack resources from CloudFormation (what's in the template)
84
+ const stackResources = [
85
+ {
86
+ logicalId: 'MyVPC',
87
+ physicalId: 'vpc-stack-managed',
88
+ resourceType: 'AWS::EC2::VPC',
89
+ },
90
+ {
91
+ logicalId: 'MyDBCluster',
92
+ physicalId: 'dev-cluster',
93
+ resourceType: 'AWS::RDS::DBCluster',
94
+ },
95
+ ];
96
+
97
+ // Mock CloudFormation stack resources
98
+ mockCFSend.mockResolvedValue({
99
+ StackResources: [
100
+ {
101
+ LogicalResourceId: 'MyVPC',
102
+ PhysicalResourceId: 'vpc-stack-managed',
103
+ ResourceType: 'AWS::EC2::VPC',
104
+ },
105
+ {
106
+ LogicalResourceId: 'MyDBCluster',
107
+ PhysicalResourceId: 'dev-cluster',
108
+ ResourceType: 'AWS::RDS::DBCluster',
109
+ },
110
+ ],
111
+ });
112
+
113
+ // Mock EC2 DescribeVpcs - returns 5 VPCs in region
114
+ mockEC2Send.mockResolvedValue({
115
+ Vpcs: [
116
+ {
117
+ // VPC #1: Managed by CloudFormation for this stack - NOT orphaned
118
+ VpcId: 'vpc-stack-managed',
119
+ CidrBlock: '10.0.0.0/16',
120
+ State: 'available',
121
+ Tags: [
122
+ { Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-dev' },
123
+ { Key: 'frigg:stack', Value: 'quo-integrations-dev' },
124
+ ],
125
+ },
126
+ {
127
+ // VPC #2: Has frigg:stack tag but not in CloudFormation - IS ORPHANED
128
+ VpcId: 'vpc-orphan',
129
+ CidrBlock: '10.1.0.0/16',
130
+ State: 'available',
131
+ Tags: [
132
+ { Key: 'frigg:stack', Value: 'quo-integrations-dev' },
133
+ // No aws:cloudformation:stack-name tag = orphan
134
+ ],
135
+ },
136
+ {
137
+ // VPC #3: Managed by DIFFERENT CloudFormation stack - NOT orphaned
138
+ VpcId: 'vpc-other-stack',
139
+ CidrBlock: '10.2.0.0/16',
140
+ State: 'available',
141
+ Tags: [
142
+ { Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-prod' },
143
+ { Key: 'frigg:stack', Value: 'quo-integrations-prod' },
144
+ ],
145
+ },
146
+ {
147
+ // VPC #4: Default VPC (no tags) - NOT orphaned
148
+ VpcId: 'vpc-default',
149
+ CidrBlock: '172.31.0.0/16',
150
+ State: 'available',
151
+ IsDefault: true,
152
+ Tags: [],
153
+ },
154
+ {
155
+ // VPC #5: Random VPC with no frigg tags - NOT orphaned
156
+ VpcId: 'vpc-unrelated',
157
+ CidrBlock: '192.168.0.0/16',
158
+ State: 'available',
159
+ Tags: [{ Key: 'Team', Value: 'platform' }],
160
+ },
161
+ ],
162
+ });
163
+
164
+ // Mock RDS DescribeDBClusters - returns 2 clusters in region
165
+ mockRDSSend.mockResolvedValue({
166
+ DBClusters: [
167
+ {
168
+ // Cluster #1: Managed by CloudFormation for this stack - NOT orphaned
169
+ DBClusterIdentifier: 'dev-cluster',
170
+ Engine: 'aurora-postgresql',
171
+ Status: 'available',
172
+ ClusterCreateTime: new Date('2024-01-01'),
173
+ TagList: [
174
+ { Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-dev' },
175
+ { Key: 'frigg:stack', Value: 'quo-integrations-dev' },
176
+ ],
177
+ },
178
+ {
179
+ // Cluster #2: From different stack - NOT orphaned (wrong frigg:stack tag)
180
+ DBClusterIdentifier: 'prod-cluster',
181
+ Engine: 'aurora-postgresql',
182
+ Status: 'available',
183
+ ClusterCreateTime: new Date('2024-01-01'),
184
+ TagList: [
185
+ { Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-prod' },
186
+ { Key: 'frigg:stack', Value: 'quo-integrations-prod' },
187
+ ],
188
+ },
189
+ ],
190
+ });
191
+
192
+ // Act
193
+ const orphans = await detector.findOrphanedResources({
194
+ stackIdentifier,
195
+ stackResources,
196
+ });
197
+
198
+ // Assert
199
+ // Should ONLY find vpc-orphan (has frigg:stack=quo-integrations-dev but no CloudFormation tag)
200
+ expect(orphans).toHaveLength(1);
201
+ expect(orphans[0].physicalId).toBe('vpc-orphan');
202
+ expect(orphans[0].resourceType).toBe('AWS::EC2::VPC');
203
+
204
+ // Should NOT include:
205
+ // - vpc-stack-managed (managed by CloudFormation)
206
+ // - vpc-other-stack (different stack)
207
+ // - vpc-default (no frigg tag)
208
+ // - vpc-unrelated (no frigg tag)
209
+ // - dev-cluster (managed by CloudFormation)
210
+ // - prod-cluster (different stack)
211
+ });
212
+
213
+ test('should handle case where stack has no orphans', async () => {
214
+ const stackIdentifier = new StackIdentifier({
215
+ stackName: 'perfect-stack',
216
+ region: 'us-east-1',
217
+ });
218
+
219
+ const stackResources = [
220
+ {
221
+ logicalId: 'MyVPC',
222
+ physicalId: 'vpc-perfect',
223
+ resourceType: 'AWS::EC2::VPC',
224
+ },
225
+ ];
226
+
227
+ mockCFSend.mockResolvedValue({
228
+ StackResources: [
229
+ {
230
+ LogicalResourceId: 'MyVPC',
231
+ PhysicalResourceId: 'vpc-perfect',
232
+ ResourceType: 'AWS::EC2::VPC',
233
+ },
234
+ ],
235
+ });
236
+
237
+ // All VPCs are either CloudFormation-managed or belong to other stacks
238
+ mockEC2Send.mockResolvedValue({
239
+ Vpcs: [
240
+ {
241
+ VpcId: 'vpc-perfect',
242
+ CidrBlock: '10.0.0.0/16',
243
+ State: 'available',
244
+ Tags: [
245
+ { Key: 'aws:cloudformation:stack-name', Value: 'perfect-stack' },
246
+ { Key: 'frigg:stack', Value: 'perfect-stack' },
247
+ ],
248
+ },
249
+ {
250
+ VpcId: 'vpc-other',
251
+ CidrBlock: '10.1.0.0/16',
252
+ State: 'available',
253
+ Tags: [{ Key: 'frigg:stack', Value: 'other-stack' }],
254
+ },
255
+ ],
256
+ });
257
+
258
+ const orphans = await detector.findOrphanedResources({
259
+ stackIdentifier,
260
+ stackResources,
261
+ });
262
+
263
+ // Should find NO orphans
264
+ expect(orphans).toEqual([]);
265
+ });
266
+
267
+ test('should check all supported resource types for orphans (not just types in stack)', async () => {
268
+ const stackIdentifier = new StackIdentifier({
269
+ stackName: 'simple-stack',
270
+ region: 'us-east-1',
271
+ });
272
+
273
+ // Stack only has VPC resources - no RDS, no KMS
274
+ const stackResources = [
275
+ {
276
+ logicalId: 'MyVPC',
277
+ physicalId: 'vpc-simple',
278
+ resourceType: 'AWS::EC2::VPC',
279
+ },
280
+ ];
281
+
282
+ mockCFSend.mockResolvedValue({
283
+ StackResources: [
284
+ {
285
+ LogicalResourceId: 'MyVPC',
286
+ PhysicalResourceId: 'vpc-simple',
287
+ ResourceType: 'AWS::EC2::VPC',
288
+ },
289
+ ],
290
+ });
291
+
292
+ mockEC2Send.mockResolvedValue({
293
+ Vpcs: [
294
+ {
295
+ VpcId: 'vpc-simple',
296
+ CidrBlock: '10.0.0.0/16',
297
+ State: 'available',
298
+ Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'simple-stack' }],
299
+ },
300
+ ],
301
+ });
302
+
303
+ await detector.findOrphanedResources({
304
+ stackIdentifier,
305
+ stackResources,
306
+ });
307
+
308
+ // Should check ALL resource types (EC2, RDS, KMS) even if stack only has VPC
309
+ // This is because orphaned resources by definition are NOT in the stack
310
+ expect(mockEC2Send).toHaveBeenCalled();
311
+ expect(mockRDSSend).toHaveBeenCalled(); // Changed: now checks all types
312
+ expect(mockKMSSend).toHaveBeenCalled(); // Changed: now checks all types
313
+ });
314
+
315
+ test('should filter out default VPCs', async () => {
316
+ const stackIdentifier = new StackIdentifier({
317
+ stackName: 'my-stack',
318
+ region: 'us-east-1',
319
+ });
320
+
321
+ const stackResources = [
322
+ {
323
+ logicalId: 'MyVPC',
324
+ physicalId: 'vpc-custom',
325
+ resourceType: 'AWS::EC2::VPC',
326
+ },
327
+ ];
328
+
329
+ mockCFSend.mockResolvedValue({
330
+ StackResources: [
331
+ {
332
+ LogicalResourceId: 'MyVPC',
333
+ PhysicalResourceId: 'vpc-custom',
334
+ ResourceType: 'AWS::EC2::VPC',
335
+ },
336
+ ],
337
+ });
338
+
339
+ mockEC2Send.mockResolvedValue({
340
+ Vpcs: [
341
+ {
342
+ VpcId: 'vpc-custom',
343
+ CidrBlock: '10.0.0.0/16',
344
+ State: 'available',
345
+ Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'my-stack' }],
346
+ },
347
+ {
348
+ // Default VPC - should be filtered out even with frigg tag
349
+ VpcId: 'vpc-default',
350
+ CidrBlock: '172.31.0.0/16',
351
+ State: 'available',
352
+ IsDefault: true,
353
+ Tags: [{ Key: 'frigg:stack', Value: 'my-stack' }],
354
+ },
355
+ ],
356
+ });
357
+
358
+ const orphans = await detector.findOrphanedResources({
359
+ stackIdentifier,
360
+ stackResources,
361
+ });
362
+
363
+ // Should NOT include default VPC
364
+ expect(orphans).toEqual([]);
365
+ });
366
+ });
367
+ });