serverless/test/unit/lib/cli/interactive-setup/aws-credentials.test.js
2021-07-22 08:42:57 +02:00

794 lines
25 KiB
JavaScript

'use strict';
const chai = require('chai');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
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;
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
const { join, resolve } = require('path');
const { remove: rmDir, outputFile: writeFile } = require('fs-extra');
const { resolveFileProfiles } = require('../../../../../lib/plugins/aws/utils/credentials');
const mockedSdk = {
organizations: {
get: () => {
return {
orgUid: 'org-uid',
};
},
},
getProviders: async () => {
return { result: [] };
},
};
const step = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'../../utils/openBrowser': async (url) => {
openBrowserUrls.push(url);
},
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => mockedSdk,
},
});
const inquirer = require('@serverless/utils/inquirer');
const configureInquirerStub = require('@serverless/test/configure-inquirer-stub');
const openBrowserUrls = [];
describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => {
const accessKeyId = 'AKIAIOSFODNN7EXAMPLE';
const secretAccessKey = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
afterEach(() => {
openBrowserUrls.length = 0;
sinon.restore();
});
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('NOT_IN_SERVICE_DIRECTORY');
});
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 = {
...mockedSdk,
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'roleArn',
providerUid: 'provideruid',
isDefault: true,
providerDetails: {
roleArn: 'arn:xxx',
},
},
],
};
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
});
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 () => {
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'roleArn',
providerUid: 'provideruid',
isDefault: false,
providerDetails: {
roleArn: 'arn:xxx',
},
},
],
};
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
'./utils': {
doesServiceInstanceHaveLinkedProvider: () => true,
},
});
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 () => {
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'roleArn',
providerUid: 'provideruid',
isDefault: false,
providerDetails: {
roleArn: 'arn:xxx',
},
},
],
};
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
'./utils': {
doesServiceInstanceHaveLinkedProvider: () => false,
},
});
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.true;
});
it('Should emit warning when dashboard is not available when fetching providers', async () => {
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
const err = new Error('unavailable');
err.statusCode = 500;
throw err;
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
});
let stdoutData = '';
await overrideStdoutWrite(
(data) => (stdoutData += data),
async () =>
expect(
await mockedStep.isApplicable({
serviceDir: process.cwd(),
configuration: { provider: { name: 'aws' }, org: 'someorg' },
configurationFilename: 'serverless.yml',
})
).to.be.false
);
expect(stdoutData).to.include('Dashboard service is currently unavailable');
});
it('Should be effective, at AWS service and no credentials are set', async () =>
expect(
await step.isApplicable({
serviceDir: process.cwd(),
configuration: { provider: { name: 'aws' } },
configurationFilename: 'serverless.yml',
})
).to.equal(true));
it('Should emit a message when user decides to skip credentials setup', async () => {
configureInquirerStub(inquirer, {
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(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', () => {
let restoreEnv;
let uncachedStep;
before(() => {
({ restoreEnv } = overrideEnv({ asCopy: true }));
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
uncachedStep = requireUncached(() =>
require('../../../../../lib/cli/interactive-setup/aws-credentials')
);
});
after(() => restoreEnv);
it('Should be ineffective, when credentials are set in environment', async () => {
expect(
await uncachedStep.isApplicable({
serviceDir: process.cwd(),
configuration: { provider: { name: 'aws' } },
configurationFilename: 'serverless.yml',
})
).to.equal(false);
});
});
describe('AWS config handling', () => {
let credentialsDirPath;
let credentialsFilePath;
before(() => {
credentialsDirPath = resolve('.aws');
credentialsFilePath = join(credentialsDirPath, 'credentials');
});
afterEach(() => rmDir(credentialsDirPath));
describe('Existing credentials case', () => {
before(() =>
writeFile(
credentialsFilePath,
[
'[some-profile]',
`aws_access_key_id = ${accessKeyId}`,
`aws_secret_access_key = ${secretAccessKey}`,
].join('\n')
)
);
it('Should be ineffective, When credentials are set in AWS config', async () =>
expect(
await step.isApplicable({
serviceDir: process.cwd(),
configuration: { provider: { name: 'aws' } },
configurationFilename: 'serverless.yml',
})
).to.equal(false));
});
it('Should setup credentials for users not having an AWS account', async () => {
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_local_' },
confirm: { hasAwsAccount: false },
input: {
createAwsAccountPrompt: '',
generateAwsCredsPrompt: '',
accessKeyId,
secretAccessKey,
},
});
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', '_continuation_'],
['generateAwsCredsPrompt', '_continuation_'],
['accessKeyId', '_user_input_'],
['secretAccessKey', '_user_input_'],
])
);
});
it('Should setup credentials for users having an AWS account', async () => {
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_local_' },
confirm: { hasAwsAccount: true },
input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey },
});
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', '_continuation_'],
['accessKeyId', '_user_input_'],
['secretAccessKey', '_user_input_'],
])
);
return resolveFileProfiles().then((profiles) => {
expect(profiles).to.deep.equal(new Map([['default', { accessKeyId, secretAccessKey }]]));
});
});
it('Should not accept invalid access key id', async () => {
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_local_' },
confirm: { hasAwsAccount: true },
input: { generateAwsCredsPrompt: '', accessKeyId: 'foo', secretAccessKey },
});
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', '_continuation_'],
['accessKeyId', undefined],
])
);
});
it('Should not accept invalid secret access key', async () => {
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_local_' },
confirm: { hasAwsAccount: true },
input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey: 'foo' },
});
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', '_continuation_'],
['accessKeyId', '_user_input_'],
['secretAccessKey', undefined],
])
);
});
});
describe('Provider config handling', () => {
it('Should correctly setup with newly created provider when no previous providers exist', async () => {
const mockedOpenBrowser = sinon.stub().returns();
const mockedDisconnect = sinon.stub().returns();
const mockedCreateProviderLink = sinon.stub().resolves();
const providerUid = 'provideruid';
const internalMockedSdk = {
...mockedSdk,
connect: ({ onEvent }) => {
onEvent({
data: {
object: {
provider_uid: providerUid,
},
},
});
},
disconnect: mockedDisconnect,
createProviderLink: mockedCreateProviderLink,
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'../../utils/openBrowser': mockedOpenBrowser,
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
});
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_create_provider_' },
});
let stdoutData = '';
const context = {
serviceDir: process.cwd(),
configuration: {
service: 'someservice',
provider: { name: 'aws' },
org: 'someorg',
app: 'someapp',
},
options: {},
configurationFilename: 'serverless.yml',
stepHistory: new StepHistory(),
};
await overrideStdoutWrite(
(data) => (stdoutData += data),
async () => await mockedStep.run(context)
);
expect(stdoutData).to.include('AWS Access Role provider was successfully created');
expect(mockedOpenBrowser).to.have.been.calledWith(
chalk.bold.white(
'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws'
)
);
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 () => {
const mockedOpenBrowser = sinon.stub().returns();
const mockedDisconnect = sinon.stub().returns();
const mockedCreateProviderLink = sinon.stub().resolves();
const providerUid = 'provideruid';
const internalMockedSdk = {
...mockedSdk,
connect: ({ onEvent }) => {
onEvent({
data: {
object: {
provider_uid: providerUid,
},
},
});
},
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'roleArn',
providerUid,
providerDetails: {
roleArn: 'arn:xxx',
},
},
],
};
},
disconnect: mockedDisconnect,
createProviderLink: mockedCreateProviderLink,
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'../../utils/openBrowser': mockedOpenBrowser,
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
});
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: '_create_provider_' },
});
const context = {
serviceDir: process.cwd(),
configuration: {
service: 'someservice',
provider: { name: 'aws' },
org: 'someorg',
app: 'someapp',
},
options: {},
configurationFilename: 'serverless.yml',
stepHistory: new StepHistory(),
};
let stdoutData = '';
await overrideStdoutWrite(
(data) => (stdoutData += data),
async () => await mockedStep.run(context)
);
expect(stdoutData).to.include('AWS Access Role provider was successfully created');
expect(mockedOpenBrowser).to.have.been.calledWith(
chalk.bold.white(
'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws'
)
);
expect(mockedDisconnect).to.have.been.called;
expect(mockedCreateProviderLink).to.have.been.calledWith(
'org-uid',
'instance',
'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 () => {
const mockedOpenBrowser = sinon.stub().returns();
const internalMockedSdk = {
...mockedSdk,
connect: () => {
const err = new Error('error');
err.statusCode = 500;
throw err;
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'../../utils/openBrowser': mockedOpenBrowser,
});
configureInquirerStub(inquirer, {
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(context)
);
expect(stdoutData).to.include('Dashboard service is currently unavailable');
expect(mockedOpenBrowser).to.have.been.calledWith(
chalk.bold.white(
'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 () => {
const providerUid = 'provideruid';
const mockedCreateProviderLink = sinon.stub().resolves();
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'accessKey',
providerUid,
providerDetails: {
accessKeyId: 'axx',
},
},
],
};
},
createProviderLink: mockedCreateProviderLink,
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
});
configureInquirerStub(inquirer, {
list: { credentialsSetupChoice: providerUid },
});
const context = {
serviceDir: process.cwd(),
configuration: {
service: 'someservice',
provider: { name: 'aws' },
org: 'someorg',
app: 'someapp',
},
options: {},
configurationFilename: 'serverless.yml',
stepHistory: new StepHistory(),
};
let stdoutData = '';
await overrideStdoutWrite(
(data) => (stdoutData += data),
async () => await mockedStep.run(context)
);
expect(mockedCreateProviderLink).to.have.been.calledWith(
'org-uid',
'instance',
'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1',
'provideruid'
);
expect(stdoutData).to.include('Selected provider was successfully linked');
expect(context.stepHistory.valuesMap()).to.deep.equal(
new Map([['credentialsSetupChoice', '_user_choice_']])
);
});
it('Should emit a warning when dashboard is not available and link cannot be created', async () => {
const providerUid = 'provideruid';
const mockedCreateProviderLink = sinon.stub().callsFake(async () => {
const err = new Error('error');
err.statusCode = 500;
throw err;
});
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
return {
result: [
{
alias: 'someprovider',
providerName: 'aws',
providerType: 'roleArn',
providerUid,
providerDetails: {
roleArn: 'arn:xxx',
},
},
],
};
},
createProviderLink: mockedCreateProviderLink,
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
});
configureInquirerStub(inquirer, {
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(context)
);
expect(stdoutData).to.include(
'Dashboard service is currently unavailable, please try again later'
);
expect(stdoutData).not.to.include('Selected provider was successfully linked');
expect(mockedCreateProviderLink).to.have.been.calledWith(
'org-uid',
'instance',
'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1',
'provideruid'
);
expect(context.stepHistory.valuesMap()).to.deep.equal(
new Map([['credentialsSetupChoice', '_user_choice_']])
);
});
it('Should emit a warning when dashboard is not available when fetching providers', async () => {
const internalMockedSdk = {
...mockedSdk,
getProviders: async () => {
const err = new Error('unavailable');
err.statusCode = 500;
throw err;
},
};
const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
'@serverless/dashboard-plugin/lib/clientUtils': {
getPlatformClientWithAccessKey: async () => internalMockedSdk,
},
'@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
});
let stdoutData = '';
await overrideStdoutWrite(
(data) => (stdoutData += data),
async () =>
await mockedStep.run({
serviceDir: process.cwd(),
configuration: { provider: { name: 'aws' }, org: 'someorg' },
options: {},
configurationFilename: 'serverless.yml',
})
);
expect(stdoutData).to.include('Dashboard service is currently unavailable');
});
});
});