serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
1,319 lines (1,271 loc) • 105 kB
JavaScript
'use strict';
/* eslint-disable no-unused-expressions */
const BbPromise = require('bluebird');
const chai = require('chai');
const jc = require('json-cycle');
const os = require('os');
const path = require('path');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const YAML = require('js-yaml');
const _ = require('lodash');
const overrideEnv = require('process-utils/override-env');
const AwsProvider = require('../plugins/aws/provider/awsProvider');
const fse = require('fs-extra');
const Serverless = require('../../lib/Serverless');
const slsError = require('./Error');
const Utils = require('../../lib/classes/Utils');
const Variables = require('../../lib/classes/Variables');
const { getTmpDirPath } = require('../../tests/utils/fs');
const skipOnDisabledSymlinksInWindows = require('@serverless/test/skip-on-disabled-symlinks-in-windows');
BbPromise.longStackTraces(true);
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
chai.should();
const expect = chai.expect;
describe('Variables', () => {
let serverless;
let restoreEnv;
beforeEach(() => {
({ restoreEnv } = overrideEnv());
serverless = new Serverless();
});
const afterCallback = () => restoreEnv();
afterEach(afterCallback);
describe('#constructor()', () => {
it('should attach serverless instance', () => {
const variablesInstance = new Variables(serverless);
expect(variablesInstance.serverless).to.equal(serverless);
});
it('should not set variableSyntax in constructor', () => {
const variablesInstance = new Variables(serverless);
expect(variablesInstance.variableSyntax).to.be.undefined;
});
});
describe('#loadVariableSyntax()', () => {
it('should set variableSyntax', () => {
// eslint-disable-next-line no-template-curly-in-string
serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}';
serverless.variables.loadVariableSyntax();
expect(serverless.variables.variableSyntax).to.be.a('RegExp');
});
});
describe('#populateService()', () => {
it('should remove problematic attributes bofore calling populateObjectImpl with the service', () => {
const prepopulateServiceStub = sinon
.stub(serverless.variables, 'prepopulateService')
.returns(BbPromise.resolve());
const populateObjectStub = sinon
.stub(serverless.variables, 'populateObjectImpl')
.callsFake(val => {
expect(val).to.equal(serverless.service);
expect(val.provider.variableSyntax).to.be.undefined;
expect(val.serverless).to.be.undefined;
return BbPromise.resolve();
});
return serverless.variables
.populateService()
.should.be.fulfilled.then()
.finally(() => {
prepopulateServiceStub.restore();
populateObjectStub.restore();
});
});
it('should clear caches and remaining state *before* [pre]populating service', () => {
const prepopulateServiceStub = sinon
.stub(serverless.variables, 'prepopulateService')
.callsFake(val => {
expect(serverless.variables.deep).to.eql([]);
expect(serverless.variables.tracker.getAll()).to.eql([]);
return BbPromise.resolve(val);
});
const populateObjectStub = sinon
.stub(serverless.variables, 'populateObjectImpl')
.callsFake(val => {
expect(serverless.variables.deep).to.eql([]);
expect(serverless.variables.tracker.getAll()).to.eql([]);
return BbPromise.resolve(val);
});
serverless.variables.deep.push('${foo:}');
const prms = BbPromise.resolve('foo');
serverless.variables.tracker.add('foo:', prms, '${foo:}');
prms.state = 'resolved';
return serverless.variables
.populateService()
.should.be.fulfilled.then()
.finally(() => {
prepopulateServiceStub.restore();
populateObjectStub.restore();
});
});
it('should clear caches and remaining *after* [pre]populating service', () => {
const prepopulateServiceStub = sinon
.stub(serverless.variables, 'prepopulateService')
.callsFake(val => {
serverless.variables.deep.push('${foo:}');
const promise = BbPromise.resolve(val);
serverless.variables.tracker.add('foo:', promise, '${foo:}');
promise.state = 'resolved';
return BbPromise.resolve();
});
const populateObjectStub = sinon
.stub(serverless.variables, 'populateObjectImpl')
.callsFake(val => {
serverless.variables.deep.push('${bar:}');
const promise = BbPromise.resolve(val);
serverless.variables.tracker.add('bar:', promise, '${bar:}');
promise.state = 'resolved';
return BbPromise.resolve();
});
return serverless.variables
.populateService()
.should.be.fulfilled.then(() => {
expect(serverless.variables.deep).to.eql([]);
expect(serverless.variables.tracker.getAll()).to.eql([]);
})
.finally(() => {
prepopulateServiceStub.restore();
populateObjectStub.restore();
});
});
});
describe('fallback', () => {
describe('should fallback if ${self} syntax fail to populate but fallback is provided', () => {
[
{ value: 'fallback123_*', description: 'regular ASCII characters' },
{ value: 'hello+world^*$@(!', description: 'different ASCII characters' },
{ value: '+++++', description: 'plus sign' },
{ value: 'システム管理者*', description: 'japanese characters' },
{ value: 'deす', description: 'mixed japanese ending' },
{ value: 'でsu', description: 'mixed japanese leading' },
{ value: 'suごi', description: 'mixed japanese middle' },
{ value: '①⑴⒈⒜Ⓐⓐⓟ ..▉가Ὠ', description: 'random unicode' },
].forEach(testCase => {
it(testCase.description, () => {
serverless.variables.service.custom = {
settings: `\${self:nonExistent, "${testCase.value}"}`,
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: testCase.value,
});
});
});
});
});
it('should fallback if ${opt} syntax fail to populate but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${opt:nonExistent, "fallback"}',
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
});
});
it('should fallback if ${env} syntax fail to populate but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${env:nonExistent, "fallback"}',
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
});
});
describe('file syntax', () => {
it('should fallback if file does not exist but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${file(~/config.yml):xyz, "fallback"}',
};
const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(false);
const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
return serverless.variables
.populateService()
.should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
})
.finally(() => {
fileExistsStub.restore();
realpathSync.restore();
});
});
it('should fallback if file exists but given key not found and fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${file(~/config.yml):xyz, "fallback"}',
};
const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns({
test: 1,
test2: 'test2',
});
return serverless.variables
.populateService()
.should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
})
.finally(() => {
fileExistsStub.restore();
realpathSync.restore();
readFileSyncStub.restore();
});
});
});
describe('ensure unique instances', () => {
it('should not produce same instances for same variable patters used more than once', () => {
serverless.variables.service.custom = {
settings1: '${file(~/config.yml)}',
settings2: '${file(~/config.yml)}',
};
const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns({
test: 1,
test2: 'test2',
});
return serverless.variables
.populateService()
.should.be.fulfilled.then(result => {
expect(result.custom.settings1).to.not.equal(result.custom.settings2);
})
.finally(() => {
fileExistsStub.restore();
realpathSync.restore();
readFileSyncStub.restore();
});
});
});
describe('aws-specific syntax', () => {
let awsProvider;
let requestStub;
beforeEach(() => {
awsProvider = new AwsProvider(serverless, {});
requestStub = sinon
.stub(awsProvider, 'request')
.callsFake(() => BbPromise.reject(new serverless.classes.Error('Not found.', 400)));
});
afterEach(() => {
requestStub.restore();
});
it('should fallback if ${s3} syntax fail to populate but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${s3:bucket/key, "fallback"}',
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
});
});
it('should fallback if ${cf} syntax fail to populate but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${cf:stack.value, "fallback"}',
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
});
});
it('should fallback if ${ssm} syntax fail to populate but fallback is provided', () => {
serverless.variables.service.custom = {
settings: '${ssm:/path/param, "fallback"}',
};
return serverless.variables.populateService().should.be.fulfilled.then(result => {
expect(result.custom).to.be.deep.eql({
settings: 'fallback',
});
});
});
it('should throw an error if fallback fails too', () => {
serverless.variables.service.custom = {
settings: '${s3:bucket/key, ${ssm:/path/param}}',
};
return serverless.variables.populateService().should.be.rejected;
});
});
});
describe('#prepopulateService', () => {
// TL;DR: call populateService to test prepopulateService (note addition of 'pre')
//
// The prepopulateService resolver basically assumes invocation of of populateService (i.e. that
// variable syntax is loaded, and that the service object is cleaned up. Just use
// populateService to do that work.
let awsProvider;
let populateObjectImplStub;
let requestStub; // just in case... don't want to actually call...
beforeEach(() => {
awsProvider = new AwsProvider(serverless, {});
populateObjectImplStub = sinon.stub(serverless.variables, 'populateObjectImpl');
populateObjectImplStub.withArgs(serverless.variables.service).returns(BbPromise.resolve());
requestStub = sinon
.stub(awsProvider, 'request')
.callsFake(() => BbPromise.reject(new Error('unexpected')));
});
afterEach(() => {
populateObjectImplStub.restore();
requestStub.restore();
});
const prepopulatedProperties = [
{ name: 'region', getter: provider => provider.getRegion() },
{ name: 'stage', getter: provider => provider.getStage() },
{ name: 'profile', getter: provider => provider.getProfile() },
{
name: 'credentials',
getter: provider => provider.serverless.service.provider.credentials,
},
{
name: 'credentials.accessKeyId',
getter: provider => provider.serverless.service.provider.credentials.accessKeyId,
},
{
name: 'credentials.secretAccessKey',
getter: provider => provider.serverless.service.provider.credentials.secretAccessKey,
},
{
name: 'credentials.sessionToken',
getter: provider => provider.serverless.service.provider.credentials.sessionToken,
},
];
describe('basic population tests', () => {
prepopulatedProperties.forEach(property => {
it(`should populate variables in ${property.name} values`, () => {
_.set(
awsProvider.serverless.service.provider,
property.name,
'${self:foobar, "default"}'
);
return serverless.variables
.populateService()
.should.be.fulfilled.then(() =>
expect(property.getter(awsProvider)).to.be.eql('default')
);
});
});
});
//
describe('dependent service rejections', () => {
const dependentConfigs = [
{ value: '${cf:stack.value}', name: 'CloudFormation' },
{ value: '${s3:bucket/key}', name: 'S3' },
{ value: '${ssm:/path/param}', name: 'SSM' },
];
prepopulatedProperties.forEach(property => {
dependentConfigs.forEach(config => {
it(`should reject ${config.name} variables in ${property.name} values`, () => {
_.set(awsProvider.serverless.service.provider, property.name, config.value);
return serverless.variables
.populateService()
.should.be.rejectedWith('Variable dependency failure');
});
it(`should reject recursively dependent ${config.name} service dependencies`, () => {
serverless.variables.service.custom = {
settings: config.value,
};
awsProvider.serverless.service.provider.region = '${self:custom.settings.region}';
return serverless.variables
.populateService()
.should.be.rejectedWith('Variable dependency failure');
});
});
});
});
describe('dependent service non-interference', () => {
const stateCombinations = [
{ region: 'foo', state: 'bar' },
{ region: 'foo', state: '${self:bar, "bar"}' },
{ region: '${self:foo, "foo"}', state: 'bar' },
{ region: '${self:foo, "foo"}', state: '${self:bar, "bar"}' },
];
stateCombinations.forEach(combination => {
it('must leave the dependent services in their original state', () => {
const dependentMethods = [
{ name: 'getValueFromCf', original: serverless.variables.getValueFromCf },
{ name: 'getValueFromS3', original: serverless.variables.getValueFromS3 },
{ name: 'getValueFromSsm', original: serverless.variables.getValueFromSsm },
];
awsProvider.serverless.service.provider.region = combination.region;
awsProvider.serverless.service.provider.state = combination.state;
return serverless.variables.populateService().should.be.fulfilled.then(() => {
dependentMethods.forEach(method => {
expect(serverless.variables[method.name]).to.equal(method.original);
});
});
});
});
});
});
describe('#getProperties', () => {
it('extracts all terminal properties of an object', () => {
const date = new Date();
const regex = /^.*$/g;
const func = () => {};
const obj = {
foo: {
bar: 'baz',
biz: 'buz',
},
b: [{ c: 'd' }, { e: 'f' }],
g: date,
h: regex,
i: func,
};
const expected = [
{ path: ['foo', 'bar'], value: 'baz' },
{ path: ['foo', 'biz'], value: 'buz' },
{ path: ['b', 0, 'c'], value: 'd' },
{ path: ['b', 1, 'e'], value: 'f' },
{ path: ['g'], value: date },
{ path: ['h'], value: regex },
{ path: ['i'], value: func },
];
const result = serverless.variables.getProperties(obj, true, obj);
expect(result).to.eql(expected);
});
it('ignores self references', () => {
const obj = {};
obj.self = obj;
const expected = [];
const result = serverless.variables.getProperties(obj, true, obj);
expect(result).to.eql(expected);
});
});
describe('#populateObject()', () => {
beforeEach(() => {
serverless.variables.loadVariableSyntax();
});
it('should populate object and return it', () => {
const object = {
stage: '${opt:stage}', // eslint-disable-line no-template-curly-in-string
};
const expectedPopulatedObject = {
stage: 'prod',
};
sinon.stub(serverless.variables, 'populateValue').resolves('prod');
return serverless.variables
.populateObject(object)
.then(populatedObject => {
expect(populatedObject).to.deep.equal(expectedPopulatedObject);
})
.finally(() => serverless.variables.populateValue.restore());
});
it('should persist keys with dot notation', () => {
const object = {
stage: '${opt:stage}', // eslint-disable-line no-template-curly-in-string
};
object['some.nested.key'] = 'hello';
const expectedPopulatedObject = {
stage: 'prod',
};
expectedPopulatedObject['some.nested.key'] = 'hello';
const populateValueStub = sinon.stub(serverless.variables, 'populateValue').callsFake(
// eslint-disable-next-line no-template-curly-in-string
val => {
return val === '${opt:stage}' ? BbPromise.resolve('prod') : BbPromise.resolve(val);
}
);
return serverless.variables
.populateObject(object)
.should.become(expectedPopulatedObject)
.then()
.finally(() => populateValueStub.restore());
});
describe('significant variable usage corner cases', () => {
let service;
const makeDefault = () => ({
service: 'my-service',
provider: {
name: 'aws',
},
});
beforeEach(() => {
service = makeDefault();
// eslint-disable-next-line no-template-curly-in-string
service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}'; // default
serverless.variables.service = service;
serverless.variables.loadVariableSyntax();
delete service.provider.variableSyntax;
});
it('should properly replace self-references', () => {
service.custom = {
me: '${self:}', // eslint-disable-line no-template-curly-in-string
};
const expected = makeDefault();
expected.custom = {
me: expected,
};
return expect(
serverless.variables.populateObject(service).then(result => {
expect(jc.stringify(result)).to.eql(jc.stringify(expected));
})
).to.be.fulfilled;
});
it('should properly populate embedded variables', () => {
service.custom = {
val0: 'my value 0',
val1: '0', // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.val${self:custom.val1}}',
};
const expected = {
val0: 'my value 0',
val1: '0',
val2: 'my value 0',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate an overwrite with a default value that is a string', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, "string"}',
};
const expected = {
val0: 'my value',
val1: 'string',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate an overwrite with a default value that is the string *', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, "*"}',
};
const expected = {
val0: 'my value',
val1: '*',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate an overwrite with a default value that is a string w/*', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, "foo*"}',
};
const expected = {
val0: 'my value',
val1: 'foo*',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites where the first value is valid', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.val0, self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2}',
};
const expected = {
val0: 'my value',
val1: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should do nothing useful on * when not wrapped in quotes', () => {
service.custom = {
val0: '${self:custom.*}',
};
const expected = { val0: undefined };
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites where the middle value is valid', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.val0, self:custom.NOT_A_VAL2}',
};
const expected = {
val0: 'my value',
val1: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites where the last value is valid', () => {
service.custom = {
val0: 'my value', // eslint-disable-next-line no-template-curly-in-string
val1: '${self:custom.NOT_A_VAL1, self:custom.NOT_A_VAL2, self:custom.val0}',
};
const expected = {
val0: 'my value',
val1: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites with nested variables in the first value', () => {
service.custom = {
val0: 'my value',
val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.val${self:custom.val1}, self:custom.NO_1, self:custom.NO_2}',
};
const expected = {
val0: 'my value',
val1: 0,
val2: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites with nested variables in the middle value', () => {
service.custom = {
val0: 'my value',
val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.NO_1, self:custom.val${self:custom.val1}, self:custom.NO_2}',
};
const expected = {
val0: 'my value',
val1: 0,
val2: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly populate overwrites with nested variables in the last value', () => {
service.custom = {
val0: 'my value',
val1: 0, // eslint-disable-next-line no-template-curly-in-string
val2: '${self:custom.NO_1, self:custom.NO_2, self:custom.val${self:custom.val1}}',
};
const expected = {
val0: 'my value',
val1: 0,
val2: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should properly replace duplicate variable declarations', () => {
service.custom = {
val0: 'my value',
val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val2: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
};
const expected = {
val0: 'my value',
val1: 'my value',
val2: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
it('should recursively populate, regardless of order and duplication', () => {
service.custom = {
val1: '${self:custom.depVal}', // eslint-disable-line no-template-curly-in-string
depVal: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val0: 'my value',
val2: '${self:custom.depVal}', // eslint-disable-line no-template-curly-in-string
};
const expected = {
val1: 'my value',
depVal: 'my value',
val0: 'my value',
val2: 'my value',
};
return expect(
serverless.variables.populateObject(service.custom).then(result => {
expect(result).to.eql(expected);
})
).to.be.fulfilled;
});
// see https://github.com/serverless/serverless/pull/4713#issuecomment-366975172
it('should handle deep references into deep variables', () => {
service.provider.stage = 'dev';
service.custom = {
stage: '${env:stage, self:provider.stage}',
secrets: '${self:custom.${self:custom.stage}}',
dev: {
SECRET: 'secret',
},
environment: {
SECRET: '${self:custom.secrets.SECRET}',
},
};
const expected = {
stage: 'dev',
secrets: {
SECRET: 'secret',
},
dev: {
SECRET: 'secret',
},
environment: {
SECRET: 'secret',
},
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variables that reference overrides', () => {
service.custom = {
val1: '${self:not.a.value, "bar"}',
val2: '${self:custom.val1}',
};
const expected = {
val1: 'bar',
val2: 'bar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep references into deep variables', () => {
service.custom = {
val0: {
foo: 'bar',
},
val1: '${self:custom.val0}',
val2: '${self:custom.val1.foo}',
};
const expected = {
val0: {
foo: 'bar',
},
val1: {
foo: 'bar',
},
val2: 'bar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variables that reference overrides', () => {
service.custom = {
val1: '${self:not.a.value, "bar"}',
val2: 'foo${self:custom.val1}',
};
const expected = {
val1: 'bar',
val2: 'foobar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle referenced deep variables that reference overrides', () => {
service.custom = {
val1: '${self:not.a.value, "bar"}',
val2: '${self:custom.val1}',
val3: '${self:custom.val2}',
};
const expected = {
val1: 'bar',
val2: 'bar',
val3: 'bar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle partial referenced deep variables that reference overrides', () => {
service.custom = {
val1: '${self:not.a.value, "bar"}',
val2: '${self:custom.val1}',
val3: 'foo${self:custom.val2}',
};
const expected = {
val1: 'bar',
val2: 'bar',
val3: 'foobar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle referenced contained deep variables that reference overrides', () => {
service.custom = {
val1: '${self:not.a.value, "bar"}',
val2: 'foo${self:custom.val1}',
val3: '${self:custom.val2}',
};
const expected = {
val1: 'bar',
val2: 'foobar',
val3: 'foobar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle multiple referenced contained deep variables referencing overrides', () => {
service.custom = {
val0: '${self:not.a.value, "foo"}',
val1: '${self:not.a.value, "bar"}',
val2: '${self:custom.val0}:${self:custom.val1}',
val3: '${self:custom.val2}',
};
const expected = {
val0: 'foo',
val1: 'bar',
val2: 'foo:bar',
val3: 'foo:bar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle overrides that are populated by unresolvable deep variables', () => {
service.custom = {
val0: 'foo',
val1: '${self:custom.val0}',
val2: '${self:custom.val1.notAnAttribute, "fallback"}',
};
const expected = {
val0: 'foo',
val1: 'foo',
val2: 'fallback',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle embedded deep variable replacements in overrides', () => {
service.custom = {
foo: 'bar',
val0: 'foo',
val1: '${self:custom.val0, "fallback 1"}',
val2: '${self:custom.${self:custom.val0, self:custom.val1}, "fallback 2"}',
};
const expected = {
foo: 'bar',
val0: 'foo',
val1: 'foo',
val2: 'bar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should deal with overwites that reference embedded deep references', () => {
service.custom = {
val0: 'val',
val1: 'val0',
val2: '${self:custom.val1}',
val3: '${self:custom.${self:custom.val2}, "fallback"}',
val4: '${self:custom.val3, self:custom.val3}',
};
const expected = {
val0: 'val',
val1: 'val0',
val2: 'val0',
val3: 'val',
val4: 'val',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should preserve whitespace in double-quote literal fallback', () => {
service.custom = {
val0: '${self:custom.val, "rate(3 hours)"}',
};
const expected = {
val0: 'rate(3 hours)',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should preserve whitespace in single-quote literal fallback', () => {
service.custom = {
val0: "${self:custom.val, 'rate(1 hour)'}",
};
const expected = {
val0: 'rate(1 hour)',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should preserve question mark in single-quote literal fallback', () => {
service.custom = {
val0: "${self:custom.val, '?'}",
};
const expected = {
val0: '?',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should preserve question mark in single-quote literal fallback', () => {
service.custom = {
val0: "${self:custom.val, 'cron(0 0 * * ? *)'}",
};
const expected = {
val0: 'cron(0 0 * * ? *)',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should accept whitespace in variables', () => {
service.custom = {
val0: '${self: custom.val}',
val: 'foobar',
};
const expected = {
val: 'foobar',
val0: 'foobar',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variables regardless of custom variableSyntax', () => {
service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
serverless.variables.loadVariableSyntax();
delete service.provider.variableSyntax;
service.custom = {
my0thStage: 'DEV',
my1stStage: '${{self:custom.my0thStage}}',
my2ndStage: '${{self:custom.my1stStage}}',
};
const expected = {
my0thStage: 'DEV',
my1stStage: 'DEV',
my2ndStage: 'DEV',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variable continuations regardless of custom variableSyntax', () => {
service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
serverless.variables.loadVariableSyntax();
delete service.provider.variableSyntax;
service.custom = {
my0thStage: { we: 'DEV' },
my1stStage: '${{self:custom.my0thStage}}',
my2ndStage: '${{self:custom.my1stStage.we}}',
};
const expected = {
my0thStage: { we: 'DEV' },
my1stStage: { we: 'DEV' },
my2ndStage: 'DEV',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variables regardless of recursion into custom variableSyntax', () => {
service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
serverless.variables.loadVariableSyntax();
delete service.provider.variableSyntax;
service.custom = {
my0thIndex: '0th',
my1stIndex: '1st',
my0thStage: 'DEV',
my1stStage: '${{self:custom.my${{self:custom.my0thIndex}}Stage}}',
my2ndStage: '${{self:custom.my${{self:custom.my1stIndex}}Stage}}',
};
const expected = {
my0thIndex: '0th',
my1stIndex: '1st',
my0thStage: 'DEV',
my1stStage: 'DEV',
my2ndStage: 'DEV',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should handle deep variables in complex recursions of custom variableSyntax', () => {
service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
serverless.variables.loadVariableSyntax();
delete service.provider.variableSyntax;
service.custom = {
i0: '0',
s0: 'DEV',
s1: '${{self:custom.s0}}! ${{self:custom.s0}}',
s2: 'I am a ${{self:custom.s0}}! A ${{self:custom.s${{self:custom.i0}}}}!',
s3: '${{self:custom.s0}}!, I am a ${{self:custom.s1}}!, ${{self:custom.s2}}',
};
const expected = {
i0: '0',
s0: 'DEV',
s1: 'DEV! DEV',
s2: 'I am a DEV! A DEV!',
s3: 'DEV!, I am a DEV! DEV!, I am a DEV! A DEV!',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
describe('file reading cases', () => {
let tmpDirPath;
beforeEach(() => {
tmpDirPath = getTmpDirPath();
fse.mkdirsSync(tmpDirPath);
serverless.config.update({ servicePath: tmpDirPath });
});
afterEach(() => {
fse.removeSync(tmpDirPath);
});
const makeTempFile = (fileName, fileContent) => {
fse.outputFileSync(path.join(tmpDirPath, fileName), fileContent);
};
const asyncFileName = 'async.load.js';
const asyncContent = `'use strict';
let i = 0
const str = () => new Promise((resolve) => {
setTimeout(() => {
i += 1 // side effect
resolve(\`my-async-value-\${i}\`)
}, 200);
});
const obj = () => new Promise((resolve) => {
setTimeout(() => {
i += 1 // side effect
resolve({
val0: \`my-async-value-\${i}\`,
val1: '\${opt:stage}',
});
}, 200);
});
module.exports = {
str,
obj,
};
`;
it('should populate any given variable only once', () => {
makeTempFile(asyncFileName, asyncContent);
service.custom = {
val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
val0: `\${file(${asyncFileName}):str}`,
};
const expected = {
val1: 'my-async-value-1',
val2: 'my-async-value-1',
val0: 'my-async-value-1',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should still work with a default file name in double or single quotes', () => {
makeTempFile(asyncFileName, asyncContent);
service.custom = {
val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
val3: `\${file(\${self:custom.nonexistent, "${asyncFileName}"}):str}`,
val0: `\${file(\${self:custom.nonexistent, '${asyncFileName}'}):str}`,
};
const expected = {
val1: 'my-async-value-1',
val2: 'my-async-value-1',
val3: 'my-async-value-1',
val0: 'my-async-value-1',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should populate any given variable only once regardless of ordering or reference count', () => {
makeTempFile(asyncFileName, asyncContent);
service.custom = {
val9: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string
val7: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string
val5: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string
val3: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
val4: '${self:custom.val3}', // eslint-disable-line no-template-curly-in-string
val6: '${self:custom.val5}', // eslint-disable-line no-template-curly-in-string
val8: '${self:custom.val7}', // eslint-disable-line no-template-curly-in-string
val0: `\${file(${asyncFileName}):str}`,
};
const expected = {
val9: 'my-async-value-1',
val7: 'my-async-value-1',
val5: 'my-async-value-1',
val3: 'my-async-value-1',
val1: 'my-async-value-1',
val2: 'my-async-value-1',
val4: 'my-async-value-1',
val6: 'my-async-value-1',
val8: 'my-async-value-1',
val0: 'my-async-value-1',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it('should populate async objects with contained variables', () => {
makeTempFile(asyncFileName, asyncContent);
serverless.variables.options = {
stage: 'dev',
};
service.custom = {
obj: `\${file(${asyncFileName}):obj}`,
};
const expected = {
obj: {
val0: 'my-async-value-1',
val1: 'dev',
},
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
it("should populate variables from filesnames including '@', e.g scoped npm packages", () => {
const fileName = `./node_modules/@scoped-org/${asyncFileName}`;
makeTempFile(fileName, asyncContent);
service.custom = {
val0: `\${file(${fileName}):str}`,
};
const expected = {
val0: 'my-async-value-1',
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
const selfFileName = 'self.yml';
const selfContent = `foo: baz
bar: \${self:custom.self.foo}
`;
it('should populate a "cyclic" reference across an unresolved dependency (issue #4687)', () => {
makeTempFile(selfFileName, selfContent);
service.custom = {
self: `\${file(${selfFileName})}`,
};
const expected = {
self: {
foo: 'baz',
bar: 'baz',
},
};
return serverless.variables.populateObject(service.custom).should.become(expected);
});
const emptyFileName = 'empty.js';
const emptyContent = `'use strict';
module.exports = {
func: () => ({ value: 'a value' }),
}
`;
it('should reject population of an attribute not exported from a file', () => {
makeTempFile(emptyFileName, emptyContent);
service.custom = {
val: `\${file(${emptyFileName}):func.notAValue}`,
};
return serverless.variables
.populateObject(service.custom)
.should.be.rejectedWith(
serverless.classes.Error,
'Invalid variable syntax when referencing file'
);
});
});
});
});
describe('#populateProperty()', () => {
beforeEach(() => {
serverless.variables.loadVariableSyntax();
});
it('should call overwrite if overwrite syntax provided', () => {
// eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage, self:provider.stage}';
serverless.variables.options = { stage: 'dev' };
serverless.service.provider.stage = 'prod';
return serverless.variables
.populateProperty(property)
.should.eventually.eql('my stage is dev');
});
it('should allow a single-quoted string if overwrite syntax provided', () => {
// eslint-disable-next-line no-template-curly-in-string
const property = "my stage is ${opt:stage, 'prod'}";
serverless.variables.options = {};
return serverless.variables
.populateProperty(property)
.should.eventually.eql('my stage is prod');
});
it('should allow a double-quoted string if overwrite syntax provided', () => {
// eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage, "prod"}';
serverless.variables.options = {};
return serverless.variables
.populateProperty(property)
.should.eventually.eql('my stage is prod');
});
it('should not allow partially double-quoted string', () => {
const property = '${opt:stage, prefix"prod"suffix}';
serverless.variables.options = {};
const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound');
return serverless.variables
.populateProperty(property)
.should.become(undefined)
.then(() => {
expect(warnIfNotFoundSpy.callCount).to.equal(1);
})
.finally(() => {
warnIfNotFoundSpy.restore();
});
});
it('should allow a boolean with value true if overwrite syntax provided', () => {
const property = '${opt:stage, true}';
serverless.variables.options = {};
return serverless.variables.populateProperty(property).should.eventually.eql(true);
});
it('should allow a boolean with value false if overwrite syntax provided', () => {
const property = '${opt:stage, false}';
serverless.variables.options = {};
return serverless.variables.populateProperty(property).should.eventually.eql(false);
});
it('should not match a boolean with value containing word true or false if overwrite syntax provided', () => {
const property = '${opt:stage, foofalsebar}';
serverless.variables.options = {};
const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound');
return serverless.variables
.populateProperty(property)
.should.become(undefined)
.then(() => {
expect(warnIfNotFoundSpy.callCount).to.equal(1);
})
.finally(() => {
warnIfNotFoundSpy.restore();
});
});
it('should allow an integer if overwrite syntax provided', () => {
const property = '${opt:quantity, 123}';
serverless.variables.options = {};
return serverless.variables.populateProperty(property).should.eventually.eql(123);
});
it('should call getValueFromSource if no overwrite syntax provided', () => {
// eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage}';
serverless.variables.options = { stage: 'prod' };
return serverless.variables
.populateProperty(property)
.should.eventually.eql('my stage is prod');
});
it('should warn if an SSM parameter does not exist', () => {
const options = {
stage: 'prod',
region: 'us-east-1',
};
serverless.variables.options = options;
const awsProvider = new AwsProvider(serverless, options);
const param = '/some/path/to/invalidparam';
const property = `\${ssm:${param}}`;
const error = Object.assign(new Error(`Parameter ${param} not found.`), {
providerError: { statusCode: 400 },
});
const requestStub = sinon
.stub(awsProvider, 'request')
.callsFake(() => BbPromise.reject(error));
const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound');
return serverless.variables
.populateProperty(property)
.should.become(undefined)
.then(() => {
expect(requestStub.callCount).to.equal(1);
expect(warnIfNotFoundSpy.callCount).to.equal(1);
})
.finally(() => {
requestStub.restore();
warnIfNotFoundSpy.restore();
});
});
it('should throw an Error if the SSM request fails', () => {
const options = {
stage: 'prod',
region: 'us-east-1',
};
serverless.v