serverless/lib/classes/Service.test.js
2019-11-20 11:11:20 +01:00

1211 lines
40 KiB
JavaScript

'use strict';
const path = require('path');
const YAML = require('js-yaml');
const _ = require('lodash');
const chai = require('chai');
const sinon = require('sinon');
const Service = require('../../lib/classes/Service');
const Utils = require('../../lib/classes/Utils');
const Serverless = require('../../lib/Serverless');
const { getTmpDirPath } = require('../../tests/utils/fs');
// Configure chai
chai.use(require('chai-as-promised'));
const expect = require('chai').expect;
describe('Service', () => {
describe('#constructor()', () => {
const serverless = new Serverless();
serverless.processedInput = { options: {} };
it('should attach serverless instance', () => {
const serviceInstance = new Service(serverless);
expect(typeof serviceInstance.serverless.version).to.be.equal('string');
});
it('should construct with defaults', () => {
const serviceInstance = new Service(serverless);
expect(serviceInstance.service).to.be.equal(null);
expect(serviceInstance.serviceObject).to.be.equal(null);
expect(serviceInstance.provider).to.deep.equal({
stage: 'dev',
variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
});
expect(serviceInstance.custom).to.deep.equal({});
expect(serviceInstance.plugins).to.deep.equal([]);
expect(serviceInstance.functions).to.deep.equal({});
expect(serviceInstance.resources).to.deep.equal({});
expect(serviceInstance.package).to.deep.equal({});
});
it('should construct with data', () => {
const data = {
service: 'testService',
provider: 'testProvider',
custom: {
customProp: 'value',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
const serviceInstance = new Service(serverless, data);
expect(serviceInstance.service).to.be.equal('testService');
expect(serviceInstance.provider).to.be.equal('testProvider');
expect(serviceInstance.custom).to.deep.equal({ customProp: 'value' });
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.functions).to.deep.equal({ functionA: {} });
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
it('should support string based provider config', () => {
const data = {
provider: 'testProvider',
};
const serviceInstance = new Service(serverless, data);
expect(serviceInstance.provider).to.be.equal('testProvider');
});
it('should support object based provider config', () => {
const data = {
provider: {
name: 'testProvider',
runtime: 'nodejs12.x',
},
};
const serviceInstance = new Service(serverless, data);
expect(serviceInstance.provider.name).to.be.equal('testProvider');
expect(serviceInstance.provider.runtime).to.be.equal('nodejs12.x');
});
});
describe('#load()', () => {
let serviceInstance;
let tmpDirPath;
beforeEach(() => {
tmpDirPath = getTmpDirPath();
});
it('should resolve if no servicePath is found', () => {
const serverless = new Serverless();
serverless.processedInput = { options: {} };
const noService = new Service(serverless);
return expect(noService.load()).to.be.fulfilled;
});
it('should load serverless.yml from filesystem', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
excludeDevDependencies: false,
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(false);
});
});
it('should load serverless.yaml from filesystem', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYml));
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});
it('should load serverless.json from filesystem', () => {
const SUtils = new Utils();
const serverlessJSON = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
SUtils.writeFileSync(
path.join(tmpDirPath, 'serverless.json'),
JSON.stringify(serverlessJSON)
);
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});
it('should load serverless.js from filesystem', () => {
const SUtils = new Utils();
const serverlessJSON = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
SUtils.writeFileSync(
path.join(tmpDirPath, 'serverless.js'),
`module.exports = ${JSON.stringify(serverlessJSON)};`
);
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});
it('should load serverless.js from filesystem', () => {
const SUtils = new Utils();
const serverlessJSON = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
SUtils.writeFileSync(
path.join(tmpDirPath, 'serverless.js'),
`module.exports = new Promise(resolve => { resolve(${JSON.stringify(serverlessJSON)}) });`
);
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.be.equal('new-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.provider.variableSyntax).to.equal(
'\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}'
);
expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
expect(serviceInstance.package.include.length).to.equal(1);
expect(serviceInstance.package.include[0]).to.equal('include-me');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
});
});
it('should throw error if serverless.js exports invalid config', () => {
const SUtils = new Utils();
SUtils.writeFileSync(
path.join(tmpDirPath, 'serverless.js'),
'module.exports = function config() {};'
);
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.be.rejectedWith(
'serverless.js must export plain object'
);
});
it('should load YAML in favor of JSON', () => {
const SUtils = new Utils();
const serverlessJSON = {
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
serverlessJSON.service = 'JSON service';
SUtils.writeFileSync(
path.join(tmpDirPath, 'serverless.json'),
JSON.stringify(serverlessJSON)
);
serverlessJSON.service = 'YAML service';
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessJSON));
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
// YAML should have been loaded instead of JSON
expect(serviceInstance.service).to.be.equal('YAML service');
});
});
it('should reject when the service name is missing', () => {
const SUtils = new Utils();
const serverlessYaml = {
service: {},
provider: 'aws',
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
'"service" is missing the "name" property in'
);
});
it('should support service objects', () => {
const SUtils = new Utils();
const serverlessYaml = {
service: {
name: 'my-service',
foo: 'bar',
},
provider: 'aws',
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
expect(serviceInstance.service).to.equal('my-service');
expect(serviceInstance.serviceObject).to.deep.equal(serverlessYaml.service);
});
});
it('should support Serverless file with a non-aws provider', () => {
const SUtils = new Utils();
const serverlessYaml = {
service: 'my-service',
provider: 'openwhisk',
functions: {
functionA: {
name: 'customFunctionName',
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
serviceInstance.setFunctionNames();
const expectedFunc = {
functionA: {
name: 'customFunctionName',
events: [],
},
};
expect(serviceInstance.service).to.be.equal('my-service');
expect(serviceInstance.provider.name).to.deep.equal('openwhisk');
expect(serviceInstance.functions).to.deep.equal(expectedFunc);
});
});
it('should support Serverless file with a .yaml extension', () => {
const SUtils = new Utils();
const serverlessYaml = {
service: 'my-service',
provider: 'aws',
functions: {
functionA: {
name: 'customFunctionName',
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
serviceInstance.setFunctionNames();
const expectedFunc = {
functionA: {
name: 'customFunctionName',
events: [],
},
};
expect(serviceInstance.service).to.be.equal('my-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.functions).to.deep.equal(expectedFunc);
});
});
it('should support Serverless file with a .yml extension', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'my-service',
provider: 'aws',
functions: {
functionA: {},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load({ stage: 'dev' })).to.eventually.be.fulfilled.then(() => {
serviceInstance.setFunctionNames();
const expectedFunc = {
functionA: {
name: 'my-service-dev-functionA',
events: [],
},
};
expect(serviceInstance.service).to.be.equal('my-service');
expect(serviceInstance.provider.name).to.deep.equal('aws');
expect(serviceInstance.functions).to.deep.equal(expectedFunc);
});
});
it('should reject if service property is missing', () => {
const SUtils = new Utils();
const serverlessYml = {
provider: 'aws',
functions: {},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
'"service" property is missing in serverless.yml'
);
});
it('should reject if provider property is missing', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'service-name',
functions: {},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
'"provider" property is missing in serverless.yml'
);
});
it('should reject if frameworkVersion is not satisfied', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'service-name',
frameworkVersion: '=1.0.0',
provider: 'aws',
functions: {},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
const getVersion = sinon.stub(serverless.utils, 'getVersion');
getVersion.returns('1.0.2');
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
/version \(1\.0\.2\).*"frameworkVersion" \(=1\.0\.0\)/
);
});
it('should pass if frameworkVersion is satisfied', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'service-name',
frameworkVersion: '>=1.0.0',
provider: 'aws',
functions: {},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
const getVersion = sinon.stub(serverless.utils, 'getVersion');
getVersion.returns('1.2.2');
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled;
});
it('should fulfill if functions property is missing', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'service-name',
provider: 'aws',
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serverless.variables.service = serverless.service;
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
// populate variables in service configuration
serverless.variables.populateService();
// validate the service configuration, now that variables are loaded
serviceInstance.validate();
expect(serviceInstance.functions).to.deep.equal({});
});
});
});
describe('#validate()', () => {
let tmpDirPath;
beforeEach(() => {
tmpDirPath = getTmpDirPath();
});
it("should throw if a function's event is not an array or a variable", () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'service-name',
provider: 'aws',
functions: {
functionA: {
events: 'not an array or a variable',
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
serverless.service = new Service(serverless);
return expect(serverless.service.load()).to.eventually.be.fulfilled.then(() => {
// validate the service configuration, now that variables are loaded
expect(() => serverless.service.validate()).to.throw(
'Events for "functionA" must be an array, not an string'
);
});
});
describe('stage name validation', () => {
function simulateRun(serverless) {
return serverless.init().then(() =>
serverless.variables.populateService(serverless.pluginManager.cliOptions).then(() => {
serverless.service.mergeArrays();
serverless.service.setFunctionNames(serverless.processedInput.options);
})
);
}
it(`should not throw an error if http event is absent and
stage contains only alphanumeric, underscore and hyphen`, function() {
this.timeout(10000); // Occasionally times out with default settings
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'xyz-101_abc-123',
},
functions: {
first: {
events: [],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serverless.processedInput = { options: {} };
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
});
});
it(`should not throw an error after variable population if http event is present and
the populated stage contains only alphanumeric, underscore and hyphen`, () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default-stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
});
});
it('should throw an error if http event is present and stage contains invalid chars', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'my@stage',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.throw(
serverless.classes.Error,
[
'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' ')
);
});
});
it(`should throw an error after variable population
if http event is present and stage contains hyphen`, () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: '${opt:stage, "default:stage"}',
},
functions: {
first: {
events: [
{
http: {
path: 'foo',
method: 'GET',
},
},
],
},
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless({ servicePath: tmpDirPath });
return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
expect(() => serverless.service.validate()).to.throw(
serverless.classes.Error,
[
'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
'for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' ')
);
});
});
});
});
describe('#mergeArrays', () => {
it('should merge resources given as an array', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.resources = [
{
Resources: {
aws: {
resourcesProp: 'value',
},
},
},
{
Resources: {
azure: {},
},
},
{
foo: 'bar',
},
];
serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.be.an('object');
expect(serviceInstance.resources.Resources).to.be.an('object');
expect(serviceInstance.resources.Resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.Resources.azure).to.deep.equal({});
expect(serviceInstance.resources.foo).to.deep.equal('bar');
});
it('should ignore an object', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.resources = {
Resources: 'foo',
};
serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.deep.eql({
Resources: 'foo',
});
});
it('should tolerate an empty string', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.resources = [
'',
{
aws: {
resourcesProp: 'value',
},
},
];
serviceInstance.mergeArrays();
expect(serviceInstance.resources).to.deep.eql({
aws: {
resourcesProp: 'value',
},
});
});
it('should throw when given a number', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.resources = [42];
expect(() => serviceInstance.mergeArrays()).to.throw(Error);
});
it('should throw when given a string', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.resources = ['string'];
expect(() => serviceInstance.mergeArrays()).to.throw(Error);
});
it('should merge functions given as an array', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = [
{
a: {},
},
{
b: {},
},
];
serviceInstance.mergeArrays();
expect(serviceInstance.functions).to.be.an('object');
expect(serviceInstance.functions.a).to.be.an('object');
expect(serviceInstance.functions.b).to.be.an('object');
});
});
describe('#setFunctionNames()', () => {
let serviceInstance;
let tmpDirPath;
beforeEach(() => {
tmpDirPath = getTmpDirPath();
});
it('should make sure function name contains the default stage', () => {
const SUtils = new Utils();
const serverlessYml = {
service: 'new-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
},
plugins: ['testPlugin'],
functions: {
functionA: {},
},
resources: {
aws: {
resourcesProp: 'value',
},
azure: {},
google: {},
},
package: {
exclude: ['exclude-me'],
include: ['include-me'],
artifact: 'some/path/foo.zip',
},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));
const serverless = new Serverless();
serverless.processedInput = { options: {} };
serverless.config.update({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
serviceInstance.setFunctionNames();
expect(serviceInstance.functions.functionA.name).to.be.equal('new-service-dev-functionA');
});
});
});
describe('#update()', () => {
it('should update service instance data', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
const newData = { service: 'newName' };
const updatedInstance = serviceInstance.update(newData);
expect(updatedInstance.service).to.be.equal('newName');
});
});
describe('#getServiceName()', () => {
it('should return the service name', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.serviceObject = {
name: 'my-service',
};
const serviceName = serviceInstance.getServiceName();
expect(serviceName).to.equal('my-service');
});
});
describe('#getServiceObject()', () => {
it('should return the service object with all properties', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
const testObject = {
name: 'my-service',
foo: 'bar',
};
// Use a clone here to check for implicit reference errors
serviceInstance.serviceObject = _.cloneDeep(testObject);
expect(serviceInstance.getServiceObject()).to.deep.equal(testObject);
});
});
describe('#getFunction()', () => {
let serviceInstance;
before(() => {
const serverless = new Serverless();
serviceInstance = new Service(serverless);
serviceInstance.functions = {
create: {
handler: 'users.create',
},
};
});
it('should return function object', () => {
expect(serviceInstance.getFunction('create')).to.deep.equal({ handler: 'users.create' });
});
it('should throw error if function does not exist', () => {
expect(() => {
serviceInstance.getFunction('random');
}).to.throw(Error);
});
});
describe('#getAllFunctionsNames', () => {
it('should return an empty array if there are no functions in Service', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = {};
const functionsNames = serviceInstance.getAllFunctionsNames();
expect(functionsNames).to.deep.equal([]);
});
it('should return array of lambda function names in Service', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = {
create: {
name: 'createUser',
},
list: {
name: 'listUsers',
},
};
const functionsNames = serviceInstance.getAllFunctionsNames();
expect(functionsNames).to.deep.equal(['createUser', 'listUsers']);
});
});
describe('#getAllFunctions()', () => {
it('should return an empty array if there are no functions in Service', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = {};
expect(serviceInstance.getAllFunctions()).to.deep.equal([]);
});
it('should return an array of function names in Service', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = {
create: {
handler: 'users.create',
},
list: {
handler: 'users.list',
},
};
expect(serviceInstance.getAllFunctions()).to.deep.equal(['create', 'list']);
});
});
describe('#getEventInFunction()', () => {
let serviceInstance;
before(() => {
const serverless = new Serverless();
serviceInstance = new Service(serverless);
serviceInstance.functions = {
create: {
events: [
{
schedule: 'rate(5 minutes)',
},
],
},
};
});
it('should return an event object based on provided function', () => {
expect(serviceInstance.getEventInFunction('schedule', 'create')).to.deep.equal({
schedule: 'rate(5 minutes)',
});
});
it('should throw error if function does not exist in service', () => {
expect(() => {
serviceInstance.getEventInFunction(null, 'list');
}).to.throw(Error);
});
it('should throw error if event doesnt exist in function', () => {
expect(() => {
serviceInstance.getEventInFunction('randomEvent', 'create');
}).to.throw(Error);
});
});
describe('#getAllEventsInFunction()', () => {
it('should return an array of events in a specified function', () => {
const serverless = new Serverless();
const serviceInstance = new Service(serverless);
serviceInstance.functions = {
create: {
events: [
{
schedule: 'rate(5 minutes)',
},
{
bucket: 'my_bucket',
},
],
},
};
expect(serviceInstance.getAllEventsInFunction('create')).to.deep.equal([
{
schedule: 'rate(5 minutes)',
},
{
bucket: 'my_bucket',
},
]);
});
});
});