serverless/lib/classes/Variables.test.js
Erik Erikson 4400ffc9e4 fix #4311 & #4734, separate PromiseTracker, minutiae
#4311
  see prepopulateService - attempts to pre-populate the region and stage settings necessary for making a request to AWS, rejecting and dependencies thereon it runs into during that process
  see the `deep` variable work.  this was a knock-on effect of providing pre-population.  it actually represents an obscure class of bugs where the recursive population previously in getDeepValue caused the caller not to be in charge of population choices (thus fixing for pre-population) but also caused potential deadlocks resulting from getDeepValue creating circular dependencies.

#4734
  see splitByComma - a regex to do the splitting but ignore quoted commas was getting very deep and involved.  Instead, identify quoted string boundaries, then identify commas (including white space around them) and take those commas not contained by quotes as the locations for splitting the given string.  Trim the string as well (removing leading and trailing white space).
  add sophistication to the overwrite syntax, allowing for whitespace and repetitions (for robustness)
  add sophistication to the string ref syntax, allowing for use in identifying multiple quoted strings in a variable (i.e. for overwrites)

#NotCreated
  fix a bug I created earlier in the branch that caused reporting to be less informative (see renderMatches)

separate PromiseTracker
  move this class into its own file for the purpose of increasing testability and offering reuse

minutiae
  filter the properties given to populateVariables so as to avoid attempting resolution on non-variables.  Efficiency and noise reduction change.  Also cleans up the code (e.g. see populateObject and its use)
  cleaning of overwrite as a side effect
  offer variable cleaning as a idiom
  reorder the Variables.js `require`s to be in alphabetical order
2018-02-16 17:13:03 -08:00

1465 lines
59 KiB
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 AwsProvider = require('../plugins/aws/provider/awsProvider');
const fse = require('../utils/fs/fse');
const Serverless = require('../../lib/Serverless');
const slsError = require('./Error');
const testUtils = require('../../tests/utils');
const Utils = require('../../lib/classes/Utils');
const Variables = require('../../lib/classes/Variables');
BbPromise.longStackTraces(true);
chai.should();
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
const expect = chai.expect;
describe('Variables', () => {
let serverless;
beforeEach(() => {
serverless = new Serverless();
});
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 call loadVariableSyntax and then populateProperty', () => {
const loadVariableSyntaxStub = sinon.stub(serverless.variables, 'loadVariableSyntax')
.returns();
const populateObjectStub = sinon.stub(serverless.variables, 'populateObject')
.returns(BbPromise.resolve());
return expect(serverless.variables.populateService()).to.be.fulfilled
.then(() => {
expect(loadVariableSyntaxStub).to.be.calledOnce;
expect(populateObjectStub).to.be.calledOnce;
expect(loadVariableSyntaxStub).to.be.calledBefore(populateObjectStub);
})
.finally(() => {
populateObjectStub.restore();
loadVariableSyntaxStub.restore();
});
});
it('should remove problematic attributes bofore calling populateObject with the service',
() => {
const populateObjectStub = sinon.stub(serverless.variables, 'populateObject', (val) => {
expect(val).to.equal(serverless.service);
expect(val.provider.variableSyntax).to.be.undefined;
expect(val.serverless).to.be.undefined;
return BbPromise.resolve();
});
return expect(serverless.variables.populateService()).to.be.fulfilled
.then().finally(() => populateObjectStub.restore());
});
});
describe('#prepopulateService', () => {
// TL;DR: call populateService to test prepopulateService (note addition of 'pre')
//
// The prepopulateService method 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 populateObjectStub;
let requestStub; // just in case... don't want to actually call...
beforeEach(() => {
awsProvider = new AwsProvider(serverless, {});
populateObjectStub = sinon.stub(serverless.variables, 'populateObject', () =>
BbPromise.resolve());
requestStub = sinon.stub(awsProvider, 'request', () =>
BbPromise.reject(new Error('unexpected')));
});
afterEach(() => {
populateObjectStub.restore();
requestStub.restore();
});
const prepopulatedProperties = [
{ name: 'region', getter: (provider) => provider.getRegion() },
{ name: 'stage', getter: (provider) => provider.getStage() },
];
describe('basic population tests', () => {
prepopulatedProperties.forEach((property) => {
it(`should populate variables in ${property.name} values`, () => {
awsProvider.options[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`, () => {
awsProvider.options[property.name] = config.value;
return serverless.variables.populateService()
.should.be.rejectedWith('Variable Failure');
});
});
});
it('should reject recursively dependent service dependencies', () => {
serverless.variables.service.custom = {
settings: '${s3:bucket/key}',
};
awsProvider.options.region = '${self:custom.settings.region}';
return serverless.variables.populateService()
.should.be.rejectedWith('Variable Failure');
});
});
});
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',
// eslint-disable-next-line no-template-curly-in-string
val => (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 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 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;
});
describe('file reading cases', () => {
let tmpDirPath;
beforeEach(() => {
tmpDirPath = testUtils.getTmpDirPath();
fse.mkdirsSync(tmpDirPath);
serverless.config.update({ servicePath: tmpDirPath });
});
afterEach(() => {
fse.removeSync(tmpDirPath);
});
const makeTempFile = (fileName, fileContent) => {
fse.writeFileSync(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 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);
});
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 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 = new Error(`Parameter ${param} not found.`, 123);
const requestStub = sinon.stub(awsProvider, 'request', () => 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.variables.options = options;
const awsProvider = new AwsProvider(serverless, options);
const param = '/some/path/to/invalidparam';
const property = `\${ssm:${param}}`;
const error = new serverless.classes.Error('Some random failure.', 123);
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
return serverless.variables.populateProperty(property)
.should.be.rejectedWith(serverless.classes.Error)
.then(() => expect(requestStub.callCount).to.equal(1))
.finally(() => requestStub.restore());
});
it('should run recursively if nested variables provided', () => {
// eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${env:${opt:name}}';
process.env.TEST_VAR = 'dev';
serverless.variables.options = { name: 'TEST_VAR' };
return serverless.variables.populateProperty(property)
.should.eventually.eql('my stage is dev')
.then().finally(() => { delete process.env.TEST_VAR; });
});
});
describe('#populateVariable()', () => {
it('should populate string variables as sub string', () => {
const valueToPopulate = 'dev';
const matchedString = '${opt:stage}'; // eslint-disable-line no-template-curly-in-string
// eslint-disable-next-line no-template-curly-in-string
const property = 'my stage is ${opt:stage}';
serverless.variables.populateVariable(property, matchedString, valueToPopulate)
.should.eql('my stage is dev');
});
it('should populate number variables as sub string', () => {
const valueToPopulate = 5;
const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
// eslint-disable-next-line no-template-curly-in-string
const property = 'your account number is ${opt:number}';
serverless.variables.populateVariable(property, matchedString, valueToPopulate)
.should.eql('your account number is 5');
});
it('should populate non string variables', () => {
const valueToPopulate = 5;
const matchedString = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
const property = '${opt:number}'; // eslint-disable-line no-template-curly-in-string
return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
.should.equal(5);
});
it('should throw error if populating non string or non number variable as sub string', () => {
const valueToPopulate = {};
const matchedString = '${opt:object}'; // eslint-disable-line no-template-curly-in-string
// eslint-disable-next-line no-template-curly-in-string
const property = 'your account number is ${opt:object}';
return expect(() =>
serverless.variables.populateVariable(property, matchedString, valueToPopulate))
.to.throw(serverless.classes.Error);
});
});
describe('#splitByComma', () => {
it('should return a given empty string', () => {
const input = '';
const expected = [input];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should return a undelimited string', () => {
const input = 'foo:bar';
const expected = [input];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should split basic comma delimited strings', () => {
const input = 'my,values,to,split';
const expected = ['my', 'values', 'to', 'split'];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should remove leading and following white space', () => {
const input = ' \t\nfoobar\n\t ';
const expected = ['foobar'];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should remove white space surrounding commas', () => {
const input = 'a,b ,c , d, e , f\t,g\n,h,\ti,\nj,\t\n , \n\tk';
const expected = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should ignore quoted commas', () => {
const input = '",", \',\', ",\', \',\'", "\',\', \',\'", \',", ","\', \'",", ","\'';
const expected = [
'","',
'\',\'',
'",\', \',\'"',
'"\',\', \',\'"',
'\',", ","\'',
'\'",", ","\'',
];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
it('should deal with a combination of these cases', () => {
const input = ' \t\n\'a\'\t\n , \n\t"foo,bar", opt:foo, ",", \',\', "\',\', \',\'", foo\n\t ';
const expected = ['\'a\'', '"foo,bar"', 'opt:foo', '","', '\',\'', '"\',\', \',\'"', 'foo'];
expect(serverless.variables.splitByComma(input)).to.eql(expected);
});
});
describe('#overwrite()', () => {
it('should overwrite undefined and null values', () => {
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(undefined);
getValueFromSourceStub.onCall(1).resolves(null);
getValueFromSourceStub.onCall(2).resolves('variableValue');
return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage'])
.should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSourceStub).to.have.been.calledThrice;
})
.finally(() => getValueFromSourceStub.restore());
});
it('should overwrite empty object values', () => {
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves({});
getValueFromSourceStub.onCall(1).resolves('variableValue');
return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSourceStub).to.have.been.calledTwice;
})
.finally(() => getValueFromSourceStub.restore());
});
it('should not overwrite 0 values', () => {
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(0);
getValueFromSourceStub.onCall(1).resolves('variableValue');
return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(0)
.then().finally(() => getValueFromSourceStub.restore());
});
it('should not overwrite false values', () => {
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(false);
getValueFromSourceStub.onCall(1).resolves('variableValue');
return serverless.variables.overwrite(['opt:stage', 'env:stage']).should.become(false)
.then().finally(() => getValueFromSourceStub.restore());
});
it('should skip getting values once a value has been found', () => {
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
getValueFromSourceStub.onCall(0).resolves(undefined);
getValueFromSourceStub.onCall(1).resolves('variableValue');
getValueFromSourceStub.onCall(2).resolves('variableValue2');
return serverless.variables.overwrite(['opt:stage', 'env:stage', 'self:provider.stage'])
.should.be.fulfilled
.then(valueToPopulate => expect(valueToPopulate).to.equal('variableValue'))
.finally(() => getValueFromSourceStub.restore());
});
it('should properly handle string values containing commas', () => {
const str = '"foo,bar"';
const getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource')
.resolves(undefined);
return serverless.variables.overwrite(['opt:stage', str])
.should.be.fulfilled
.then(() => expect(getValueFromSourceStub.getCall(1).args[0]).to.eql(str))
.finally(() => getValueFromSourceStub.restore());
});
});
describe('#getValueFromSource()', () => {
it('should call getValueFromEnv if referencing env var', () => {
const getValueFromEnvStub = sinon.stub(serverless.variables, 'getValueFromEnv')
.resolves('variableValue');
return serverless.variables.getValueFromSource('env:TEST_VAR').should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromEnvStub).to.have.been.called;
expect(getValueFromEnvStub).to.have.been.calledWith('env:TEST_VAR');
})
.finally(() => getValueFromEnvStub.restore());
});
it('should call getValueFromOptions if referencing an option', () => {
const getValueFromOptionsStub = sinon
.stub(serverless.variables, 'getValueFromOptions')
.resolves('variableValue');
return serverless.variables.getValueFromSource('opt:stage').should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromOptionsStub).to.have.been.called;
expect(getValueFromOptionsStub).to.have.been.calledWith('opt:stage');
})
.finally(() => getValueFromOptionsStub.restore());
});
it('should call getValueFromSelf if referencing from self', () => {
const getValueFromSelfStub = sinon.stub(serverless.variables, 'getValueFromSelf')
.resolves('variableValue');
return serverless.variables.getValueFromSource('self:provider').should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSelfStub).to.have.been.called;
expect(getValueFromSelfStub).to.have.been.calledWith('self:provider');
})
.finally(() => getValueFromSelfStub.restore());
});
it('should call getValueFromFile if referencing from another file', () => {
const getValueFromFileStub = sinon.stub(serverless.variables, 'getValueFromFile')
.resolves('variableValue');
return serverless.variables.getValueFromSource('file(./config.yml)').should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromFileStub).to.have.been.called;
expect(getValueFromFileStub).to.have.been.calledWith('file(./config.yml)');
})
.finally(() => getValueFromFileStub.restore());
});
it('should call getValueFromCf if referencing CloudFormation Outputs', () => {
const getValueFromCfStub = sinon.stub(serverless.variables, 'getValueFromCf')
.resolves('variableValue');
return serverless.variables.getValueFromSource('cf:test-stack.testOutput').should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromCfStub).to.have.been.called;
expect(getValueFromCfStub).to.have.been.calledWith('cf:test-stack.testOutput');
})
.finally(() => getValueFromCfStub.restore());
});
it('should call getValueFromS3 if referencing variable in S3', () => {
const getValueFromS3Stub = sinon.stub(serverless.variables, 'getValueFromS3')
.resolves('variableValue');
return serverless.variables.getValueFromSource('s3:test-bucket/path/to/key')
.should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromS3Stub).to.have.been.called;
expect(getValueFromS3Stub).to.have.been.calledWith('s3:test-bucket/path/to/key');
})
.finally(() => getValueFromS3Stub.restore());
});
it('should call getValueFromSsm if referencing variable in SSM', () => {
const getValueFromSsmStub = sinon.stub(serverless.variables, 'getValueFromSsm')
.resolves('variableValue');
return serverless.variables.getValueFromSource('ssm:/test/path/to/param')
.should.be.fulfilled
.then((valueToPopulate) => {
expect(valueToPopulate).to.equal('variableValue');
expect(getValueFromSsmStub).to.have.been.called;
expect(getValueFromSsmStub).to.have.been.calledWith('ssm:/test/path/to/param');
})
.finally(() => getValueFromSsmStub.restore());
});
it('should reject invalid sources', () =>
serverless.variables.getValueFromSource('weird:source')
.should.be.rejectedWith(serverless.classes.Error));
describe('caching', () => {
const sources = [
{ function: 'getValueFromEnv', variableString: 'env:NODE_ENV' },
{ function: 'getValueFromOptions', variableString: 'opt:stage' },
{ function: 'getValueFromSelf', variableString: 'self:provider' },
{ function: 'getValueFromFile', variableString: 'file(./config.yml)' },
{ function: 'getValueFromCf', variableString: 'cf:test-stack.testOutput' },
{ function: 'getValueFromS3', variableString: 's3:test-bucket/path/to/ke' },
{ function: 'getValueFromSsm', variableString: 'ssm:/test/path/to/param' },
];
sources.forEach((source) => {
it(`should only call ${source.function} once, returning the cached value otherwise`, () => {
const value = 'variableValue';
const getValueFunctionStub = sinon.stub(serverless.variables, source.function)
.resolves(value);
return BbPromise.all([
serverless.variables.getValueFromSource(source.variableString).should.become(value),
BbPromise.delay(100).then(() =>
serverless.variables.getValueFromSource(source.variableString).should.become(value)),
]).then(() => {
expect(getValueFunctionStub).to.have.been.calledOnce;
expect(getValueFunctionStub).to.have.been.calledWith(source.variableString);
}).finally(() =>
getValueFunctionStub.restore());
});
});
});
});
describe('#getValueFromEnv()', () => {
it('should get variable from environment variables', () => {
process.env.TEST_VAR = 'someValue';
return serverless.variables.getValueFromEnv('env:TEST_VAR')
.finally(() => { delete process.env.TEST_VAR; })
.should.become('someValue');
});
it('should allow top-level references to the environment variables hive', () => {
process.env.TEST_VAR = 'someValue';
return serverless.variables.getValueFromEnv('env:').then((valueToPopulate) => {
expect(valueToPopulate.TEST_VAR).to.be.equal('someValue');
})
.finally(() => { delete process.env.TEST_VAR; });
});
});
describe('#getValueFromOptions()', () => {
it('should get variable from options', () => {
serverless.variables.options = { stage: 'prod' };
return serverless.variables.getValueFromOptions('opt:stage').should.become('prod');
});
it('should allow top-level references to the options hive', () => {
serverless.variables.options = { stage: 'prod' };
return serverless.variables.getValueFromOptions('opt:')
.should.become(serverless.variables.options);
});
});
describe('#getValueFromSelf()', () => {
it('should get variable from self serverless.yml file', () => {
serverless.variables.service = {
service: 'testService',
provider: serverless.service.provider,
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getValueFromSelf('self:service').should.become('testService');
});
it('should handle self-references to the root of the serverless.yml file', () => {
serverless.variables.service = {
service: 'testService',
provider: 'testProvider',
defaults: serverless.service.defaults,
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getValueFromSelf('self:')
.should.eventually.equal(serverless.variables.service);
});
});
describe('#getValueFromFile()', () => {
it('should work for absolute paths with ~ ', () => {
const expectedFileName = `${os.homedir()}/somedir/config.yml`;
const configYml = {
test: 1,
test2: 'test2',
testObj: {
sub: 2,
prob: 'prob',
},
};
const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
const realpathSync = sinon.stub(fse, 'realpathSync').returns(expectedFileName);
const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns(configYml);
return serverless.variables.getValueFromFile('file(~/somedir/config.yml)').should.be.fulfilled
.then((valueToPopulate) => {
expect(realpathSync).to.not.have.been.called;
expect(fileExistsStub).to.have.been.calledWithMatch(expectedFileName);
expect(readFileSyncStub).to.have.been.calledWithMatch(expectedFileName);
expect(valueToPopulate).to.deep.equal(configYml);
})
.finally(() => {
realpathSync.restore();
readFileSyncStub.restore();
fileExistsStub.restore();
});
});
it('should populate an entire variable file', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
test: 1,
test2: 'test2',
testObj: {
sub: 2,
prob: 'prob',
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./config.yml)')
.should.eventually.eql(configYml);
});
it('should get undefined if non existing file and the second argument is true', () => {
const tmpDirPath = testUtils.getTmpDirPath();
serverless.config.update({ servicePath: tmpDirPath });
const realpathSync = sinon.spy(fse, 'realpathSync');
const existsSync = sinon.spy(fse, 'existsSync');
return serverless.variables.getValueFromFile('file(./non-existing.yml)').should.be.fulfilled
.then((valueToPopulate) => {
expect(realpathSync).to.not.have.been.called;
expect(existsSync).to.have.been.calledOnce;
expect(valueToPopulate).to.be.undefined;
})
.finally(() => {
realpathSync.restore();
existsSync.restore();
});
});
it('should populate non json/yml files', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world');
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./someFile)')
.should.become('hello world');
});
it('should populate symlinks', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const realFilePath = path.join(tmpDirPath, 'someFile');
const symlinkPath = path.join(tmpDirPath, 'refSomeFile');
SUtils.writeFileSync(realFilePath, 'hello world');
fse.ensureSymlinkSync(realFilePath, symlinkPath);
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./refSomeFile)')
.should.become('hello world')
.then().finally(() => {
fse.removeSync(realFilePath);
fse.removeSync(symlinkPath);
});
});
it('should trim trailing whitespace and new line character', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
SUtils.writeFileSync(path.join(tmpDirPath, 'someFile'), 'hello world \n');
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./someFile)')
.should.become('hello world');
});
it('should populate from another file when variable is of any type', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
test0: 0,
test1: 'test1',
test2: {
sub: 2,
prob: 'prob',
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./config.yml):test2.sub')
.should.become(configYml.test2.sub);
});
it('should populate from a javascript file', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports.hello=function(){return "hello world";};';
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./hello.js):hello')
.should.become('hello world');
});
it('should populate an entire variable exported by a javascript file', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports=function(){return { hello: "hello world" };};';
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./hello.js)')
.should.become({ hello: 'hello world' });
});
it('should throw if property exported by a javascript file is not a function', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = 'module.exports={ hello: "hello world" };';
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./hello.js)')
.should.be.rejectedWith(serverless.classes.Error);
});
it('should populate deep object from a javascript file', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = `module.exports.hello=function(){
return {one:{two:{three: 'hello world'}}}
};`;
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
serverless.variables.loadVariableSyntax();
return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three')
.should.become('hello world');
});
it('should preserve the exported function context when executing', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const jsData = `
module.exports.one = {two: {three: 'hello world'}}
module.exports.hello=function(){ return this; };`;
SUtils.writeFileSync(path.join(tmpDirPath, 'hello.js'), jsData);
serverless.config.update({ servicePath: tmpDirPath });
serverless.variables.loadVariableSyntax();
return serverless.variables.getValueFromFile('file(./hello.js):hello.one.two.three')
.should.become('hello world');
});
it('should file variable not using ":" syntax', () => {
const SUtils = new Utils();
const tmpDirPath = testUtils.getTmpDirPath();
const configYml = {
test: 1,
test2: 'test2',
testObj: {
sub: 2,
prob: 'prob',
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'config.yml'), YAML.dump(configYml));
serverless.config.update({ servicePath: tmpDirPath });
return serverless.variables.getValueFromFile('file(./config.yml).testObj.sub')
.should.be.rejectedWith(serverless.classes.Error);
});
});
describe('#getValueFromCf()', () => {
it('should get variable from CloudFormation', () => {
const options = {
stage: 'prod',
region: 'us-west-2',
};
const awsProvider = new AwsProvider(serverless, options);
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
const awsResponseMock = {
Stacks: [{
Outputs: [{
OutputKey: 'MockExport',
OutputValue: 'MockValue',
}],
}],
};
const cfStub = sinon.stub(serverless.getProvider('aws'), 'request',
() => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromCf('cf:some-stack.MockExport')
.should.become('MockValue')
.then(() => {
expect(cfStub).to.have.been.calledOnce;
expect(cfStub).to.have.been.calledWithExactly(
'CloudFormation',
'describeStacks',
{ StackName: 'some-stack' },
{ useCache: true });
})
.finally(() => cfStub.restore());
});
it('should reject CloudFormation variables that do not exist', () => {
const options = {
stage: 'prod',
region: 'us-west-2',
};
const awsProvider = new AwsProvider(serverless, options);
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
const awsResponseMock = {
Stacks: [{
Outputs: [{
OutputKey: 'MockExport',
OutputValue: 'MockValue',
}],
}],
};
const cfStub = sinon.stub(serverless.getProvider('aws'), 'request',
() => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromCf('cf:some-stack.DoestNotExist')
.should.be.rejectedWith(serverless.classes.Error,
/to request a non exported variable from CloudFormation/)
.then(() => {
expect(cfStub).to.have.been.calledOnce;
expect(cfStub).to.have.been.calledWithExactly(
'CloudFormation',
'describeStacks',
{ StackName: 'some-stack' },
{ useCache: true });
})
.finally(() => cfStub.restore());
});
});
describe('#getValueFromS3()', () => {
let awsProvider;
beforeEach(() => {
const options = {
stage: 'prod',
region: 'us-west-2',
};
awsProvider = new AwsProvider(serverless, options);
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
});
it('should get variable from S3', () => {
const awsResponseMock = {
Body: 'MockValue',
};
const s3Stub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromS3('s3:some.bucket/path/to/key')
.should.become('MockValue')
.then(() => {
expect(s3Stub).to.have.been.calledOnce;
expect(s3Stub).to.have.been.calledWithExactly(
'S3',
'getObject',
{
Bucket: 'some.bucket',
Key: 'path/to/key',
},
{ useCache: true });
})
.finally(() => s3Stub.restore());
});
it('should throw error if error getting value from S3', () => {
const error = new Error('The specified bucket is not valid');
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
return expect(serverless.variables.getValueFromS3('s3:some.bucket/path/to/key'))
.to.be.rejectedWith(
serverless.classes.Error,
'Error getting value for s3:some.bucket/path/to/key. The specified bucket is not valid')
.then().finally(() => requestStub.restore());
});
});
describe('#getValueFromSsm()', () => {
const param = 'Param-01_valid.chars';
const value = 'MockValue';
const awsResponseMock = {
Parameter: {
Value: value,
},
};
let awsProvider;
beforeEach(() => {
const options = {
stage: 'prod',
region: 'us-west-2',
};
awsProvider = new AwsProvider(serverless, options);
serverless.setProvider('aws', awsProvider);
serverless.variables.options = options;
});
it('should get variable from Ssm using regular-style param', () => {
const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromSsm(`ssm:${param}`)
.should.become(value)
.then(() => {
expect(ssmStub).to.have.been.calledOnce;
expect(ssmStub).to.have.been.calledWithExactly(
'SSM',
'getParameter',
{
Name: param,
WithDecryption: false,
},
{ useCache: true });
})
.finally(() => ssmStub.restore());
});
it('should get variable from Ssm using path-style param', () => {
const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromSsm(`ssm:${param}`)
.should.become(value)
.then(() => {
expect(ssmStub).to.have.been.calledOnce;
expect(ssmStub).to.have.been.calledWithExactly(
'SSM',
'getParameter',
{
Name: param,
WithDecryption: false,
},
{ useCache: true });
})
.finally(() => ssmStub.restore());
});
it('should get encrypted variable from Ssm using extended syntax', () => {
const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromSsm(`ssm:${param}~true`)
.should.become(value)
.then(() => {
expect(ssmStub).to.have.been.calledOnce;
expect(ssmStub).to.have.been.calledWithExactly(
'SSM',
'getParameter',
{
Name: param,
WithDecryption: true,
},
{ useCache: true });
})
.finally(() => ssmStub.restore());
});
it('should get unencrypted variable from Ssm using extended syntax', () => {
const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromSsm(`ssm:${param}~false`)
.should.become(value)
.then(() => {
expect(ssmStub).to.have.been.calledOnce;
expect(ssmStub).to.have.been.calledWithExactly(
'SSM',
'getParameter',
{
Name: param,
WithDecryption: false,
},
{ useCache: true });
})
.finally(() => ssmStub.restore());
});
it('should ignore bad values for extended syntax', () => {
const ssmStub = sinon.stub(awsProvider, 'request', () => BbPromise.resolve(awsResponseMock));
return serverless.variables.getValueFromSsm(`ssm:${param}~badvalue`)
.should.become(value)
.then(() => {
expect(ssmStub).to.have.been.calledOnce;
expect(ssmStub).to.have.been.calledWithExactly(
'SSM',
'getParameter',
{
Name: param,
WithDecryption: false,
},
{ useCache: true });
})
.finally(() => ssmStub.restore());
});
it('should return undefined if SSM parameter does not exist', () => {
const error = new Error(`Parameter ${param} not found.`);
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
return serverless.variables.getValueFromSsm(`ssm:${param}`)
.should.become(undefined)
.then().finally(() => requestStub.restore());
});
it('should reject if SSM request returns unexpected error', () => {
const error = new Error(
'User: <arn> is not authorized to perform: ssm:GetParameter on resource: <arn>');
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
return serverless.variables.getValueFromSsm(`ssm:${param}`)
.should.be.rejected
.then().finally(() => requestStub.restore());
});
});
describe('#getDeepValue()', () => {
it('should get deep values', () => {
const valueToPopulateMock = {
service: 'testService',
custom: {
subProperty: {
deep: 'deepValue',
},
},
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep'],
valueToPopulateMock).should.become('deepValue');
});
it('should not throw error if referencing invalid properties', () => {
const valueToPopulateMock = {
service: 'testService',
custom: {
subProperty: 'hello',
},
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep', 'deeper'],
valueToPopulateMock).should.eventually.deep.equal({});
});
it('should return a simple deep variable when final deep value is variable', () => {
serverless.variables.service = {
service: 'testService',
custom: {
subProperty: {
// eslint-disable-next-line no-template-curly-in-string
deep: '${self:custom.anotherVar.veryDeep}',
},
},
provider: serverless.service.provider,
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getDeepValue(
['custom', 'subProperty', 'deep'],
serverless.variables.service
).should.become('${deep:0}');
});
it('should return a deep continuation when middle deep value is variable', () => {
serverless.variables.service = {
service: 'testService',
custom: {
anotherVar: '${self:custom.var}', // eslint-disable-line no-template-curly-in-string
},
provider: serverless.service.provider,
};
serverless.variables.loadVariableSyntax();
return serverless.variables.getDeepValue(
['custom', 'anotherVar', 'veryDeep'],
serverless.variables.service)
.should.become('${deep:0.veryDeep}');
});
});
describe('#warnIfNotFound()', () => {
let logWarningSpy;
let consoleLogStub;
let varProxy;
beforeEach(() => {
logWarningSpy = sinon.spy(slsError, 'logWarning');
consoleLogStub = sinon.stub(console, 'log').returns();
const ProxyQuiredVariables = proxyquire('./Variables.js', { './Error': logWarningSpy });
varProxy = new ProxyQuiredVariables(serverless);
});
afterEach(() => {
logWarningSpy.restore();
consoleLogStub.restore();
});
it('should do nothing if variable has valid value.', () => {
varProxy.warnIfNotFound('self:service', 'a-valid-value');
expect(logWarningSpy).to.not.have.been.calledOnce;
});
it('should log if variable has null value.', () => {
varProxy.warnIfNotFound('self:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
});
it('should log if variable has undefined value.', () => {
varProxy.warnIfNotFound('self:service', undefined);
expect(logWarningSpy).to.have.been.calledOnce;
});
it('should log if variable has empty object value.', () => {
varProxy.warnIfNotFound('self:service', {});
expect(logWarningSpy).to.have.been.calledOnce;
});
it('should detect the "environment variable" variable type', () => {
varProxy.warnIfNotFound('env:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('environment variable');
});
it('should detect the "option" variable type', () => {
varProxy.warnIfNotFound('opt:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('option');
});
it('should detect the "service attribute" variable type', () => {
varProxy.warnIfNotFound('self:service', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('service attribute');
});
it('should detect the "file" variable type', () => {
varProxy.warnIfNotFound('file(service)', null);
expect(logWarningSpy).to.have.been.calledOnce;
expect(logWarningSpy.args[0][0]).to.contain('file');
});
});
});