From 0eba2dcdfeabba58920462ac2cd54d86e3101e05 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Fri, 11 Jun 2021 12:25:02 +0200 Subject: [PATCH] feat(CLI Onboarding): Add telemetry for interactive flow --- lib/cli/interactive-setup/aws-credentials.js | 74 ++++-- lib/cli/interactive-setup/deploy.js | 16 +- lib/cli/interactive-setup/index.js | 21 +- lib/cli/interactive-setup/service.js | 51 +++- lib/utils/telemetry/generatePayload.js | 5 + scripts/serverless.js | 16 +- .../interactive-setup/aws-credentials.test.js | 225 ++++++++++++------ .../lib/cli/interactive-setup/deploy.test.js | 118 +++++---- .../lib/cli/interactive-setup/service.test.js | 178 +++++++++++--- .../utils/telemetry/generatePayload.test.js | 1 + 10 files changed, 508 insertions(+), 197 deletions(-) diff --git a/lib/cli/interactive-setup/aws-credentials.js b/lib/cli/interactive-setup/aws-credentials.js index 6f65153b8ab..bcb236c8bf4 100644 --- a/lib/cli/interactive-setup/aws-credentials.js +++ b/lib/cli/interactive-setup/aws-credentials.js @@ -77,8 +77,8 @@ const getProviders = memoizee( } ); -const awsAccessKeyIdInput = async () => - ( +const awsAccessKeyIdInput = async ({ stepHistory }) => { + const accessKeyId = ( await inquirer.prompt({ message: 'AWS Access Key Id:', type: 'input', @@ -89,9 +89,12 @@ const awsAccessKeyIdInput = async () => }, }) ).accessKeyId.trim(); + stepHistory.set('accessKeyId', '_user_provided_'); + return accessKeyId; +}; -const awsSecretAccessKeyInput = async () => - ( +const awsSecretAccessKeyInput = async ({ stepHistory }) => { + const secretAccessKey = ( await inquirer.prompt({ message: 'AWS Secret Access Key:', type: 'input', @@ -105,8 +108,11 @@ const awsSecretAccessKeyInput = async () => }, }) ).secretAccessKey.trim(); + stepHistory.set('secretAccessKey', '_user_provided_'); + return secretAccessKey; +}; -const credentialsSetupChoice = async (providers) => { +const credentialsSetupChoice = async (context, providers) => { let credentialsSetupChoices = []; let message = 'No AWS credentials found, what credentials do you want to use?'; @@ -142,7 +148,7 @@ const credentialsSetupChoice = async (providers) => { { name: 'Skip', value: CREDENTIALS_SETUP_CHOICE.SKIP } ); - return ( + const result = ( await inquirer.prompt({ message, type: 'list', @@ -150,6 +156,12 @@ const credentialsSetupChoice = async (providers) => { choices: credentialsSetupChoices, }) ).credentialsSetupChoice; + + context.stepHistory.set( + 'credentialsSetupChoice', + result.startsWith('_') ? result : '_user_provided_' + ); + return result; }; const steps = { @@ -158,15 +170,16 @@ const steps = { http://slss.io/aws-creds-setup\n`), - ensureAwsAccount: async () => { + ensureAwsAccount: async ({ stepHistory }) => { if (await confirm('Do you have an AWS account?', { name: 'hasAwsAccount' })) return; openBrowser('https://portal.aws.amazon.com/billing/signup'); await inquirer.prompt({ message: 'Press Enter to continue after creating an AWS account', name: 'createAwsAccountPrompt', }); + stepHistory.set('createAwsAccountPrompt', true); }, - ensureAwsCredentials: async ({ options, configuration }) => { + ensureAwsCredentials: async ({ options, configuration, stepHistory }) => { const region = options.region || configuration.provider.region || 'us-east-1'; openBrowser( `https://console.aws.amazon.com/iam/home?region=${region}#/users$new?step=final&accessKey&userNames=serverless&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess` @@ -175,10 +188,11 @@ const steps = { message: 'Press Enter to continue after creating an AWS user with access keys', name: 'generateAwsCredsPrompt', }); + stepHistory.set('generateAwsCredsPrompt', true); }, - inputAwsCredentials: async () => { - const accessKeyId = await awsAccessKeyIdInput(); - const secretAccessKey = await awsSecretAccessKeyInput(); + inputAwsCredentials: async (context) => { + const accessKeyId = await awsAccessKeyIdInput(context); + const secretAccessKey = await awsSecretAccessKeyInput(context); await awsCredentials.saveFileProfiles(new Map([['default', { accessKeyId, secretAccessKey }]])); process.stdout.write( `\n${chalk.green( @@ -188,7 +202,7 @@ const steps = { )}\n` ); }, - handleProviderCreation: async ({ configuration: { org: orgName } }) => { + handleProviderCreation: async ({ configuration: { org: orgName }, stepHistory }) => { const providersUrl = `https://app.serverless.com/${orgName}/settings/providers?source=cli&providerId=new&provider=aws`; openBrowser(chalk.bold.white(providersUrl)); process.stdout.write( @@ -210,7 +224,10 @@ const steps = { name: 'skipProviderSetup', }); - inquirerPrompt.then(() => resolve(null)); + inquirerPrompt.then(() => { + stepHistory.set('skipProviderSetup', true); + resolve(null); + }); }, timeoutDuration); onEvent = (event) => { @@ -307,10 +324,17 @@ module.exports = { _.get(configuration, 'provider') !== 'aws' && _.get(configuration, 'provider.name') !== 'aws' ) { + context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER'; + return false; + } + if (new AWS.S3().config.credentials) { + context.inapplicabilityReasonCode = 'LOCAL_CREDENTIALS_CONFIGURED'; + return false; + } + if ((await awsCredentials.resolveFileProfiles()).size) { + context.inapplicabilityReasonCode = 'LOCAL_CREDENTIAL_PROFILES_CONFIGURED'; return false; } - if (new AWS.S3().config.credentials) return false; - if ((await awsCredentials.resolveFileProfiles()).size) return false; const orgName = configuration.org; if (orgName && isAuthenticated()) { @@ -326,7 +350,10 @@ module.exports = { } const hasDefaultProvider = providers.some((provider) => provider.isDefault); - if (hasDefaultProvider) return false; + if (hasDefaultProvider) { + context.inapplicabilityReasonCode = 'DEFAULT_PROVIDER_CONFIGURED'; + return false; + } // For situation where it is invoked for already existing service // We need to check if service already has a linked provider @@ -335,6 +362,7 @@ module.exports = { !history.has('service') && (await doesServiceInstanceHaveLinkedProvider({ configuration, options })) ) { + context.inapplicabilityReasonCode = 'LINKED_PROVIDER_CONFIGURED'; return false; } } @@ -357,7 +385,7 @@ module.exports = { throw err; } } - const credentialsSetupChoiceAnswer = await credentialsSetupChoice(providers); + const credentialsSetupChoiceAnswer = await credentialsSetupChoice(context, providers); if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER) { try { @@ -386,9 +414,9 @@ module.exports = { steps.writeOnSetupSkip(); return; } else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.LOCAL) { - await steps.ensureAwsAccount(); + await steps.ensureAwsAccount(context); await steps.ensureAwsCredentials(context); - await steps.inputAwsCredentials(); + await steps.inputAwsCredentials(context); return; } @@ -406,4 +434,12 @@ module.exports = { } }, steps, + configuredQuestions: [ + 'credentialsSetupChoice', + 'createAwsAccountPrompt', + 'generateAwsCredsPrompt', + 'accessKeyId', + 'secretAccessKey', + 'skipProviderSetup', + ], }; diff --git a/lib/cli/interactive-setup/deploy.js b/lib/cli/interactive-setup/deploy.js index 69ff597afc0..16c5d03453f 100644 --- a/lib/cli/interactive-setup/deploy.js +++ b/lib/cli/interactive-setup/deploy.js @@ -100,8 +100,10 @@ const configurePlugin = (serverless, originalStdWrite) => { }; module.exports = { - async isApplicable({ configuration, serviceDir, history, options }) { + async isApplicable(context) { + const { configuration, serviceDir, history, options } = context; if (!serviceDir) { + context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY'; return false; } @@ -109,6 +111,7 @@ module.exports = { _.get(configuration, 'provider') !== 'aws' && _.get(configuration, 'provider.name') !== 'aws' ) { + context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER'; return false; } @@ -127,11 +130,17 @@ module.exports = { // We want to proceed if local credentials are available if (new AWS.Config().credentials) return true; + context.inapplicabilityReasonCode = 'NO_CREDENTIALS_CONFIGURED'; return false; }, - async run({ configuration, configurationFilename, serviceDir }) { + async run({ configuration, configurationFilename, serviceDir, stepHistory }) { const serviceName = configuration.service; - if (!(await confirm('Do you want to deploy your project?', { name: 'shouldDeploy' }))) { + const shouldDeploy = await confirm('Do you want to deploy your project?', { + name: 'shouldDeploy', + }); + stepHistory.set('shouldDeploy', shouldDeploy); + + if (!shouldDeploy) { printMessage({ serviceName, hasBeenDeployed: false, @@ -174,4 +183,5 @@ module.exports = { dashboardPlugin: serverless.pluginManager.dashboardPlugin, }); }, + configuredQuestions: ['shouldDeploy'], }; diff --git a/lib/cli/interactive-setup/index.js b/lib/cli/interactive-setup/index.js index de6e3f3677c..c53ee3efbd1 100644 --- a/lib/cli/interactive-setup/index.js +++ b/lib/cli/interactive-setup/index.js @@ -1,6 +1,7 @@ 'use strict'; const inquirer = require('@serverless/utils/inquirer'); +const { StepHistory } = require('@serverless/utils/telemetry'); const steps = { service: require('./service'), @@ -12,14 +13,32 @@ const steps = { module.exports = async (context) => { context = { ...context, inquirer, history: new Map() }; + const stepsDetails = new Map(); for (const [stepName, step] of Object.entries(steps)) { delete context.stepHistory; + delete context.inapplicabilityReasonCode; const stepData = await step.isApplicable(context); + stepsDetails.set(stepName, { + isApplicable: Boolean(stepData), + inapplicabilityReasonCode: context.inapplicabilityReasonCode, + timestamp: Date.now(), + configuredQuestions: step.configuredQuestions, + }); if (stepData) { process.stdout.write('\n'); - context.stepHistory = []; + context.stepHistory = new StepHistory(); context.history.set(stepName, context.stepHistory); await step.run(context, stepData); } } + + const commandUsage = Array.from(stepsDetails.entries()).map(([step, stepDetails]) => { + const stepHistory = context.history.get(step); + return { + name: step, + ...stepDetails, + history: stepHistory ? stepHistory.toJSON() : [], + }; + }); + return { commandUsage, configuration: context.configuration }; }; diff --git a/lib/cli/interactive-setup/service.js b/lib/cli/interactive-setup/service.js index e9dadd5a9f4..db4c25c5f50 100644 --- a/lib/cli/interactive-setup/service.js +++ b/lib/cli/interactive-setup/service.js @@ -32,8 +32,8 @@ const initializeProjectChoices = [ { name: 'Other', value: 'other' }, ]; -const projectTypeChoice = async () => - ( +const projectTypeChoice = async (stepHistory) => { + const projectType = ( await inquirer.prompt({ message: 'What do you want to make?', type: 'list', @@ -43,14 +43,18 @@ const projectTypeChoice = async () => }) ).projectType; + stepHistory.set('projectType', projectType); + return projectType; +}; + const INVALID_PROJECT_NAME_MESSAGE = 'Project name is not valid.\n' + ' - It should only contain alphanumeric and hyphens.\n' + ' - It should start with an alphabetic character.\n' + " - Shouldn't exceed 128 characters"; -const projectNameInput = async (workingDir, projectType) => - ( +const projectNameInput = async (workingDir, projectType, stepHistory) => { + const projectName = ( await inquirer.prompt({ message: 'What do you want to call this project?', type: 'input', @@ -72,7 +76,11 @@ const projectNameInput = async (workingDir, projectType) => }) ).projectName.trim(); -const resolveProjectNameInput = async (options, workingDir, projectType = null) => { + stepHistory.set('projectName', '_user_provided_'); + return projectName; +}; + +const resolveProjectNameInput = async ({ options, workingDir, projectType, stepHistory }) => { if (options.name) { if (!isValidServiceName(options.name)) { throw new ServerlessError(INVALID_PROJECT_NAME_MESSAGE, 'INVALID_PROJECT_NAME'); @@ -96,11 +104,12 @@ const resolveProjectNameInput = async (options, workingDir, projectType = null) return options.name; } - return projectNameInput(workingDir, projectType); + return projectNameInput(workingDir, projectType, stepHistory); }; module.exports = { - isApplicable({ options, serviceDir }) { + isApplicable(context) { + const { options, serviceDir } = context; const notApplicableOptions = new Set(['name', 'template-path', 'template', 'template-url']); if (serviceDir && Object.keys(options).some((key) => notApplicableOptions.has(key))) { throw new ServerlessError( @@ -113,7 +122,11 @@ module.exports = { ); } - return !serviceDir; + const inServiceDir = Boolean(serviceDir); + if (inServiceDir) { + context.inapplicabilityReasonCode = 'IN_SERVICE_DIRECTORY'; + } + return !inServiceDir; }, async run(context) { const workingDir = context.cwd || process.cwd(); @@ -132,7 +145,11 @@ module.exports = { let projectDir; let projectName; if (context.options['template-path']) { - projectName = await resolveProjectNameInput(context.options, workingDir); + projectName = await resolveProjectNameInput({ + options: context.options, + workingDir, + stepHistory: context.stepHistory, + }); projectDir = join(workingDir, projectName); await createFromLocalTemplate({ templatePath: context.options['template-path'], @@ -140,7 +157,11 @@ module.exports = { projectName, }); } else if (context.options['template-url']) { - projectName = await resolveProjectNameInput(context.options, workingDir); + projectName = await resolveProjectNameInput({ + options: context.options, + workingDir, + stepHistory: context.stepHistory, + }); projectDir = join(workingDir, projectName); const templateUrl = context.options['template-url']; process.stdout.write(`\nDownloading template from provided url: ${templateUrl}...\n`); @@ -159,7 +180,7 @@ module.exports = { if (context.options.template) { projectType = context.options.template; } else { - projectType = await projectTypeChoice(); + projectType = await projectTypeChoice(context.stepHistory); if (projectType === 'other') { process.stdout.write( '\nRun “serverless create --help” to view available templates and create a new project ' + @@ -168,7 +189,12 @@ module.exports = { return; } } - projectName = await resolveProjectNameInput(context.options, workingDir, projectType); + projectName = await resolveProjectNameInput({ + options: context.options, + workingDir, + projectType, + stepHistory: context.stepHistory, + }); projectDir = join(workingDir, projectName); const templateUrl = `https://github.com/serverless/examples/tree/master/${projectType}`; process.stdout.write(`\nDownloading "${projectType}" template...\n`); @@ -238,4 +264,5 @@ module.exports = { context.configuration = await readConfiguration(configurationPath); await resolveVariables(context); }, + configuredQuestions: ['projectType', 'projectName'], }; diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index 9ecf3ef95aa..10874ba75df 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -123,6 +123,7 @@ module.exports = async ({ serviceDir, configuration, serverless, + commandUsage, }) => { let commandDurationMs; @@ -268,5 +269,9 @@ module.exports = async ({ payload.dashboard.orgUid = serverless && serverless.service.orgUid; } + if (commandUsage) { + payload.commandUsage = commandUsage; + } + return payload; }; diff --git a/scripts/serverless.js b/scripts/serverless.js index 1d7ec5d2dd0..c44f0a7244f 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -403,12 +403,13 @@ const processSpanPromise = (async () => { 'INTERACTIVE_SETUP_IN_NON_TTY' ); } - await require('../lib/cli/interactive-setup')({ - configuration, - serviceDir, - configurationFilename, - options, - }); + const { commandUsage, configuration: configurationFromInteractive } = + await require('../lib/cli/interactive-setup')({ + configuration, + serviceDir, + configurationFilename, + options, + }); hasTelemetryBeenReported = true; if (!isTelemetryDisabled) { await storeTelemetryLocally( @@ -417,7 +418,8 @@ const processSpanPromise = (async () => { options, commandSchema, serviceDir, - configuration, + configuration: configurationFromInteractive, + commandUsage, }) ); await sendTelemetry({ serverlessExecutionSpan: processSpanPromise }); diff --git a/test/unit/lib/cli/interactive-setup/aws-credentials.test.js b/test/unit/lib/cli/interactive-setup/aws-credentials.test.js index 2a6809fe530..ba793afa5c3 100644 --- a/test/unit/lib/cli/interactive-setup/aws-credentials.test.js +++ b/test/unit/lib/cli/interactive-setup/aws-credentials.test.js @@ -7,6 +7,7 @@ const overrideEnv = require('process-utils/override-env'); const overrideStdoutWrite = require('process-utils/override-stdout-write'); const requireUncached = require('ncjsm/require-uncached'); const chalk = require('chalk'); +const { StepHistory } = require('@serverless/utils/telemetry'); const { expect } = chai; @@ -53,17 +54,21 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { sinon.restore(); }); - it('Should be ineffective, when not at service path', async () => - expect(await step.isApplicable({})).to.equal(false)); + it('Should be ineffective, when not at service path', async () => { + const context = {}; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER'); + }); - it('Should be ineffective, when not at AWS service', async () => - expect( - await step.isApplicable({ - serviceDir: process.cwd(), - configuration: {}, - configurationFilename: 'serverless.yml', - }) - ).to.equal(false)); + it('Should be ineffective, when not at AWS service', async () => { + const context = { + serviceDir: process.cwd(), + configuration: {}, + configurationFilename: 'serverless.yml', + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER'); + }); it('Should be ineffective, when user has default provider set', async () => { const internalMockedSdk = { @@ -92,13 +97,13 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { '@serverless/dashboard-plugin/lib/isAuthenticated': () => true, }); - expect( - await mockedStep.isApplicable({ - serviceDir: process.cwd(), - configuration: { provider: { name: 'aws' }, org: 'someorg' }, - configurationFilename: 'serverless.yml', - }) - ).to.be.false; + const context = { + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' }, org: 'someorg' }, + configurationFilename: 'serverless.yml', + }; + expect(await mockedStep.isApplicable(context)).to.be.false; + expect(context.inapplicabilityReasonCode).to.equal('DEFAULT_PROVIDER_CONFIGURED'); }); it('Should be ineffective, when existing service already has a provider set', async () => { @@ -131,20 +136,20 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }, }); - expect( - await mockedStep.isApplicable({ - history: new Set(), - serviceDir: process.cwd(), - configuration: { - provider: { name: 'aws' }, - org: 'someorg', - app: 'someapp', - service: 'service', - }, - options: {}, - configurationFilename: 'serverless.yml', - }) - ).to.be.false; + const context = { + history: new Set(), + serviceDir: process.cwd(), + configuration: { + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + service: 'service', + }, + options: {}, + configurationFilename: 'serverless.yml', + }; + expect(await mockedStep.isApplicable(context)).to.be.false; + expect(context.inapplicabilityReasonCode).to.equal('LINKED_PROVIDER_CONFIGURED'); }); it('Should be effective, when existing service instance does not have a provider set', async () => { @@ -239,18 +244,23 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { list: { credentialsSetupChoice: '_skip_' }, }); + const context = { + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' } }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + options: {}, + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await step.run({ - serviceDir: process.cwd(), - configuration: { provider: { name: 'aws' }, org: 'someorg' }, - configurationFilename: 'serverless.yml', - }) + async () => await step.run(context) ); expect(stdoutData).to.include('You can setup your AWS account later'); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_skip_']]) + ); }); describe('In environment credentials', () => { @@ -323,13 +333,27 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { secretAccessKey, }, }); - await step.run({ configuration: { provider: {} }, options: {} }); + const context = { + configuration: { provider: {} }, + options: {}, + stepHistory: new StepHistory(), + }; + await step.run(context); expect(openBrowserUrls.length).to.equal(2); expect(openBrowserUrls[0].includes('signup')).to.be.true; expect(openBrowserUrls[1].includes('console.aws.amazon.com')).to.be.true; resolveFileProfiles().then((profiles) => { expect(profiles).to.deep.equal(new Map([['default', { accessKeyId, secretAccessKey }]])); }); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['credentialsSetupChoice', '_local_'], + ['createAwsAccountPrompt', true], + ['generateAwsCredsPrompt', true], + ['accessKeyId', '_user_provided_'], + ['secretAccessKey', '_user_provided_'], + ]) + ); }); it('Should setup credentials for users having an AWS account', async () => { @@ -338,9 +362,22 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey }, }); - await step.run({ configuration: { provider: {} }, options: {} }); + const context = { + configuration: { provider: {} }, + options: {}, + stepHistory: new StepHistory(), + }; + await step.run(context); expect(openBrowserUrls.length).to.equal(1); expect(openBrowserUrls[0].includes('console.aws.amazon.com')).to.be.true; + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['credentialsSetupChoice', '_local_'], + ['generateAwsCredsPrompt', true], + ['accessKeyId', '_user_provided_'], + ['secretAccessKey', '_user_provided_'], + ]) + ); return resolveFileProfiles().then((profiles) => { expect(profiles).to.deep.equal(new Map([['default', { accessKeyId, secretAccessKey }]])); }); @@ -352,12 +389,21 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId: 'foo', secretAccessKey }, }); - await expect( - step.run({ - configuration: { provider: {} }, - options: {}, - }) - ).to.eventually.be.rejected.and.have.property('code', 'INVALID_ANSWER'); + const context = { + configuration: { provider: {} }, + options: {}, + stepHistory: new StepHistory(), + }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( + 'code', + 'INVALID_ANSWER' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['credentialsSetupChoice', '_local_'], + ['generateAwsCredsPrompt', true], + ]) + ); }); it('Should not accept invalid secret access key', async () => { @@ -366,12 +412,22 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey: 'foo' }, }); - await expect( - step.run({ - configuration: { provider: {} }, - options: {}, - }) - ).to.eventually.be.rejected.and.have.property('code', 'INVALID_ANSWER'); + const context = { + configuration: { provider: {} }, + options: {}, + stepHistory: new StepHistory(), + }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( + 'code', + 'INVALID_ANSWER' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['credentialsSetupChoice', '_local_'], + ['generateAwsCredsPrompt', true], + ['accessKeyId', '_user_provided_'], + ]) + ); }); }); @@ -418,6 +474,7 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }, options: {}, configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), }; await overrideStdoutWrite( (data) => (stdoutData += data), @@ -432,6 +489,9 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { ); expect(mockedDisconnect).to.have.been.called; expect(mockedCreateProviderLink).not.to.have.been.called; + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_create_provider_']]) + ); }); it('Should correctly setup with newly created provider when previous providers exist', async () => { @@ -490,6 +550,7 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }, options: {}, configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), }; let stdoutData = ''; await overrideStdoutWrite( @@ -510,6 +571,9 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { 'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1', providerUid ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_create_provider_']]) + ); }); it('Should emit warning when dashboard unavailable when connecting to it', async () => { @@ -533,20 +597,21 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { list: { credentialsSetupChoice: '_create_provider_' }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await mockedStep.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - org: 'someorg', - app: 'someapp', - }, - configurationFilename: 'serverless.yml', - }) + async () => await mockedStep.run(context) ); expect(stdoutData).to.include('Dashboard service is currently unavailable'); @@ -555,6 +620,9 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { 'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws' ) ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_create_provider_']]) + ); }); it('Should correctly setup with existing provider', async () => { @@ -599,6 +667,7 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }, options: {}, configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), }; let stdoutData = ''; await overrideStdoutWrite( @@ -613,6 +682,9 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { 'provideruid' ); expect(stdoutData).to.include('Selected provider was successfully linked'); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_user_provided_']]) + ); }); it('Should emit a warning when dashboard is not available and link cannot be created', async () => { @@ -651,21 +723,22 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { list: { credentialsSetupChoice: providerUid }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + options: {}, + stepHistory: new StepHistory(), + configurationFilename: 'serverless.yml', + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await mockedStep.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - org: 'someorg', - app: 'someapp', - }, - options: {}, - configurationFilename: 'serverless.yml', - }) + async () => await mockedStep.run(context) ); expect(stdoutData).to.include( @@ -678,6 +751,10 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { 'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1', 'provideruid' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['credentialsSetupChoice', '_user_provided_']]) + ); }); it('Should emit a warning when dashboard is not available when fetching providers', async () => { diff --git a/test/unit/lib/cli/interactive-setup/deploy.test.js b/test/unit/lib/cli/interactive-setup/deploy.test.js index af4eceae271..b0a38c8e4d5 100644 --- a/test/unit/lib/cli/interactive-setup/deploy.test.js +++ b/test/unit/lib/cli/interactive-setup/deploy.test.js @@ -8,6 +8,7 @@ const overrideEnv = require('process-utils/override-env'); const step = require('../../../../../lib/cli/interactive-setup/deploy'); const proxyquire = require('proxyquire'); const overrideStdoutWrite = require('process-utils/override-stdout-write'); +const { StepHistory } = require('@serverless/utils/telemetry'); const { expect } = chai; @@ -17,18 +18,24 @@ chai.use(require('sinon-chai')); const inquirer = require('@serverless/utils/inquirer'); describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => { - it('Should be not applied, when not at service path', async () => - expect(await step.isApplicable({ options: {} })).to.equal(false)); + it('Should be not applied, when not at service path', async () => { + const context = { + options: {}, + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NOT_IN_SERVICE_DIRECTORY'); + }); - it('Should be not applied, when service is not configured with AWS provider', async () => - expect( - await step.isApplicable({ - configuration: { provider: { name: 'notaws' } }, - serviceDir: '/foo', - options: {}, - history: new Map(), - }) - ).to.equal(false)); + it('Should be not applied, when service is not configured with AWS provider', async () => { + const context = { + configuration: { provider: { name: 'notaws' } }, + serviceDir: '/foo', + options: {}, + history: new Map([['service', []]]), + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER'); + }); it('Should be applied, if awsCredentials step was not executed which means user already had credentials', async () => expect( @@ -80,22 +87,24 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => { confirm: { shouldDeploy: false }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await step.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - }, - configurationFilename: 'serverless.yml', - }) + async () => await step.run(context) ); expect(stdoutData).to.include('Your project is ready for deployment'); expect(stdoutData).to.include(`Run ${chalk.bold('serverless')} in the project directory`); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['shouldDeploy', false]])); }); it('should correctly handle skipping deployment for service not configured with dashboard', async () => { @@ -103,24 +112,26 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => { confirm: { shouldDeploy: false }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await step.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - org: 'someorg', - app: 'someapp', - }, - configurationFilename: 'serverless.yml', - }) + async () => await step.run(context) ); expect(stdoutData).to.include('Your project is ready for deployment'); expect(stdoutData).to.include('Invoke your functions and view logs in the dashboard'); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['shouldDeploy', false]])); }); it('should correctly handle deployment for service configured with dashboard', async () => { @@ -156,26 +167,29 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => { confirm: { shouldDeploy: true }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await mockedStep.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - org: 'someorg', - app: 'someapp', - }, - configurationFilename: 'serverless.yml', - }) + async () => await mockedStep.run(context) ); expect(stdoutData).to.include('Your project is live and available'); expect(stdoutData).to.include( `Open ${chalk.bold('https://app.serverless-dev.com/path/to/dashboard')}` ); + + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['shouldDeploy', true]])); }); it('should correctly handle deployment for service not configured with dashboard', async () => { @@ -207,22 +221,24 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => { confirm: { shouldDeploy: true }, }); + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + }, + configurationFilename: 'serverless.yml', + stepHistory: new StepHistory(), + }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => - await mockedStep.run({ - serviceDir: process.cwd(), - configuration: { - service: 'someservice', - provider: { name: 'aws' }, - }, - configurationFilename: 'serverless.yml', - }) + async () => await mockedStep.run(context) ); expect(stdoutData).to.include('Your project is live and available'); expect(stdoutData).to.include(`Run ${chalk.bold('serverless')}`); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['shouldDeploy', true]])); }); }); }); diff --git a/test/unit/lib/cli/interactive-setup/service.test.js b/test/unit/lib/cli/interactive-setup/service.test.js index 0f01bb257ea..2dfda841e7f 100644 --- a/test/unit/lib/cli/interactive-setup/service.test.js +++ b/test/unit/lib/cli/interactive-setup/service.test.js @@ -8,6 +8,7 @@ const step = require('../../../../../lib/cli/interactive-setup/service'); const proxyquire = require('proxyquire'); const overrideStdoutWrite = require('process-utils/override-stdout-write'); const ServerlessError = require('../../../../../lib/serverless-error'); +const { StepHistory } = require('@serverless/utils/telemetry'); const templatesPath = path.resolve(__dirname, '../../../../../lib/plugins/create/templates'); @@ -23,12 +24,24 @@ const confirmEmptyWorkingDir = async () => expect(await fsp.readdir(process.cwd())).to.deep.equal([]); describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { - afterEach(() => sinon.restore()); + afterEach(() => { + sinon.restore(); + }); + + it('Should be not applied, when at service path', () => { + const context = { + serviceDir: '/foo', + options: {}, + }; + expect(step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('IN_SERVICE_DIRECTORY'); + }); - it('Should be not applied, when at service path', () => - expect(step.isApplicable({ serviceDir: '/foo', options: {} })).to.equal(false)); - it('Should be applied, when not at service path', () => - expect(step.isApplicable({ options: {} })).to.equal(true)); + it('Should be applied, when not at service path', () => { + const context = { options: {} }; + expect(step.isApplicable(context)).to.equal(true); + expect(context.inapplicabilityReasonCode).to.be.undefined; + }); it('Should result in an error when at service path with `template-path` options provided', () => { expect(() => @@ -56,7 +69,9 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { list: { projectType: 'other' }, }); - await step.run({ options: {} }); + const context = { options: {}, stepHistory: new StepHistory() }; + await step.run(context); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'other']])); return confirmEmptyWorkingDir(); }); @@ -84,7 +99,8 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-project' }, }); - await mockedStep.run({ options: {} }); + const context = { options: {}, stepHistory: new StepHistory() }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project/serverless.yml'); expect(stats.isFile()).to.be.true; expect(downloadTemplateFromRepoStub).to.have.been.calledWith( @@ -92,6 +108,12 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { 'aws-nodejs', 'test-project' ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should remove `serverless.template.yml` if its a part of the template', async () => { @@ -118,7 +140,8 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-project-template' }, }); - await mockedStep.run({ options: {} }); + const context = { options: {}, stepHistory: new StepHistory() }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project-template/serverless.yml'); expect(stats.isFile()).to.be.true; expect(downloadTemplateFromRepoStub).to.have.been.calledWith( @@ -129,6 +152,13 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { await expect( fsp.lstat('test-proejct-template/serverless.template.yml') ).to.eventually.be.rejected.and.have.property('code', 'ENOENT'); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should run `npm install` if `package.json` present', async () => { @@ -157,7 +187,8 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-project-package-json' }, }); - await mockedStep.run({ options: {} }); + const context = { options: {}, stepHistory: new StepHistory() }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project-package-json/serverless.yml'); expect(stats.isFile()).to.be.true; expect(downloadTemplateFromRepoStub).to.have.been.calledWith( @@ -168,6 +199,13 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { expect(spawnStub).to.have.been.calledWith('npm', ['install'], { cwd: path.join(process.cwd(), 'test-project-package-json'), }); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should emit warning if npm installation not found', async () => { @@ -196,15 +234,23 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { input: { projectName: 'test-project-missing-npm' }, }); + const context = { options: {}, stepHistory: new StepHistory() }; let stdoutData = ''; await overrideStdoutWrite( (data) => (stdoutData += data), - async () => mockedStep.run({ options: {} }) + async () => mockedStep.run(context) ); const stats = await fsp.lstat('test-project-missing-npm/serverless.yml'); expect(stats.isFile()).to.be.true; expect(stdoutData).to.include('Cannot install dependencies'); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should emit warning if npm installation not found', async () => { @@ -233,19 +279,35 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { input: { projectName: 'test-project-failed-install' }, }); - await expect(mockedStep.run({ options: {} })).to.be.eventually.rejected.and.have.property( + const context = { options: {}, stepHistory: new StepHistory() }; + await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property( 'code', 'DEPENDENCIES_INSTALL_FAILED' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should create project at not existing directory from a provided `template-path`', async () => { configureInquirerStub(inquirer, { input: { projectName: 'test-project-from-local-template' }, }); - await step.run({ options: { 'template-path': path.join(templatesPath, 'aws-nodejs') } }); + const context = { + options: { 'template-path': path.join(templatesPath, 'aws-nodejs') }, + stepHistory: new StepHistory(), + }; + await step.run(context); const stats = await fsp.lstat('test-project-from-local-template/serverless.yml'); expect(stats.isFile()).to.be.true; + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectName', '_user_provided_']]) + ); }); it('Should create project at not existing directory with provided `name`', async () => { @@ -268,9 +330,16 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { list: { projectType: 'aws-nodejs' }, }); - await mockedStep.run({ options: { name: 'test-project-from-cli-option' } }); + const context = { + options: { name: 'test-project-from-cli-option' }, + stepHistory: new StepHistory(), + }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project-from-cli-option/serverless.yml'); expect(stats.isFile()).to.be.true; + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectType', 'aws-nodejs']]) + ); }); it('Should create project at not existing directory with provided template', async () => { @@ -294,7 +363,8 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { input: { projectName: 'test-project-from-provided-template' }, }); - await mockedStep.run({ options: { template: 'test-template' } }); + const context = { options: { template: 'test-template' }, stepHistory: new StepHistory() }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project-from-provided-template/serverless.yml'); expect(stats.isFile()).to.be.true; expect(downloadTemplateFromRepoStub).to.have.been.calledWith( @@ -302,6 +372,10 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { 'test-template', 'test-project-from-provided-template' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectName', '_user_provided_']]) + ); }); it('Should create project at not existing directory with provided `template-url`', async () => { @@ -327,7 +401,11 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { input: { projectName: 'test-project-from-provided-template-url' }, }); - await mockedStep.run({ options: { 'template-url': providedTemplateUrl } }); + const context = { + options: { 'template-url': providedTemplateUrl }, + stepHistory: new StepHistory(), + }; + await mockedStep.run(context); const stats = await fsp.lstat('test-project-from-provided-template-url/serverless.yml'); expect(stats.isFile()).to.be.true; expect(downloadTemplateFromRepoStub).to.have.been.calledWith( @@ -335,6 +413,10 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { null, 'test-project-from-provided-template-url' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectName', '_user_provided_']]) + ); }); it('Should throw an error when template cannot be downloaded', async () => { @@ -349,10 +431,18 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'test-error-during-download' }, }); - await expect(mockedStep.run({ options: {} })).to.be.eventually.rejected.and.have.property( + const context = { options: {}, stepHistory: new StepHistory() }; + await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property( 'code', 'TEMPLATE_DOWNLOAD_FAILED' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['projectType', 'aws-nodejs'], + ['projectName', '_user_provided_'], + ]) + ); }); it('Should throw an error when provided template cannot be found', async () => { @@ -364,9 +454,14 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { input: { projectName: 'test-error-during-download' }, }); - await expect( - mockedStep.run({ options: { template: 'test-template' } }) - ).to.be.eventually.rejected.and.have.property('code', 'INVALID_TEMPLATE'); + const context = { options: { template: 'test-template' }, stepHistory: new StepHistory() }; + await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property( + 'code', + 'INVALID_TEMPLATE' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectName', '_user_provided_']]) + ); }); it('Should throw an error when template provided with url cannot be found', async () => { @@ -380,9 +475,18 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { configureInquirerStub(inquirer, { input: { projectName: 'test-error-during-download-custom-template' }, }); - await expect( - mockedStep.run({ options: { 'template-url': 'test-template-url' } }) - ).to.be.eventually.rejected.and.have.property('code', 'INVALID_TEMPLATE_URL'); + const context = { + options: { 'template-url': 'test-template-url' }, + stepHistory: new StepHistory(), + }; + await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property( + 'code', + 'INVALID_TEMPLATE_URL' + ); + + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['projectName', '_user_provided_']]) + ); }); }); @@ -394,10 +498,13 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { await fsp.mkdir('existing'); - await expect(step.run({ options: {} })).to.eventually.be.rejected.and.have.property( + const context = { options: {}, stepHistory: new StepHistory() }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( 'code', 'INVALID_ANSWER' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']])); }); it('Should not allow project creation in a directory in which already service is configured when `name` flag provided', async () => { @@ -407,9 +514,13 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { await fsp.mkdir('anotherexisting'); - await expect( - step.run({ options: { name: 'anotherexisting' } }) - ).to.eventually.be.rejected.and.have.property('code', 'TARGET_FOLDER_ALREADY_EXISTS'); + const context = { options: { name: 'anotherexisting' }, stepHistory: new StepHistory() }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( + 'code', + 'TARGET_FOLDER_ALREADY_EXISTS' + ); + + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']])); }); it('Should not allow project creation using an invalid project name', async () => { @@ -417,19 +528,26 @@ describe('test/unit/lib/cli/interactive-setup/service.test.js', () => { list: { projectType: 'aws-nodejs' }, input: { projectName: 'elo grzegżółka' }, }); - await expect(step.run({ options: {} })).to.eventually.be.rejected.and.have.property( + const context = { options: {}, stepHistory: new StepHistory() }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( 'code', 'INVALID_ANSWER' ); + + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']])); }); it('Should not allow project creation using an invalid project name when `name` flag provided', async () => { configureInquirerStub(inquirer, { list: { projectType: 'aws-nodejs' }, }); - await expect( - step.run({ options: { name: 'elo grzegżółka' } }) - ).to.eventually.be.rejected.and.have.property('code', 'INVALID_PROJECT_NAME'); + const context = { options: { name: 'elo grzegżółka' }, stepHistory: new StepHistory() }; + await expect(step.run(context)).to.eventually.be.rejected.and.have.property( + 'code', + 'INVALID_PROJECT_NAME' + ); + + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']])); }); it('Should not allow project creation if multiple template-related options are provided', async () => { diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index d17754c4847..74744897163 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -6,6 +6,7 @@ const fs = require('fs'); const os = require('os'); const overrideEnv = require('process-utils/override-env'); const overrideCwd = require('process-utils/override-cwd'); +const sinon = require('sinon'); const resolveLocalServerless = require('../../../../../lib/cli/resolve-local-serverless-path'); const commandsSchema = require('../../../../../lib/cli/commands-schema');