UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,075 lines 202 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // We need to mock the chokidar library, used by 'cdk watch' const mockChokidarWatcherOn = jest.fn(); const fakeChokidarWatcher = { on: mockChokidarWatcherOn, }; const fakeChokidarWatcherOn = { get readyCallback() { expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(1); // The call to the first 'watcher.on()' in the production code is the one we actually want here. // This is a pretty fragile, but at least with this helper class, // we would have to change it only in one place if it ever breaks const firstCall = mockChokidarWatcherOn.mock.calls[0]; // let's make sure the first argument is the 'ready' event, // just to be double safe expect(firstCall[0]).toBe('ready'); // the second argument is the callback return firstCall[1]; }, get fileEventCallback() { expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(2); const secondCall = mockChokidarWatcherOn.mock.calls[1]; // let's make sure the first argument is not the 'ready' event, // just to be double safe expect(secondCall[0]).not.toBe('ready'); // the second argument is the callback return secondCall[1]; }, }; const mockChokidarWatch = jest.fn(); jest.mock('chokidar', () => ({ watch: mockChokidarWatch, })); const fakeChokidarWatch = { get includeArgs() { expect(mockChokidarWatch.mock.calls.length).toBe(1); // the include args are the first parameter to the 'watch()' call return mockChokidarWatch.mock.calls[0][0]; }, get excludeArgs() { expect(mockChokidarWatch.mock.calls.length).toBe(1); // the ignore args are a property of the second parameter to the 'watch()' call const chokidarWatchOpts = mockChokidarWatch.mock.calls[0][1]; return chokidarWatchOpts.ignored; }, }; const mockData = jest.fn(); jest.mock('../../lib/logging', () => ({ ...jest.requireActual('../../lib/logging'), data: mockData, })); jest.setTimeout(30000); require("aws-sdk-client-mock"); const os = require("os"); const path = require("path"); const cxschema = require("@aws-cdk/cloud-assembly-schema"); const cloud_assembly_schema_1 = require("@aws-cdk/cloud-assembly-schema"); const cxapi = require("@aws-cdk/cx-api"); const client_cloudformation_1 = require("@aws-sdk/client-cloudformation"); const client_ssm_1 = require("@aws-sdk/client-ssm"); const fs = require("fs-extra"); const promptly = require("promptly"); const bootstrap_1 = require("../../lib/api/bootstrap"); const deployments_1 = require("../../lib/api/deployments"); const common_1 = require("../../lib/api/hotswap/common"); const plugin_1 = require("../../lib/api/plugin"); const cdk_toolkit_1 = require("../../lib/cli/cdk-toolkit"); const user_configuration_1 = require("../../lib/cli/user-configuration"); const diff_1 = require("../../lib/diff"); const cli_io_host_1 = require("../../lib/toolkit/cli-io-host"); const util_1 = require("../../lib/util"); const util_2 = require("../util"); const mock_sdk_1 = require("../util/mock-sdk"); (0, cdk_toolkit_1.markTesting)(); const defaultBootstrapSource = { source: 'default' }; const bootstrapEnvironmentMock = jest.spyOn(bootstrap_1.Bootstrapper.prototype, 'bootstrapEnvironment'); let cloudExecutable; let stderrMock; beforeEach(() => { jest.resetAllMocks(); (0, mock_sdk_1.restoreSdkMocksToDefault)(); mockChokidarWatch.mockReturnValue(fakeChokidarWatcher); // on() in chokidar's Watcher returns 'this' mockChokidarWatcherOn.mockReturnValue(fakeChokidarWatcher); bootstrapEnvironmentMock.mockResolvedValue({ noOp: false, outputs: {}, type: 'did-deploy-stack', stackArn: 'fake-arn', }); cloudExecutable = new util_2.MockCloudExecutable({ stacks: [MockStack.MOCK_STACK_A, MockStack.MOCK_STACK_B], nestedAssemblies: [ { stacks: [MockStack.MOCK_STACK_C], }, ], }); cli_io_host_1.CliIoHost.instance().isCI = false; stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); }); function defaultToolkitSetup() { return new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, 'Test-Stack-B': { Baz: 'Zinga!' }, 'Test-Stack-C': { Baz: 'Zinga!' }, }), }); } const mockSdk = new mock_sdk_1.MockSdk(); describe('readCurrentTemplate', () => { let template; let mockCloudExecutable; let sdkProvider; let mockForEnvironment; beforeEach(() => { jest.resetAllMocks(); template = { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Key: 'Value', }, }, }, }; mockCloudExecutable = new util_2.MockCloudExecutable({ stacks: [ { stackName: 'Test-Stack-C', template, properties: { assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', lookupRole: { arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}', requiresBootstrapStackVersion: 5, bootstrapStackVersionSsmParameter: '/bootstrap/parameter', }, }, }, { stackName: 'Test-Stack-A', template, properties: { assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', }, }, ], }); sdkProvider = mockCloudExecutable.sdkProvider; mockForEnvironment = jest .spyOn(sdkProvider, 'forEnvironment') .mockResolvedValue({ sdk: mockSdk, didAssumeRole: true }); mock_sdk_1.mockCloudFormationClient .on(client_cloudformation_1.GetTemplateCommand) .resolves({ TemplateBody: JSON.stringify(template), }) .on(client_cloudformation_1.DescribeStacksCommand) .resolves({ Stacks: [ { StackName: 'Test-Stack-C', StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, CreationTime: new Date(), }, { StackName: 'Test-Stack-A', StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, CreationTime: new Date(), }, ], }); }); test('lookup role is used', async () => { // GIVEN mock_sdk_1.mockSSMClient.on(client_ssm_1.GetParameterCommand).resolves({ Parameter: { Value: '6' } }); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect(mock_sdk_1.mockSSMClient).toHaveReceivedCommandWith(client_ssm_1.GetParameterCommand, { Name: '/bootstrap/parameter', }); expect(mockForEnvironment).toHaveBeenCalledTimes(2); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop-lookup:here:123456789012', assumeRoleExternalId: undefined, }); }); test('fallback to deploy role if bootstrap stack version is not valid', async () => { // GIVEN mock_sdk_1.mockSSMClient.on(client_ssm_1.GetParameterCommand).resolves({ Parameter: { Value: '1' } }); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect((0, util_1.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ expect.stringContaining("Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5"), ])); expect(mock_sdk_1.mockSSMClient).toHaveReceivedCommandWith(client_ssm_1.GetParameterCommand, { Name: '/bootstrap/parameter', }); expect(mockForEnvironment).toHaveBeenCalledTimes(3); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop-lookup:here:123456789012', assumeRoleExternalId: undefined, }); expect(mockForEnvironment).toHaveBeenNthCalledWith(2, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop:here:123456789012', assumeRoleExternalId: undefined, }); }); test('fallback to deploy role if bootstrap version parameter not found', async () => { // GIVEN mock_sdk_1.mockSSMClient.on(client_ssm_1.GetParameterCommand).callsFake(() => { const e = new Error('not found'); e.code = e.name = 'ParameterNotFound'; throw e; }); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect((0, util_1.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([expect.stringMatching(/SSM parameter.*not found./)])); expect(mockForEnvironment).toHaveBeenCalledTimes(3); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop-lookup:here:123456789012', assumeRoleExternalId: undefined, }); expect(mockForEnvironment).toHaveBeenNthCalledWith(2, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop:here:123456789012', assumeRoleExternalId: undefined, }); }); test('fallback to deploy role if forEnvironment throws', async () => { // GIVEN // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest mockForEnvironment = jest.spyOn(sdkProvider, 'forEnvironment').mockImplementationOnce(() => { throw new Error('TheErrorThatGetsThrown'); }); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect(mock_sdk_1.mockSSMClient).not.toHaveReceivedAnyCommand(); expect((0, util_1.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([expect.stringMatching(/TheErrorThatGetsThrown/)])); expect(mockForEnvironment).toHaveBeenCalledTimes(3); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop-lookup:here:123456789012', assumeRoleExternalId: undefined, }); expect(mockForEnvironment).toHaveBeenNthCalledWith(2, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: 'bloop:here:123456789012', assumeRoleExternalId: undefined, }); }); test('dont lookup bootstrap version parameter if default credentials are used', async () => { // GIVEN mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockSdk, didAssumeRole: false }; }); mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect((0, util_1.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ expect.stringMatching(/Lookup role.*was not assumed. Proceeding with default credentials./), ])); expect(mock_sdk_1.mockSSMClient).not.toHaveReceivedAnyCommand(); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, plugin_1.Mode.ForReading, { assumeRoleArn: 'bloop-lookup:here:123456789012', assumeRoleExternalId: undefined, }); expect(mockForEnvironment).toHaveBeenNthCalledWith(2, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, plugin_1.Mode.ForWriting, { assumeRoleArn: 'bloop:here:123456789012', assumeRoleExternalId: undefined, }); }); test('do not print warnings if lookup role not provided in stack artifact', async () => { // GIVEN const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, sdkProvider: mockCloudExecutable.sdkProvider, deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider, }), }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-A'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); // THEN expect((0, util_1.flatten)(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([ expect.stringMatching(/Could not assume/), expect.stringMatching(/please upgrade to bootstrap version/), ])); expect(mock_sdk_1.mockSSMClient).not.toHaveReceivedAnyCommand(); expect(mockForEnvironment).toHaveBeenCalledTimes(2); expect(mockForEnvironment).toHaveBeenNthCalledWith(1, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 0, { assumeRoleArn: undefined, assumeRoleExternalId: undefined, }); expect(mockForEnvironment).toHaveBeenNthCalledWith(2, { account: '123456789012', name: 'aws://123456789012/here', region: 'here', }, 1, { assumeRoleArn: 'bloop:here:123456789012', assumeRoleExternalId: undefined, }); }); }); describe('bootstrap', () => { test('accepts qualifier from context', async () => { // GIVEN const toolkit = defaultToolkitSetup(); const configuration = new user_configuration_1.Configuration(); configuration.context.set('@aws-cdk/core:bootstrapQualifier', 'abcde'); // WHEN await toolkit.bootstrap(['aws://56789/south-pole'], { source: defaultBootstrapSource, parameters: { qualifier: configuration.context.get('@aws-cdk/core:bootstrapQualifier'), }, }); // THEN expect(bootstrapEnvironmentMock).toHaveBeenCalledWith(expect.anything(), expect.anything(), { parameters: { qualifier: 'abcde', }, source: defaultBootstrapSource, }); }); }); describe('deploy', () => { test('fails when no valid stack names are given', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-D'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); }); describe('with hotswap deployment', () => { test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => { // GIVEN const mockCfnDeployments = (0, util_2.instanceMockFrom)(deployments_1.Deployments); mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ type: 'did-deploy-stack', noOp: false, outputs: {}, stackArn: 'stackArn', stackArtifact: (0, util_2.instanceMockFrom)(cxapi.CloudFormationStackArtifact), })); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: mockCfnDeployments, }); // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-A-Display-Name'] }, requireApproval: diff_1.RequireApproval.Never, hotswap: common_1.HotswapMode.FALL_BACK, }); // THEN expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.FALL_BACK, })); }); }); describe('makes correct CloudFormation calls', () => { test('without options', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('with stacks all stacks specified as double wildcard', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ selector: { patterns: ['**'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('with one stack specified', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ selector: { patterns: ['Test-Stack-A-Display-Name'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('with stacks all stacks specified as wildcard', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ selector: { patterns: ['*'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); describe('sns notification arns', () => { beforeEach(() => { cloudExecutable = new util_2.MockCloudExecutable({ stacks: [ MockStack.MOCK_STACK_A, MockStack.MOCK_STACK_B, MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, ], }); }); test('with sns notification arns as options', async () => { // GIVEN const notificationArns = [ 'arn:aws:sns:us-east-2:444455556666:MyTopic', 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', ]; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await toolkit.deploy({ // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. selector: { patterns: ['Test-Stack-A-Display-Name'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('fail with incorrect sns notification arns as options', async () => { // GIVEN const notificationArns = ['arn:::cfn-my-cool-topic']; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await expect(() => toolkit.deploy({ // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. selector: { patterns: ['Test-Stack-A-Display-Name'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); test('with sns notification arns in the executable', async () => { // GIVEN const expectedNotificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, }, expectedNotificationArns), }); // WHEN await toolkit.deploy({ selector: { patterns: ['Test-Stack-Notification-Arns'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('fail with incorrect sns notification arns in the executable', async () => { // GIVEN const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, }), }); // WHEN await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); }); test('with sns notification arns in the executable and as options', async () => { // GIVEN const notificationArns = [ 'arn:aws:sns:us-east-2:444455556666:MyTopic', 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', ]; const expectedNotificationArns = notificationArns.concat([ 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', ]); const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, }, expectedNotificationArns), }); // WHEN await toolkit.deploy({ selector: { patterns: ['Test-Stack-Notification-Arns'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); }); test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { // GIVEN const notificationArns = ['arn:::cfn-my-cool-topic']; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { // GIVEN const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); }); test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { // GIVEN const notificationArns = ['arn:::cfn-my-cool-topic']; const toolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-Notification-Arns'] }, notificationArns, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, })).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); }); }); test('globless bootstrap uses environment without question', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN await toolkit.bootstrap(['aws://56789/south-pole'], { source: defaultBootstrapSource, }); // THEN expect(bootstrapEnvironmentMock).toHaveBeenCalledWith({ account: '56789', region: 'south-pole', name: 'aws://56789/south-pole', }, expect.anything(), expect.anything()); expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); }); test('globby bootstrap uses whats in the stacks', async () => { // GIVEN const toolkit = defaultToolkitSetup(); cloudExecutable.configuration.settings.set(['app'], 'something'); // WHEN await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], { source: defaultBootstrapSource, }); // THEN expect(bootstrapEnvironmentMock).toHaveBeenCalledWith({ account: '123456789012', region: 'bermuda-triangle-1', name: 'aws://123456789012/bermuda-triangle-1', }, expect.anything(), expect.anything()); expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); }); test('bootstrap can be invoked without the --app argument', async () => { // GIVEN cloudExecutable.configuration.settings.clear(); const mockSynthesize = jest.fn(); cloudExecutable.synthesize = mockSynthesize; const toolkit = defaultToolkitSetup(); // WHEN await toolkit.bootstrap(['aws://123456789012/west-pole'], { source: defaultBootstrapSource, }); // THEN expect(bootstrapEnvironmentMock).toHaveBeenCalledWith({ account: '123456789012', region: 'west-pole', name: 'aws://123456789012/west-pole', }, expect.anything(), expect.anything()); expect(bootstrapEnvironmentMock).toHaveBeenCalledTimes(1); expect(cloudExecutable.hasApp).toEqual(false); expect(mockSynthesize).not.toHaveBeenCalled(); }); }); describe('destroy', () => { test('destroy correct stack', async () => { const toolkit = defaultToolkitSetup(); expect(() => { return toolkit.destroy({ selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, exclusively: true, force: true, fromDeploy: true, }); }).resolves; }); }); describe('watch', () => { test("fails when no 'watch' settings are found", async () => { const toolkit = defaultToolkitSetup(); await expect(() => { return toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + 'Make sure to add a "watch" key to your cdk.json'); }); test('observes only the root directory by default', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); const includeArgs = fakeChokidarWatch.includeArgs; expect(includeArgs.length).toBe(1); }); test("allows providing a single string in 'watch.include'", async () => { cloudExecutable.configuration.settings.set(['watch'], { include: 'my-dir', }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); }); test("allows providing an array of strings in 'watch.include'", async () => { cloudExecutable.configuration.settings.set(['watch'], { include: ['my-dir1', '**/my-dir2/*'], }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); }); test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); expect(fakeChokidarWatch.excludeArgs).toStrictEqual(['cdk.out/**', '**/.*', '**/.*/**', '**/node_modules/**']); }); test("allows providing a single string in 'watch.exclude'", async () => { cloudExecutable.configuration.settings.set(['watch'], { exclude: 'my-dir', }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); const excludeArgs = fakeChokidarWatch.excludeArgs; expect(excludeArgs.length).toBe(5); expect(excludeArgs[0]).toBe('my-dir'); }); test("allows providing an array of strings in 'watch.exclude'", async () => { cloudExecutable.configuration.settings.set(['watch'], { exclude: ['my-dir1', '**/my-dir2'], }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); const excludeArgs = fakeChokidarWatch.excludeArgs; expect(excludeArgs.length).toBe(6); expect(excludeArgs[0]).toBe('my-dir1'); expect(excludeArgs[1]).toBe('**/my-dir2'); }); test('allows watching with deploy concurrency', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, concurrency: 3, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ concurrency: 3 })); }); describe.each([common_1.HotswapMode.FALL_BACK, common_1.HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { test('passes through the correct hotswap mode to deployStack()', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode, }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: hotswapMode })); }); }); test('respects HotswapMode.HOTSWAP_ONLY', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.HOTSWAP_ONLY })); }); test('respects HotswapMode.FALL_BACK', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.FALL_BACK, }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.FALL_BACK })); }); test('respects HotswapMode.FULL_DEPLOYMENT', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT, }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.FULL_DEPLOYMENT })); }); describe('with file change events', () => { let toolkit; let cdkDeployMock; beforeEach(async () => { cloudExecutable.configuration.settings.set(['watch'], {}); toolkit = defaultToolkitSetup(); cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY, }); }); test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); expect(cdkDeployMock).not.toHaveBeenCalled(); }); describe("when the 'ready' event has already fired", () => { beforeEach(() => { // The ready callback triggers a deployment so each test // that uses this function will see 'cdkDeployMock' called // an additional time. fakeChokidarWatcherOn.readyCallback(); }); test("an initial 'deploy' is triggered, without any file changes", async () => { expect(cdkDeployMock).toHaveBeenCalledTimes(1); }); test("does trigger a 'deploy' for a file change", async () => { await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); expect(cdkDeployMock).toHaveBeenCalledTimes(2); }); test("triggers a 'deploy' twice for two file changes", async () => { // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism await Promise.all([ fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), ]); expect(cdkDeployMock).toHaveBeenCalledTimes(3); }); test("batches file changes that happen during 'deploy'", async () => { // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism await Promise.all([ fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'), fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'), ]); expect(cdkDeployMock).toHaveBeenCalledTimes(3); }); }); }); }); describe('synth', () => { test('successful synth outputs hierarchical stack ids', async () => { const toolkit = defaultToolkitSetup(); await toolkit.synth([], false, false); // Separate tests as colorizing hampers detection expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name'); expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B'); }); test('with no stdout option', async () => { // GIVE const toolkit = defaultToolkitSetup(); // THEN await toolkit.synth(['Test-Stack-A-Display-Name'], false, true); expect(mockData.mock.calls.length).toEqual(0); }); describe('migrate', () => { const testResourcePath = [__dirname, '..', 'commands', 'test-resources']; const templatePath = [...testResourcePath, 'templates']; const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); const s3TemplatePath = path.join(...templatePath, 's3-template.json'); test('migrate fails when both --from-path and --from-stack are provided', async () => { const toolkit = defaultToolkitSetup(); await expect(() => toolkit.migrate({ stackName: 'no-source', fromPath: './here/template.yml', fromStack: true, })).rejects.toThrow('Only one of `--from-path` or `--from-stack` may be provided.'); expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); }); test('migrate fails when --from-path is invalid', async () => { const toolkit = defaultToolkitSetup(); await expect(() => toolkit.migrate({ stackName: 'bad-local-source', fromPath: './here/template.yml', })).rejects.toThrow("'./here/template.yml' is not a valid path."); expect(stderrMock.mock.calls[1][0]).toContain(" ❌ Migrate failed for `bad-local-source`: './here/template.yml' is not a valid path."); }); test('migrate fails when --from-stack is used and stack does not exist in account', async () => { const mockSdkProvider = new mock_sdk_1.MockSdkProvider(); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).rejects(new Error('Stack does not exist in this environment')); const mockCloudExecutable = new util_2.MockCloudExecutable({ stacks: [], }); const cdkToolkit = new cdk_toolkit_1.CdkToolkit({ cloudExecutable: mockCloudExecutable, deployments: new deployments_1.Deployments({ sdkProvider: mockSdkProvider }), sdkProvider: mockSdkProvider, configuration: mockCloudExecutable.configuration, }); await expect(() => cdkToolkit.migrate({ stackName: 'bad-cloudformation-source', fromStack: true, })).rejects.toThrow('Stack does not exist in this environment'); expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); }); test('migrate fails when stack cannot be generated', async () => { const toolkit = defaultToolkitSetup(); await expect(() => toolkit.migrate({ stackName: 'cannot-generate-template', fromPath: sqsTemplatePath, language: 'rust', })).rejects.toThrow('CannotGenerateTemplateStack could not be generated because rust is not a supported language'); expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: CannotGenerateTemplateStack could not be generated because rust is not a supported language'); }); cliTest('migrate succeeds for valid template from local path when no language is provided', async (workDir) => { const toolkit = defaultToolkitSetup(); await toolkit.migrate({ stackName: 'SQSTypeScript', fromPath: sqsTemplatePath, outputPath: workDir, }); // Packages created for typescript expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); }); cliTest('migrate succeeds for valid template from local path when language is provided', async (workDir) => { const toolkit = defaultToolkitSetup(); await toolkit.migrate({ stackName: 'S3Python', fromPath: s3TemplatePath, outputPath: workDir, language: 'python', }); // Packages created for typescript expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); }); cliTest('migrate call is idempotent', async (workDir) => { const toolkit = defaultToolkitSetup(); await toolkit.migrate({ stackName: 'AutoscalingCSharp', fromPath: autoscalingTemplatePath, outputPath: workDir, language: 'csharp', }); // Packages created for typescript expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); // One more time await toolkit.migrate({ stackName: 'AutoscalingCSharp', fromPath: autoscalingTemplatePath, outputPath: workDir, language: 'csharp', }); // Packages created for typescript expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); }); }); describe('stack with error and flagged for validation', () => { beforeEach(() => { cloudExecutable = new util_2.MockCloudExecutable({ stacks: [MockStack.MOCK_STACK_A, MockStack.MOCK_STACK_B], nestedAssemblies: [ { stacks: [ { properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR, }, ], }, ], }); }); test('causes synth to fail if autoValidate=true', async () => { const toolkit = defaultToolkitSetup(); const autoValidate = true; await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); }); test('causes synth to succeed if autoValidate=false', async () => { const toolkit = de