From 9d144df5f1f647663587472f70986ee353a46698 Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Wed, 1 Nov 2023 12:11:48 +0100 Subject: [PATCH 1/2] test(redshift): unit tests for cluster parameter change reboot handler --- ...er-parameter-change-reboot-handler.test.ts | 367 ++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts diff --git a/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts b/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts new file mode 100644 index 0000000000000..17067a4b433f9 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts @@ -0,0 +1,367 @@ +const mockRebootCluster = jest.fn(); +const mockDescribeClusters = jest.fn(); +const mockRedshift = { + rebootCluster: mockRebootCluster, + describeClusters: mockDescribeClusters, +}; + +jest.mock('@aws-sdk/client-redshift', () => { + return { + Redshift: jest.fn(() => mockRedshift), + }; +}); + +jest.setTimeout(35_000); + +import { handler } from '../lib/cluster-parameter-change-reboot-handler'; + +describe('cluster-parameter-change-reboot-handler', () => { + + beforeEach(() => { + jest.spyOn(global, 'setTimeout'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('reboots cluster with "pending-reboot" status', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockRebootCluster).toHaveBeenCalled(); + }); + + test('reboots cluster with "apply-deferred" status', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'apply-deferred', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockRebootCluster).toHaveBeenCalled(); + }); + + test('reboots cluster with "apply-error" status', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'apply-deferred', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockRebootCluster).toHaveBeenCalled(); + }); + + test('retries cluster with "applying" status', async () => { + // GIVEN + mockDescribeClusters + .mockImplementationOnce(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'applying', + }], + }], + }; + }).mockImplementationOnce(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(setTimeout).toHaveBeenCalled(); + expect(mockDescribeClusters).toHaveBeenCalledTimes(2); + expect(mockRebootCluster).toHaveBeenCalledTimes(1); + }); + + test('retries cluster with "retry" status', async () => { + // GIVEN + mockDescribeClusters + .mockImplementationOnce(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'retry', + }], + }], + }; + }).mockImplementationOnce(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(setTimeout).toHaveBeenCalled(); + expect(mockDescribeClusters).toHaveBeenCalledTimes(2); + expect(mockRebootCluster).toHaveBeenCalledTimes(1); + }); + + test('retries if rebootCluster throws InvalidClusterStateFault error', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + mockRebootCluster + .mockImplementationOnce(async () => { + const { InvalidClusterStateFault } = jest.requireActual('@aws-sdk/client-redshift'); + return Promise.reject(new InvalidClusterStateFault()); + }) + .mockImplementationOnce(jest.fn()); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(setTimeout).toHaveBeenCalled(); + expect(mockDescribeClusters).toHaveBeenCalledTimes(1); + expect(mockRebootCluster).toHaveBeenCalledTimes(2); + }); + + test('retries if rebootCluster throws generic error', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + mockRebootCluster.mockImplementation(async () => { + return Promise.reject(new Error('error')); + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + // THEN + await expect(() => invokeHandler(event)).rejects.toThrow('error'); + }); + + test('fails if cannot find cluster details', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{}], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + // THEN + await expect(() =>invokeHandler(event)) + .rejects.toThrow(/Unable to find any Parameter Groups associated with ClusterId "cluster-id"./); + }); + + test('fails if cannot find cluster parameter group', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'unknown', + ParameterApplyStatus: 'pending-reboot', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + // THEN + await expect(() =>invokeHandler(event)) + .rejects.toThrow(/Unable to find Parameter Group named "parameter-group-name" associated with ClusterId "cluster-id"./); + }); + + test('does not reboot if request type is Delete', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockRebootCluster).not.toHaveBeenCalled(); + }); + + test('does not reboot if apply status is not recognized', async () => { + // GIVEN + mockDescribeClusters.mockImplementation(() => { + return { + Clusters: [{ + ClusterParameterGroups: [{ + ParameterGroupName: 'parameter-group-name', + ParameterApplyStatus: 'other-status', + }], + }], + }; + }); + + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'service-token', + ClusterId: 'cluster-id', + ParameterGroupName: 'parameter-group-name', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockRebootCluster).not.toHaveBeenCalled(); + }); +}); + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} From 3d0cdde30114c00f8779a7d25256744e86db7897 Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Thu, 2 Nov 2023 14:21:07 +0100 Subject: [PATCH 2/2] fix test description --- .../test/cluster-parameter-change-reboot-handler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts b/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts index 17067a4b433f9..8b12d818a7ce4 100644 --- a/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts +++ b/packages/@aws-cdk/aws-redshift-alpha/test/cluster-parameter-change-reboot-handler.test.ts @@ -231,7 +231,7 @@ describe('cluster-parameter-change-reboot-handler', () => { expect(mockRebootCluster).toHaveBeenCalledTimes(2); }); - test('retries if rebootCluster throws generic error', async () => { + test('fails if rebootCluster throws generic error', async () => { // GIVEN mockDescribeClusters.mockImplementation(() => { return {