'use strict'; /* eslint-disable no-unused-expressions */ const chai = require('chai'); const overrideEnv = require('process-utils/override-env'); const cjsResolve = require('ncjsm/resolve/sync'); const spawn = require('child-process-ext/spawn'); const resolveAwsEnv = require('@serverless/test/resolve-env'); const Serverless = require('../../lib/Serverless'); const CLI = require('../../lib/classes/CLI'); const Create = require('../../lib/plugins/create/create'); const _ = require('lodash'); const path = require('path'); const fs = require('fs'); const fse = require('fs-extra'); const mockRequire = require('mock-require'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); const BbPromise = require('bluebird'); const getCacheFilePath = require('../utils/getCacheFilePath'); const { installPlugin } = require('../../test/utils/plugins'); const { getTmpDirPath } = require('../../test/utils/fs'); chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); const expect = chai.expect; describe('PluginManager', () => { const env = resolveAwsEnv(); let pluginManager; let serverless; class ServicePluginMock1 {} class ServicePluginMock2 {} class EnterprisePluginMock {} const brokenPluginError = new Error('Broken plugin'); class BrokenPluginMock { constructor() { throw brokenPluginError; } } class Provider1PluginMock { constructor() { this.provider = 'provider1'; this.commands = { deploy: { lifecycleEvents: ['resources'], }, }; this.hooks = { 'deploy:functions': this.functions.bind(this), }; // used to test if the function was executed correctly this.deployedFunctions = 0; } functions() { this.deployedFunctions += 1; } } class Provider2PluginMock { constructor() { this.provider = 'provider2'; this.commands = { deploy: { lifecycleEvents: ['resources'], }, }; this.hooks = { 'deploy:functions': this.functions.bind(this), }; // used to test if the function was executed correctly this.deployedFunctions = 0; } functions() { this.deployedFunctions += 1; } } class PromisePluginMock { constructor() { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, }, }, }, }; this.hooks = { 'deploy:functions': this.functions.bind(this), 'before:deploy:onpremises:functions': this.resources.bind(this), }; // used to test if the function was executed correctly this.deployedFunctions = 0; this.deployedResources = 0; } functions() { return new BbPromise(resolve => { this.deployedFunctions += 1; return resolve(); }); } resources() { return new BbPromise(resolve => { this.deployedResources += 1; return resolve(); }); } } class SynchronousPluginMock { constructor() { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, }, }, }, }; this.hooks = { 'deploy:functions': this.functions.bind(this), 'before:deploy:onpremises:functions': this.resources.bind(this), }; // used to test if the function was executed correctly this.deployedFunctions = 0; this.deployedResources = 0; } functions() { this.deployedFunctions += 1; } resources() { this.deployedResources += 1; } } class AliasPluginMock { constructor() { this.commands = { deploy: { usage: 'Deploy to the default infrastructure', lifecycleEvents: ['resources', 'functions'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, commands: { onpremises: { usage: 'Deploy to your On-Premises infrastructure', lifecycleEvents: ['resources', 'functions'], aliases: ['on:premise', 'premise'], options: { resource: { usage: 'The resource you want to deploy (e.g. --resource db)', }, function: { usage: 'The function you want to deploy (e.g. --function create)', }, }, }, }, }, }; this.hooks = { 'deploy:functions': this.functions.bind(this), 'before:deploy:onpremises:functions': this.resources.bind(this), }; // used to test if the function was executed correctly this.deployedFunctions = 0; this.deployedResources = 0; } functions() { this.deployedFunctions += 1; } resources() { this.deployedResources += 1; } } class EntrypointPluginMock { constructor() { this.commands = { myep: { type: 'entrypoint', lifecycleEvents: ['initialize', 'finalize'], commands: { // EP, not public command because its parent is decalred as EP mysubep: { lifecycleEvents: ['initialize', 'finalize'], }, // EP that will spawn sub lifecycles spawnep: { lifecycleEvents: ['event1', 'event2'], }, }, }, // public command mycmd: { lifecycleEvents: ['run'], commands: { // public subcommand mysubcmd: { lifecycleEvents: ['initialize', 'finalize'], }, // command that will spawn sub lifecycles spawncmd: { lifecycleEvents: ['event1', 'event2'], }, spawnep: { type: 'entrypoint', lifecycleEvents: ['event1', 'event2'], }, }, }, }; this.hooks = { 'myep:initialize': this.initialize.bind(this), 'myep:finalize': this.finalize.bind(this), 'myep:mysubep:initialize': this.subEPInitialize.bind(this), 'myep:mysubep:finalize': this.subEPFinalize.bind(this), 'mycmd:mysubcmd:initialize': this.subInitialize.bind(this), 'mycmd:mysubcmd:finalize': this.subFinalize.bind(this), 'mycmd:run': this.run.bind(this), // Event1 spawns mysubcmd, then myep // Event2 spawns mycmd, then mysubep 'myep:spawnep:event1': () => pluginManager.spawn(['mycmd', 'mysubcmd']).then(() => pluginManager.spawn(['myep'])), 'myep:spawnep:event2': () => pluginManager.spawn(['mycmd']).then(() => pluginManager.spawn(['myep', 'mysubep'])), 'mycmd:spawncmd:event1': () => pluginManager.spawn(['mycmd', 'mysubcmd']).then(() => pluginManager.spawn(['myep'])), 'mycmd:spawncmd:event2': () => pluginManager.spawn(['mycmd']).then(() => pluginManager.spawn(['myep', 'mysubep'])), }; this.callResult = ''; } initialize() { this.callResult += '>initialize'; } finalize() { this.callResult += '>finalize'; } subEPInitialize() { this.callResult += '>subEPInitialize'; } subEPFinalize() { this.callResult += '>subEPFinalize'; } subInitialize() { this.callResult += '>subInitialize'; } subFinalize() { this.callResult += '>subFinalize'; } run() { this.callResult += '>run'; } } class ContainerPluginMock { constructor() { this.commands = { // not a public command because its declared as a Container mycontainer: { type: 'container', commands: { // public command because its children of a container mysubcmd: { lifecycleEvents: ['event1', 'event2'], }, }, }, }; this.hooks = { 'mycontainer:mysubcmd:event1': this.eventOne.bind(this), 'mycontainer:mysubcmd:event2': this.eventTwo.bind(this), }; this.callResult = ''; } eventOne() { this.callResult += '>mysubcmdEvent1'; } eventTwo() { this.callResult += '>mysubcmdEvent2'; } } class DeprecatedLifecycleEventsPluginMock { constructor() { this.hooks = { 'deprecated:deprecated': this.deprecated, // NOTE: we assume that this is deprecated 'untouched:untouched': this.untouched, }; } deprecated() { return; } untouched() { return; } } const resolveStub = (directory, pluginPath) => { switch (pluginPath) { case 'BrokenPluginMock': case 'ServicePluginMock1': case 'ServicePluginMock2': return { realPath: pluginPath }; case './RelativePath/ServicePluginMock2': return { realPath: `${servicePath}/RelativePath/ServicePluginMock2` }; default: return cjsResolve(directory, pluginPath); } }; let restoreEnv; let servicePath; let PluginManager = proxyquire('../../lib/classes/PluginManager', { 'ncjsm/resolve/sync': resolveStub, }); beforeEach(() => { ({ restoreEnv } = overrideEnv({ whitelist: ['APPDATA', 'PATH'] })); serverless = new Serverless(); serverless.cli = new CLI(); serverless.processedInput = { commands: [], options: {} }; pluginManager = new PluginManager(serverless); servicePath = pluginManager.serverless.config.servicePath = 'foo'; }); afterEach(() => restoreEnv()); describe('#constructor()', () => { it('should set the serverless instance', () => { expect(pluginManager.serverless).to.deep.equal(serverless); }); it('should create an empty cliOptions object', () => { expect(pluginManager.cliOptions).to.deep.equal({}); }); it('should create an empty cliCommands array', () => { expect(pluginManager.cliCommands.length).to.equal(0); }); it('should create an empty plugins array', () => { expect(pluginManager.plugins.length).to.equal(0); }); it('should create an empty commands object', () => { expect(pluginManager.commands).to.deep.equal({}); }); }); describe('#setCliOptions()', () => { it('should set the cliOptions object', () => { const options = { foo: 'bar' }; pluginManager.setCliOptions(options); expect(pluginManager.cliOptions).to.deep.equal(options); }); }); describe('#setCliCommands()', () => { it('should set the cliCommands array', () => { const commands = ['foo', 'bar']; pluginManager.setCliCommands(commands); expect(pluginManager.cliCommands).to.equal(commands); }); }); describe('#updateAutocompleteCacheFile()', () => { let writeFileStub; let cacheFilePath; let getCommandsStub; beforeEach(() => { writeFileStub = sinon.stub().returns(BbPromise.resolve()); PluginManager = proxyquire('./PluginManager.js', { '../utils/fs/writeFile': writeFileStub, 'ncjsm/resolve/sync': resolveStub, }); pluginManager = new PluginManager(serverless); pluginManager.serverless.config = { servicePath: 'somePath' }; servicePath = pluginManager.serverless.config.servicePath; cacheFilePath = getCacheFilePath(servicePath); getCommandsStub = sinon.stub(pluginManager, 'getCommands'); }); afterEach(() => { pluginManager.getCommands.restore(); }); it('should update autocomplete cache file', () => { const commandsMock = { deploy: { options: { stage: {}, }, commands: { function: {}, }, }, invoke: { options: { region: {}, }, commands: { local: {}, }, }, remove: {}, }; const expectedCommands = { deploy: ['--stage', 'function'], invoke: ['--region', 'local'], remove: [], }; getCommandsStub.returns(commandsMock); return pluginManager.updateAutocompleteCacheFile().then(() => { expect(getCommandsStub.calledOnce).to.equal(true); expect(writeFileStub.calledOnce).to.equal(true); expect(writeFileStub.getCall(0).args[0]).to.equal(cacheFilePath); expect(writeFileStub.getCall(0).args[1].commands).to.deep.equal(expectedCommands); expect(typeof writeFileStub.getCall(0).args[1].validationHash).to.equal('string'); }); }); }); describe('#convertShortcutsIntoOptions()', () => { it('should convert shortcuts into options when a one level deep command matches', () => { const cliOptionsMock = { r: 'eu-central-1', region: 'us-east-1' }; const cliCommandsMock = ['deploy']; // command with one level deepness const commandMock = { options: { region: { shortcut: 'r', }, }, }; pluginManager.setCliCommands(cliCommandsMock); pluginManager.setCliOptions(cliOptionsMock); pluginManager.convertShortcutsIntoOptions(commandMock); expect(pluginManager.cliOptions.region).to.equal(cliOptionsMock.r); }); it('should not convert shortcuts into options when the shortcut is not given', () => { const cliOptionsMock = { r: 'eu-central-1', region: 'us-east-1' }; const cliCommandsMock = ['deploy']; const commandMock = { options: { region: {}, }, }; pluginManager.setCliCommands(cliCommandsMock); pluginManager.setCliOptions(cliOptionsMock); pluginManager.convertShortcutsIntoOptions(commandMock); expect(pluginManager.cliOptions.region).to.equal(cliOptionsMock.region); }); }); describe('#addPlugin()', () => { it('should add a plugin instance to the plugins array', () => { pluginManager.addPlugin(SynchronousPluginMock); expect(pluginManager.plugins[0]).to.be.instanceof(SynchronousPluginMock); }); it('should not load plugins twice', () => { pluginManager.addPlugin(SynchronousPluginMock); pluginManager.addPlugin(SynchronousPluginMock); expect(pluginManager.plugins[0]).to.be.instanceof(SynchronousPluginMock); expect(pluginManager.plugins.length).to.equal(1); }); it('should load two plugins that happen to have the same class name', () => { function getFirst() { return class PluginMock {}; } function getSecond() { return class PluginMock {}; } const first = getFirst(); const second = getSecond(); pluginManager.addPlugin(first); pluginManager.addPlugin(second); expect(pluginManager.plugins[0]).to.be.instanceof(first); expect(pluginManager.plugins[1]).to.be.instanceof(second); expect(pluginManager.plugins.length).to.equal(2); }); it('should load the plugin commands', () => { pluginManager.addPlugin(SynchronousPluginMock); expect(pluginManager.commands).to.have.property('deploy'); }); it('should skip service related plugins which not match the services provider', () => { pluginManager.serverless.service.provider.name = 'someProvider'; class Plugin { constructor() { this.provider = 'someOtherProvider'; } } pluginManager.addPlugin(Plugin); expect(pluginManager.plugins.length).to.equal(0); }); it('should add service related plugins when provider property is the providers name', () => { pluginManager.serverless.service.provider.name = 'someProvider'; class Plugin { constructor() { this.provider = 'someProvider'; } } pluginManager.addPlugin(Plugin); expect(pluginManager.plugins[0]).to.be.an.instanceOf(Plugin); }); it('should add service related plugins when provider propery is provider plugin', () => { pluginManager.serverless.service.provider.name = 'someProvider'; class ProviderPlugin { static getProviderName() { return 'someProvider'; } } const providerPlugin = new ProviderPlugin(); class Plugin { constructor() { this.provider = providerPlugin; } } pluginManager.addPlugin(Plugin); expect(pluginManager.plugins[0]).to.be.an.instanceOf(Plugin); }); }); describe('#asyncPluginInit()', () => { it('should call async init on plugins that have it', () => { const plugin1 = new ServicePluginMock1(); plugin1.asyncInit = sinon.stub().returns(BbPromise.resolve()); pluginManager.plugins = [plugin1]; return pluginManager.asyncPluginInit().then(() => { expect(plugin1.asyncInit.calledOnce).to.equal(true); }); }); }); describe('#loadAllPlugins()', () => { beforeEach(() => { mockRequire('ServicePluginMock1', ServicePluginMock1); mockRequire('ServicePluginMock2', ServicePluginMock2); mockRequire('BrokenPluginMock', BrokenPluginMock); mockRequire('@serverless/enterprise-plugin', EnterprisePluginMock); }); it('should load only core plugins when no service plugins are given', () => { // Note: We need the Create plugin for this test to pass pluginManager.loadAllPlugins(); // note: this test will be refactored as the Create plugin will be moved // to another directory expect(pluginManager.plugins.length).to.be.above(0); }); it('should load all plugins when service plugins are given', () => { const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; pluginManager.loadAllPlugins(servicePlugins); expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock1)).to.equal( true ); expect(pluginManager.plugins.some(plugin => plugin instanceof ServicePluginMock2)).to.equal( true ); expect(pluginManager.plugins.some(plugin => plugin instanceof EnterprisePluginMock)).to.equal( true ); // note: this test will be refactored as the Create plugin will be moved // to another directory expect(pluginManager.plugins.length).to.be.above(2); }); it('should load all plugins in the correct order', () => { const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; pluginManager.loadAllPlugins(servicePlugins); const pluginIndexes = [ pluginManager.plugins.findIndex(plugin => plugin instanceof Create), pluginManager.plugins.findIndex(plugin => plugin instanceof ServicePluginMock1), pluginManager.plugins.findIndex(plugin => plugin instanceof ServicePluginMock2), pluginManager.plugins.findIndex(plugin => plugin instanceof EnterprisePluginMock), ]; expect(pluginIndexes).to.deep.equal(pluginIndexes.slice().sort((a, b) => a - b)); }); it('should load the Serverless core plugins', () => { pluginManager.loadAllPlugins(); expect(pluginManager.plugins.length).to.be.above(1); }); it('should throw an error when trying to load unknown plugin', () => { const servicePlugins = ['ServicePluginMock3', 'ServicePluginMock1']; expect(() => pluginManager.loadAllPlugins(servicePlugins)).to.throw(serverless.classes.Error); }); it('should not throw error when trying to load unknown plugin with help flag', () => { const servicePlugins = ['ServicePluginMock3', 'ServicePluginMock1']; pluginManager.setCliOptions({ help: true }); expect(() => pluginManager.loadAllPlugins(servicePlugins)).to.not.throw( serverless.classes.Error ); }); it('should pass through an error when trying to load a broken plugin', () => { const servicePlugins = ['BrokenPluginMock']; expect(() => pluginManager.loadAllPlugins(servicePlugins)).to.throw(brokenPluginError); }); it('should not throw error when running the plugin commands and given plugins does not exist', () => { const servicePlugins = ['ServicePluginMock3']; const cliCommandsMock = ['plugin']; pluginManager.setCliCommands(cliCommandsMock); expect(() => pluginManager.loadAllPlugins(servicePlugins)).to.not.throw( serverless.classes.Error ); }); afterEach(() => { // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); mockRequire.stop('BrokenPluginMock'); mockRequire.stop('@serverless/enterprise-plugin'); }); }); describe('#resolveServicePlugins()', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback mockRequire('ServicePluginMock1', ServicePluginMock1); // Plugins loaded via a relative path should be required relative to the service path mockRequire(`${servicePath}/RelativePath/ServicePluginMock2`, ServicePluginMock2); }); it('should resolve the service plugins', () => { const servicePlugins = ['ServicePluginMock1', './RelativePath/ServicePluginMock2']; expect(pluginManager.resolveServicePlugins(servicePlugins)).to.deep.equal([ ServicePluginMock1, ServicePluginMock2, ]); }); it('should not error if plugins = null', () => { // Happens when `plugins` property exists but is empty const servicePlugins = null; pluginManager.resolveServicePlugins(servicePlugins); }); it('should not error if plugins = undefined', () => { // Happens when `plugins` property does not exist const servicePlugins = undefined; pluginManager.resolveServicePlugins(servicePlugins); }); afterEach(() => { // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); }); }); describe('#parsePluginsObject()', () => { const parsePluginsObjectAndVerifyResult = (servicePlugins, expectedResult) => { const result = pluginManager.parsePluginsObject(servicePlugins); expect(result).to.deep.equal(expectedResult); }; it('should parse array object', () => { const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; parsePluginsObjectAndVerifyResult(servicePlugins, { modules: servicePlugins, localPath: path.join(serverless.config.servicePath, '.serverless_plugins'), }); }); it('should parse plugins object', () => { const servicePlugins = { modules: ['ServicePluginMock1', 'ServicePluginMock2'], localPath: './myplugins', }; parsePluginsObjectAndVerifyResult(servicePlugins, { modules: servicePlugins.modules, localPath: servicePlugins.localPath, }); }); it('should parse plugins object if format is not correct', () => { const servicePlugins = {}; parsePluginsObjectAndVerifyResult(servicePlugins, { modules: [], localPath: path.join(serverless.config.servicePath, '.serverless_plugins'), }); }); it('should parse plugins object if modules property is not an array', () => { const servicePlugins = { modules: {} }; parsePluginsObjectAndVerifyResult(servicePlugins, { modules: [], localPath: path.join(serverless.config.servicePath, '.serverless_plugins'), }); }); it('should parse plugins object if localPath is not correct', () => { const servicePlugins = { modules: ['ServicePluginMock1', 'ServicePluginMock2'], localPath: {}, }; parsePluginsObjectAndVerifyResult(servicePlugins, { modules: servicePlugins.modules, localPath: path.join(serverless.config.servicePath, '.serverless_plugins'), }); }); }); describe('command aliases', () => { describe('#getAliasCommandTarget', () => { it('should return an alias target', () => { pluginManager.aliases = { cmd1: { cmd2: { command: 'command1', }, cmd3: { cmd4: { command: 'command2', }, }, }, }; expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd2'])).to.equal('command1'); expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3', 'cmd4'])).to.equal('command2'); }); it('should return undefined if alias does not exist', () => { pluginManager.aliases = { cmd1: { cmd2: { command: 'command1', }, cmd3: { cmd4: { command: 'command2', }, }, }, }; expect(pluginManager.getAliasCommandTarget(['cmd1'])).to.be.undefined; expect(pluginManager.getAliasCommandTarget(['cmd1', 'cmd3'])).to.be.undefined; }); }); describe('#createCommandAlias', () => { it('should create an alias for a command', () => { pluginManager.aliases = {}; expect(pluginManager.createCommandAlias('cmd1:alias2', 'cmd2:cmd3:cmd4')).to.not.throw; expect(pluginManager.createCommandAlias('cmd1:alias2:alias3', 'cmd2:cmd3:cmd5')).to.not .throw; expect(pluginManager.aliases).to.deep.equal({ cmd1: { alias2: { command: 'cmd2:cmd3:cmd4', alias3: { command: 'cmd2:cmd3:cmd5', }, }, }, }); }); it('should fail if the alias already exists', () => { pluginManager.aliases = { cmd1: { alias2: { command: 'cmd2:cmd3:cmd4', alias3: { command: 'cmd2:cmd3:cmd5', }, }, }, }; expect(() => pluginManager.createCommandAlias('cmd1:alias2', 'mycmd')).to.throw( /Alias "cmd1:alias2" is already defined/ ); }); it('should fail if the alias overwrites a command', () => { const synchronousPluginMockInstance = new SynchronousPluginMock(); pluginManager.loadCommands(synchronousPluginMockInstance); expect(() => pluginManager.createCommandAlias('deploy', 'mycmd')).to.throw( /Command "deploy" cannot be overriden/ ); }); it('should fail if the alias overwrites the very own command', () => { const synchronousPluginMockInstance = new SynchronousPluginMock(); synchronousPluginMockInstance.commands.deploy.commands.onpremises.aliases = ['deploy']; expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)).to.throw( /Command "deploy" cannot be overriden/ ); }); }); }); describe('#loadCommands()', () => { it('should load the plugin commands', () => { const synchronousPluginMockInstance = new SynchronousPluginMock(); pluginManager.loadCommands(synchronousPluginMockInstance); expect(pluginManager.commands).to.have.property('deploy'); }); it('should merge plugin commands', () => { pluginManager.loadCommands({ commands: { deploy: { lifecycleEvents: ['one'], options: { foo: {}, }, }, }, }); pluginManager.loadCommands({ commands: { deploy: { lifecycleEvents: ['one', 'two'], options: { bar: {}, }, commands: { fn: {}, }, }, }, }); expect(pluginManager.commands.deploy) .to.have.property('options') .that.has.all.keys('foo', 'bar'); expect(pluginManager.commands.deploy) .to.have.property('lifecycleEvents') .that.is.an('array') .that.deep.equals(['one', 'two']); expect(pluginManager.commands.deploy.commands).to.have.property('fn'); }); it('should fail if there is already an alias for a command', () => { pluginManager.aliases = { deploy: { command: 'my:deploy', }, }; const synchronousPluginMockInstance = new SynchronousPluginMock(); expect(() => pluginManager.loadCommands(synchronousPluginMockInstance)).to.throw( /Command "deploy" cannot override an existing alias/ ); }); it('should log the alias when SLS_DEBUG is set', () => { const consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); const synchronousPluginMockInstance = new SynchronousPluginMock(); synchronousPluginMockInstance.commands.deploy.aliases = ['info']; _.set(process.env, 'SLS_DEBUG', '*'); pluginManager.loadCommands(synchronousPluginMockInstance); expect(consoleLogStub).to.have.been.calledWith(' -> @info'); }); }); describe('#loadHooks()', () => { let deprecatedPluginInstance; let consoleLogStub; beforeEach(() => { deprecatedPluginInstance = new DeprecatedLifecycleEventsPluginMock(); pluginManager.deprecatedEvents = { 'deprecated:deprecated': 'new:new', }; consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); }); afterEach(() => { pluginManager.deprecatedEvents = {}; pluginManager.serverless.cli.log.restore(); }); it('should replace deprecated events with the new ones', () => { pluginManager.loadHooks(deprecatedPluginInstance); expect(pluginManager.hooks['deprecated:deprecated']).to.equal(undefined); expect(pluginManager.hooks['new:new'][0].pluginName).to.equal( 'DeprecatedLifecycleEventsPluginMock' ); expect(pluginManager.hooks['untouched:untouched'][0].pluginName).to.equal( 'DeprecatedLifecycleEventsPluginMock' ); expect(consoleLogStub.calledOnce).to.equal(false); }); it('should log a debug message about deprecated when using SLS_DEBUG', () => { process.env.SLS_DEBUG = '1'; pluginManager.loadHooks(deprecatedPluginInstance); expect(consoleLogStub.calledOnce).to.equal(true); }); }); describe('#loadVariableResolvers()', () => { it('should load the plugin variable resolvers with short sytnax', () => { class VariableResolverPlugin { getTestVariable() { return Promise.resolve('testVariable'); } constructor() { this.variableResolvers = { test: this.getTestVariable, }; } } pluginManager.serverless.variables = { variableResolvers: [], }; const pluginInstance = new VariableResolverPlugin(); pluginManager.loadVariableResolvers(pluginInstance); expect('test:asdfsd').to.match(pluginManager.serverless.variables.variableResolvers[0].regex); expect( pluginManager.serverless.variables.variableResolvers[0].isDisabledAtPrepopulation ).to.equal(false); expect(pluginManager.serverless.variables.variableResolvers[0].serviceName).to.equal(null); expect(typeof pluginManager.serverless.variables.variableResolvers[0].resolver).to.equal( 'function' ); }); it('should load the plugin variable resolvers with long syntax', () => { class VariableResolverPlugin { getTestVariable() { return Promise.resolve('testVariable'); } constructor() { this.variableResolvers = { test: this.getTestVariable, }; } } pluginManager.serverless.variables = { variableResolvers: [], }; const pluginInstance = new VariableResolverPlugin(); pluginManager.loadVariableResolvers(pluginInstance); expect('test:asdfsd').to.match(pluginManager.serverless.variables.variableResolvers[0].regex); expect( pluginManager.serverless.variables.variableResolvers[0].isDisabledAtPrepopulation ).to.equal(false); expect(pluginManager.serverless.variables.variableResolvers[0].serviceName).to.equal(null); expect(typeof pluginManager.serverless.variables.variableResolvers[0].resolver).to.equal( 'function' ); }); it('should validate the plugin variable resolvers function', () => { class VariableResolverPlugin { getTestVariable() { return Promise.resolve('testVariable'); } constructor() { this.variableResolvers = { test: { resolver: 'not a function' }, }; } } pluginManager.serverless.variables = { variableResolvers: [], }; const pluginInstance = new VariableResolverPlugin(); expect(() => pluginManager.loadVariableResolvers(pluginInstance)).to.throw( "Custom variable resolver for test defined by VariableResolverPlugin specifies a resolver that isn't a function: not a function" ); }); it('should load the plugin variable resolvers name if necessary', () => { class VariableResolverPlugin { getTestVariable() { return Promise.resolve('testVariable'); } constructor() { this.variableResolvers = { test: { resolver: this.getTestVariable, serviceName: 'test', isDisabledAtPrepopulation: true, }, }; } } pluginManager.serverless.variables = { variableResolvers: [], }; const pluginInstance = new VariableResolverPlugin(); pluginManager.loadVariableResolvers(pluginInstance); expect('test:asdfsd').to.match(pluginManager.serverless.variables.variableResolvers[0].regex); expect( pluginManager.serverless.variables.variableResolvers[0].isDisabledAtPrepopulation ).to.equal(true); expect(pluginManager.serverless.variables.variableResolvers[0].serviceName).to.equal('test'); expect(typeof pluginManager.serverless.variables.variableResolvers[0].resolver).to.equal( 'function' ); }); it('should validate the plugin variable resolvers name if necessary', () => { class VariableResolverPlugin { getTestVariable() { return Promise.resolve('testVariable'); } constructor() { this.variableResolvers = { test: { resolver: this.getTestVariable, isDisabledAtPrepopulation: true, }, }; } } pluginManager.serverless.variables = { variableResolvers: [], }; const pluginInstance = new VariableResolverPlugin(); expect(() => pluginManager.loadVariableResolvers(pluginInstance)).to.throw( /Custom variable resolver for test defined by VariableResolverPlugin specifies isDisabledAtPrepopulation but doesn't provide a string for a name/ ); }); }); describe('#getEvents()', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); it('should get all the matching events for a root level command in the correct order', () => { const command = pluginManager.getCommand(['deploy']); const events = pluginManager.getEvents(command); expect(events[0]).to.equal('before:deploy:resources'); expect(events[1]).to.equal('deploy:resources'); expect(events[2]).to.equal('after:deploy:resources'); expect(events[3]).to.equal('before:deploy:functions'); expect(events[4]).to.equal('deploy:functions'); expect(events[5]).to.equal('after:deploy:functions'); }); it('should get all the matching events for a nested level command in the correct order', () => { const command = pluginManager.getCommand(['deploy', 'onpremises']); const events = pluginManager.getEvents(command); expect(events[0]).to.equal('before:deploy:onpremises:resources'); expect(events[1]).to.equal('deploy:onpremises:resources'); expect(events[2]).to.equal('after:deploy:onpremises:resources'); expect(events[3]).to.equal('before:deploy:onpremises:functions'); expect(events[4]).to.equal('deploy:onpremises:functions'); expect(events[5]).to.equal('after:deploy:onpremises:functions'); }); }); describe('#getHooks()', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); it('should get hooks for an event with some registered', () => { expect(pluginManager.getHooks(['deploy:functions'])) .to.be.an('Array') .with.length(1); }); it('should have the plugin name and function on the hook', () => { const hooks = pluginManager.getHooks(['deploy:functions']); expect(hooks[0].pluginName).to.equal('SynchronousPluginMock'); expect(hooks[0].hook).to.be.a('Function'); }); it('should not get hooks for an event that does not have any', () => { expect(pluginManager.getHooks(['deploy:resources'])) .to.be.an('Array') .with.length(0); }); it('should accept a single event in place of an array', () => { expect(pluginManager.getHooks('deploy:functions')) .to.be.an('Array') .with.length(1); }); }); describe('#getPlugins()', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback mockRequire('ServicePluginMock1', ServicePluginMock1); mockRequire('ServicePluginMock2', ServicePluginMock2); }); it('should return all loaded plugins', () => { const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']; pluginManager.loadAllPlugins(servicePlugins); const plugins = pluginManager.getPlugins(); expect(plugins.length).to.be.above(3); expect(plugins.some(plugin => plugin instanceof ServicePluginMock1)).to.be.true; expect(plugins.some(plugin => plugin instanceof ServicePluginMock2)).to.be.true; }); afterEach(() => { // eslint-disable-line prefer-arrow-callback mockRequire.stop('ServicePluginMock1'); mockRequire.stop('ServicePluginMock2'); }); }); describe('#validateCommand()', () => { it('should find commands', () => { pluginManager.addPlugin(EntrypointPluginMock); expect(() => pluginManager.validateCommand(['mycmd', 'mysubcmd'])).to.not.throw( serverless.classes.Error ); }); it('should find container children commands', () => { pluginManager.addPlugin(ContainerPluginMock); expect(() => pluginManager.validateCommand(['mycontainer', 'mysubcmd'])).to.not.throw( serverless.classes.Error ); }); it('should throw on entrypoints', () => { pluginManager.addPlugin(EntrypointPluginMock); expect(() => pluginManager.validateCommand(['myep', 'mysubep'])).to.throw( /command ".*" not found/ ); }); it('should throw on container', () => { pluginManager.addPlugin(ContainerPluginMock); expect(() => pluginManager.validateCommand(['mycontainer'])).to.throw( /command ".*" not found/ ); }); }); describe('#assignDefaultOptions()', () => { it('should assign default values to empty options', () => { pluginManager.commands = { foo: { options: { bar: { required: true, default: 'foo', }, }, }, }; const foo = pluginManager.commands.foo; pluginManager.assignDefaultOptions(foo); expect(pluginManager.cliOptions.bar).to.equal(foo.options.bar.default); }); it('should not assign default values to non-empty options', () => { pluginManager.commands = { foo: { options: { bar: { required: true, default: 'foo', }, }, }, }; const foo = pluginManager.commands.foo; pluginManager.setCliOptions({ bar: 100 }); pluginManager.assignDefaultOptions(foo); expect(pluginManager.cliOptions.bar).to.equal(100); }); [0, '', false].forEach(defaultValue => { it(`assigns valid falsy default value '${defaultValue} to empty options`, () => { pluginManager.commands = { foo: { options: { bar: { required: true, default: defaultValue, }, }, }, }; const foo = pluginManager.commands.foo; pluginManager.assignDefaultOptions(foo); expect(pluginManager.cliOptions.bar).to.equal(defaultValue); }); }); }); describe('#validateServerlessConfigDependency()', () => { let serverlessInstance; let pluginManagerInstance; beforeEach(() => { serverlessInstance = new Serverless(); serverlessInstance.config.servicePath = 'my-service'; pluginManagerInstance = new PluginManager(serverlessInstance); }); it('should continue loading if the configDependent property is absent', () => { pluginManagerInstance.serverlessConfigFile = null; pluginManagerInstance.commands = { foo: {}, }; const foo = pluginManagerInstance.commands.foo; expect(pluginManagerInstance.validateServerlessConfigDependency(foo)).to.be.undefined; }); it('should load if the configDependent property is false and config is null', () => { pluginManagerInstance.serverlessConfigFile = null; pluginManagerInstance.commands = { foo: { configDependent: false, }, }; const foo = pluginManagerInstance.commands.foo; expect(pluginManagerInstance.validateServerlessConfigDependency(foo)).to.be.undefined; }); it('should throw an error if configDependent is true and no config is found', () => { pluginManagerInstance.serverlessConfigFile = null; pluginManagerInstance.commands = { foo: { configDependent: true, }, }; const foo = pluginManagerInstance.commands.foo; expect(() => { pluginManager.validateServerlessConfigDependency(foo); }).to.throw(Error); }); it('should throw an error if configDependent is true and config is an empty string', () => { pluginManagerInstance.serverlessConfigFile = ''; pluginManagerInstance.commands = { foo: { configDependent: true, }, }; const foo = pluginManagerInstance.commands.foo; expect(() => { pluginManager.validateServerlessConfigDependency(foo); }).to.throw(Error); }); it('should load if the configDependent property is true and config exists', () => { pluginManagerInstance.serverlessConfigFile = { servicePath: 'foo', }; pluginManagerInstance.commands = { foo: { configDependent: true, }, }; const foo = pluginManagerInstance.commands.foo; expect(pluginManagerInstance.validateServerlessConfigDependency(foo)).to.be.undefined; }); }); describe('#validateOptions()', () => { it('should throw an error if a required option is not set', () => { pluginManager.commands = { foo: { options: { baz: { shortcut: 'b', required: true, }, }, }, bar: { options: { baz: { required: true, }, }, }, }; const foo = pluginManager.commands.foo; const bar = pluginManager.commands.bar; expect(() => { pluginManager.validateOptions(foo); }).to.throw(Error); expect(() => { pluginManager.validateOptions(bar); }).to.throw(Error); }); it('should throw an error if a customValidation is not met', () => { pluginManager.setCliOptions({ bar: 'dev' }); pluginManager.commands = { foo: { options: { bar: { customValidation: { regularExpression: /^[0-9]+$/, errorMessage: 'Custom Error Message', }, }, }, }, }; const command = pluginManager.commands.foo; expect(() => { pluginManager.validateOptions(command); }).to.throw(Error); }); it('should succeeds if a custom regex matches in a plain commands object', () => { pluginManager.setCliOptions({ bar: 100 }); pluginManager.commands = { foo: { options: { bar: { customValidation: { regularExpression: /^[0-9]+$/, errorMessage: 'Custom Error Message', }, }, }, }, }; const commandsArray = ['foo']; expect(() => { pluginManager.validateOptions(commandsArray); }).to.not.throw(Error); }); }); describe('#run()', () => { it('should throw an error when the given command is not available', () => { pluginManager.addPlugin(SynchronousPluginMock); const commandsArray = ['foo']; return expect(pluginManager.run(commandsArray)).to.be.rejectedWith(Error); }); it('should throw an error when the given command is an entrypoint', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['myep']; expect(pluginManager.run(commandsArray)).to.be.rejectedWith(Error); }); it('should throw an error when the given command is a container', () => { pluginManager.addPlugin(ContainerPluginMock); const commandsArray = ['mycontainer']; expect(pluginManager.run(commandsArray)).to.be.rejectedWith(Error); }); it('should NOT throw an error when the given command is a child of a container', () => { pluginManager.addPlugin(ContainerPluginMock); const commandsArray = ['mycontainer', 'mysubcmd']; expect(pluginManager.run(commandsArray)).to.not.be.rejectedWith(Error); }); it('should throw an error when the given command is a child of an entrypoint', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['mysubcmd']; expect(pluginManager.run(commandsArray)).to.be.rejectedWith(Error); }); it('should show warning if in debug mode and the given command has no hooks', () => { const consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); process.env.SLS_DEBUG = '*'; class HooklessPlugin { constructor() { this.commands = { foo: {}, }; } } pluginManager.addPlugin(HooklessPlugin); const commandsArray = ['foo']; return pluginManager.run(commandsArray).then(() => { expect(consoleLogStub.called).is.equal(true); pluginManager.serverless.cli.log.restore(); }); }); it('should run the hooks in the correct order', () => { class CorrectHookOrderPluginMock { constructor() { this.commands = { run: { usage: 'Pushes the current hook status on the hookStatus array', lifecycleEvents: ['beforeHookStatus', 'midHookStatus', 'afterHookStatus'], }, }; this.hooks = { 'initialize': this.initializeHookStatus.bind(this), 'before:run:beforeHookStatus': this.beforeHookStatus.bind(this), 'run:midHookStatus': this.midHookStatus.bind(this), 'after:run:afterHookStatus': this.afterHookStatus.bind(this), }; // used to test if the hooks were run in the correct order this.hookStatus = []; } initializeHookStatus() { this.hookStatus.push('initialize'); } beforeHookStatus() { this.hookStatus.push('before'); } midHookStatus() { this.hookStatus.push('mid'); } afterHookStatus() { this.hookStatus.push('after'); } } pluginManager.addPlugin(CorrectHookOrderPluginMock); const commandsArray = ['run']; return pluginManager.run(commandsArray).then(() => { expect(pluginManager.plugins[0].hookStatus[0]).to.equal('initialize'); expect(pluginManager.plugins[0].hookStatus[1]).to.equal('before'); expect(pluginManager.plugins[0].hookStatus[2]).to.equal('mid'); expect(pluginManager.plugins[0].hookStatus[3]).to.equal('after'); }); }); describe('when using a synchronous hook function', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(SynchronousPluginMock); }); describe('when running a simple command', () => { it('should run a simple command', () => { const commandsArray = ['deploy']; return pluginManager .run(commandsArray) .then(() => expect(pluginManager.plugins[0].deployedFunctions).to.equal(1)); }); }); describe('when running a nested command', () => { it('should run the nested command', () => { const commandsArray = ['deploy', 'onpremises']; return pluginManager .run(commandsArray) .then(() => expect(pluginManager.plugins[0].deployedResources).to.equal(1)); }); }); }); describe('when using a promise based hook function', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback pluginManager.addPlugin(PromisePluginMock); }); describe('when running a simple command', () => { it('should run the simple command', () => { const commandsArray = ['deploy']; return pluginManager .run(commandsArray) .then(() => expect(pluginManager.plugins[0].deployedFunctions).to.equal(1)); }); }); describe('when running a nested command', () => { it('should run the nested command', () => { const commandsArray = ['deploy', 'onpremises']; return pluginManager .run(commandsArray) .then(() => expect(pluginManager.plugins[0].deployedResources).to.equal(1)); }); }); }); describe('when using provider specific plugins', () => { beforeEach(() => { // eslint-disable-line prefer-arrow-callback pluginManager.serverless.service.provider.name = 'provider1'; pluginManager.addPlugin(Provider1PluginMock); pluginManager.addPlugin(Provider2PluginMock); // this plugin should be run each and every time as it doesn't specify any provider pluginManager.addPlugin(SynchronousPluginMock); }); it('should load only the providers plugins (if the provider is specified)', () => { const commandsArray = ['deploy']; return pluginManager.run(commandsArray).then(() => { expect(pluginManager.plugins.length).to.equal(2); expect(pluginManager.plugins[0].deployedFunctions).to.equal(1); expect(pluginManager.plugins[0].provider).to.equal('provider1'); expect(pluginManager.plugins[1].deployedFunctions).to.equal(1); expect(pluginManager.plugins[1].provider).to.equal(undefined); }); }); }); it('should run commands with internal lifecycles', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['mycmd', 'spawncmd']; return pluginManager.run(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal( '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' ); }); }); }); describe('#getCommands()', () => { it('should hide entrypoints on any level and only return commands', () => { pluginManager.addPlugin(EntrypointPluginMock); const commands = pluginManager.getCommands(); expect(commands).to.have.a.property('mycmd'); expect(commands).to.have.a.nested.property('mycmd.commands.mysubcmd'); expect(commands).to.have.a.nested.property('mycmd.commands.spawncmd'); // Check for omitted entrypoints expect(commands).to.not.have.a.property('myep'); expect(commands).to.not.have.a.nested.property('myep.commands.mysubep'); expect(commands).to.not.have.a.nested.property('mycmd.commands.spawnep'); }); it('should return aliases', () => { pluginManager.addPlugin(AliasPluginMock); const commands = pluginManager.getCommands(); expect(commands) .to.have.a.property('on') .that.has.a.nested.property('commands.premise'); expect(commands).to.have.a.property('premise'); }); }); describe('#getCommand()', () => { beforeEach(() => { pluginManager.addPlugin(SynchronousPluginMock); pluginManager.serverless.cli.loadedCommands = { create: { usage: 'Create new Serverless service', lifecycleEvents: ['create'], options: { template: { usage: 'Template for the service. Available templates: ", "aws-nodejs", "..."', shortcut: 't', }, }, key: 'create', pluginName: 'Create', }, deploy: { usage: 'Deploy a Serverless service', configDependent: true, lifecycleEvents: ['cleanup', 'initialize'], options: { conceal: { usage: 'Hide secrets from the output (e.g. API Gateway key values)', }, stage: { usage: 'Stage of the service', shortcut: 's', }, }, key: 'deploy', pluginName: 'Deploy', commands: { function: { usage: 'Deploy a single function from the service', lifecycleEvents: ['initialize', 'packageFunction', 'deploy'], options: { function: { usage: 'Name of the function', shortcut: 'f', required: true, }, }, key: 'deploy:function', pluginName: 'Deploy', }, list: { usage: 'List deployed version of your Serverless Service', lifecycleEvents: ['log'], key: 'deploy:list', pluginName: 'Deploy', commands: { functions: { usage: 'List all the deployed functions and their versions', lifecycleEvents: ['log'], key: 'deploy:list:functions', pluginName: 'Deploy', }, }, }, }, }, }; }); it('should give a suggestion for an unknown command', done => { try { pluginManager.getCommand(['creet']); done('Test failed. Expected an error to be thrown'); } catch (error) { expect(error.name).to.eql('ServerlessError'); expect(error.message).to.eql( 'Serverless command "creet" not found. ' + 'Did you mean "create"? Run "serverless help" for a list of all available commands.' ); done(); } }); it('should not give a suggestion for valid top level command', done => { try { pluginManager.getCommand(['deploy', 'function-misspelled']); done('Test failed. Expected an error to be thrown'); } catch (error) { expect(error.name).to.eql('ServerlessError'); expect(error.message).to.eql( '"function-misspelled" is not a valid sub command. ' + 'Run "serverless deploy" to see a more helpful error message for this command.' ); done(); } }); }); describe('#spawn()', () => { it('should throw an error when the given command is not available', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['foo']; expect(() => { pluginManager.spawn(commandsArray); }).to.throw(Error); }); it('should show warning in debug mode and when the given command has no hooks', () => { const consoleLogStub = sinon.stub(pluginManager.serverless.cli, 'log').returns(); process.env.SLS_DEBUG = '*'; class HooklessPlugin { constructor() { this.commands = { foo: {}, }; } } pluginManager.addPlugin(HooklessPlugin); const commandsArray = ['foo']; return pluginManager.run(commandsArray).then(() => { expect(consoleLogStub.called).is.equal(true); pluginManager.serverless.cli.log.restore(); }); }); describe('when invoking a command', () => { it('should succeed', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['mycmd']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>run'); }); }); it('should spawn nested commands', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['mycmd', 'mysubcmd']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>subInitialize>subFinalize'); }); }); it('should terminate the hook chain if requested', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['mycmd', 'mysubcmd']; return expect( pluginManager.spawn(commandsArray, { terminateLifecycleAfterExecution: true }) ) .to.be.rejectedWith('Terminating mycmd:mysubcmd') .then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>subInitialize>subFinalize'); }); }); }); describe('when invoking a container', () => { it('should fail', () => { pluginManager.addPlugin(ContainerPluginMock); const commandsArray = ['mycontainer']; return expect(() => pluginManager.spawn(commandsArray)).to.throw(/command ".*" not found/); }); it('should spawn nested commands', () => { pluginManager.addPlugin(ContainerPluginMock); const commandsArray = ['mycontainer', 'mysubcmd']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>mysubcmdEvent1>mysubcmdEvent2'); }); }); }); describe('when invoking an entrypoint', () => { it('should succeed', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['myep']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); }); }); it('should spawn nested entrypoints', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['myep', 'mysubep']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>subEPInitialize>subEPFinalize'); }); }); describe('with string formatted syntax', () => { it('should succeed', () => { pluginManager.addPlugin(EntrypointPluginMock); return pluginManager.spawn('myep').then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>initialize>finalize'); }); }); it('should spawn nested entrypoints', () => { pluginManager.addPlugin(EntrypointPluginMock); return pluginManager.spawn('myep:mysubep').then(() => { expect(pluginManager.plugins[0].callResult).to.equal('>subEPInitialize>subEPFinalize'); }); }); }); }); it('should spawn entrypoints with internal lifecycles', () => { pluginManager.addPlugin(EntrypointPluginMock); const commandsArray = ['myep', 'spawnep']; return pluginManager.spawn(commandsArray).then(() => { expect(pluginManager.plugins[0].callResult).to.equal( '>subInitialize>subFinalize>initialize>finalize>run>subEPInitialize>subEPFinalize' ); }); }); }); describe('Plugin / Load local plugins', () => { const cwd = process.cwd(); let serviceDir; let tmpDir; beforeEach(() => { // eslint-disable-line prefer-arrow-callback tmpDir = getTmpDirPath(); serviceDir = path.join(tmpDir, 'service'); fse.mkdirsSync(serviceDir); process.chdir(serviceDir); pluginManager.serverless.config.servicePath = serviceDir; }); it('should load plugins from .serverless_plugins', () => { const localPluginDir = path.join(serviceDir, '.serverless_plugins', 'local-plugin'); installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadAllPlugins(['local-plugin']); expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') ); }); it('should load plugins from custom folder', () => { const localPluginDir = path.join(serviceDir, 'serverless-plugins-custom', 'local-plugin'); installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadAllPlugins({ localPath: path.join(serviceDir, 'serverless-plugins-custom'), modules: ['local-plugin'], }); // Had to use contructor.name because the class will be loaded via // require and the reference will not match with SynchronousPluginMock expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') ); }); it('should load plugins from custom folder outside of serviceDir', () => { serviceDir = path.join(tmpDir, 'serverless-plugins-custom'); const localPluginDir = path.join(serviceDir, 'local-plugin'); installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadAllPlugins({ localPath: serviceDir, modules: ['local-plugin'], }); // Had to use contructor.name because the class will be loaded via // require and the reference will not match with SynchronousPluginMock expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') ); }); it('should load plugins from custom folder outside of serviceDir', () => { serviceDir = path.join(tmpDir, 'serverless-plugins-custom'); const localPluginDir = path.join(serviceDir, 'local-plugin'); installPlugin(localPluginDir, SynchronousPluginMock); pluginManager.loadAllPlugins({ localPath: '../serverless-plugins-custom', modules: ['local-plugin'], }); // Had to use contructor.name because the class will be loaded via // require and the reference will not match with SynchronousPluginMock expect(pluginManager.plugins).to.satisfy(plugins => plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock') ); }); afterEach(() => { // eslint-disable-line prefer-arrow-callback process.chdir(cwd); try { fse.removeSync(tmpDir); } catch (e) { // Couldn't delete temporary file } }); }); describe('Plugin / CLI integration', function() { this.timeout(1000 * 60 * 10); let serverlessInstance; let serviceDir; const serverlessExec = require('../../test/serverlessBinary'); beforeEach(() => { // eslint-disable-line prefer-arrow-callback serverlessInstance = new Serverless(); return serverlessInstance.init().then(() => { // Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows. const tmpDir = getTmpDirPath(); serviceDir = path.join(tmpDir, 'service'); fse.mkdirsSync(serviceDir); return spawn(serverlessExec, ['create', '--template', 'aws-nodejs'], { env, cwd: serviceDir, }); }); }); it('should expose a working integration between the CLI and the plugin system', () => { expect( serverlessInstance.utils.fileExistsSync(path.join(serviceDir, 'serverless.yml')) ).to.equal(true); expect(serverlessInstance.utils.fileExistsSync(path.join(serviceDir, 'handler.js'))).to.equal( true ); }); it('should load plugins relatively to the working directory', () => { const localPluginDir = path.join(serviceDir, 'node_modules', 'local-plugin'); const parentPluginDir = path.join(serviceDir, '..', 'node_modules', 'parent-plugin'); installPlugin(localPluginDir, SynchronousPluginMock); installPlugin(parentPluginDir, PromisePluginMock); fs.appendFileSync( path.join(serviceDir, 'serverless.yml'), 'plugins:\n - local-plugin\n - parent-plugin' ); return spawn(serverlessExec, [], { env, cwd: serviceDir }).then(({ stdoutBuffer }) => { const stringifiedOutput = String(stdoutBuffer); expect(stringifiedOutput).to.contain('SynchronousPluginMock'); expect(stringifiedOutput).to.contain('PromisePluginMock'); }); }); afterEach(() => { try { fse.removeSync(serviceDir); } catch (e) { // Couldn't delete temporary file } }); }); });