From 3ca78ba1a32f07fc4609cfaa6b65ff33dca6a2c6 Mon Sep 17 00:00:00 2001 From: vicheey Date: Mon, 11 Nov 2024 11:24:07 -0800 Subject: [PATCH] test(lambda): fix sync integration test #5948 ## Problem The integration tests for sync is consistently failing. There seems to be some issue with mocking. Since these tests are mocking the process run, we are moving these test to unit test to be executed for every CI ## Solution - Add more test to sync unit test in `sync.test.ts` - Remove sync integration test case from `sam.test.ts` in favor of added unit test in `sync.tes.ts` - Move `runInTerminal()` and `ProcessTerminal` class to a separate file` (more decomposition for `sync.ts` will follow) - Add unit test for `DeployTypeWizard.test.ts` - Add assertion methods to `PrompterTester` class. --- packages/core/src/shared/sam/build.ts | 5 +- packages/core/src/shared/sam/deploy.ts | 12 +- .../core/src/shared/sam/processTerminal.ts | 146 +++ packages/core/src/shared/sam/sync.ts | 167 +-- packages/core/src/shared/sam/utils.ts | 24 +- .../{ => explorer}/detectSamProjects.test.ts | 8 +- .../{ => explorer}/samProject.test.ts | 14 +- .../wizards/deployTypeWizard.test.ts | 114 ++ .../core/src/test/shared/sam/deploy.test.ts | 40 +- .../core/src/test/shared/sam/sync.test.ts | 1148 +++++++++++++++-- .../src/test/shared/wizards/prompterTester.ts | 67 +- packages/core/src/testInteg/sam.test.ts | 290 +---- 12 files changed, 1421 insertions(+), 614 deletions(-) create mode 100644 packages/core/src/shared/sam/processTerminal.ts rename packages/core/src/test/awsService/appBuilder/{ => explorer}/detectSamProjects.test.ts (92%) rename packages/core/src/test/awsService/appBuilder/{ => explorer}/samProject.test.ts (93%) create mode 100644 packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts diff --git a/packages/core/src/shared/sam/build.ts b/packages/core/src/shared/sam/build.ts index c3513363993..49cc78fad8a 100644 --- a/packages/core/src/shared/sam/build.ts +++ b/packages/core/src/shared/sam/build.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' -import { TemplateItem, createTemplatePrompter, getSamCliPathAndVersion, runInTerminal } from './sync' +import { TemplateItem, createTemplatePrompter } from './sync' import { ChildProcess } from '../utilities/processUtils' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { Wizard } from '../wizards/wizard' @@ -19,8 +19,9 @@ import globals from '../extensionGlobals' import { TreeNode } from '../treeview/resourceTreeDataProvider' import { telemetry } from '../telemetry/telemetry' import { getSpawnEnv } from '../env/resolveEnv' -import { getProjectRoot, isDotnetRuntime } from './utils' +import { getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils' import { getConfigFileUri, validateSamBuildConfig } from './config' +import { runInTerminal } from './processTerminal' export interface BuildParams { readonly template: TemplateItem diff --git a/packages/core/src/shared/sam/deploy.ts b/packages/core/src/shared/sam/deploy.ts index 6154e2e3def..3763dcab83d 100644 --- a/packages/core/src/shared/sam/deploy.ts +++ b/packages/core/src/shared/sam/deploy.ts @@ -23,15 +23,9 @@ import { CancellationError } from '../utilities/timeoutUtils' import { Wizard } from '../wizards/wizard' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { validateSamDeployConfig, SamConfig, writeSamconfigGlobal } from './config' -import { - TemplateItem, - createStackPrompter, - createBucketPrompter, - createTemplatePrompter, - getSamCliPathAndVersion, - runInTerminal, -} from './sync' -import { getProjectRoot, getSource } from './utils' +import { TemplateItem, createStackPrompter, createBucketPrompter, createTemplatePrompter } from './sync' +import { getProjectRoot, getSamCliPathAndVersion, getSource } from './utils' +import { runInTerminal } from './processTerminal' export interface DeployParams { readonly paramsSource: ParamsSource diff --git a/packages/core/src/shared/sam/processTerminal.ts b/packages/core/src/shared/sam/processTerminal.ts new file mode 100644 index 00000000000..e383b956d52 --- /dev/null +++ b/packages/core/src/shared/sam/processTerminal.ts @@ -0,0 +1,146 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' + +import { ToolkitError, UnknownError } from '../errors' +import globals from '../extensionGlobals' +import { isCloud9 } from '../extensionUtilities' +import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' +import { CancellationError } from '../utilities/timeoutUtils' +import { getLogger } from '../logger' +import { removeAnsi } from '../utilities/textUtilities' +import { isAutomation } from '../vscode/env' + +let oldTerminal: ProcessTerminal | undefined +export async function runInTerminal(proc: ChildProcess, cmd: string) { + const handleResult = (result?: ChildProcessResult) => { + if (result && result.exitCode !== 0) { + const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}` + if (result.stderr.includes('is up to date')) { + throw ToolkitError.chain(result.error, message, { + code: 'NoUpdateExitCode', + }) + } + throw ToolkitError.chain(result.error, message, { + code: 'NonZeroExitCode', + }) + } + } + + // `createTerminal` doesn't work on C9 so we use the output channel instead + if (isCloud9()) { + globals.outputChannel.show() + + const result = proc.run({ + onStdout: (text) => globals.outputChannel.append(removeAnsi(text)), + onStderr: (text) => globals.outputChannel.append(removeAnsi(text)), + }) + await proc.send('\n') + + return handleResult(await result) + } + + // The most recent terminal won't get garbage collected until the next run + if (oldTerminal?.stopped === true) { + oldTerminal.close() + } + const pty = (oldTerminal = new ProcessTerminal(proc)) + const terminal = vscode.window.createTerminal({ pty, name: `SAM ${cmd}` }) + terminal.sendText('\n') + terminal.show() + + const result = await new Promise((resolve) => pty.onDidExit(resolve)) + if (pty.cancelled) { + throw result.error !== undefined + ? ToolkitError.chain(result.error, 'SAM CLI was cancelled before exiting', { cancelled: true }) + : new CancellationError('user') + } else { + return handleResult(result) + } +} + +// This is a decent improvement over using the output channel but it isn't a tty/pty +// SAM CLI uses `click` which has reduced functionality if `os.isatty` returns false +// Historically, Windows lack of a pty-equivalent is why it's not available in libuv +// Maybe it's doable now with the ConPTY API? https://github.com/libuv/libuv/issues/2640 +class ProcessTerminal implements vscode.Pseudoterminal { + private readonly onDidCloseEmitter = new vscode.EventEmitter() + private readonly onDidWriteEmitter = new vscode.EventEmitter() + private readonly onDidExitEmitter = new vscode.EventEmitter() + public readonly onDidWrite = this.onDidWriteEmitter.event + public readonly onDidClose = this.onDidCloseEmitter.event + public readonly onDidExit = this.onDidExitEmitter.event + + public constructor(private readonly process: ChildProcess) { + // Used in integration tests + if (isAutomation()) { + // Disable because it is a test. + // eslint-disable-next-line aws-toolkits/no-console-log + this.onDidWrite((text) => console.log(text.trim())) + } + } + + #cancelled = false + public get cancelled() { + return this.#cancelled + } + + public get stopped() { + return this.process.stopped + } + + public open(initialDimensions: vscode.TerminalDimensions | undefined): void { + this.process + .run({ + onStdout: (text) => this.mapStdio(text), + onStderr: (text) => this.mapStdio(text), + }) + .then((result) => this.onDidExitEmitter.fire(result)) + .catch((err) => + this.onDidExitEmitter.fire({ error: UnknownError.cast(err), exitCode: -1, stderr: '', stdout: '' }) + ) + .finally(() => this.onDidWriteEmitter.fire('\r\nPress any key to close this terminal')) + } + + public close(): void { + this.process.stop() + this.onDidCloseEmitter.fire() + } + + public handleInput(data: string) { + // ETX + if (data === '\u0003' || this.process.stopped) { + this.#cancelled ||= data === '\u0003' + return this.close() + } + + // enter + if (data === '\u000D') { + this.process.send('\n').then(undefined, (e) => { + getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) + }) + this.onDidWriteEmitter.fire('\r\n') + } else { + this.process.send(data).then(undefined, (e) => { + getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) + }) + this.onDidWriteEmitter.fire(data) + } + } + + private mapStdio(text: string): void { + const lines = text.split('\n') + const first = lines.shift() + + if (first) { + this.onDidWriteEmitter.fire(first) + } + + for (const line of lines) { + this.onDidWriteEmitter.fire('\r\n') + this.onDidWriteEmitter.fire(line) + } + } +} diff --git a/packages/core/src/shared/sam/sync.ts b/packages/core/src/shared/sam/sync.ts index a0d691dc981..e21d89899f9 100644 --- a/packages/core/src/shared/sam/sync.ts +++ b/packages/core/src/shared/sam/sync.ts @@ -17,25 +17,20 @@ import * as CloudFormation from '../cloudformation/cloudformation' import { DefaultEcrClient } from '../clients/ecrClient' import { createRegionPrompter } from '../ui/common/region' import { CancellationError } from '../utilities/timeoutUtils' -import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' +import { ChildProcess } from '../utilities/processUtils' import { keys, selectFrom } from '../utilities/tsUtils' import { AWSTreeNodeBase } from '../treeview/nodes/awsTreeNodeBase' -import { ToolkitError, UnknownError } from '../errors' +import { ToolkitError } from '../errors' import { telemetry } from '../telemetry/telemetry' import { createCommonButtons } from '../ui/buttons' import { ToolkitPromptSettings } from '../settings' import { getLogger } from '../logger' -import { getSamInitDocUrl, isCloud9 } from '../extensionUtilities' -import { removeAnsi } from '../utilities/textUtilities' +import { getSamInitDocUrl } from '../extensionUtilities' import { createExitPrompter } from '../ui/common/exitPrompter' import { StackSummary } from 'aws-sdk/clients/cloudformation' -import { SamCliSettings } from './cli/samCliSettings' import { getConfigFileUri, SamConfig, validateSamSyncConfig, writeSamconfigGlobal } from './config' import { cast, Optional } from '../utilities/typeConstructors' import { pushIf, toRecord } from '../utilities/collectionUtils' -import { SamCliInfoInvocation } from './cli/samCliInfo' -import { parse } from 'semver' -import { isAutomation } from '../vscode/env' import { getOverriddenParameters } from '../../lambda/config/parameterUtils' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { samSyncParamUrl, samSyncUrl, samUpgradeUrl } from '../constants' @@ -46,7 +41,8 @@ import { IamConnection } from '../../auth/connection' import { CloudFormationTemplateRegistry } from '../fs/templateRegistry' import { TreeNode } from '../treeview/resourceTreeDataProvider' import { getSpawnEnv } from '../env/resolveEnv' -import { getProjectRoot, getProjectRootUri, getSource } from './utils' +import { getProjectRoot, getProjectRootUri, getSamCliPathAndVersion, getSource } from './utils' +import { runInTerminal } from './processTerminal' const localize = nls.loadMessageBundle() @@ -459,71 +455,6 @@ export async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boun return { boundArgs } } -export async function getSamCliPathAndVersion() { - const { path: samCliPath } = await SamCliSettings.instance.getOrDetectSamCli() - if (samCliPath === undefined) { - throw new ToolkitError('SAM CLI could not be found', { code: 'MissingExecutable' }) - } - - const info = await new SamCliInfoInvocation(samCliPath).execute() - const parsedVersion = parse(info.version) - telemetry.record({ version: info.version }) - - if (parsedVersion?.compare('1.53.0') === -1) { - throw new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) - } - - return { path: samCliPath, parsedVersion } -} - -let oldTerminal: ProcessTerminal | undefined -export async function runInTerminal(proc: ChildProcess, cmd: string) { - const handleResult = (result?: ChildProcessResult) => { - if (result && result.exitCode !== 0) { - const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}` - if (result.stderr.includes('is up to date')) { - throw ToolkitError.chain(result.error, message, { - code: 'NoUpdateExitCode', - }) - } - throw ToolkitError.chain(result.error, message, { - code: 'NonZeroExitCode', - }) - } - } - - // `createTerminal` doesn't work on C9 so we use the output channel instead - if (isCloud9()) { - globals.outputChannel.show() - - const result = proc.run({ - onStdout: (text) => globals.outputChannel.append(removeAnsi(text)), - onStderr: (text) => globals.outputChannel.append(removeAnsi(text)), - }) - await proc.send('\n') - - return handleResult(await result) - } - - // The most recent terminal won't get garbage collected until the next run - if (oldTerminal?.stopped === true) { - oldTerminal.close() - } - const pty = (oldTerminal = new ProcessTerminal(proc)) - const terminal = vscode.window.createTerminal({ pty, name: `SAM ${cmd}` }) - terminal.sendText('\n') - terminal.show() - - const result = await new Promise((resolve) => pty.onDidExit(resolve)) - if (pty.cancelled) { - throw result.error !== undefined - ? ToolkitError.chain(result.error, 'SAM CLI was cancelled before exiting', { cancelled: true }) - : new CancellationError('user') - } else { - return handleResult(result) - } -} - async function loadLegacyParameterOverrides(template: TemplateItem) { try { const params = await getOverriddenParameters(template.uri) @@ -581,6 +512,7 @@ export async function runSamSync(args: SyncParams) { env: await getSpawnEnv(process.env, { promptForInvalidCredential: true }), }), }) + await runInTerminal(sam, 'sync') const { paramsSource, stackName, region, projectRoot } = args const shouldWriteSyncSamconfigGlobal = paramsSource !== ParamsSource.SamConfig && !!stackName && !!region @@ -660,6 +592,7 @@ export async function prepareSyncParams( } else if (arg instanceof vscode.Uri) { if (arg.path.endsWith('samconfig.toml')) { // "Deploy" command was invoked on a samconfig.toml file. + // TODO: add step to verify samconfig content to skip param source prompter const config = await SamConfig.fromConfigFileUri(arg) const params = getSyncParamsFromConfig(config) const projectRoot = vscode.Uri.joinPath(config.location, '..') @@ -675,7 +608,7 @@ export async function prepareSyncParams( // Always use the dependency layer if the user specified to do so const skipDependencyLayer = !config.getCommandParam('sync', 'dependency_layer') - return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } + return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } as SyncParams } // "Deploy" command was invoked on a template.yaml file. @@ -777,90 +710,6 @@ Confirm that you are synchronizing a development stack. } } -// This is a decent improvement over using the output channel but it isn't a tty/pty -// SAM CLI uses `click` which has reduced functionality if `os.isatty` returns false -// Historically, Windows lack of a pty-equivalent is why it's not available in libuv -// Maybe it's doable now with the ConPTY API? https://github.com/libuv/libuv/issues/2640 -class ProcessTerminal implements vscode.Pseudoterminal { - private readonly onDidCloseEmitter = new vscode.EventEmitter() - private readonly onDidWriteEmitter = new vscode.EventEmitter() - private readonly onDidExitEmitter = new vscode.EventEmitter() - public readonly onDidWrite = this.onDidWriteEmitter.event - public readonly onDidClose = this.onDidCloseEmitter.event - public readonly onDidExit = this.onDidExitEmitter.event - - public constructor(private readonly process: ChildProcess) { - // Used in integration tests - if (isAutomation()) { - // Disable because it is a test. - // eslint-disable-next-line aws-toolkits/no-console-log - this.onDidWrite((text) => console.log(text.trim())) - } - } - - #cancelled = false - public get cancelled() { - return this.#cancelled - } - - public get stopped() { - return this.process.stopped - } - - public open(initialDimensions: vscode.TerminalDimensions | undefined): void { - this.process - .run({ - onStdout: (text) => this.mapStdio(text), - onStderr: (text) => this.mapStdio(text), - }) - .then((result) => this.onDidExitEmitter.fire(result)) - .catch((err) => - this.onDidExitEmitter.fire({ error: UnknownError.cast(err), exitCode: -1, stderr: '', stdout: '' }) - ) - .finally(() => this.onDidWriteEmitter.fire('\r\nPress any key to close this terminal')) - } - - public close(): void { - this.process.stop() - this.onDidCloseEmitter.fire() - } - - public handleInput(data: string) { - // ETX - if (data === '\u0003' || this.process.stopped) { - this.#cancelled ||= data === '\u0003' - return this.close() - } - - // enter - if (data === '\u000D') { - this.process.send('\n').then(undefined, (e) => { - getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) - }) - this.onDidWriteEmitter.fire('\r\n') - } else { - this.process.send(data).then(undefined, (e) => { - getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) - }) - this.onDidWriteEmitter.fire(data) - } - } - - private mapStdio(text: string): void { - const lines = text.split('\n') - const first = lines.shift() - - if (first) { - this.onDidWriteEmitter.fire(first) - } - - for (const line of lines) { - this.onDidWriteEmitter.fire('\r\n') - this.onDidWriteEmitter.fire(line) - } - } -} - function resolveSyncArgConflict(boundArgs: string[]): string[] { const boundArgsSet = new Set(boundArgs) if (boundArgsSet.has('--watch')) { diff --git a/packages/core/src/shared/sam/utils.ts b/packages/core/src/shared/sam/utils.ts index f95f408b797..a0c700657d2 100644 --- a/packages/core/src/shared/sam/utils.ts +++ b/packages/core/src/shared/sam/utils.ts @@ -4,12 +4,17 @@ */ import * as vscode from 'vscode' -import path from 'path' +import * as path from 'path' import { AWSTreeNodeBase } from '../treeview/nodes/awsTreeNodeBase' import { TreeNode, isTreeNode } from '../treeview/resourceTreeDataProvider' import * as CloudFormation from '../cloudformation/cloudformation' import { TemplateItem } from './sync' import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime' +import { telemetry } from '../telemetry' +import { ToolkitError } from '../errors' +import { SamCliSettings } from './cli/samCliSettings' +import { SamCliInfoInvocation } from './cli/samCliInfo' +import { parse } from 'semver' /** * @description determines the root directory of the project given Template Item @@ -61,3 +66,20 @@ export async function isDotnetRuntime(templateUri: vscode.Uri, contents?: string const globalRuntime = samTemplate.template.Globals?.Function?.Runtime as string return globalRuntime ? getFamily(globalRuntime) === RuntimeFamily.DotNet : false } + +export async function getSamCliPathAndVersion() { + const { path: samCliPath } = await SamCliSettings.instance.getOrDetectSamCli() + if (samCliPath === undefined) { + throw new ToolkitError('SAM CLI could not be found', { code: 'MissingExecutable' }) + } + + const info = await new SamCliInfoInvocation(samCliPath).execute() + const parsedVersion = parse(info.version) + telemetry.record({ version: info.version }) + + if (parsedVersion?.compare('1.53.0') === -1) { + throw new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) + } + + return { path: samCliPath, parsedVersion } +} diff --git a/packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts similarity index 92% rename from packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts rename to packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts index dbac6f573e8..b675b2452f8 100644 --- a/packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { TestFolder } from '../../testUtil' -import { detectSamProjects, getFiles } from '../../../awsService/appBuilder/explorer/detectSamProjects' +import { TestFolder } from '../../../testUtil' +import { detectSamProjects, getFiles } from '../../../../awsService/appBuilder/explorer/detectSamProjects' import assert from 'assert' import * as sinon from 'sinon' import path from 'path' -import { ToolkitError } from '../../../shared' -import { assertLogsContain } from '../../globalSetup.test' +import { ToolkitError } from '../../../../shared' +import { assertLogsContain } from '../../../globalSetup.test' describe('detectSamProjects', () => { let sandbox: sinon.SinonSandbox diff --git a/packages/core/src/test/awsService/appBuilder/samProject.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts similarity index 93% rename from packages/core/src/test/awsService/appBuilder/samProject.test.ts rename to packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts index 1d884ddd694..b093e6bb847 100644 --- a/packages/core/src/test/awsService/appBuilder/samProject.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts @@ -3,22 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { getApp, getStackName, SamAppLocation } from '../../../awsService/appBuilder/explorer/samProject' +import { getApp, getStackName, SamAppLocation } from '../../../../awsService/appBuilder/explorer/samProject' import * as sinon from 'sinon' import assert from 'assert' -import { ToolkitError } from '../../../shared' -import * as CloudformationModule from '../../../shared/cloudformation/cloudformation' +import { ToolkitError } from '../../../../shared' +import * as CloudformationModule from '../../../../shared/cloudformation/cloudformation' import path from 'path' -import { TestFolder } from '../../testUtil' +import { TestFolder } from '../../../testUtil' import { generateSamconfigData, samconfigCompleteData, samconfigInvalidData, validTemplateData, -} from '../../shared/sam/samTestUtils' -import { assertLogsContain } from '../../globalSetup.test' -import { getTestWindow } from '../../shared/vscode/window' +} from '../../../shared/sam/samTestUtils' +import { assertLogsContain } from '../../../globalSetup.test' +import { getTestWindow } from '../../../shared/vscode/window' describe('samProject', () => { let sandbox: sinon.SinonSandbox diff --git a/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts new file mode 100644 index 00000000000..86292ebe856 --- /dev/null +++ b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts @@ -0,0 +1,114 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +import { PrompterTester } from '../../../shared/wizards/prompterTester' +import assert from 'assert' +import { DeployTypeWizard } from '../../../../awsService/appBuilder/wizards/deployTypeWizard' +import { getSyncWizard } from '../../../../shared/sam/sync' +import { TestFolder } from '../../../testUtil' +import { samconfigCompleteData, validTemplateData } from '../../../shared/sam/samTestUtils' +import { getDeployWizard } from '../../../../shared/sam/deploy' + +describe('DeployTypeWizard', function () { + let testFolder: TestFolder + let templateFile: vscode.Uri + let syncWizard: any + let deployWizard: any + + let deployTypeWizard: DeployTypeWizard + + before(async () => { + testFolder = await TestFolder.create() + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + await testFolder.write('samconfig.toml', samconfigCompleteData) + + deployWizard = await getDeployWizard(templateFile) + syncWizard = await getSyncWizard('infra', templateFile, undefined, false) + }) + + it('customer abort wizard should not call any command function', async function () { + // Given + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.dispose() + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert(!choices) + prompterTester.assertCall('Select deployment command', 1) + }) + + it('deploy is selected', async function () { + /** + * This test focus on that deploy wizard get triggered when customer choose to use sam deploy + * Selection for deploy wizard speficy here focus on only one case + * More cases are test in Deploy.test.ts + * + */ + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.acceptItem(picker.items[1]) + }) + .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert.strictEqual(choices?.choice, 'deploy') + prompterTester.assertCallAll() + }) + + it('sync is selected', async function () { + /** + * This test focus on that sync wizard get triggered when customer choose to use sam sync + * Selection for deploy wizard speficy here focus on only one case + * More cases are test in Sync.test.ts + * + */ + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert.strictEqual(choices?.choice, 'sync') + prompterTester.assertCallAll() + }) +}) diff --git a/packages/core/src/test/shared/sam/deploy.test.ts b/packages/core/src/test/shared/sam/deploy.test.ts index 1da271fbdfb..01eb6df3423 100644 --- a/packages/core/src/test/shared/sam/deploy.test.ts +++ b/packages/core/src/test/shared/sam/deploy.test.ts @@ -14,6 +14,7 @@ import { getDeployWizard, runDeploy, } from '../../../shared/sam/deploy' +import * as UtilsModule from '../../../shared/sam/utils' import { globals, ToolkitError } from '../../../shared' import sinon from 'sinon' import { samconfigCompleteData, samconfigInvalidData, validTemplateData } from './samTestUtils' @@ -29,8 +30,8 @@ import { DefaultS3Client } from '../../../shared/clients/s3Client' import * as CloudFormationClientModule from '../../../shared/clients/cloudFormationClient' import * as S3ClientModule from '../../../shared/clients/s3Client' import * as ProcessUtilsModule from '../../../shared/utilities/processUtils' +import * as ProcessTerminalModule from '../../../shared/sam/processTerminal' import * as ResolveEnvModule from '../../../shared/env/resolveEnv' -import * as SyncModule from '../../../shared/sam/sync' import * as SamConfiModule from '../../../shared/sam/config' import { RequiredProps } from '../../../shared/utilities/tsUtils' import { UserAgent as __UserAgent } from '@smithy/types' @@ -92,7 +93,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigInvalidData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -142,6 +143,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.region, 'us-west-2') assert.strictEqual(parameters.stackName, 'stack1') assert.strictEqual(parameters.bucketSource, 0) + prompterTester.assertCallAll() }) it('happy path with valid samconfig.toml', async () => { @@ -162,7 +164,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -193,6 +195,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -300,7 +303,7 @@ describe('DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -330,6 +333,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.region, 'us-west-2') assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -361,7 +365,7 @@ describe('DeployWizard', async function () { * - bucketName : [Skip] automatically set for bucketSource option 1 */ - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -411,6 +415,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.stackName, 'stack2') assert.strictEqual(parameters.bucketSource, 0) assert(!parameters.bucketName) + prompterTester.assertCallAll() }) it('happy path with valid samconfig.toml', async () => { @@ -431,7 +436,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -462,6 +467,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -486,7 +492,7 @@ describe('DeployWizard', async function () { const templateFile2 = vscode.Uri.file(await testFolder2.write('template.yaml', validTemplateData)) await (await globals.templateRegistry).addItem(templateFile2) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -543,6 +549,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.stackName, 'stack3') assert.strictEqual(parameters.bucketSource, 1) assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + prompterTester.assertCallAll() }) it('happy path with samconfig.toml', async () => { @@ -568,7 +575,7 @@ describe('DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) // Simulate return of deployed stacks - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -595,6 +602,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) }) @@ -647,13 +655,13 @@ describe('SAM Deploy', () => { describe(':) path', () => { beforeEach(() => { mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) mockRunInTerminal = sandbox.stub().resolves(Promise.resolve()) - sandbox.stub(SyncModule, 'runInTerminal').callsFake(mockRunInTerminal) + sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake(mockRunInTerminal) }) it('[ParamsSource.SamConfig] should instantiate the correct ChildProcess', async () => { @@ -927,7 +935,7 @@ describe('SAM Deploy', () => { // Break point mockGetSamCliPath = sandbox - .stub(SyncModule, 'getSamCliPathAndVersion') + .stub(UtilsModule, 'getSamCliPathAndVersion') .rejects( new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) ) @@ -936,7 +944,7 @@ describe('SAM Deploy', () => { mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) mockRunInTerminal = sandbox.stub().resolves(Promise.resolve()) - sandbox.stub(SyncModule, 'runInTerminal').callsFake(mockRunInTerminal) + sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake(mockRunInTerminal) await runDeploy(appNode) assert.fail('Should have thrown an Error') @@ -955,13 +963,13 @@ describe('SAM Deploy', () => { // Happy Stub sandbox.stub(DeployWizard.prototype, 'run').resolves(mockDeployParams) mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) // Breaking point mockRunInTerminal = sandbox - .stub(SyncModule, 'runInTerminal') + .stub(ProcessTerminalModule, 'runInTerminal') .rejects(new ToolkitError('SAM CLI was cancelled before exiting', { cancelled: true })) await runDeploy(appNode) @@ -981,12 +989,12 @@ describe('SAM Deploy', () => { // Happy Stub sandbox.stub(DeployWizard.prototype, 'run').resolves(mockDeployParams) mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) // Breaking point - mockRunInTerminal = sandbox.stub(SyncModule, 'runInTerminal').callsFake((input, cmd) => { + mockRunInTerminal = sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake((input, cmd) => { if (cmd === 'deploy') { throw new ToolkitError('The stack is up to date', { code: 'NoUpdateExitCode', diff --git a/packages/core/src/test/shared/sam/sync.test.ts b/packages/core/src/test/shared/sam/sync.test.ts index dfe73dac9cb..10bff92a0f9 100644 --- a/packages/core/src/test/shared/sam/sync.test.ts +++ b/packages/core/src/test/shared/sam/sync.test.ts @@ -4,12 +4,19 @@ */ import * as vscode from 'vscode' -import * as sync from '../../../shared/sam/sync' +import * as SyncModule from '../../../shared/sam/sync' +import * as UtilsModule from '../../../shared/sam/utils' +import * as ProcessTerminalUtils from '../../../shared/sam/processTerminal' import * as awsConsole from '../../../shared/awsConsole' import * as S3ClientModule from '../../../shared/clients/s3Client' +import * as SamConfigModule from '../../../shared/sam/config' +import * as ResolveEnvModule from '../../../shared/env/resolveEnv' +import * as ProcessUtilsModule from '../../../shared/utilities/processUtils' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' import * as CloudFormationClientModule from '../../../shared/clients/cloudFormationClient' import * as buttons from '../../../shared/ui/buttons' -import assert from 'assert' +import assert, { fail } from 'assert' + import { createBucketPrompter, createEcrPrompter, @@ -18,9 +25,11 @@ import { createTemplatePrompter, ensureBucket, getSyncParamsFromConfig, + getSyncWizard, ParamsSource, paramsSourcePrompter, prepareSyncParams, + runSync, saveAndBindArgs, syncFlagItems, SyncParams, @@ -32,8 +41,6 @@ import { createBaseTemplate, makeSampleSamTemplateYaml, } from '../cloudformation/cloudformationTestUtils' -import * as deploySamApplication from '../../../shared/sam/deploy' -import * as syncSam from '../../../shared/sam/sync' import { createWizardTester } from '../wizards/wizardTestUtils' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' @@ -60,10 +67,19 @@ import { AppNode } from '../../../awsService/appBuilder/explorer/nodes/appNode' import * as Cfn from '../../../shared/cloudformation/cloudformation' import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry' import { WatchedItem } from '../../../shared/fs/watchedFiles' -import { validTemplateData } from '../../shared/sam/samTestUtils' +import { samconfigCompleteData, samconfigInvalidData, validTemplateData } from '../../shared/sam/samTestUtils' //import { beforeEach } from 'mocha' -import { assertEqualPaths, getWorkspaceFolder, TestFolder } from '../../testUtil' +import { + assertEqualPaths, + assertTelemetry, + assertTelemetryCurried, + getWorkspaceFolder, + TestFolder, +} from '../../testUtil' import { samSyncUrl } from '../../../shared/constants' +import { PrompterTester } from '../wizards/prompterTester' +import { createTestRegionProvider } from '../regions/testUtil' +import { ToolkitPromptSettings } from '../../../shared/settings' describe('SyncWizard', async function () { const createTester = async (params?: Partial) => @@ -383,7 +399,7 @@ describe('createBucketPrompter', () => { const stub = sandbox.stub(s3Client, 'listBucketsIterable').callsFake(() => { return buckets }) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) // Mock recent bucket + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Mock recent bucket // Act const prompter = createBucketPrompter(s3Client) @@ -407,7 +423,7 @@ describe('createBucketPrompter', () => { const stub = sandbox.stub(s3Client, 'listBucketsIterable').callsFake(() => { return [] as unknown as AsyncCollection & { readonly region: string }> }) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) // Mock recent bucket + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Mock recent bucket // Act const prompter = createBucketPrompter(s3Client) @@ -481,7 +497,7 @@ describe('createStackPrompter', () => { }, ] const listAllStacksStub = sandbox.stub(cfnClient, 'listAllStacks').returns(intoCollection(stackSummaries)) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -508,7 +524,7 @@ describe('createStackPrompter', () => { it('should include no items found message if no stacks exist', async () => { const listAllStacksStub = sandbox.stub(cfnClient, 'listAllStacks').returns(intoCollection([])) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -592,7 +608,7 @@ describe('createEcrPrompter', () => { }, ] const listAllRepositoriesStub = sandbox.stub(ecrClient, 'listAllRepositories').returns(intoCollection(ecrRepos)) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -619,7 +635,7 @@ describe('createEcrPrompter', () => { it('should include no items found message if no repos exist', async () => { const listAllStacksStub = sandbox.stub(ecrClient, 'listAllRepositories').returns(intoCollection([])) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -682,7 +698,7 @@ describe('createEnvironmentPrompter', () => { const envs: Environment[] = [defaultEnv, stagingEnv, prodEnv] listEnvironmentsStub.returns(envs) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Act const prompter = createEnvironmentPrompter(config) @@ -734,7 +750,7 @@ describe('createTemplatePrompter', () => { it('should create quick pick items from registry items', () => { // Arrange const recentTemplatePathStub = sinon.stub().returns(undefined) - sandbox.replace(sync, 'getRecentResponse', recentTemplatePathStub) + sandbox.replace(SyncModule, 'getRecentResponse', recentTemplatePathStub) const workspaceFolder = vscode.workspace.workspaceFolders?.[0] assert.ok(workspaceFolder) @@ -883,6 +899,7 @@ describe('SyncWizard', async () => { let mockDefaultCFNClient: sinon.SinonStubbedInstance let mockDefaultS3Client: sinon.SinonStubbedInstance + let registry: CloudFormationTemplateRegistry beforeEach(async () => { testFolder = await TestFolder.create() @@ -902,147 +919,1038 @@ describe('SyncWizard', async () => { // generate template.yaml in temporary test folder and add to registery templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) - await (await globals.templateRegistry).addItem(templateFile) + registry = await globals.templateRegistry + await registry.addItem(templateFile) }) afterEach(() => { sandbox.restore() + registry.reset() }) - describe('deploy sync prompt', function () { - let sandbox: sinon.SinonSandbox - beforeEach(function () { - sandbox = sinon.createSandbox() + describe('entry: template file', () => { + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 1. 'stack1' + * - bucketName : [Select] 1. 'stack-1-bucket' + * - syncFlags : [Select] ["--dependency-layer","--use-container","--save-params"] + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigInvalidData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (quickPick) => { + // The prompt will need some time to generate option + await quickPick.untilReady() + + assert.strictEqual(quickPick.items.length, 3) + assert.strictEqual(quickPick.items[0].label, 'stack1') + assert.strictEqual(quickPick.items[1].label, 'stack2') + assert.strictEqual(quickPick.items[2].label, 'stack3') + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + const saveParam = picker.items.filter((item) => item.label === 'Save parameters')[0] + picker.acceptItems(dependencyLayer, useContainer, saveParam) + }) + .build() + + const parameters = await (await getSyncWizard('infra', templateFile, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack1') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.bucketName, 'stack-1-bucket') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--save-params"]') + prompterTester.assertCallAll() }) - afterEach(function () { - sandbox.restore() + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] null; will use 'us-west-2' from samconfig + * - stackName : [Skip] null; will use 'project-1' from samconfig + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 3) + assert.strictEqual(quickPick.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(quickPick.items[1].label, 'Specify required parameters') + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', templateFile, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert(!parameters.region) + assert(!parameters.stackName) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.bucketName) + assert.strictEqual(parameters.skipDependencyLayer, true) + assert(!parameters.syncFlags) + prompterTester.assertCallAll() }) + }) + + describe('entry: appBuilder', () => { + let appNode: AppNode - it('customer exit should not call any function', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + beforeEach(async () => { + const expectedSamAppLocation = { + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, + } + appNode = new AppNode(expectedSamAppLocation) + }) + + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 2. ('Specify required parameters') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 3. stack-3-bucket + * - syncFlags : [Select] ["--save-params"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) - picker.dispose() - } - }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.notCalled) - assert(sync.notCalled) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', async (picker) => { + await picker.untilReady() + const select = picker.items.filter((i) => i.detail === 'us-west-2')[0] + picker.acceptItem(select || picker.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const saveParam = picker.items.filter((item) => item.label === 'Save parameters')[0] + picker.acceptItems(saveParam) + }) + .build() + + const parameters = await (await getSyncWizard('infra', appNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.path, templateFile.path) + assert.strictEqual(parameters.projectRoot.path, projectRoot.path) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--save-params"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] null; will use value from samconfig file + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', appNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.path, templateFile.path) + assert.strictEqual(parameters.projectRoot.path, projectRoot.path) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.region) + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() }) + }) - it('deploy is selected', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + describe('entry: region node', () => { + const expectedRegionId = 'us-west-2' + let regionNode: RegionNode + + beforeEach(async () => { + // Create RegionNode as entry point + regionNode = new RegionNode( + { id: expectedRegionId, name: 'US West (N. California)' }, + createTestRegionProvider() + ) + }) + + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 2. ('Specify required parameters') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 2. stack-2-bucket + * - syncFlags : [Select] ["--dependency-layer","--use-container"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') picker.acceptItem(picker.items[1]) - } else { + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + const parameters = await (await getSyncWizard('infra', regionNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-2-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template.yaml + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', regionNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.region, 'us-west-2') + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() + }) + }) + + describe('entry: samconfig file context menu', () => { + it('sad path with invalid samconfig.toml should throw parsing config file error', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigInvalidData)) + try { + await (await getSyncWizard('infra', samconfigFile, false, false)).run() + } catch (error: any) { + assert.strictEqual(error.code, 'samConfigParseError') + } + }) + + it('happy path with valid samconfig.toml', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', samconfigFile, false, false)).run() + assert(parameters) + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'project-1') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.bucketName, 'aws-sam-cli-managed-default-samclisourcebucket-lftqponsaxsr') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert(!parameters.syncFlags) + prompterTester.assertCallAll() + }) + + it('happy path with empty samconfig.toml', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', '')) + /** + * Selection: + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 2. stack-2-bucket + * - syncFlags : [Select] ["--dependency-layer","--use-container","--watch"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + const watch = picker.items.filter((item) => item.label === 'Watch')[0] + picker.acceptItems(dependencyLayer, useContainer, watch) + }) + .build() + + const parameters = await (await getSyncWizard('infra', samconfigFile, false, false)).run() + assert(parameters) + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-2-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--watch"]') + prompterTester.assertCallAll() + }) + }) + + describe('entry: command palette', () => { + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 3. 'stack3' + * - bucketName : [select] 3. stack-3-bucket + * - syncFlags : [Select] all + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') picker.acceptItem(picker.items[0]) - } + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SpecifyAndSave) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack3') + assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template.yaml + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.region) + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert(!parameters.syncFlags) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() + }) + }) +}) + +describe('SAM Sync', () => { + let sandbox: sinon.SinonSandbox + let testFolder: TestFolder + let projectRoot: vscode.Uri + let workspaceFolder: vscode.WorkspaceFolder + let templateFile: vscode.Uri + + let mockGetSpawnEnv: sinon.SinonStub + let mockGetSamCliPath: sinon.SinonStub + let mockChildProcessClass: sinon.SinonStub + let mockSamSyncChildProcess: sinon.SinonStub + + let spyWriteSamconfigGlobal: sinon.SinonSpy + let spyRunInterminal: sinon.SinonSpy + + let mockDefaultCFNClient: sinon.SinonStubbedInstance + let mockDefaultS3Client: sinon.SinonStubbedInstance + let registry: CloudFormationTemplateRegistry + + // Dependency clients + beforeEach(async function () { + testFolder = await TestFolder.create() + projectRoot = vscode.Uri.file(testFolder.path) + workspaceFolder = getWorkspaceFolder(testFolder.path) + sandbox = sinon.createSandbox() + registry = await globals.templateRegistry + + // Create template.yaml in temporary test folder and add to registery + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + + await registry.addItem(templateFile) + + // Simulate return of deployed stacks + mockDefaultCFNClient = sandbox.createStubInstance(CloudFormationClientModule.DefaultCloudFormationClient) + sandbox.stub(CloudFormationClientModule, 'DefaultCloudFormationClient').returns(mockDefaultCFNClient) + mockDefaultCFNClient.listAllStacks.returns(intoCollection(stackSummaries)) + + // Simulate return of list bucket + mockDefaultS3Client = sandbox.createStubInstance(S3ClientModule.DefaultS3Client) + sandbox.stub(S3ClientModule, 'DefaultS3Client').returns(mockDefaultS3Client) + mockDefaultS3Client.listBucketsIterable.returns(intoCollection(s3BucketListSummary)) + + // Create Spy for validation + spyWriteSamconfigGlobal = sandbox.spy(SamConfigModule, 'writeSamconfigGlobal') + spyRunInterminal = sandbox.spy(ProcessTerminalUtils, 'runInTerminal') + + // generate template.yaml in temporary test folder and add to registery + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + await (await globals.templateRegistry).addItem(templateFile) + + mockGetSpawnEnv = sandbox.stub(ResolveEnvModule, 'getSpawnEnv').callsFake( + sandbox.stub().resolves({ + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.called) - assert(sync.notCalled) + ) + }) + + afterEach(() => { + sandbox.restore() + registry.reset() + }) + + describe(':) path', () => { + beforeEach(() => { + mockGetSamCliPath = sandbox + .stub(UtilsModule, 'getSamCliPathAndVersion') + .callsFake(sandbox.stub().resolves({ path: 'sam-cli-path' })) + + // Confirm confirmDevStack message + // getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) + getTestWindow().onDidShowMessage((message) => { + message.selectItem("OK, and don't show this again") + }) + + // Mock child process with required properties that get called in ProcessTerminal + mockSamSyncChildProcess = Object.create(ProcessUtilsModule.ChildProcess.prototype, { + stopped: { get: sandbox.stub().returns(false) }, + stop: { value: sandbox.stub().resolves({}) }, + run: { + value: sandbox.stub().resolves({ + exitCode: 0, + stdout: 'Mock successful sync command execution ', + stderr: '', + }), + }, + }) + mockChildProcessClass = sandbox.stub(ProcessUtilsModule, 'ChildProcess').returns(mockSamSyncChildProcess) }) - it('sync is selected', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + afterEach(() => { + sandbox.restore() + }) + + it('[entry: command palette] specify and save flag should instantiate correct process in terminal', async () => { + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') - assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') picker.acceptItem(picker.items[0]) - } else { + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + // Invoke sync command from command palette + await runSync('code', undefined) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--code', + '--template', + `${templateFile.fsPath}`, + '--s3-bucket', + 'stack-3-bucket', + '--stack-name', + 'stack3', + '--region', + 'us-west-2', + '--no-dependency-layer', + '--save-params', + '--dependency-layer', + '--use-container', + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.calledOnce) + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: undefined }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'CodeOnly', + source: undefined, + }) + prompterTester.assertCallAll() + }) + + it('[entry: template file] specify flag should instantiate correct process in terminal', async () => { + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (quickPick) => { + // The prompt will need some time to generate option + await quickPick.untilReady() + assert.strictEqual(quickPick.items[0].label, 'stack1') + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') picker.acceptItem(picker.items[0]) - } + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + await runSync('infra', templateFile) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--template', + `${templateFile.fsPath}`, + '--s3-bucket', + 'stack-1-bucket', + '--stack-name', + 'stack1', + '--region', + 'us-west-2', + '--no-dependency-layer', + '--dependency-layer', + '--use-container', + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.calledOnce) + + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: 'template' }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'AllResources', + source: 'template', + }) + prompterTester.assertCallAll() + }) + + it('[entry: appBuilder] use samconfig should instantiate correct process in terminal', async () => { + const expectedSamAppLocation = { + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, + } + const appNode = new AppNode(expectedSamAppLocation) + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + await runSync('infra', appNode) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--template', + `${templateFile.fsPath}`, + '--no-dependency-layer', + '--config-file', + `${samconfigFile.fsPath}`, + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.notCalled) + + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: 'appBuilderDeploy' }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'AllResources', + source: 'appBuilderDeploy', }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.notCalled) - assert(sync.called) + prompterTester.assertCallAll() }) - }), - describe('appBuilder', () => { - let appNode: AppNode - beforeEach(async () => { - const expectedSamAppLocation = { - workspaceFolder: workspaceFolder, - samTemplateUri: templateFile, - projectRoot: projectRoot, - } - appNode = new AppNode(expectedSamAppLocation) + }) + + describe(':( path', () => { + let appNode: AppNode + beforeEach(async () => { + mockGetSamCliPath = sandbox + .stub(UtilsModule, 'getSamCliPathAndVersion') + .callsFake(sandbox.stub().resolves({ path: 'sam-cli-path' })) + + appNode = new AppNode({ + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, }) - afterEach(() => { - sandbox.restore() + await testFolder.write('samconfig.toml', samconfigCompleteData) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should abort when customer cancel sync dev stack agreement', async () => { + // Set the + await ToolkitPromptSettings.instance.update('samcliConfirmDevStack', false) + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((message) => { + message.dispose() }) - it('should return correct params from quickPicks', async () => { - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Specify parameters for deploy') { - assert.strictEqual(picker.items.length, 2) - assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') - assert.strictEqual(picker.items[1].label, 'Specify required parameters') - picker.acceptItem(picker.items[1]) - } else if (picker.title === 'Select a region') { - await picker.untilReady() - const select = picker.items.filter((i) => i.detail === 'us-west-2')[0] - picker.acceptItem(select || picker.items[0]) - } else if (picker.title === 'Select a CloudFormation Stack') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'stack1') - assert.strictEqual(picker.items[1].label, 'stack2') - assert.strictEqual(picker.items[2].label, 'stack3') - picker.acceptItem(picker.items[1]) - } else if (picker.title === 'Select an S3 Bucket') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'stack-1-bucket') - assert.strictEqual(picker.items[1].label, 'stack-2-bucket') - assert.strictEqual(picker.items[2].label, 'stack-3-bucket') - picker.acceptItem(picker.items[0]) - } else if (picker.title === 'Specify parameters for sync') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 9) - picker.acceptDefault() - } - }) + try { + await runSync('infra', appNode) + fail('should have thrown CancellationError') + } catch (error: any) { + assert(error instanceof CancellationError) + assert.strictEqual(error.agent, 'user') + } + }) - const parameters = await new SyncWizard( - { deployType: 'infra', template: { uri: appNode.resource.samTemplateUri, data: {} } }, - await globals.templateRegistry - ).run() + it('should abort when customer cancel sync wizard', async () => { + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) + sandbox.stub(SyncModule.SyncWizard.prototype, 'run').resolves(undefined) + + try { + await runSync('infra', appNode) + fail('should have thrown CancellationError') + } catch (error: any) { + assert(error instanceof CancellationError) + assert.strictEqual(error.agent, 'user') + } + }) - assert(parameters) + it('should throw ToolkitError when sync command fail', async () => { + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) - assert.strictEqual(parameters.template.uri.path, templateFile.path) - assert.strictEqual(parameters.projectRoot.path, projectRoot.path) - assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) - assert.strictEqual(parameters.region, 'us-west-2') - assert.strictEqual(parameters.stackName, 'stack2') - assert.strictEqual(parameters.bucketName, 'stack-1-bucket') + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + // Mock child process with required properties that get called in ProcessTerminal + mockSamSyncChildProcess = Object.create(ProcessUtilsModule.ChildProcess.prototype, { + stopped: { get: sandbox.stub().returns(false) }, + stop: { value: sandbox.stub().resolves({}) }, + run: { + value: sandbox.stub().resolves({ + exitCode: -1, + stdout: 'Mock sync command execution failure', + stderr: '', + }), + }, }) + mockChildProcessClass = sandbox.stub(ProcessUtilsModule, 'ChildProcess').returns(mockSamSyncChildProcess) + + try { + await runSync('infra', appNode) + fail('should have thrown ToolkitError') + } catch (error: any) { + assert(error instanceof ToolkitError) + assert.strictEqual(error.message, 'Failed to sync SAM application') + } + prompterTester.assertCallAll() }) + }) }) describe('saveAndBindArgs', () => { @@ -1054,7 +1962,7 @@ describe('saveAndBindArgs', () => { getConfigFileUriStub = sandbox.stub() // Replace the real implementations with stubs - sandbox.stub(sync, 'updateRecentResponse').resolves() + sandbox.stub(SyncModule, 'updateRecentResponse').resolves() }) afterEach(() => { diff --git a/packages/core/src/test/shared/wizards/prompterTester.ts b/packages/core/src/test/shared/wizards/prompterTester.ts index f9e34002405..432944ce947 100644 --- a/packages/core/src/test/shared/wizards/prompterTester.ts +++ b/packages/core/src/test/shared/wizards/prompterTester.ts @@ -11,8 +11,8 @@ export class PrompterTester { private quickPickHandlers: Map void> = new Map() private inputBoxHanlder: Map void> = new Map() private testWindow: TestWindow - private calledOrder = 0 - private report = new Map() + private callLog = Array() + private callLogCount = new Map() private constructor(testWindow?: TestWindow) { this.testWindow = testWindow || getTestWindow() @@ -24,30 +24,76 @@ export class PrompterTester { handleQuickPick(titlePattern: string, handler: (input: TestQuickPick) => void): PrompterTester { this.quickPickHandlers.set(titlePattern, handler) + this.callLogCount.set(titlePattern, 0) return this } handleInputBox(titlePattern: string, handler: (input: TestInputBox) => void): PrompterTester { this.inputBoxHanlder.set(titlePattern, handler) + this.callLogCount.set(titlePattern, 0) return this } - build(): void { + build(): PrompterTester { this.testWindow.onDidShowQuickPick((input) => { return this.handle(input, this.quickPickHandlers) }) this.testWindow.onDidShowInputBox((input) => { return this.handle(input, this.inputBoxHanlder) }) + return this } private record(title: string): void { - this.calledOrder++ - this.report.set(title, this.calledOrder) + this.callLog.push(title) + this.callLogCount.set(title, (this.callLogCount.get(title) ?? 0) + 1) + } + + /** + * Asserts that a specific prompter handler has been called the expected number of times. + * + * @param title - The title prompter to check. + * @param expectedCall - The expected number of times the prompted handler should have been called. + * @throws AssertionError if the actual number of calls doesn't match the expected number. + */ + assertCall(title: string, expectedCall: number) { + assert.strictEqual(this.callLogCount.get(title), expectedCall, title) + } + + /** + * Asserts that a specific prompter handler was called in the expected order. + * + * @param title - The title or identifier of the handler to check. + * @param expectedOrder - The expected position in the call order (one-based index). + * @throws AssertionError if the handler wasn't called in the expected order. + */ + assertCallOrder(title: string, expectedOrder: number) { + assert.strictEqual(this.callLog[expectedOrder - 1], title) } - public assertOrder(title: string, expectedOrder: number) { - assert.strictEqual(this.report.get(title) || 0, expectedOrder) + /** + * Asserts that all specified prompter handlers were called in the expected number of times. + * + * @param titles - The array of propmter handler titles to check. + * If not provided, all registered handler titles are used + * @param expectedCall - The expected number of times all specified handlers should have been called. + * If not provided, it defaults to 1. + * @throws AssertionError if the actual number of calls doesn't match the expected number. + */ + assertCallAll(titles: string[] = this.getHandlers(), expectedOrder: number = 1) { + titles.every((handler) => { + this.assertCall(handler, expectedOrder) + }) + } + + /** + * Retrieves all registered handler titles. + * + * @returns An array of strings containing all handler titles, including both + * quick pick handlers and input box handlers. + */ + getHandlers(): string[] { + return [...this.quickPickHandlers.keys(), ...this.inputBoxHanlder.keys()] } private handle(input: any, handlers: any) { @@ -55,7 +101,14 @@ export class PrompterTester { if (input.title?.includes(pattern)) { handler(input) this.record(pattern) + return } } + this.handleUnknownPrompter(input) + } + + private handleUnknownPrompter(input: any) { + input.dispose() + throw assert.fail(`Unexpected prompter titled: "${input.title}"`) } } diff --git a/packages/core/src/testInteg/sam.test.ts b/packages/core/src/testInteg/sam.test.ts index 03d00165e22..f2181e4ba77 100644 --- a/packages/core/src/testInteg/sam.test.ts +++ b/packages/core/src/testInteg/sam.test.ts @@ -25,7 +25,7 @@ import { AwsSamDebuggerConfiguration } from '../shared/sam/debugger/awsSamDebugC import { AwsSamTargetType } from '../shared/sam/debugger/awsSamDebugConfiguration' import { insertTextIntoFile } from '../shared/utilities/textUtilities' import globals from '../shared/extensionGlobals' -import { assertTelemetry, closeAllEditors, getWorkspaceFolder } from '../test/testUtil' +import { closeAllEditors, getWorkspaceFolder } from '../test/testUtil' import { ToolkitError } from '../shared/errors' import { SamAppLocation } from '../awsService/appBuilder/explorer/samProject' import { AppNode } from '../awsService/appBuilder/explorer/nodes/appNode' @@ -34,15 +34,6 @@ import { getTestWindow } from '../test/shared/vscode/window' import { ParamsSource, runBuild } from '../shared/sam/build' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import fs from '../shared/fs/fs' -import { BucketSource, DeployParams, DeployWizard, runDeploy } from '../shared/sam/deploy' -import * as sync from '../shared/sam/sync' -import * as deploy from '../shared/sam/deploy' -import * as processUtils from '../shared/utilities/processUtils' -import * as resolveEnv from '../shared/env/resolveEnv' -import * as cfnClient from '../shared/clients/cloudFormationClient' -import { runSync, SyncParams, SyncWizard, TemplateItem } from '../shared/sam/sync' -import { AsyncCollection } from '../shared/utilities/asyncCollection' -import { StackSummary } from 'aws-sdk/clients/cloudformation' const projectFolder = testUtils.getTestWorkspaceFolder() @@ -742,285 +733,6 @@ describe('SAM Integration Tests', async function () { }) }) - describe('SAM Deploy', () => { - let sandbox: sinon.SinonSandbox - let workspaceFolder: vscode.WorkspaceFolder - let childProcessStub: sinon.SinonStub - // We're testing only one case (python 3.10 (ZIP)) on the integration tests. More niche cases are handled as unit tests. - const scenarioIndex = 2 - const scenario = scenarios[scenarioIndex] - let testRoot: string - let testDir: string - let cfnTemplatePath: string - let mockDeployParams: DeployParams - let mockGetSpawnEnv: sinon.SinonStub - let mockGetSamCliPath: sinon.SinonStub - let mockRunInTerminal: sinon.SinonStub - let cfnClientStub: sinon.SinonStub - let appNode: AppNode - - before(async function () { - workspaceFolder = getWorkspaceFolder(testSuiteRoot) - testRoot = path.join(testSuiteRoot, scenario.runtime) - await fs.mkdir(testRoot) - - testDir = mkdtempSync(path.join(testRoot, 'samapp-')) - console.log(`testDir: ${testDir}`) - - await createSamApplication(testDir, scenario) - - cfnTemplatePath = path.join(testDir, samApplicationName, 'template.yaml') - - mockDeployParams = { - paramsSource: deploy.ParamsSource.Specify, - region: 'us-east-1', - stackName: 'my-stack', - bucketName: 'my-bucket-name', - template: { uri: vscode.Uri.file(cfnTemplatePath), data: {} } as TemplateItem, - bucketSource: BucketSource.UserProvided, - projectRoot: vscode.Uri.file(path.join(testDir, samApplicationName)), - } - }) - - after(async function () { - await tryRemoveFolder(testSuiteRoot) - // don't clean up after java tests so the java language server doesn't freak out - if (scenario.language !== 'java') { - await tryRemoveFolder(testRoot) - } - }) - - beforeEach(() => { - sandbox = sinon.createSandbox() - childProcessStub = sandbox.stub().returns({}) - cfnClientStub = sandbox.stub().returns({ - listAllStacks: () => - Promise.resolve([ - [ - { - StackName: 'my-stack', - StackStatus: 'CREATE_COMPLETE', - }, - ], - ] as unknown as AsyncCollection), - }) - sandbox.stub(cfnClient, 'DefaultCloudFormationClient').returns(cfnClientStub) - sandbox.stub(processUtils, 'ChildProcess').callsFake(childProcessStub) - mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - - const samAppLocation = { - samTemplateUri: vscode.Uri.file(cfnTemplatePath), - workspaceFolder: workspaceFolder, - } as SamAppLocation - appNode = new AppNode(samAppLocation) - }) - - afterEach(() => { - sandbox.restore() - }) - - it('should instantiate ChildProcess with the correct arguments for build', async () => { - // Stubbing necessary functions - sandbox.stub(DeployWizard.prototype, 'run').returns(Promise.resolve(mockDeployParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - - // Execute the function - await runDeploy(appNode) - - // Check that ChildProcess is instantiated correctly - assert.strictEqual(childProcessStub.callCount, 2) // ChildProcess should be called twice' - assert.deepEqual(childProcessStub.getCall(0).args, [ - 'sam-cli-path', - ['build', '--cached'], - { - spawnOptions: { - cwd: mockDeployParams.projectRoot?.fsPath, - env: { - AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', - SAM_CLI_TELEMETRY: '0', - }, - }, - }, - ]) - - // Check that runInTerminal is called with the correct arguments for build - assert.strictEqual(mockRunInTerminal.callCount, 2) - const buildProcess = childProcessStub.getCall(0).returnValue // Get the instance from the first call - assert.deepEqual(mockRunInTerminal.getCall(0).args, [buildProcess, 'build']) - assertTelemetry('sam_deploy', { result: 'Succeeded', source: 'appBuilderDeploy' }) - }) - - it('should instantiate ChildProcess with the correct arguments for deploy', async () => { - const mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - const mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - const mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - - // Stubbing necessary functions - sandbox.stub(DeployWizard.prototype, 'run').returns(Promise.resolve(mockDeployParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - - // Execute the function - await runDeploy(appNode) - - // Check that ChildProcess is instantiated correctly for deploy - assert.strictEqual(childProcessStub.callCount, 2) // ChildProcess should be called twice' - assert.deepEqual(childProcessStub.getCall(1).args[0], 'sam-cli-path') - assert.deepEqual(childProcessStub.getCall(1).args[1], [ - 'deploy', - '--no-confirm-changeset', - '--region', - `${mockDeployParams.region}`, - '--stack-name', - `${mockDeployParams.stackName}`, - '--s3-bucket', - `${mockDeployParams.bucketName}`, - '--capabilities', - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ]) - - // Check that runInTerminal is called with the correct arguments for deploy - const deployProcess = childProcessStub.getCall(1).returnValue // Get the instance from the second call - assert.ok( - mockRunInTerminal.calledTwice, - 'runInTerminal should be called twice, once for build and once for deploy' - ) - assert.deepEqual(mockRunInTerminal.getCall(1).args, [deployProcess, 'deploy']) - assertTelemetry('sam_deploy', { result: 'Succeeded', source: 'appBuilderDeploy' }) - }) - }) - - describe('SAM Sync', () => { - let sandbox: sinon.SinonSandbox - let childProcessStub: sinon.SinonStub - let workspaceFolder: vscode.WorkspaceFolder - // We're testing only one case (python 3.10 (ZIP)) on the integration tests. More niche cases are handled as unit tests. - const scenarioIndex = 2 - const scenario = scenarios[scenarioIndex] - let testRoot: string - let testDir: string - let cfnTemplatePath: string - let mockSyncParams: SyncParams - let mockGetSpawnEnv: sinon.SinonStub - let mockGetSamCliPath: sinon.SinonStub - let mockRunInTerminal: sinon.SinonStub - - before(async function () { - workspaceFolder = getWorkspaceFolder(testSuiteRoot) - testRoot = path.join(testSuiteRoot, scenario.runtime) - await fs.mkdir(testRoot) - - testDir = mkdtempSync(path.join(testRoot, 'samapp-')) - console.log(`testDir: ${testDir}`) - - await createSamApplication(testDir, scenario) - - cfnTemplatePath = path.join(testDir, samApplicationName, 'template.yaml') - - mockSyncParams = { - paramsSource: sync.ParamsSource.SpecifyAndSave, - region: 'us-east-1', - stackName: 'my-stack', - bucketName: 'my-bucket-name', - deployType: 'code', - skipDependencyLayer: true, - connection: { - id: '', - type: 'iam', - label: '', - getCredentials: () => Promise.resolve({ accessKeyId: 'AAAAA', secretAccessKey: 'XXXXXX' }), - }, - template: { uri: vscode.Uri.file(cfnTemplatePath), data: {} } as TemplateItem, - bucketSource: BucketSource.UserProvided, - projectRoot: vscode.Uri.file(path.join(testDir, samApplicationName)), - } - }) - - after(async function () { - await tryRemoveFolder(testSuiteRoot) - // don't clean up after java tests so the java language server doesn't freak out - if (scenario.language !== 'java') { - await tryRemoveFolder(testRoot) - } - }) - - beforeEach(() => { - sandbox = sinon.createSandbox() - childProcessStub = sandbox.stub().returns({}) - sandbox.stub(processUtils, 'ChildProcess').callsFake(childProcessStub) - mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - }) - - afterEach(() => { - sandbox.restore() - }) - - it('should instantiate ChildProcess with the correct arguments for sync', async () => { - // Stubbing necessary functions - sandbox.stub(SyncWizard.prototype, 'run').returns(Promise.resolve(mockSyncParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - const mockConfirmDevStack = sandbox.stub().returns(Promise.resolve()) - sandbox.stub(sync, 'confirmDevStack').callsFake(mockConfirmDevStack) - - const samAppLocation = { - samTemplateUri: vscode.Uri.file(cfnTemplatePath), - workspaceFolder: workspaceFolder, - } as SamAppLocation - const appNode = new AppNode(samAppLocation) - - // Execute the function - await runSync('infra', appNode) - - // Check that ChildProcess is instantiated correctly - assert.strictEqual(childProcessStub.callCount, 1) // ChildProcess should be called once' - assert.deepEqual(childProcessStub.getCall(0).args, [ - 'sam-cli-path', - [ - 'sync', - '--codeOnly', - '--template', - `${mockSyncParams.template.uri.fsPath}`, - '--s3-bucket', - `${mockSyncParams.bucketName}`, - '--stack-name', - `${mockSyncParams.stackName}`, - '--region', - `${mockSyncParams.region}`, - '--no-dependency-layer', - ], - { - spawnOptions: { - cwd: mockSyncParams.projectRoot?.fsPath, - env: { - AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', - SAM_CLI_TELEMETRY: '0', - }, - }, - }, - ]) - - // Check that runInTerminal is called with the correct arguments for build - assert.strictEqual(mockRunInTerminal.callCount, 1) - const syncProcess = childProcessStub.getCall(0).returnValue // Get the instance from the first call - assert.deepEqual(mockRunInTerminal.getCall(0).args, [syncProcess, 'sync']) - assertTelemetry('sam_sync', { - result: 'Succeeded', - syncedResources: 'AllResources', - source: 'appBuilderSync', - }) - }) - }) - async function createSamApplication(location: string, scenario: TestScenario): Promise { const initArguments: SamCliInitArgs = { name: samApplicationName,