@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +633 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportProgressMonitor Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for monitoring CloudFormation import operation progress
|
|
5
|
+
* Domain Layer - Service Tests
|
|
6
|
+
*
|
|
7
|
+
* Tests the import progress monitoring functionality that:
|
|
8
|
+
* - Polls CloudFormation stack events during import
|
|
9
|
+
* - Tracks progress per resource
|
|
10
|
+
* - Detects failures and errors
|
|
11
|
+
* - Handles timeout scenarios
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { ImportProgressMonitor } = require('../import-progress-monitor');
|
|
15
|
+
|
|
16
|
+
describe('ImportProgressMonitor', () => {
|
|
17
|
+
let monitor;
|
|
18
|
+
let mockCloudFormationRepository;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Enable fake timers for testing time-based polling
|
|
22
|
+
jest.useFakeTimers();
|
|
23
|
+
|
|
24
|
+
// Mock CloudFormation repository
|
|
25
|
+
mockCloudFormationRepository = {
|
|
26
|
+
getStackEvents: jest.fn(),
|
|
27
|
+
getStackStatus: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
monitor = new ImportProgressMonitor({
|
|
31
|
+
cloudFormationRepository: mockCloudFormationRepository,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
// Restore real timers
|
|
37
|
+
jest.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('monitorImport', () => {
|
|
41
|
+
it('should monitor successful import and track progress for all resources', async () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const stackIdentifier = {
|
|
44
|
+
stackName: 'test-stack',
|
|
45
|
+
region: 'us-east-1',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const resourceLogicalIds = ['FriggVPC', 'FriggPrivateSubnet1', 'FriggLambdaSecurityGroup'];
|
|
49
|
+
|
|
50
|
+
const progressUpdates = [];
|
|
51
|
+
const onProgress = jest.fn((progress) => {
|
|
52
|
+
progressUpdates.push(progress);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Mock stack events - simulate import progress over time
|
|
56
|
+
let eventCallCount = 0;
|
|
57
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
58
|
+
eventCallCount++;
|
|
59
|
+
|
|
60
|
+
// First poll: VPC import in progress
|
|
61
|
+
if (eventCallCount === 1) {
|
|
62
|
+
return Promise.resolve([
|
|
63
|
+
{
|
|
64
|
+
LogicalResourceId: 'FriggVPC',
|
|
65
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
66
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Second poll: VPC complete, Subnet in progress
|
|
72
|
+
if (eventCallCount === 2) {
|
|
73
|
+
return Promise.resolve([
|
|
74
|
+
{
|
|
75
|
+
LogicalResourceId: 'FriggVPC',
|
|
76
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
77
|
+
Timestamp: new Date('2025-10-27T10:00:05Z'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
81
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
82
|
+
Timestamp: new Date('2025-10-27T10:00:06Z'),
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Third poll: Subnet complete, SecurityGroup in progress
|
|
88
|
+
if (eventCallCount === 3) {
|
|
89
|
+
return Promise.resolve([
|
|
90
|
+
{
|
|
91
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
92
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
93
|
+
Timestamp: new Date('2025-10-27T10:00:10Z'),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
97
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
98
|
+
Timestamp: new Date('2025-10-27T10:00:11Z'),
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fourth poll: All complete
|
|
104
|
+
if (eventCallCount === 4) {
|
|
105
|
+
return Promise.resolve([
|
|
106
|
+
{
|
|
107
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
108
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
109
|
+
Timestamp: new Date('2025-10-27T10:00:15Z'),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Promise.resolve([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const resultPromise = monitor.monitorImport({
|
|
121
|
+
stackIdentifier,
|
|
122
|
+
resourceLogicalIds,
|
|
123
|
+
onProgress,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Advance timers to trigger polling (4 polls * 2 seconds each)
|
|
127
|
+
for (let i = 0; i < 4; i++) {
|
|
128
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = await resultPromise;
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
expect(result.success).toBe(true);
|
|
135
|
+
expect(result.importedCount).toBe(3);
|
|
136
|
+
expect(result.failedCount).toBe(0);
|
|
137
|
+
expect(result.failedResources).toEqual([]);
|
|
138
|
+
|
|
139
|
+
// Verify progress callbacks were called correctly
|
|
140
|
+
expect(onProgress).toHaveBeenCalled();
|
|
141
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
142
|
+
|
|
143
|
+
// Check that all resources were tracked
|
|
144
|
+
const completedResources = progressUpdates.filter((p) => p.status === 'COMPLETE');
|
|
145
|
+
expect(completedResources).toHaveLength(3);
|
|
146
|
+
expect(completedResources.map((p) => p.logicalId)).toEqual(
|
|
147
|
+
expect.arrayContaining(['FriggVPC', 'FriggPrivateSubnet1', 'FriggLambdaSecurityGroup'])
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should track progress with correct progress counts', async () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const stackIdentifier = {
|
|
154
|
+
stackName: 'test-stack',
|
|
155
|
+
region: 'us-east-1',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const resourceLogicalIds = ['Resource1', 'Resource2'];
|
|
159
|
+
|
|
160
|
+
const progressUpdates = [];
|
|
161
|
+
const onProgress = jest.fn((progress) => {
|
|
162
|
+
progressUpdates.push(progress);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
let eventCallCount = 0;
|
|
166
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
167
|
+
eventCallCount++;
|
|
168
|
+
|
|
169
|
+
if (eventCallCount === 1) {
|
|
170
|
+
return Promise.resolve([
|
|
171
|
+
{
|
|
172
|
+
LogicalResourceId: 'Resource1',
|
|
173
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
174
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (eventCallCount === 2) {
|
|
180
|
+
return Promise.resolve([
|
|
181
|
+
{
|
|
182
|
+
LogicalResourceId: 'Resource2',
|
|
183
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
184
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
185
|
+
},
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Promise.resolve([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
193
|
+
|
|
194
|
+
// Act
|
|
195
|
+
const resultPromise = monitor.monitorImport({
|
|
196
|
+
stackIdentifier,
|
|
197
|
+
resourceLogicalIds,
|
|
198
|
+
onProgress,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Advance timers
|
|
202
|
+
for (let i = 0; i < 2; i++) {
|
|
203
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await resultPromise;
|
|
207
|
+
|
|
208
|
+
// Assert - check progress counts
|
|
209
|
+
const resource1Progress = progressUpdates.find(
|
|
210
|
+
(p) => p.logicalId === 'Resource1' && p.status === 'COMPLETE'
|
|
211
|
+
);
|
|
212
|
+
expect(resource1Progress).toEqual({
|
|
213
|
+
logicalId: 'Resource1',
|
|
214
|
+
status: 'COMPLETE',
|
|
215
|
+
progress: 1,
|
|
216
|
+
total: 2,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const resource2Progress = progressUpdates.find(
|
|
220
|
+
(p) => p.logicalId === 'Resource2' && p.status === 'COMPLETE'
|
|
221
|
+
);
|
|
222
|
+
expect(resource2Progress).toEqual({
|
|
223
|
+
logicalId: 'Resource2',
|
|
224
|
+
status: 'COMPLETE',
|
|
225
|
+
progress: 2,
|
|
226
|
+
total: 2,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should detect IMPORT_FAILED events and collect reasons', async () => {
|
|
231
|
+
// Arrange
|
|
232
|
+
const stackIdentifier = {
|
|
233
|
+
stackName: 'test-stack',
|
|
234
|
+
region: 'us-east-1',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const resourceLogicalIds = ['FriggVPC', 'FriggPrivateSubnet1'];
|
|
238
|
+
|
|
239
|
+
const progressUpdates = [];
|
|
240
|
+
const onProgress = jest.fn((progress) => {
|
|
241
|
+
progressUpdates.push(progress);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let eventCallCount = 0;
|
|
245
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
246
|
+
eventCallCount++;
|
|
247
|
+
|
|
248
|
+
// First poll: VPC succeeds
|
|
249
|
+
if (eventCallCount === 1) {
|
|
250
|
+
return Promise.resolve([
|
|
251
|
+
{
|
|
252
|
+
LogicalResourceId: 'FriggVPC',
|
|
253
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
254
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Second poll: Subnet fails
|
|
260
|
+
if (eventCallCount === 2) {
|
|
261
|
+
return Promise.resolve([
|
|
262
|
+
{
|
|
263
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
264
|
+
ResourceStatus: 'IMPORT_FAILED',
|
|
265
|
+
ResourceStatusReason: 'Resource does not match template properties',
|
|
266
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
267
|
+
},
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return Promise.resolve([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_ROLLBACK_COMPLETE');
|
|
275
|
+
|
|
276
|
+
// Act
|
|
277
|
+
const resultPromise = monitor.monitorImport({
|
|
278
|
+
stackIdentifier,
|
|
279
|
+
resourceLogicalIds,
|
|
280
|
+
onProgress,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Advance timers
|
|
284
|
+
for (let i = 0; i < 2; i++) {
|
|
285
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const result = await resultPromise;
|
|
289
|
+
|
|
290
|
+
// Assert
|
|
291
|
+
expect(result.success).toBe(false);
|
|
292
|
+
expect(result.importedCount).toBe(1);
|
|
293
|
+
expect(result.failedCount).toBe(1);
|
|
294
|
+
expect(result.failedResources).toEqual([
|
|
295
|
+
{
|
|
296
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
297
|
+
reason: 'Resource does not match template properties',
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// Verify failure callback
|
|
302
|
+
const failureUpdate = progressUpdates.find(
|
|
303
|
+
(p) => p.logicalId === 'FriggPrivateSubnet1' && p.status === 'FAILED'
|
|
304
|
+
);
|
|
305
|
+
expect(failureUpdate).toEqual({
|
|
306
|
+
logicalId: 'FriggPrivateSubnet1',
|
|
307
|
+
status: 'FAILED',
|
|
308
|
+
reason: 'Resource does not match template properties',
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should detect stack rollback (IMPORT_ROLLBACK_IN_PROGRESS)', async () => {
|
|
313
|
+
// Arrange
|
|
314
|
+
const stackIdentifier = {
|
|
315
|
+
stackName: 'test-stack',
|
|
316
|
+
region: 'us-east-1',
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
320
|
+
|
|
321
|
+
const onProgress = jest.fn();
|
|
322
|
+
|
|
323
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
324
|
+
{
|
|
325
|
+
LogicalResourceId: 'FriggVPC',
|
|
326
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
327
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_ROLLBACK_IN_PROGRESS');
|
|
332
|
+
|
|
333
|
+
// Act - start monitoring (don't await yet)
|
|
334
|
+
const resultPromise = monitor.monitorImport({
|
|
335
|
+
stackIdentifier,
|
|
336
|
+
resourceLogicalIds,
|
|
337
|
+
onProgress,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Advance timer to trigger first poll (2 seconds)
|
|
341
|
+
// This will call getStackEvents, then getStackStatus which returns IMPORT_ROLLBACK_IN_PROGRESS
|
|
342
|
+
jest.advanceTimersByTime(2000);
|
|
343
|
+
|
|
344
|
+
// Wait for promises to settle
|
|
345
|
+
await Promise.resolve();
|
|
346
|
+
await Promise.resolve();
|
|
347
|
+
|
|
348
|
+
// Assert - expect rejection after rollback status detected
|
|
349
|
+
await expect(resultPromise).rejects.toThrow('Import operation failed and rolled back');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should detect stack rollback (IMPORT_ROLLBACK_COMPLETE)', async () => {
|
|
353
|
+
// Arrange
|
|
354
|
+
const stackIdentifier = {
|
|
355
|
+
stackName: 'test-stack',
|
|
356
|
+
region: 'us-east-1',
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
360
|
+
|
|
361
|
+
const onProgress = jest.fn();
|
|
362
|
+
|
|
363
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
364
|
+
{
|
|
365
|
+
LogicalResourceId: 'FriggVPC',
|
|
366
|
+
ResourceStatus: 'IMPORT_FAILED',
|
|
367
|
+
ResourceStatusReason: 'Template mismatch',
|
|
368
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_ROLLBACK_COMPLETE');
|
|
373
|
+
|
|
374
|
+
// Act - start monitoring (don't await yet)
|
|
375
|
+
const resultPromise = monitor.monitorImport({
|
|
376
|
+
stackIdentifier,
|
|
377
|
+
resourceLogicalIds,
|
|
378
|
+
onProgress,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Advance timer to trigger first poll (2 seconds)
|
|
382
|
+
// This will process IMPORT_FAILED event, exit loop, then check final status
|
|
383
|
+
jest.advanceTimersByTime(2000);
|
|
384
|
+
|
|
385
|
+
// Wait for promises to settle
|
|
386
|
+
await Promise.resolve();
|
|
387
|
+
await Promise.resolve();
|
|
388
|
+
|
|
389
|
+
// Assert - expect rejection (complete failure: 0 imported, 1 failed)
|
|
390
|
+
await expect(resultPromise).rejects.toThrow('Import operation failed and rolled back');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should timeout after 5 minutes', async () => {
|
|
394
|
+
// Arrange
|
|
395
|
+
const stackIdentifier = {
|
|
396
|
+
stackName: 'test-stack',
|
|
397
|
+
region: 'us-east-1',
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
401
|
+
|
|
402
|
+
const onProgress = jest.fn();
|
|
403
|
+
|
|
404
|
+
// Mock events that never complete
|
|
405
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
406
|
+
{
|
|
407
|
+
LogicalResourceId: 'FriggVPC',
|
|
408
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
409
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_IN_PROGRESS');
|
|
414
|
+
|
|
415
|
+
// Act & Assert - wrap in expect().rejects to properly handle async rejection
|
|
416
|
+
await expect(
|
|
417
|
+
(async () => {
|
|
418
|
+
const resultPromise = monitor.monitorImport({
|
|
419
|
+
stackIdentifier,
|
|
420
|
+
resourceLogicalIds,
|
|
421
|
+
onProgress,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Advance timers beyond 5 minutes (300,000ms) in chunks
|
|
425
|
+
// Simulate 151 polls (302 seconds = 302,000ms) to exceed timeout
|
|
426
|
+
for (let i = 0; i < 151; i++) {
|
|
427
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return await resultPromise;
|
|
431
|
+
})()
|
|
432
|
+
).rejects.toThrow('Import operation timed out');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should filter events by resourceLogicalIds', async () => {
|
|
436
|
+
// Arrange
|
|
437
|
+
const stackIdentifier = {
|
|
438
|
+
stackName: 'test-stack',
|
|
439
|
+
region: 'us-east-1',
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const resourceLogicalIds = ['FriggVPC', 'FriggPrivateSubnet1'];
|
|
443
|
+
|
|
444
|
+
const progressUpdates = [];
|
|
445
|
+
const onProgress = jest.fn((progress) => {
|
|
446
|
+
progressUpdates.push(progress);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
let eventCallCount = 0;
|
|
450
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
451
|
+
eventCallCount++;
|
|
452
|
+
|
|
453
|
+
if (eventCallCount === 1) {
|
|
454
|
+
return Promise.resolve([
|
|
455
|
+
{
|
|
456
|
+
LogicalResourceId: 'FriggVPC',
|
|
457
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
458
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
LogicalResourceId: 'OtherResource', // Not in resourceLogicalIds
|
|
462
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
463
|
+
Timestamp: new Date('2025-10-27T10:00:02Z'),
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
467
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
468
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
469
|
+
},
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return Promise.resolve([]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
477
|
+
|
|
478
|
+
// Act
|
|
479
|
+
const resultPromise = monitor.monitorImport({
|
|
480
|
+
stackIdentifier,
|
|
481
|
+
resourceLogicalIds,
|
|
482
|
+
onProgress,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Advance timer
|
|
486
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
487
|
+
|
|
488
|
+
const result = await resultPromise;
|
|
489
|
+
|
|
490
|
+
// Assert
|
|
491
|
+
expect(result.importedCount).toBe(2);
|
|
492
|
+
expect(progressUpdates.every((p) => resourceLogicalIds.includes(p.logicalId))).toBe(true);
|
|
493
|
+
expect(progressUpdates.some((p) => p.logicalId === 'OtherResource')).toBe(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should sort events by timestamp (oldest first)', async () => {
|
|
497
|
+
// Arrange
|
|
498
|
+
const stackIdentifier = {
|
|
499
|
+
stackName: 'test-stack',
|
|
500
|
+
region: 'us-east-1',
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const resourceLogicalIds = ['Resource1', 'Resource2', 'Resource3'];
|
|
504
|
+
|
|
505
|
+
const progressUpdates = [];
|
|
506
|
+
const onProgress = jest.fn((progress) => {
|
|
507
|
+
progressUpdates.push(progress);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Return events in unsorted order
|
|
511
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
512
|
+
{
|
|
513
|
+
LogicalResourceId: 'Resource3',
|
|
514
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
515
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
LogicalResourceId: 'Resource1',
|
|
519
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
520
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
LogicalResourceId: 'Resource2',
|
|
524
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
525
|
+
Timestamp: new Date('2025-10-27T10:00:02Z'),
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
530
|
+
|
|
531
|
+
// Act
|
|
532
|
+
const resultPromise = monitor.monitorImport({
|
|
533
|
+
stackIdentifier,
|
|
534
|
+
resourceLogicalIds,
|
|
535
|
+
onProgress,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Advance timer
|
|
539
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
540
|
+
|
|
541
|
+
const result = await resultPromise;
|
|
542
|
+
|
|
543
|
+
// Assert
|
|
544
|
+
expect(result.importedCount).toBe(3);
|
|
545
|
+
|
|
546
|
+
// Progress updates should be in timestamp order
|
|
547
|
+
const completeUpdates = progressUpdates.filter((p) => p.status === 'COMPLETE');
|
|
548
|
+
expect(completeUpdates.map((p) => p.logicalId)).toEqual([
|
|
549
|
+
'Resource1',
|
|
550
|
+
'Resource2',
|
|
551
|
+
'Resource3',
|
|
552
|
+
]);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should handle IMPORT_IN_PROGRESS status with progress callbacks', async () => {
|
|
556
|
+
// Arrange
|
|
557
|
+
const stackIdentifier = {
|
|
558
|
+
stackName: 'test-stack',
|
|
559
|
+
region: 'us-east-1',
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
563
|
+
|
|
564
|
+
const progressUpdates = [];
|
|
565
|
+
const onProgress = jest.fn((progress) => {
|
|
566
|
+
progressUpdates.push(progress);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
let eventCallCount = 0;
|
|
570
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
571
|
+
eventCallCount++;
|
|
572
|
+
|
|
573
|
+
if (eventCallCount === 1) {
|
|
574
|
+
return Promise.resolve([
|
|
575
|
+
{
|
|
576
|
+
LogicalResourceId: 'FriggVPC',
|
|
577
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
578
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
579
|
+
},
|
|
580
|
+
]);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (eventCallCount === 2) {
|
|
584
|
+
return Promise.resolve([
|
|
585
|
+
{
|
|
586
|
+
LogicalResourceId: 'FriggVPC',
|
|
587
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
588
|
+
Timestamp: new Date('2025-10-27T10:00:05Z'),
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return Promise.resolve([]);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
597
|
+
|
|
598
|
+
// Act
|
|
599
|
+
const resultPromise = monitor.monitorImport({
|
|
600
|
+
stackIdentifier,
|
|
601
|
+
resourceLogicalIds,
|
|
602
|
+
onProgress,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Advance timers
|
|
606
|
+
for (let i = 0; i < 2; i++) {
|
|
607
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
await resultPromise;
|
|
611
|
+
|
|
612
|
+
// Assert
|
|
613
|
+
expect(progressUpdates.some((p) => p.status === 'IN_PROGRESS')).toBe(true);
|
|
614
|
+
|
|
615
|
+
const inProgressUpdate = progressUpdates.find(
|
|
616
|
+
(p) => p.logicalId === 'FriggVPC' && p.status === 'IN_PROGRESS'
|
|
617
|
+
);
|
|
618
|
+
expect(inProgressUpdate).toEqual({
|
|
619
|
+
logicalId: 'FriggVPC',
|
|
620
|
+
status: 'IN_PROGRESS',
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should poll stack events every 2 seconds', async () => {
|
|
625
|
+
// Arrange
|
|
626
|
+
const stackIdentifier = {
|
|
627
|
+
stackName: 'test-stack',
|
|
628
|
+
region: 'us-east-1',
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
632
|
+
|
|
633
|
+
const onProgress = jest.fn();
|
|
634
|
+
|
|
635
|
+
let eventCallCount = 0;
|
|
636
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
637
|
+
eventCallCount++;
|
|
638
|
+
|
|
639
|
+
if (eventCallCount === 3) {
|
|
640
|
+
return Promise.resolve([
|
|
641
|
+
{
|
|
642
|
+
LogicalResourceId: 'FriggVPC',
|
|
643
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
644
|
+
Timestamp: new Date('2025-10-27T10:00:06Z'),
|
|
645
|
+
},
|
|
646
|
+
]);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return Promise.resolve([
|
|
650
|
+
{
|
|
651
|
+
LogicalResourceId: 'FriggVPC',
|
|
652
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
653
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
654
|
+
},
|
|
655
|
+
]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_IN_PROGRESS');
|
|
659
|
+
|
|
660
|
+
// Act
|
|
661
|
+
const resultPromise = monitor.monitorImport({
|
|
662
|
+
stackIdentifier,
|
|
663
|
+
resourceLogicalIds,
|
|
664
|
+
onProgress,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Advance timers 3 times (3 * 2 seconds = 6 seconds)
|
|
668
|
+
for (let i = 0; i < 3; i++) {
|
|
669
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
await resultPromise;
|
|
673
|
+
|
|
674
|
+
// Assert - getStackEvents should be called 3 times (once per 2-second interval)
|
|
675
|
+
expect(mockCloudFormationRepository.getStackEvents).toHaveBeenCalledTimes(3);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should handle empty progress callback gracefully', async () => {
|
|
679
|
+
// Arrange
|
|
680
|
+
const stackIdentifier = {
|
|
681
|
+
stackName: 'test-stack',
|
|
682
|
+
region: 'us-east-1',
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
686
|
+
|
|
687
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
688
|
+
{
|
|
689
|
+
LogicalResourceId: 'FriggVPC',
|
|
690
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
691
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
692
|
+
},
|
|
693
|
+
]);
|
|
694
|
+
|
|
695
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
696
|
+
|
|
697
|
+
// Act - No onProgress callback provided
|
|
698
|
+
const resultPromise = monitor.monitorImport({
|
|
699
|
+
stackIdentifier,
|
|
700
|
+
resourceLogicalIds,
|
|
701
|
+
onProgress: null,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Advance timer
|
|
705
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
706
|
+
|
|
707
|
+
const result = await resultPromise;
|
|
708
|
+
|
|
709
|
+
// Assert - should complete successfully without callback
|
|
710
|
+
expect(result.success).toBe(true);
|
|
711
|
+
expect(result.importedCount).toBe(1);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should handle multiple failures for different resources', async () => {
|
|
715
|
+
// Arrange
|
|
716
|
+
const stackIdentifier = {
|
|
717
|
+
stackName: 'test-stack',
|
|
718
|
+
region: 'us-east-1',
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const resourceLogicalIds = ['Resource1', 'Resource2', 'Resource3'];
|
|
722
|
+
|
|
723
|
+
const onProgress = jest.fn();
|
|
724
|
+
|
|
725
|
+
mockCloudFormationRepository.getStackEvents.mockResolvedValue([
|
|
726
|
+
{
|
|
727
|
+
LogicalResourceId: 'Resource1',
|
|
728
|
+
ResourceStatus: 'IMPORT_FAILED',
|
|
729
|
+
ResourceStatusReason: 'Template mismatch for Resource1',
|
|
730
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
LogicalResourceId: 'Resource2',
|
|
734
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
735
|
+
Timestamp: new Date('2025-10-27T10:00:02Z'),
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
LogicalResourceId: 'Resource3',
|
|
739
|
+
ResourceStatus: 'IMPORT_FAILED',
|
|
740
|
+
ResourceStatusReason: 'Resource3 not found',
|
|
741
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
742
|
+
},
|
|
743
|
+
]);
|
|
744
|
+
|
|
745
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_ROLLBACK_COMPLETE');
|
|
746
|
+
|
|
747
|
+
// Act
|
|
748
|
+
const resultPromise = monitor.monitorImport({
|
|
749
|
+
stackIdentifier,
|
|
750
|
+
resourceLogicalIds,
|
|
751
|
+
onProgress,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Advance timer
|
|
755
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
756
|
+
|
|
757
|
+
const result = await resultPromise;
|
|
758
|
+
|
|
759
|
+
// Assert
|
|
760
|
+
expect(result.success).toBe(false);
|
|
761
|
+
expect(result.importedCount).toBe(1);
|
|
762
|
+
expect(result.failedCount).toBe(2);
|
|
763
|
+
expect(result.failedResources).toEqual([
|
|
764
|
+
{
|
|
765
|
+
logicalId: 'Resource1',
|
|
766
|
+
reason: 'Template mismatch for Resource1',
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
logicalId: 'Resource3',
|
|
770
|
+
reason: 'Resource3 not found',
|
|
771
|
+
},
|
|
772
|
+
]);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should recognize UPDATE_COMPLETE as successful import (tagging phase)', async () => {
|
|
776
|
+
// Arrange
|
|
777
|
+
// This test validates the fix for the timeout bug where resources
|
|
778
|
+
// successfully import (IMPORT_COMPLETE) but then CloudFormation
|
|
779
|
+
// applies stack-level tags (UPDATE_COMPLETE), and the monitor
|
|
780
|
+
// must recognize UPDATE_COMPLETE as a completion status.
|
|
781
|
+
const stackIdentifier = {
|
|
782
|
+
stackName: 'test-stack',
|
|
783
|
+
region: 'us-east-1',
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const resourceLogicalIds = ['FriggVPC', 'FriggPrivateSubnet1'];
|
|
787
|
+
|
|
788
|
+
const progressUpdates = [];
|
|
789
|
+
const onProgress = jest.fn((progress) => {
|
|
790
|
+
progressUpdates.push(progress);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
let eventCallCount = 0;
|
|
794
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
795
|
+
eventCallCount++;
|
|
796
|
+
|
|
797
|
+
// First poll: Import phase complete
|
|
798
|
+
if (eventCallCount === 1) {
|
|
799
|
+
return Promise.resolve([
|
|
800
|
+
{
|
|
801
|
+
LogicalResourceId: 'FriggVPC',
|
|
802
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
803
|
+
Timestamp: new Date('2025-10-27T10:00:01Z'),
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
807
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
808
|
+
Timestamp: new Date('2025-10-27T10:00:02Z'),
|
|
809
|
+
},
|
|
810
|
+
]);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Second poll: Tagging phase (UPDATE_COMPLETE)
|
|
814
|
+
// This is the real-world sequence from CloudFormation
|
|
815
|
+
if (eventCallCount === 2) {
|
|
816
|
+
return Promise.resolve([
|
|
817
|
+
{
|
|
818
|
+
LogicalResourceId: 'FriggVPC',
|
|
819
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
820
|
+
Timestamp: new Date('2025-10-27T10:00:03Z'),
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
LogicalResourceId: 'FriggPrivateSubnet1',
|
|
824
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
825
|
+
Timestamp: new Date('2025-10-27T10:00:04Z'),
|
|
826
|
+
},
|
|
827
|
+
]);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return Promise.resolve([]);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
834
|
+
|
|
835
|
+
// Act
|
|
836
|
+
const resultPromise = monitor.monitorImport({
|
|
837
|
+
stackIdentifier,
|
|
838
|
+
resourceLogicalIds,
|
|
839
|
+
onProgress,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// Advance timers for 2 polls
|
|
843
|
+
for (let i = 0; i < 2; i++) {
|
|
844
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const result = await resultPromise;
|
|
848
|
+
|
|
849
|
+
// Assert
|
|
850
|
+
expect(result.success).toBe(true);
|
|
851
|
+
expect(result.importedCount).toBe(2);
|
|
852
|
+
expect(result.failedCount).toBe(0);
|
|
853
|
+
|
|
854
|
+
// Verify both resources reached COMPLETE status
|
|
855
|
+
const completeUpdates = progressUpdates.filter((p) => p.status === 'COMPLETE');
|
|
856
|
+
expect(completeUpdates).toHaveLength(2);
|
|
857
|
+
expect(completeUpdates.map((p) => p.logicalId)).toEqual(
|
|
858
|
+
expect.arrayContaining(['FriggVPC', 'FriggPrivateSubnet1'])
|
|
859
|
+
);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('should handle real CloudFormation event sequence (IMPORT_COMPLETE then UPDATE_COMPLETE)', async () => {
|
|
863
|
+
// Arrange
|
|
864
|
+
// This test simulates the EXACT event sequence from real CloudFormation:
|
|
865
|
+
// 1. IMPORT_IN_PROGRESS
|
|
866
|
+
// 2. IMPORT_COMPLETE (resource imported)
|
|
867
|
+
// 3. UPDATE_IN_PROGRESS (tagging starts)
|
|
868
|
+
// 4. UPDATE_COMPLETE (tagging complete)
|
|
869
|
+
const stackIdentifier = {
|
|
870
|
+
stackName: 'test-stack',
|
|
871
|
+
region: 'us-east-1',
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const resourceLogicalIds = ['FriggVPC'];
|
|
875
|
+
|
|
876
|
+
const progressUpdates = [];
|
|
877
|
+
const onProgress = jest.fn((progress) => {
|
|
878
|
+
progressUpdates.push(progress);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
let eventCallCount = 0;
|
|
882
|
+
mockCloudFormationRepository.getStackEvents.mockImplementation(() => {
|
|
883
|
+
eventCallCount++;
|
|
884
|
+
|
|
885
|
+
// First poll: Import in progress
|
|
886
|
+
if (eventCallCount === 1) {
|
|
887
|
+
return Promise.resolve([
|
|
888
|
+
{
|
|
889
|
+
LogicalResourceId: 'FriggVPC',
|
|
890
|
+
ResourceStatus: 'IMPORT_IN_PROGRESS',
|
|
891
|
+
Timestamp: new Date('2025-10-27T16:10:30Z'),
|
|
892
|
+
},
|
|
893
|
+
]);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Second poll: Import complete
|
|
897
|
+
if (eventCallCount === 2) {
|
|
898
|
+
return Promise.resolve([
|
|
899
|
+
{
|
|
900
|
+
LogicalResourceId: 'FriggVPC',
|
|
901
|
+
ResourceStatus: 'IMPORT_COMPLETE',
|
|
902
|
+
Timestamp: new Date('2025-10-27T16:10:34Z'),
|
|
903
|
+
},
|
|
904
|
+
]);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Third poll: Update in progress (tagging)
|
|
908
|
+
if (eventCallCount === 3) {
|
|
909
|
+
return Promise.resolve([
|
|
910
|
+
{
|
|
911
|
+
LogicalResourceId: 'FriggVPC',
|
|
912
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
913
|
+
Timestamp: new Date('2025-10-27T16:10:35Z'),
|
|
914
|
+
},
|
|
915
|
+
]);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Fourth poll: Update complete (tagging done)
|
|
919
|
+
if (eventCallCount === 4) {
|
|
920
|
+
return Promise.resolve([
|
|
921
|
+
{
|
|
922
|
+
LogicalResourceId: 'FriggVPC',
|
|
923
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
924
|
+
Timestamp: new Date('2025-10-27T16:10:36Z'),
|
|
925
|
+
},
|
|
926
|
+
]);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return Promise.resolve([]);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
mockCloudFormationRepository.getStackStatus.mockResolvedValue('IMPORT_COMPLETE');
|
|
933
|
+
|
|
934
|
+
// Act
|
|
935
|
+
const resultPromise = monitor.monitorImport({
|
|
936
|
+
stackIdentifier,
|
|
937
|
+
resourceLogicalIds,
|
|
938
|
+
onProgress,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Advance timers for 4 polls (real sequence takes ~6 seconds)
|
|
942
|
+
for (let i = 0; i < 4; i++) {
|
|
943
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const result = await resultPromise;
|
|
947
|
+
|
|
948
|
+
// Assert
|
|
949
|
+
expect(result.success).toBe(true);
|
|
950
|
+
expect(result.importedCount).toBe(1);
|
|
951
|
+
expect(result.failedCount).toBe(0);
|
|
952
|
+
|
|
953
|
+
// Verify progress callbacks captured full sequence
|
|
954
|
+
expect(progressUpdates.length).toBeGreaterThanOrEqual(2);
|
|
955
|
+
|
|
956
|
+
// Should have IN_PROGRESS callback
|
|
957
|
+
const inProgressUpdate = progressUpdates.find(
|
|
958
|
+
(p) => p.logicalId === 'FriggVPC' && p.status === 'IN_PROGRESS'
|
|
959
|
+
);
|
|
960
|
+
expect(inProgressUpdate).toBeDefined();
|
|
961
|
+
|
|
962
|
+
// Should have COMPLETE callback (from either IMPORT_COMPLETE or UPDATE_COMPLETE)
|
|
963
|
+
const completeUpdate = progressUpdates.find(
|
|
964
|
+
(p) => p.logicalId === 'FriggVPC' && p.status === 'COMPLETE'
|
|
965
|
+
);
|
|
966
|
+
expect(completeUpdate).toBeDefined();
|
|
967
|
+
expect(completeUpdate.progress).toBe(1);
|
|
968
|
+
expect(completeUpdate.total).toBe(1);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
});
|