mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
Delete bucket was still using them Hopefully all :) Further test fixes. .... worked too long yesterday Fixed Variable tests Remove not used parameters from request() and add options with warning
1701 lines
59 KiB
JavaScript
1701 lines
59 KiB
JavaScript
'use strict';
|
|
|
|
/* eslint-disable no-unused-expressions */
|
|
|
|
const jc = require('json-cycle');
|
|
const path = require('path');
|
|
const proxyquire = require('proxyquire');
|
|
const YAML = require('js-yaml');
|
|
const chai = require('chai');
|
|
const Variables = require('../../lib/classes/Variables');
|
|
const Utils = require('../../lib/classes/Utils');
|
|
const fse = require('../utils/fs/fse');
|
|
const Serverless = require('../../lib/Serverless');
|
|
const sinon = require('sinon');
|
|
const testUtils = require('../../tests/utils');
|
|
const slsError = require('./Error');
|
|
const AwsProvider = require('../plugins/aws/provider/awsProvider');
|
|
const BbPromise = require('bluebird');
|
|
const os = require('os');
|
|
|
|
chai.use(require('chai-as-promised'));
|
|
chai.use(require('sinon-chai'));
|
|
|
|
const expect = chai.expect;
|
|
|
|
describe('Variables', () => {
|
|
describe('#constructor()', () => {
|
|
const serverless = new Serverless();
|
|
|
|
it('should attach serverless instance', () => {
|
|
const variablesInstance = new Variables(serverless);
|
|
expect(typeof variablesInstance.serverless.version).to.be.equal('string');
|
|
});
|
|
|
|
it('should not set variableSyntax in constructor', () => {
|
|
const variablesInstance = new Variables(serverless);
|
|
expect(variablesInstance.variableSyntax).to.be.undefined;
|
|
});
|
|
});
|
|
|
|
describe('#loadVariableSyntax()', () => {
|
|
it('should set variableSyntax', () => {
|
|
const serverless = new Serverless();
|
|
|
|
serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}';
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
expect(serverless.variables.variableSyntax).to.be.a('RegExp');
|
|
});
|
|
});
|
|
|
|
describe('#populateService()', () => {
|
|
it('should call populateProperty method', () => {
|
|
const serverless = new Serverless();
|
|
|
|
const populatePropertyStub = sinon
|
|
.stub(serverless.variables, 'populateObject').resolves();
|
|
|
|
return expect(serverless.variables.populateService()).to.be.fulfilled
|
|
.then(() => {
|
|
expect(populatePropertyStub.called).to.be.true;
|
|
})
|
|
.finally(() => serverless.variables.populateObject.restore());
|
|
});
|
|
|
|
it('should use variableSyntax', () => {
|
|
const serverless = new Serverless();
|
|
|
|
const variableSyntax = '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}';
|
|
const fooValue = '${clientId()}';
|
|
const barValue = 'test';
|
|
|
|
serverless.service.provider.variableSyntax = variableSyntax;
|
|
|
|
serverless.service.custom = {
|
|
var: barValue,
|
|
};
|
|
|
|
serverless.service.resources = {
|
|
foo: fooValue,
|
|
bar: '${{self:custom.var}}',
|
|
};
|
|
|
|
return serverless.variables.populateService().then(() => {
|
|
expect(serverless.service.provider.variableSyntax).to.equal(variableSyntax);
|
|
expect(serverless.service.resources.foo).to.equal(fooValue);
|
|
expect(serverless.service.resources.bar).to.equal(barValue);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#populateObject()', () => {
|
|
it('should call populateProperty method', () => {
|
|
const serverless = new Serverless();
|
|
const object = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
|
|
const populatePropertyStub = sinon
|
|
.stub(serverless.variables, 'populateProperty').resolves('prod');
|
|
|
|
return serverless.variables.populateObject(object).then(() => {
|
|
expect(populatePropertyStub.called).to.be.true;
|
|
})
|
|
.finally(() => serverless.variables.populateProperty.restore());
|
|
});
|
|
|
|
it('should populate object and return it', () => {
|
|
const serverless = new Serverless();
|
|
const object = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
const expectedPopulatedObject = {
|
|
stage: 'prod',
|
|
};
|
|
|
|
sinon.stub(serverless.variables, 'populateProperty').resolves('prod');
|
|
|
|
return serverless.variables.populateObject(object).then(populatedObject => {
|
|
expect(populatedObject).to.deep.equal(expectedPopulatedObject);
|
|
})
|
|
.finally(() => serverless.variables.populateProperty.restore());
|
|
});
|
|
|
|
it('should persist keys with dot notation', () => {
|
|
const serverless = new Serverless();
|
|
const object = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
object['some.nested.key'] = 'hello';
|
|
const expectedPopulatedObject = {
|
|
stage: 'prod',
|
|
};
|
|
expectedPopulatedObject['some.nested.key'] = 'hello';
|
|
|
|
const populatePropertyStub = sinon.stub(serverless.variables, 'populateProperty');
|
|
populatePropertyStub.onCall(0).resolves('prod');
|
|
populatePropertyStub.onCall(1).resolves('hello');
|
|
|
|
return serverless.variables.populateObject(object).then(populatedObject => {
|
|
expect(populatedObject).to.deep.equal(expectedPopulatedObject);
|
|
})
|
|
.finally(() => serverless.variables.populateProperty.restore());
|
|
});
|
|
describe('significant variable usage corner cases', () => {
|
|
let serverless;
|
|
let service;
|
|
const makeDefault = () => ({
|
|
service: 'my-service',
|
|
provider: {
|
|
name: 'aws',
|
|
},
|
|
});
|
|
beforeEach(() => {
|
|
serverless = new Serverless();
|
|
service = makeDefault();
|
|
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:}',
|
|
};
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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,
|
|
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,
|
|
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,
|
|
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}',
|
|
val2: '${self:custom.val0}',
|
|
};
|
|
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}',
|
|
depVal: '${self:custom.val0}',
|
|
val0: 'my value',
|
|
val2: '${self:custom.depVal}',
|
|
};
|
|
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;
|
|
});
|
|
const pathAsyncLoadJs = 'async.load.js';
|
|
const makeAsyncLoadJs = () => {
|
|
const SUtils = new Utils();
|
|
const tmpDirPath = testUtils.getTmpDirPath();
|
|
const fileContent = `'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,
|
|
};
|
|
`;
|
|
SUtils.writeFileSync(path.join(tmpDirPath, pathAsyncLoadJs), fileContent);
|
|
serverless.config.update({ servicePath: tmpDirPath });
|
|
};
|
|
it('should populate any given variable only once', () => {
|
|
makeAsyncLoadJs();
|
|
service.custom = {
|
|
val1: '${self:custom.val0}',
|
|
val2: '${self:custom.val1}',
|
|
val0: `\${file(${pathAsyncLoadJs}):str}`,
|
|
};
|
|
const expected = {
|
|
val1: 'my-async-value-1',
|
|
val2: 'my-async-value-1',
|
|
val0: 'my-async-value-1',
|
|
};
|
|
return expect(serverless.variables.populateObject(service.custom).then((result) => {
|
|
expect(result).to.eql(expected);
|
|
})).to.be.fulfilled;
|
|
});
|
|
it('should populate any given variable only once regardless of ordering or reference count',
|
|
() => {
|
|
makeAsyncLoadJs();
|
|
service.custom = {
|
|
val9: '${self:custom.val7}',
|
|
val7: '${self:custom.val5}',
|
|
val5: '${self:custom.val3}',
|
|
val3: '${self:custom.val1}',
|
|
val1: '${self:custom.val0}',
|
|
val2: '${self:custom.val1}',
|
|
val4: '${self:custom.val3}',
|
|
val6: '${self:custom.val5}',
|
|
val8: '${self:custom.val7}',
|
|
val0: `\${file(${pathAsyncLoadJs}):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 expect(serverless.variables.populateObject(service.custom).then((result) => {
|
|
expect(result).to.eql(expected);
|
|
})).to.be.fulfilled;
|
|
}
|
|
);
|
|
it('should populate async objects with contained variables',
|
|
() => {
|
|
makeAsyncLoadJs();
|
|
serverless.variables.options = {
|
|
stage: 'dev',
|
|
};
|
|
service.custom = {
|
|
obj: `\${file(${pathAsyncLoadJs}):obj}`,
|
|
};
|
|
const expected = {
|
|
obj: {
|
|
val0: 'my-async-value-1',
|
|
val1: 'dev',
|
|
},
|
|
};
|
|
return expect(serverless.variables.populateObject(service.custom).then((result) => {
|
|
expect(result).to.eql(expected);
|
|
})).to.be.fulfilled;
|
|
}
|
|
);
|
|
const pathEmptyJs = 'empty.js';
|
|
const makeEmptyJs = () => {
|
|
const SUtils = new Utils();
|
|
const tmpDirPath = testUtils.getTmpDirPath();
|
|
const fileContent = `'use strict';
|
|
module.exports = {
|
|
func: () => ({ value: 'a value' }),
|
|
}
|
|
`;
|
|
SUtils.writeFileSync(path.join(tmpDirPath, pathEmptyJs), fileContent);
|
|
serverless.config.update({ servicePath: tmpDirPath });
|
|
};
|
|
it('should reject population of an attribute not exported from a file',
|
|
() => {
|
|
makeEmptyJs();
|
|
service.custom = {
|
|
val: `\${file(${pathEmptyJs}):func.notAValue}`,
|
|
};
|
|
return expect(serverless.variables.populateObject(service.custom))
|
|
.to.eventually.be.rejected;
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('#populateProperty()', () => {
|
|
let serverless;
|
|
let overwriteStub;
|
|
let populateObjectStub;
|
|
let getValueFromSourceStub;
|
|
let populateVariableStub;
|
|
|
|
beforeEach(() => {
|
|
serverless = new Serverless();
|
|
overwriteStub = sinon.stub(serverless.variables, 'overwrite');
|
|
populateObjectStub = sinon.stub(serverless.variables, 'populateObject');
|
|
getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
|
|
populateVariableStub = sinon.stub(serverless.variables, 'populateVariable');
|
|
});
|
|
|
|
afterEach(() => {
|
|
serverless.variables.overwrite.restore();
|
|
serverless.variables.populateObject.restore();
|
|
serverless.variables.getValueFromSource.restore();
|
|
serverless.variables.populateVariable.restore();
|
|
});
|
|
|
|
it('should call overwrite if overwrite syntax provided', () => {
|
|
const property = 'my stage is ${opt:stage, self:provider.stage}';
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
overwriteStub.resolves('dev');
|
|
populateVariableStub.resolves('my stage is dev');
|
|
|
|
return serverless.variables.populateProperty(property).then(newProperty => {
|
|
expect(overwriteStub.called).to.equal(true);
|
|
expect(populateVariableStub.called).to.equal(true);
|
|
expect(newProperty).to.equal('my stage is dev');
|
|
|
|
return BbPromise.resolve();
|
|
});
|
|
});
|
|
|
|
it('should allow a single-quoted string if overwrite syntax provided', () => {
|
|
const property = "my stage is ${opt:stage, 'prod'}";
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
overwriteStub.resolves('\'prod\'');
|
|
populateVariableStub.resolves('my stage is prod');
|
|
|
|
return expect(serverless.variables.populateProperty(property)).to.be.fulfilled
|
|
.then(newProperty => expect(newProperty).to.equal('my stage is prod'));
|
|
});
|
|
|
|
it('should allow a double-quoted string if overwrite syntax provided', () => {
|
|
const property = 'my stage is ${opt:stage, "prod"}';
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
overwriteStub.resolves('\'prod\'');
|
|
populateVariableStub.resolves('my stage is prod');
|
|
|
|
return expect(serverless.variables.populateProperty(property)).to.be.fulfilled
|
|
.then(newProperty => expect(newProperty).to.equal('my stage is prod'));
|
|
});
|
|
|
|
it('should call getValueFromSource if no overwrite syntax provided', () => {
|
|
const property = 'my stage is ${opt:stage}';
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
getValueFromSourceStub.resolves('prod');
|
|
populateVariableStub.resolves('my stage is prod');
|
|
|
|
return serverless.variables.populateProperty(property).then(newProperty => {
|
|
expect(getValueFromSourceStub.called).to.be.true;
|
|
expect(populateVariableStub.called).to.be.true;
|
|
expect(newProperty).to.equal('my stage is prod');
|
|
|
|
return BbPromise.resolve();
|
|
});
|
|
});
|
|
|
|
it('should NOT call populateObject if variable value is a circular object', () => {
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
};
|
|
const property = '${opt:stage}';
|
|
const variableValue = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
const variableValuePopulated = {
|
|
stage: 'prod',
|
|
};
|
|
|
|
serverless.variables.cache['opt:stage'] = variableValuePopulated;
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
populateObjectStub.resolves(variableValuePopulated);
|
|
getValueFromSourceStub.resolves(variableValue);
|
|
populateVariableStub.resolves(variableValuePopulated);
|
|
|
|
return serverless.variables.populateProperty(property).then(newProperty => {
|
|
expect(populateObjectStub.called).to.equal(false);
|
|
expect(getValueFromSourceStub.called).to.equal(true);
|
|
expect(populateVariableStub.called).to.equal(true);
|
|
expect(newProperty).to.deep.equal(variableValuePopulated);
|
|
|
|
return BbPromise.resolve();
|
|
});
|
|
});
|
|
|
|
it('should warn if an SSM parameter does not exist', () => {
|
|
const awsProvider = new AwsProvider(serverless, { stage: 'prod', region: 'us-west-2' });
|
|
const param = '/some/path/to/invalidparam';
|
|
const property = `\${ssm:${param}}`;
|
|
const error = new Error(`Parameter ${param} not found.`);
|
|
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
region: 'us-east-1',
|
|
};
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
serverless.variables.getValueFromSource.restore();
|
|
serverless.variables.populateVariable.restore();
|
|
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
|
|
const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound');
|
|
|
|
return expect(serverless.variables.populateProperty(property)
|
|
.then(newProperty => {
|
|
expect(requestStub.callCount).to.equal(1);
|
|
expect(warnIfNotFoundSpy.callCount).to.equal(1);
|
|
expect(newProperty).to.be.undefined;
|
|
})
|
|
.finally(() => {
|
|
getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
|
|
populateVariableStub = sinon.stub(serverless.variables, 'populateVariable');
|
|
})).to.be.fulfilled;
|
|
});
|
|
|
|
it('should throw an Error if the SSM request fails', () => {
|
|
const awsProvider = new AwsProvider(serverless, { stage: 'prod', region: 'us-west-2' });
|
|
const param = '/some/path/to/invalidparam';
|
|
const property = `\${ssm:${param}}`;
|
|
const error = new Error('Some random failure.');
|
|
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
region: 'us-east-1',
|
|
};
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
serverless.variables.getValueFromSource.restore();
|
|
const requestStub = sinon.stub(awsProvider, 'request', () => BbPromise.reject(error));
|
|
|
|
return expect(serverless.variables.populateProperty(property)
|
|
.finally(() => {
|
|
getValueFromSourceStub = sinon.stub(serverless.variables, 'getValueFromSource');
|
|
expect(requestStub.callCount).to.equal(1);
|
|
})).to.be.rejectedWith(serverless.classes.Error);
|
|
});
|
|
|
|
it('should run recursively if nested variables provided', () => {
|
|
const property = 'my stage is ${env:${opt.name}}';
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
getValueFromSourceStub.onCall(0).resolves('stage');
|
|
getValueFromSourceStub.onCall(1).resolves('dev');
|
|
populateVariableStub.onCall(0).resolves('my stage is ${env:stage}');
|
|
populateVariableStub.onCall(1).resolves('my stage is dev');
|
|
|
|
return serverless.variables.populateProperty(property).then(newProperty => {
|
|
expect(getValueFromSourceStub.callCount).to.equal(2);
|
|
expect(populateVariableStub.callCount).to.equal(2);
|
|
expect(newProperty).to.equal('my stage is dev');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#populateVariable()', () => {
|
|
it('should populate string variables as sub string', () => {
|
|
const serverless = new Serverless();
|
|
const valueToPopulate = 'dev';
|
|
const matchedString = '${opt:stage}';
|
|
const property = 'my stage is ${opt:stage}';
|
|
|
|
return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
|
|
.then(newProperty => {
|
|
expect(newProperty).to.equal('my stage is dev');
|
|
});
|
|
});
|
|
|
|
it('should populate number variables as sub string', () => {
|
|
const serverless = new Serverless();
|
|
const valueToPopulate = 5;
|
|
const matchedString = '${opt:number}';
|
|
const property = 'your account number is ${opt:number}';
|
|
|
|
return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
|
|
.then(newProperty => {
|
|
expect(newProperty).to.equal('your account number is 5');
|
|
});
|
|
});
|
|
|
|
it('should populate non string variables', () => {
|
|
const serverless = new Serverless();
|
|
const valueToPopulate = 5;
|
|
const matchedString = '${opt:number}';
|
|
const property = '${opt:number}';
|
|
|
|
return serverless.variables.populateVariable(property, matchedString, valueToPopulate)
|
|
.then(newProperty => {
|
|
expect(newProperty).to.equal(5);
|
|
});
|
|
});
|
|
|
|
it('should throw error if populating non string or non number variable as sub string', () => {
|
|
const serverless = new Serverless();
|
|
const valueToPopulate = {};
|
|
const matchedString = '${opt:object}';
|
|
const property = 'your account number is ${opt:object}';
|
|
expect(() => serverless.variables
|
|
.populateVariable(property, matchedString, valueToPopulate))
|
|
.to.throw(Error);
|
|
});
|
|
});
|
|
|
|
describe('#overwrite()', () => {
|
|
it('should overwrite undefined and null values', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromSourceStub).to.have.been.calledThrice;
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSource.restore());
|
|
});
|
|
|
|
it('should overwrite empty object values', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromSourceStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSource');
|
|
|
|
getValueFromSourceStub.onCall(0).resolves({});
|
|
getValueFromSourceStub.onCall(1).resolves('variableValue');
|
|
|
|
return serverless.variables.overwrite('opt:stage,env:stage').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromSourceStub).to.have.been.calledTwice;
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSource.restore());
|
|
});
|
|
|
|
it('should not overwrite 0 values', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromSourceStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSource');
|
|
|
|
getValueFromSourceStub.onCall(0).resolves(0);
|
|
getValueFromSourceStub.onCall(1).resolves('variableValue');
|
|
getValueFromSourceStub.onCall(2).resolves('variableValue2');
|
|
return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal(0);
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSource.restore());
|
|
});
|
|
|
|
it('should not overwrite false values', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromSourceStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSource');
|
|
|
|
getValueFromSourceStub.onCall(0).resolves(false);
|
|
getValueFromSourceStub.onCall(1).resolves('variableValue');
|
|
getValueFromSourceStub.onCall(2).resolves('variableValue2');
|
|
|
|
return serverless.variables.overwrite('opt:stage,env:stage,self:provider.stage')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.false;
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSource.restore());
|
|
});
|
|
|
|
it('should skip getting values once a value has been found', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSource.restore());
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromSource()', () => {
|
|
it('should call getValueFromEnv if referencing env var', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromEnvStub = sinon
|
|
.stub(serverless.variables, 'getValueFromEnv').resolves('variableValue');
|
|
return serverless.variables.getValueFromSource('env:TEST_VAR')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromEnvStub).to.have.been.called;
|
|
expect(getValueFromEnvStub.calledWith('env:TEST_VAR')).to.equal(true);
|
|
})
|
|
.finally(() => serverless.variables.getValueFromEnv.restore());
|
|
});
|
|
|
|
it('should call getValueFromOptions if referencing an option', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromOptionsStub = sinon
|
|
.stub(serverless.variables, 'getValueFromOptions')
|
|
.resolves('variableValue');
|
|
|
|
return serverless.variables.getValueFromSource('opt:stage')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromOptionsStub).to.have.been.called;
|
|
expect(getValueFromOptionsStub.calledWith('opt:stage')).to.equal(true);
|
|
})
|
|
.finally(() => serverless.variables.getValueFromOptions.restore());
|
|
});
|
|
|
|
it('should call getValueFromSelf if referencing from self', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromSelfStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSelf').resolves('variableValue');
|
|
|
|
return serverless.variables.getValueFromSource('self:provider')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromSelfStub).to.have.been.called;
|
|
expect(getValueFromSelfStub.calledWith('self:provider')).to.equal(true);
|
|
})
|
|
.finally(() => serverless.variables.getValueFromSelf.restore());
|
|
});
|
|
|
|
it('should call getValueFromFile if referencing from another file', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromFileStub = sinon
|
|
.stub(serverless.variables, 'getValueFromFile').resolves('variableValue');
|
|
|
|
return serverless.variables.getValueFromSource('file(./config.yml)')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromFileStub).to.have.been.called;
|
|
expect(getValueFromFileStub).to.have.been.calledWith('file(./config.yml)');
|
|
})
|
|
.finally(() => serverless.variables.getValueFromFile.restore());
|
|
});
|
|
|
|
it('should call getValueFromCf if referencing CloudFormation Outputs', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromCfStub = sinon
|
|
.stub(serverless.variables, 'getValueFromCf').resolves('variableValue');
|
|
return serverless.variables.getValueFromSource('cf:test-stack.testOutput')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('variableValue');
|
|
expect(getValueFromCfStub).to.have.been.called;
|
|
expect(getValueFromCfStub).to.have.been.calledWith('cf:test-stack.testOutput');
|
|
})
|
|
.finally(() => serverless.variables.getValueFromCf.restore());
|
|
});
|
|
|
|
it('should call getValueFromS3 if referencing variable in S3', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromS3Stub = sinon
|
|
.stub(serverless.variables, 'getValueFromS3').resolves('variableValue');
|
|
return serverless.variables.getValueFromSource('s3:test-bucket/path/to/key')
|
|
.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(() => serverless.variables.getValueFromS3.restore());
|
|
});
|
|
|
|
it('should call getValueFromSsm if referencing variable in SSM', () => {
|
|
const serverless = new Serverless();
|
|
const getValueFromSsmStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSsm').resolves('variableValue');
|
|
return serverless.variables.getValueFromSource('ssm:/test/path/to/param')
|
|
.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(() => serverless.variables.getValueFromSsm.restore());
|
|
});
|
|
|
|
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 serverless = new Serverless();
|
|
const getValueFunctionStub = sinon
|
|
.stub(serverless.variables, source.function).resolves('variableValue');
|
|
const firstCall = serverless.variables.getValueFromSource(source.variableString);
|
|
const secondCall = BbPromise.delay(100)
|
|
.then(() => serverless.variables.getValueFromSource(source.variableString));
|
|
return BbPromise.all([firstCall, secondCall])
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.deep.equal(['variableValue', 'variableValue']);
|
|
expect(getValueFunctionStub).to.have.been.calledOnce;
|
|
expect(getValueFunctionStub).to.have.been.calledWith(source.variableString);
|
|
})
|
|
.finally(() => serverless.variables[source.function].restore());
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should call populateObject if variable value is an object', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
};
|
|
const property = 'self:stage';
|
|
const variableValue = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
const variableValuePopulated = {
|
|
stage: 'prod',
|
|
};
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
const populateObjectStub = sinon
|
|
.stub(serverless.variables, 'populateObject')
|
|
.resolves(variableValuePopulated);
|
|
const getValueFromSelfStub = sinon
|
|
.stub(serverless.variables, 'getValueFromSelf')
|
|
.resolves(variableValue);
|
|
|
|
return serverless.variables.getValueFromSource(property)
|
|
.then(newProperty => {
|
|
expect(populateObjectStub.called).to.equal(true);
|
|
expect(getValueFromSelfStub.called).to.equal(true);
|
|
expect(newProperty).to.deep.equal(variableValuePopulated);
|
|
|
|
return BbPromise.resolve();
|
|
})
|
|
.finally(() => {
|
|
serverless.variables.populateObject.restore();
|
|
serverless.variables.getValueFromSelf.restore();
|
|
});
|
|
});
|
|
|
|
it('should NOT call populateObject if variable value is already cached', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
};
|
|
const property = 'opt:stage';
|
|
const variableValue = {
|
|
stage: '${opt:stage}',
|
|
};
|
|
const variableValuePopulated = {
|
|
stage: 'prod',
|
|
};
|
|
|
|
serverless.variables.cache['opt:stage'] = BbPromise.resolve(variableValuePopulated);
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
const populateObjectStub = sinon
|
|
.stub(serverless.variables, 'populateObject')
|
|
.resolves(variableValuePopulated);
|
|
const getValueFromOptionsStub = sinon
|
|
.stub(serverless.variables, 'getValueFromOptions')
|
|
.resolves(variableValue);
|
|
|
|
return serverless.variables.getValueFromSource(property)
|
|
.then(newProperty => {
|
|
expect(populateObjectStub.called).to.equal(false);
|
|
expect(getValueFromOptionsStub.called).to.equal(false);
|
|
expect(newProperty).to.deep.equal(variableValuePopulated);
|
|
|
|
return BbPromise.resolve();
|
|
})
|
|
.finally(() => {
|
|
serverless.variables.populateObject.restore();
|
|
serverless.variables.getValueFromOptions.restore();
|
|
});
|
|
});
|
|
|
|
it('should throw error if referencing an invalid source', () => {
|
|
const serverless = new Serverless();
|
|
expect(() => serverless.variables.getValueFromSource('weird:source'))
|
|
.to.throw(Error);
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromEnv()', () => {
|
|
it('should get variable from environment variables', () => {
|
|
const serverless = new Serverless();
|
|
process.env.TEST_VAR = 'someValue';
|
|
return serverless.variables.getValueFromEnv('env:TEST_VAR').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('someValue');
|
|
})
|
|
.finally(() => {
|
|
delete process.env.TEST_VAR;
|
|
});
|
|
});
|
|
|
|
it('should allow top-level references to the environment variables hive', () => {
|
|
const serverless = new Serverless();
|
|
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', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
};
|
|
return serverless.variables.getValueFromOptions('opt:stage').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('prod');
|
|
});
|
|
});
|
|
|
|
it('should allow top-level references to the options hive', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.options = {
|
|
stage: 'prod',
|
|
};
|
|
return serverless.variables.getValueFromOptions('opt:').then(valueToPopulate => {
|
|
expect(valueToPopulate.stage).to.be.equal('prod');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromSelf()', () => {
|
|
it('should get variable from self serverless.yml file', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.service = {
|
|
service: 'testService',
|
|
provider: serverless.service.provider,
|
|
};
|
|
serverless.variables.loadVariableSyntax();
|
|
return serverless.variables.getValueFromSelf('self:service').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('testService');
|
|
});
|
|
});
|
|
|
|
it('should handle self-references to the root of the serverless.yml file', () => {
|
|
const serverless = new Serverless();
|
|
serverless.variables.service = {
|
|
service: 'testService',
|
|
provider: 'testProvider',
|
|
defaults: serverless.service.defaults,
|
|
};
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
return serverless.variables.getValueFromSelf('self:').then(valueToPopulate => {
|
|
expect(valueToPopulate.provider).to.be.equal('testProvider');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromFile()', () => {
|
|
it('should work for absolute paths with ~ ', () => {
|
|
const serverless = new Serverless();
|
|
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)')
|
|
.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 serverless = new Serverless();
|
|
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)').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.deep.equal(configYml);
|
|
});
|
|
});
|
|
|
|
it('should get undefined if non existing file and the second argument is true', () => {
|
|
const serverless = new Serverless();
|
|
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)')
|
|
.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 serverless = new Serverless();
|
|
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)').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should populate symlinks', () => {
|
|
const serverless = new Serverless();
|
|
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 expect(serverless.variables.getValueFromFile('file(./refSomeFile)')).to.be.fulfilled
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
})
|
|
.finally(() => {
|
|
fse.removeSync(realFilePath);
|
|
fse.removeSync(symlinkPath);
|
|
});
|
|
});
|
|
|
|
it('should trim trailing whitespace and new line character', () => {
|
|
const serverless = new Serverless();
|
|
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)').then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should populate from another file when variable is of any type', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal(2);
|
|
});
|
|
});
|
|
|
|
it('should populate from a javascript file', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should populate an entire variable exported by a javascript file', () => {
|
|
const serverless = new Serverless();
|
|
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)')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate.hello).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should throw if property exported by a javascript file is not a function', () => {
|
|
const serverless = new Serverless();
|
|
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 });
|
|
|
|
expect(() => serverless.variables
|
|
.getValueFromFile('file(./hello.js)')).to.throw(Error);
|
|
});
|
|
|
|
it('should populate deep object from a javascript file', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should preserve the exported function context when executing', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.equal('hello world');
|
|
});
|
|
});
|
|
|
|
it('should throw error if not using ":" syntax', () => {
|
|
const serverless = new Serverless();
|
|
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 });
|
|
|
|
expect(() => serverless.variables
|
|
.getValueFromFile('file(./config.yml).testObj.sub')).to.throw(Error);
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromCf()', () => {
|
|
it('should get variable from CloudFormation', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.resolves(awsResponseMock);
|
|
return serverless.variables.getValueFromCf('cf:some-stack.MockExport')
|
|
.then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('MockValue');
|
|
expect(cfStub).to.have.been.calledOnce;
|
|
expect(cfStub).to.have.been.calledWithExactly(
|
|
'CloudFormation',
|
|
'describeStacks',
|
|
{
|
|
StackName: 'some-stack',
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
})
|
|
.finally(() => serverless.getProvider('aws').request.restore());
|
|
});
|
|
|
|
it('should throw an error when variable from CloudFormation does not exist', () => {
|
|
const serverless = new Serverless();
|
|
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')
|
|
.resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromCf('cf:some-stack.DoestNotExist')
|
|
.then()
|
|
.catch(error => {
|
|
expect(cfStub).to.have.been.calledOnce;
|
|
expect(cfStub).to.have.been.calledWithExactly(
|
|
'CloudFormation',
|
|
'describeStacks',
|
|
{
|
|
StackName: 'some-stack',
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
expect(error).to.be.an.instanceof(Error);
|
|
expect(error.message).to.match(/to request a non exported variable from CloudFormation/);
|
|
})
|
|
.finally(() => serverless.getProvider('aws').request.restore());
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromS3()', () => {
|
|
let serverless;
|
|
let awsProvider;
|
|
|
|
beforeEach(() => {
|
|
serverless = new Serverless();
|
|
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').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromS3('s3:some.bucket/path/to/key').then(value => {
|
|
expect(value).to.be.equal('MockValue');
|
|
expect(s3Stub).to.have.been.calledOnce;
|
|
expect(s3Stub).to.have.been.calledWithExactly(
|
|
'S3',
|
|
'getObject',
|
|
{
|
|
Bucket: 'some.bucket',
|
|
Key: 'path/to/key',
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
})
|
|
.finally(() => serverless.getProvider('aws').request.restore());
|
|
});
|
|
|
|
it('should throw error if error getting value from S3', () => {
|
|
const error = new Error('The specified bucket is not valid');
|
|
sinon.stub(awsProvider, 'request').rejects(error);
|
|
|
|
return expect(serverless.variables.getValueFromS3('s3:some.bucket/path/to/key'))
|
|
.to.be.rejectedWith('Error getting value for s3:some.bucket/path/to/key. ' +
|
|
'The specified bucket is not valid');
|
|
});
|
|
});
|
|
|
|
describe('#getValueFromSsm()', () => {
|
|
let serverless;
|
|
let awsProvider;
|
|
|
|
beforeEach(() => {
|
|
serverless = new Serverless();
|
|
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 param = 'Param-01_valid.chars';
|
|
const value = 'MockValue';
|
|
const awsResponseMock = {
|
|
Parameter: {
|
|
Value: value,
|
|
},
|
|
};
|
|
const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromSsm(`ssm:${param}`).then(resolved => {
|
|
expect(resolved).to.be.equal(value);
|
|
expect(ssmStub).to.have.been.calledOnce;
|
|
expect(ssmStub).to.have.been.calledWithExactly(
|
|
'SSM',
|
|
'getParameter',
|
|
{
|
|
Name: param,
|
|
WithDecryption: false,
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should get variable from Ssm using path-style param', () => {
|
|
const param = '/path/to/Param-01_valid.chars';
|
|
const value = 'MockValue';
|
|
const awsResponseMock = {
|
|
Parameter: {
|
|
Value: value,
|
|
},
|
|
};
|
|
const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromSsm(`ssm:${param}`).then(resolved => {
|
|
expect(resolved).to.be.equal(value);
|
|
expect(ssmStub).to.have.been.calledOnce;
|
|
expect(ssmStub).to.have.been.calledWithExactly(
|
|
'SSM',
|
|
'getParameter',
|
|
{
|
|
Name: param,
|
|
WithDecryption: false,
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should get encrypted variable from Ssm using extended syntax', () => {
|
|
const param = '/path/to/Param-01_valid.chars';
|
|
const value = 'MockValue';
|
|
const awsResponseMock = {
|
|
Parameter: {
|
|
Value: value,
|
|
},
|
|
};
|
|
const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromSsm(`ssm:${param}~true`).then(resolved => {
|
|
expect(resolved).to.be.equal(value);
|
|
expect(ssmStub).to.have.been.calledOnce;
|
|
expect(ssmStub).to.have.been.calledWithExactly(
|
|
'SSM',
|
|
'getParameter',
|
|
{
|
|
Name: param,
|
|
WithDecryption: true,
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should get unencrypted variable from Ssm using extended syntax', () => {
|
|
const param = '/path/to/Param-01_valid.chars';
|
|
const value = 'MockValue';
|
|
const awsResponseMock = {
|
|
Parameter: {
|
|
Value: value,
|
|
},
|
|
};
|
|
const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromSsm(`ssm:${param}~false`).then(resolved => {
|
|
expect(resolved).to.be.equal(value);
|
|
expect(ssmStub).to.have.been.calledOnce;
|
|
expect(ssmStub).to.have.been.calledWithExactly(
|
|
'SSM',
|
|
'getParameter',
|
|
{
|
|
Name: param,
|
|
WithDecryption: false,
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should ignore bad values for extended syntax', () => {
|
|
const param = '/path/to/Param-01_valid.chars';
|
|
const value = 'MockValue';
|
|
const awsResponseMock = {
|
|
Parameter: {
|
|
Value: value,
|
|
},
|
|
};
|
|
const ssmStub = sinon.stub(awsProvider, 'request').resolves(awsResponseMock);
|
|
|
|
return serverless.variables.getValueFromSsm(`ssm:${param}~badvalue`).then(resolved => {
|
|
expect(resolved).to.be.equal(value);
|
|
expect(ssmStub).to.have.been.calledOnce;
|
|
expect(ssmStub).to.have.been.calledWithExactly(
|
|
'SSM',
|
|
'getParameter',
|
|
{
|
|
Name: param,
|
|
WithDecryption: false,
|
|
},
|
|
{ useCache: true }
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should return undefined if SSM parameter does not exist', () => {
|
|
const param = 'ssm:/some/path/to/invalidparam';
|
|
const error = new Error(`Parameter ${param} not found.`);
|
|
sinon.stub(awsProvider, 'request').rejects(error);
|
|
|
|
return expect(() => serverless.variables.getValueFromSsm(param).to.be(undefined));
|
|
});
|
|
|
|
it('should throw exception if SSM request returns unexpected error', () => {
|
|
const param = 'ssm:/some/path/to/invalidparam';
|
|
const error = new Error(
|
|
'User: <arn> is not authorized to perform: ssm:GetParameter on resource: <arn>');
|
|
sinon.stub(awsProvider, 'request').rejects(error);
|
|
|
|
return expect(() => serverless.variables.getValueFromSsm(param).to.throw(error));
|
|
});
|
|
});
|
|
|
|
describe('#getDeepValue()', () => {
|
|
it('should get deep values', () => {
|
|
const serverless = new Serverless();
|
|
|
|
const valueToPopulateMock = {
|
|
service: 'testService',
|
|
custom: {
|
|
subProperty: {
|
|
deep: 'deepValue',
|
|
},
|
|
},
|
|
};
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep'],
|
|
valueToPopulateMock).then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('deepValue');
|
|
});
|
|
});
|
|
|
|
it('should not throw error if referencing invalid properties', () => {
|
|
const serverless = new Serverless();
|
|
|
|
const valueToPopulateMock = {
|
|
service: 'testService',
|
|
custom: {
|
|
subProperty: 'hello',
|
|
},
|
|
};
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep', 'deeper'],
|
|
valueToPopulateMock).then(valueToPopulate => {
|
|
expect(valueToPopulate).to.deep.equal({});
|
|
});
|
|
});
|
|
|
|
it('should get deep values with variable references', () => {
|
|
const serverless = new Serverless();
|
|
|
|
serverless.variables.service = {
|
|
service: 'testService',
|
|
custom: {
|
|
anotherVar: '${self:custom.var}',
|
|
subProperty: {
|
|
deep: '${self:custom.anotherVar.veryDeep}',
|
|
},
|
|
var: {
|
|
veryDeep: 'someValue',
|
|
},
|
|
},
|
|
provider: serverless.service.provider,
|
|
};
|
|
|
|
serverless.variables.loadVariableSyntax();
|
|
|
|
return serverless.variables.getDeepValue(['custom', 'subProperty', 'deep'],
|
|
serverless.variables.service).then(valueToPopulate => {
|
|
expect(valueToPopulate).to.be.equal('someValue');
|
|
});
|
|
});
|
|
});
|
|
|
|
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(new 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');
|
|
});
|
|
});
|
|
});
|