Skip to content

Commit fddacc7

Browse files
9paceclaude
andcommitted
test(cli): add unit tests for lock rollbackValidate and validateTemplateDrift
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f128db commit fddacc7

2 files changed

Lines changed: 225 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { AmplifyGen2MigrationValidations } from '../../../commands/gen2-migration/_validations';
2+
import { $TSContext } from '@aws-amplify/amplify-cli-core';
3+
import { Logger } from '../../../commands/gen2-migration';
4+
5+
jest.mock('@aws-sdk/client-cloudformation');
6+
jest.mock('bottleneck', () => {
7+
return jest.fn().mockImplementation(() => ({
8+
schedule: jest.fn((fn) => fn()),
9+
}));
10+
});
11+
12+
const mockSyncCloudBackendFromS3 = jest.fn();
13+
const mockGetClient = jest.fn();
14+
jest.mock('../../../commands/drift-detection/services', () => ({
15+
CloudFormationService: jest.fn().mockImplementation(() => ({
16+
syncCloudBackendFromS3: mockSyncCloudBackendFromS3,
17+
getClient: mockGetClient,
18+
})),
19+
}));
20+
21+
const mockDetectTemplateDrift = jest.fn();
22+
jest.mock('../../../commands/drift-detection/detect-template-drift', () => ({
23+
detectTemplateDrift: (...args: any[]) => mockDetectTemplateDrift(...args),
24+
}));
25+
26+
describe('AmplifyGen2MigrationValidations - validateTemplateDrift', () => {
27+
let mockContext: $TSContext;
28+
let validations: AmplifyGen2MigrationValidations;
29+
const mockCfnClient = {};
30+
31+
beforeEach(() => {
32+
mockContext = {} as $TSContext;
33+
validations = new AmplifyGen2MigrationValidations(new Logger('mock', 'mock', 'mock'), 'amplify-test-stack', 'dev', mockContext);
34+
mockGetClient.mockResolvedValue(mockCfnClient);
35+
jest.clearAllMocks();
36+
});
37+
38+
it('should pass when no template drift is detected', async () => {
39+
mockSyncCloudBackendFromS3.mockResolvedValue(true);
40+
mockDetectTemplateDrift.mockResolvedValue({
41+
changes: [],
42+
skipped: false,
43+
});
44+
45+
await expect(validations.validateTemplateDrift()).resolves.not.toThrow();
46+
expect(mockSyncCloudBackendFromS3).toHaveBeenCalledWith(mockContext);
47+
expect(mockDetectTemplateDrift).toHaveBeenCalledWith('amplify-test-stack', expect.any(Object), mockCfnClient);
48+
});
49+
50+
it('should throw MigrationError when template drift is detected', async () => {
51+
mockSyncCloudBackendFromS3.mockResolvedValue(true);
52+
mockDetectTemplateDrift.mockResolvedValue({
53+
changes: [{ LogicalResourceId: 'SomeResource', Action: 'Modify' }],
54+
skipped: false,
55+
});
56+
57+
await expect(validations.validateTemplateDrift()).rejects.toMatchObject({
58+
name: 'MigrationError',
59+
message: 'Template drift detected',
60+
});
61+
});
62+
63+
it('should throw MigrationError when S3 sync fails', async () => {
64+
mockSyncCloudBackendFromS3.mockResolvedValue(false);
65+
66+
await expect(validations.validateTemplateDrift()).rejects.toMatchObject({
67+
name: 'MigrationError',
68+
message: 'Failed to sync cloud backend from S3',
69+
resolution: 'Ensure the project is deployed and S3 bucket is accessible.',
70+
});
71+
expect(mockDetectTemplateDrift).not.toHaveBeenCalled();
72+
});
73+
74+
it('should throw MigrationError when template drift detection is skipped', async () => {
75+
mockSyncCloudBackendFromS3.mockResolvedValue(true);
76+
mockDetectTemplateDrift.mockResolvedValue({
77+
changes: [],
78+
skipped: true,
79+
skipReason: 'No cached CloudFormation template found',
80+
});
81+
82+
await expect(validations.validateTemplateDrift()).rejects.toMatchObject({
83+
name: 'MigrationError',
84+
message: 'Template drift detection was skipped: No cached CloudFormation template found',
85+
});
86+
});
87+
88+
it('should throw MigrationError when detection is skipped due to nested stack errors', async () => {
89+
mockSyncCloudBackendFromS3.mockResolvedValue(true);
90+
mockDetectTemplateDrift.mockResolvedValue({
91+
changes: [],
92+
skipped: true,
93+
skipReason: 'One or more nested stacks could not be analyzed',
94+
});
95+
96+
await expect(validations.validateTemplateDrift()).rejects.toMatchObject({
97+
name: 'MigrationError',
98+
message: 'Template drift detection was skipped: One or more nested stacks could not be analyzed',
99+
});
100+
});
101+
102+
it('should pass the correct stack name and CFN client to detectTemplateDrift', async () => {
103+
mockSyncCloudBackendFromS3.mockResolvedValue(true);
104+
mockDetectTemplateDrift.mockResolvedValue({
105+
changes: [],
106+
skipped: false,
107+
});
108+
109+
await validations.validateTemplateDrift();
110+
111+
expect(mockGetClient).toHaveBeenCalledWith(mockContext);
112+
expect(mockDetectTemplateDrift).toHaveBeenCalledWith(
113+
'amplify-test-stack',
114+
expect.objectContaining({
115+
info: expect.any(Function),
116+
debug: expect.any(Function),
117+
warn: expect.any(Function),
118+
blankLine: expect.any(Function),
119+
success: expect.any(Function),
120+
error: expect.any(Function),
121+
}),
122+
mockCfnClient,
123+
);
124+
});
125+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { AmplifyMigrationLockStep } from '../../../commands/gen2-migration/lock';
2+
import { $TSContext } from '@aws-amplify/amplify-cli-core';
3+
import { Logger } from '../../../commands/gen2-migration';
4+
5+
const mockValidateDeploymentStatus = jest.fn();
6+
const mockValidateLockStatus = jest.fn();
7+
const mockValidateTemplateDrift = jest.fn();
8+
9+
jest.mock('../../../commands/gen2-migration/_validations', () => ({
10+
AmplifyGen2MigrationValidations: jest.fn().mockImplementation(() => ({
11+
validateDeploymentStatus: mockValidateDeploymentStatus,
12+
validateLockStatus: mockValidateLockStatus,
13+
validateTemplateDrift: mockValidateTemplateDrift,
14+
validateDrift: jest.fn(),
15+
})),
16+
}));
17+
18+
jest.mock('@aws-sdk/client-cloudformation');
19+
jest.mock('@aws-sdk/client-amplify');
20+
jest.mock('@aws-sdk/client-dynamodb');
21+
jest.mock('@aws-sdk/client-appsync');
22+
jest.mock('@aws-sdk/client-cognito-identity-provider');
23+
jest.mock('@aws-amplify/amplify-cli-core', () => ({
24+
...jest.requireActual('@aws-amplify/amplify-cli-core'),
25+
stateManager: {
26+
getMeta: jest.fn().mockReturnValue({}),
27+
},
28+
}));
29+
30+
describe('AmplifyMigrationLockStep - rollbackValidate', () => {
31+
let step: AmplifyMigrationLockStep;
32+
let mockContext: $TSContext;
33+
34+
beforeEach(() => {
35+
mockContext = {} as $TSContext;
36+
step = new AmplifyMigrationLockStep(
37+
new Logger('lock', 'test-app', 'dev'),
38+
'dev',
39+
'test-app',
40+
'test-app-id',
41+
'amplify-test-stack',
42+
'us-east-1',
43+
mockContext,
44+
);
45+
jest.clearAllMocks();
46+
});
47+
48+
it('should call all three validations in order', async () => {
49+
mockValidateDeploymentStatus.mockResolvedValue(undefined);
50+
mockValidateLockStatus.mockResolvedValue(undefined);
51+
mockValidateTemplateDrift.mockResolvedValue(undefined);
52+
53+
await step.rollbackValidate();
54+
55+
expect(mockValidateDeploymentStatus).toHaveBeenCalledTimes(1);
56+
expect(mockValidateLockStatus).toHaveBeenCalledTimes(1);
57+
expect(mockValidateTemplateDrift).toHaveBeenCalledTimes(1);
58+
});
59+
60+
it('should propagate error when validateDeploymentStatus fails', async () => {
61+
const error = new Error('Stack not found');
62+
error.name = 'StackNotFoundError';
63+
mockValidateDeploymentStatus.mockRejectedValue(error);
64+
65+
await expect(step.rollbackValidate()).rejects.toThrow('Stack not found');
66+
expect(mockValidateLockStatus).not.toHaveBeenCalled();
67+
expect(mockValidateTemplateDrift).not.toHaveBeenCalled();
68+
});
69+
70+
it('should propagate error when validateLockStatus fails', async () => {
71+
mockValidateDeploymentStatus.mockResolvedValue(undefined);
72+
const error = new Error('Stack is not locked');
73+
error.name = 'MigrationError';
74+
mockValidateLockStatus.mockRejectedValue(error);
75+
76+
await expect(step.rollbackValidate()).rejects.toThrow('Stack is not locked');
77+
expect(mockValidateDeploymentStatus).toHaveBeenCalledTimes(1);
78+
expect(mockValidateTemplateDrift).not.toHaveBeenCalled();
79+
});
80+
81+
it('should propagate error when validateTemplateDrift fails', async () => {
82+
mockValidateDeploymentStatus.mockResolvedValue(undefined);
83+
mockValidateLockStatus.mockResolvedValue(undefined);
84+
const error = new Error('Template drift detected');
85+
error.name = 'MigrationError';
86+
mockValidateTemplateDrift.mockRejectedValue(error);
87+
88+
await expect(step.rollbackValidate()).rejects.toThrow('Template drift detected');
89+
expect(mockValidateDeploymentStatus).toHaveBeenCalledTimes(1);
90+
expect(mockValidateLockStatus).toHaveBeenCalledTimes(1);
91+
});
92+
93+
it('should pass when all validations succeed with no drift', async () => {
94+
mockValidateDeploymentStatus.mockResolvedValue(undefined);
95+
mockValidateLockStatus.mockResolvedValue(undefined);
96+
mockValidateTemplateDrift.mockResolvedValue(undefined);
97+
98+
await expect(step.rollbackValidate()).resolves.not.toThrow();
99+
});
100+
});

0 commit comments

Comments
 (0)