mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
1751 lines
52 KiB
JavaScript
1751 lines
52 KiB
JavaScript
'use strict'
|
|
|
|
/* eslint-disable no-unused-expressions */
|
|
|
|
const chai = require('chai')
|
|
const overrideEnv = require('process-utils/override-env')
|
|
const overrideArgv = require('process-utils/override-argv')
|
|
const runServerless = require('../../../utils/run-serverless')
|
|
const fixtures = require('../../../fixtures/programmatic')
|
|
const Serverless = require('../../../../lib/serverless')
|
|
const CLI = require('../../../../lib/classes/cli')
|
|
const resolveInput = require('../../../../lib/cli/resolve-input')
|
|
const Create = require('../../../../lib/plugins/create/create')
|
|
const ServerlessError = require('../../../../lib/serverless-error')
|
|
const getRequire = require('../../../../lib/utils/get-require')
|
|
|
|
const path = require('path')
|
|
const fsp = require('fs').promises
|
|
const fse = require('fs-extra')
|
|
const mockRequire = require('mock-require')
|
|
const sinon = require('sinon')
|
|
const proxyquire = require('proxyquire')
|
|
const { installPlugin } = require('../../../utils/plugins')
|
|
const { getTmpDirPath } = require('../../../utils/fs')
|
|
|
|
chai.use(require('chai-as-promised'))
|
|
chai.use(require('sinon-chai'))
|
|
|
|
const expect = chai.expect
|
|
|
|
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)',
|
|
},
|
|
},
|
|
},
|
|
other: {
|
|
usage: 'Deploy to other infrastructure',
|
|
lifecycleEvents: ['resources', 'functions'],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
this.hooks = {
|
|
'deploy:functions': async () => this.functions(),
|
|
'before:deploy:onpremises:functions': async () => this.resources(),
|
|
}
|
|
|
|
// used to test if the function was executed correctly
|
|
this.deployedFunctions = 0
|
|
this.deployedResources = 0
|
|
}
|
|
|
|
async functions() {
|
|
this.deployedFunctions += 1
|
|
}
|
|
|
|
async resources() {
|
|
this.deployedResources += 1
|
|
}
|
|
}
|
|
|
|
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)',
|
|
type: 'string',
|
|
},
|
|
function: {
|
|
usage: 'The function you want to deploy (e.g. --function create)',
|
|
type: 'string',
|
|
},
|
|
},
|
|
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)',
|
|
type: 'string',
|
|
},
|
|
function: {
|
|
usage:
|
|
'The function you want to deploy (e.g. --function create)',
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
this.hooks = {
|
|
'deploy:functions': async () => this.functions(),
|
|
'before:deploy:onpremises:functions': async () => this.resources(),
|
|
}
|
|
|
|
// used to test if the function was executed correctly
|
|
this.deployedFunctions = 0
|
|
this.deployedResources = 0
|
|
}
|
|
|
|
functions() {
|
|
this.deployedFunctions += 1
|
|
}
|
|
|
|
resources() {
|
|
this.deployedResources += 1
|
|
}
|
|
}
|
|
|
|
describe('PluginManager', () => {
|
|
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': async () => this.functions(),
|
|
}
|
|
|
|
// 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': async () => this.functions(),
|
|
}
|
|
|
|
// used to test if the function was executed correctly
|
|
this.deployedFunctions = 0
|
|
}
|
|
|
|
functions() {
|
|
this.deployedFunctions += 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)',
|
|
type: 'string',
|
|
},
|
|
function: {
|
|
usage: 'The function you want to deploy (e.g. --function create)',
|
|
type: 'string',
|
|
},
|
|
},
|
|
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)',
|
|
type: 'string',
|
|
},
|
|
function: {
|
|
usage:
|
|
'The function you want to deploy (e.g. --function create)',
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
this.hooks = {
|
|
'deploy:functions': async () => this.functions(),
|
|
'before:deploy:onpremises:functions': async () => this.resources(),
|
|
}
|
|
|
|
// 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': async () => this.initialize(),
|
|
'myep:finalize': async () => this.finalize(),
|
|
'myep:mysubep:initialize': async () => this.subEPInitialize(),
|
|
'myep:mysubep:finalize': async () => this.subEPFinalize(),
|
|
'mycmd:mysubcmd:initialize': async () => this.subInitialize(),
|
|
'mycmd:mysubcmd:finalize': async () => this.subFinalize(),
|
|
'mycmd:run': async () => this.run(),
|
|
// Event1 spawns mysubcmd, then myep
|
|
// Event2 spawns mycmd, then mysubep
|
|
'myep:spawnep:event1': async () =>
|
|
pluginManager
|
|
.spawn(['mycmd', 'mysubcmd'])
|
|
.then(() => pluginManager.spawn(['myep'])),
|
|
'myep:spawnep:event2': async () =>
|
|
pluginManager
|
|
.spawn(['mycmd'])
|
|
.then(() => pluginManager.spawn(['myep', 'mysubep'])),
|
|
'mycmd:spawncmd:event1': async () =>
|
|
pluginManager
|
|
.spawn(['mycmd', 'mysubcmd'])
|
|
.then(() => pluginManager.spawn(['myep'])),
|
|
'mycmd:spawncmd:event2': async () =>
|
|
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': async () => this.eventOne(),
|
|
'mycontainer:mysubcmd:event2': async () => this.eventTwo(),
|
|
}
|
|
|
|
this.callResult = ''
|
|
}
|
|
|
|
eventOne() {
|
|
this.callResult += '>mysubcmdEvent1'
|
|
}
|
|
|
|
eventTwo() {
|
|
this.callResult += '>mysubcmdEvent2'
|
|
}
|
|
}
|
|
|
|
class DeprecatedLifecycleEventsPluginMock {
|
|
constructor() {
|
|
this.hooks = {
|
|
'deprecated:deprecated': async () => this.deprecated(), // NOTE: we assume that this is deprecated
|
|
'untouched:untouched': async () => this.untouched(),
|
|
}
|
|
}
|
|
|
|
deprecated() {
|
|
return
|
|
}
|
|
|
|
untouched() {
|
|
return
|
|
}
|
|
}
|
|
|
|
const resolveStub = (directory, pluginPath) => {
|
|
switch (pluginPath) {
|
|
case 'BrokenPluginMock':
|
|
case 'ServicePluginMock1':
|
|
case 'ServicePluginMock2':
|
|
return pluginPath
|
|
case './RelativePath/ServicePluginMock2':
|
|
return `${serviceDir}/RelativePath/ServicePluginMock2`
|
|
default:
|
|
return getRequire(directory).resolve(pluginPath)
|
|
}
|
|
}
|
|
|
|
let restoreEnv
|
|
let serviceDir
|
|
const PluginManager = proxyquire('../../../../lib/classes/plugin-manager', {
|
|
'../utils/get-require': (directory) => {
|
|
const resultRequire = require('module').createRequire(
|
|
path.resolve(directory, 'req'),
|
|
)
|
|
resultRequire.resolve = (pluginPath) => resolveStub(directory, pluginPath)
|
|
return resultRequire
|
|
},
|
|
})
|
|
|
|
beforeEach(() => {
|
|
;({ restoreEnv } = overrideEnv({ whitelist: ['APPDATA', 'PATH'] }))
|
|
serverless = new Serverless({ commands: [], options: {} })
|
|
serverless.cli = new CLI()
|
|
serverless.processedInput = { commands: ['print'], options: {} }
|
|
pluginManager = new PluginManager(serverless)
|
|
serviceDir = pluginManager.serverless.serviceDir = '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('#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 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', async () => {
|
|
const plugin1 = new ServicePluginMock1()
|
|
plugin1.asyncInit = sinon.stub().returns(Promise.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)
|
|
})
|
|
|
|
it('should load only core plugins when no service plugins are given', async () => {
|
|
// Note: We need the Create plugin for this test to pass
|
|
await 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', async () => {
|
|
const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']
|
|
await 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', async () => {
|
|
const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']
|
|
|
|
await 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', async () => {
|
|
await 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']
|
|
|
|
return expect(
|
|
pluginManager.loadAllPlugins(servicePlugins),
|
|
).to.be.rejectedWith(ServerlessError)
|
|
})
|
|
|
|
it('should not throw error when trying to load unknown plugin with help flag', async () => {
|
|
const servicePlugins = ['ServicePluginMock3', 'ServicePluginMock1']
|
|
|
|
pluginManager.setCliOptions({ help: true })
|
|
|
|
resolveInput.clear()
|
|
return overrideArgv({ args: ['serverless', '--help'] }, () => {
|
|
return expect(
|
|
pluginManager.loadAllPlugins(servicePlugins),
|
|
).to.not.be.rejectedWith(ServerlessError)
|
|
})
|
|
})
|
|
|
|
it('should pass through an error when plugin load fails', () => {
|
|
const servicePlugins = ['BrokenPluginMock']
|
|
|
|
return expect(
|
|
pluginManager.loadAllPlugins(servicePlugins),
|
|
).to.be.rejectedWith(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)
|
|
|
|
return expect(
|
|
pluginManager.loadAllPlugins(servicePlugins),
|
|
).to.not.be.rejectedWith(ServerlessError)
|
|
})
|
|
|
|
afterEach(() => {
|
|
mockRequire.stop('ServicePluginMock1')
|
|
mockRequire.stop('ServicePluginMock2')
|
|
mockRequire.stop('BrokenPluginMock')
|
|
})
|
|
})
|
|
|
|
describe('#resolveServicePlugins()', () => {
|
|
beforeEach(() => {
|
|
mockRequire('ServicePluginMock1', ServicePluginMock1)
|
|
// Plugins loaded via a relative path should be required relative to the service path
|
|
mockRequire(
|
|
`${serviceDir}/RelativePath/ServicePluginMock2`,
|
|
ServicePluginMock2,
|
|
)
|
|
})
|
|
|
|
it('should resolve the service plugins', async () => {
|
|
const servicePlugins = [
|
|
'ServicePluginMock1',
|
|
'./RelativePath/ServicePluginMock2',
|
|
]
|
|
expect(
|
|
await 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
|
|
return expect(pluginManager.resolveServicePlugins(servicePlugins)).to.not
|
|
.be.rejected
|
|
})
|
|
|
|
it('should not error if plugins = undefined', () => {
|
|
// Happens when `plugins` property does not exist
|
|
const servicePlugins = undefined
|
|
return expect(pluginManager.resolveServicePlugins(servicePlugins)).to.not
|
|
.be.rejected
|
|
})
|
|
|
|
afterEach(() => {
|
|
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.serviceDir, '.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.serviceDir, '.serverless_plugins'),
|
|
})
|
|
})
|
|
|
|
it('should parse plugins object if modules property is not an array', () => {
|
|
const servicePlugins = { modules: {} }
|
|
|
|
parsePluginsObjectAndVerifyResult(servicePlugins, {
|
|
modules: [],
|
|
localPath: path.join(serverless.serviceDir, '.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.serviceDir, '.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/)
|
|
})
|
|
})
|
|
|
|
describe('#loadHooks()', () => {
|
|
let deprecatedPluginInstance
|
|
|
|
beforeEach(() => {
|
|
deprecatedPluginInstance = new DeprecatedLifecycleEventsPluginMock()
|
|
pluginManager.deprecatedEvents = {
|
|
'deprecated:deprecated': 'new:new',
|
|
}
|
|
})
|
|
|
|
afterEach(() => {
|
|
pluginManager.deprecatedEvents = {}
|
|
})
|
|
|
|
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',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#getPlugins()', () => {
|
|
beforeEach(() => {
|
|
mockRequire('ServicePluginMock1', ServicePluginMock1)
|
|
mockRequire('ServicePluginMock2', ServicePluginMock2)
|
|
})
|
|
|
|
it('should return all loaded plugins', async () => {
|
|
const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2']
|
|
await 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(() => {
|
|
mockRequire.stop('ServicePluginMock1')
|
|
mockRequire.stop('ServicePluginMock2')
|
|
})
|
|
})
|
|
|
|
describe('#validateCommand()', () => {
|
|
it('should find commands', () => {
|
|
pluginManager.addPlugin(EntrypointPluginMock)
|
|
|
|
expect(() =>
|
|
pluginManager.validateCommand(['mycmd', 'mysubcmd']),
|
|
).to.not.throw(ServerlessError)
|
|
})
|
|
|
|
it('should find container children commands', () => {
|
|
pluginManager.addPlugin(ContainerPluginMock)
|
|
|
|
expect(() =>
|
|
pluginManager.validateCommand(['mycontainer', 'mysubcmd']),
|
|
).to.not.throw(ServerlessError)
|
|
})
|
|
})
|
|
|
|
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({ commands: [], options: {} })
|
|
serverlessInstance.configurationInput = null
|
|
serverlessInstance.serviceDir = 'my-service'
|
|
pluginManagerInstance = new PluginManager(serverlessInstance)
|
|
})
|
|
|
|
it('should continue loading if the configDependent property is absent', () => {
|
|
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.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.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.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.serverless.configurationInput = {
|
|
servicePath: 'foo',
|
|
}
|
|
|
|
pluginManagerInstance.commands = {
|
|
foo: {
|
|
configDependent: true,
|
|
},
|
|
}
|
|
|
|
const foo = pluginManagerInstance.commands.foo
|
|
|
|
expect(pluginManagerInstance.validateServerlessConfigDependency(foo)).to
|
|
.be.undefined
|
|
})
|
|
})
|
|
|
|
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']
|
|
|
|
return 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']
|
|
|
|
return 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']
|
|
|
|
return expect(pluginManager.run(commandsArray)).to.be.rejectedWith(Error)
|
|
})
|
|
|
|
it('should run the hooks in the correct order', async () => {
|
|
class CorrectHookOrderPluginMock {
|
|
constructor() {
|
|
this.commands = {
|
|
run: {
|
|
usage: 'Pushes the current hook status on the hookStatus array',
|
|
lifecycleEvents: [
|
|
'beforeHookStatus',
|
|
'midHookStatus',
|
|
'afterHookStatus',
|
|
],
|
|
},
|
|
}
|
|
|
|
this.hooks = {
|
|
initialize: async () => this.initializeHookStatus(),
|
|
'before:run:beforeHookStatus': async () => this.beforeHookStatus(),
|
|
'run:midHookStatus': async () => this.midHookStatus(),
|
|
'after:run:afterHookStatus': async () => this.afterHookStatus(),
|
|
}
|
|
|
|
// 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(() => {
|
|
pluginManager.addPlugin(SynchronousPluginMock)
|
|
})
|
|
|
|
describe('when running a simple command', () => {
|
|
it('should run a simple command', async () => {
|
|
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', async () => {
|
|
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(() => {
|
|
pluginManager.addPlugin(PromisePluginMock)
|
|
})
|
|
|
|
describe('when running a simple command', () => {
|
|
it('should run the simple command', async () => {
|
|
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', async () => {
|
|
const commandsArray = ['deploy', 'onpremises']
|
|
return pluginManager
|
|
.run(commandsArray)
|
|
.then(() =>
|
|
expect(pluginManager.plugins[0].deployedResources).to.equal(1),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when using provider specific plugins', () => {
|
|
beforeEach(() => {
|
|
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)', async () => {
|
|
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', async () => {
|
|
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',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('#spawn()', () => {
|
|
it('should throw an error when the given command is not available', async () => {
|
|
pluginManager.addPlugin(EntrypointPluginMock)
|
|
|
|
const commandsArray = ['foo']
|
|
|
|
return expect(
|
|
pluginManager.spawn(commandsArray),
|
|
).to.eventually.be.rejectedWith(Error)
|
|
})
|
|
|
|
describe('when invoking a command', () => {
|
|
it('should succeed', async () => {
|
|
pluginManager.addPlugin(EntrypointPluginMock)
|
|
|
|
const commandsArray = ['mycmd']
|
|
|
|
return pluginManager.spawn(commandsArray).then(() => {
|
|
expect(pluginManager.plugins[0].callResult).to.equal('>run')
|
|
})
|
|
})
|
|
|
|
it('should spawn nested commands', async () => {
|
|
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', async () => {
|
|
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 spawn nested commands', async () => {
|
|
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', async () => {
|
|
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', async () => {
|
|
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', async () => {
|
|
pluginManager.addPlugin(EntrypointPluginMock)
|
|
|
|
return pluginManager.spawn('myep').then(() => {
|
|
expect(pluginManager.plugins[0].callResult).to.equal(
|
|
'>initialize>finalize',
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should spawn nested entrypoints', async () => {
|
|
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', async () => {
|
|
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 tmpDir
|
|
beforeEach(() => {
|
|
tmpDir = getTmpDirPath()
|
|
serviceDir = path.join(tmpDir, 'service')
|
|
fse.mkdirsSync(serviceDir)
|
|
process.chdir(serviceDir)
|
|
pluginManager.serverless.serviceDir = serviceDir
|
|
})
|
|
|
|
it('should load plugins from .serverless_plugins', async () => {
|
|
const localPluginDir = path.join(
|
|
serviceDir,
|
|
'.serverless_plugins',
|
|
'local-plugin',
|
|
)
|
|
installPlugin(localPluginDir, SynchronousPluginMock)
|
|
|
|
await pluginManager.loadAllPlugins(['local-plugin'])
|
|
expect(pluginManager.plugins).to.satisfy((plugins) =>
|
|
plugins.some(
|
|
(plugin) => plugin.constructor.name === 'SynchronousPluginMock',
|
|
),
|
|
)
|
|
})
|
|
|
|
it('should load plugins from custom folder', async () => {
|
|
const localPluginDir = path.join(
|
|
serviceDir,
|
|
'serverless-plugins-custom',
|
|
'local-plugin',
|
|
)
|
|
installPlugin(localPluginDir, SynchronousPluginMock)
|
|
|
|
await pluginManager.loadAllPlugins({
|
|
localPath: path.join(serviceDir, 'serverless-plugins-custom'),
|
|
modules: ['local-plugin'],
|
|
})
|
|
// Had to use constructor.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', async () => {
|
|
serviceDir = path.join(tmpDir, 'serverless-plugins-custom')
|
|
const localPluginDir = path.join(serviceDir, 'local-plugin')
|
|
installPlugin(localPluginDir, SynchronousPluginMock)
|
|
|
|
await pluginManager.loadAllPlugins({
|
|
localPath: serviceDir,
|
|
modules: ['local-plugin'],
|
|
})
|
|
// Had to use constructor.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(() => {
|
|
process.chdir(cwd)
|
|
try {
|
|
fse.removeSync(tmpDir)
|
|
} catch (e) {
|
|
// Couldn't delete temporary file
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('test/unit/lib/classes/PluginManager.test.js', () => {
|
|
it('should load plugins relatively to the working directory', async () => {
|
|
const { servicePath: serviceDir } = await fixtures.setup('aws')
|
|
const localPluginDir = path.join(serviceDir, 'node_modules', 'local-plugin')
|
|
const parentPluginDir = path.join(
|
|
serviceDir,
|
|
'..',
|
|
'node_modules',
|
|
'parent-plugin',
|
|
)
|
|
installPlugin(localPluginDir, SynchronousPluginMock)
|
|
installPlugin(parentPluginDir, PromisePluginMock)
|
|
await fsp.appendFile(
|
|
path.join(serviceDir, 'serverless.yml'),
|
|
'plugins:\n - local-plugin\n - parent-plugin',
|
|
)
|
|
|
|
const { serverless } = await runServerless({
|
|
cwd: serviceDir,
|
|
command: 'print',
|
|
})
|
|
|
|
const pluginNames = new Set(
|
|
serverless.pluginManager.plugins.map((plugin) => plugin.constructor.name),
|
|
)
|
|
expect(pluginNames).to.contain('SynchronousPluginMock')
|
|
expect(pluginNames).to.contain('PromisePluginMock')
|
|
})
|
|
|
|
it('should pass log writers to external plugins', async () => {
|
|
const { serverless } = await runServerless({
|
|
fixture: 'plugin',
|
|
command: 'print',
|
|
})
|
|
const plugin = Array.from(serverless.pluginManager.externalPlugins).find(
|
|
(externalPlugin) => externalPlugin.constructor.name === 'TestPlugin',
|
|
)
|
|
expect(typeof plugin.utils.log).to.equal('function')
|
|
expect(typeof plugin.utils.progress.create).to.equal('function')
|
|
expect(typeof plugin.utils.writeText).to.equal('function')
|
|
})
|
|
|
|
it('should error out for duplicate plugin definiton', async () => {
|
|
await expect(
|
|
runServerless({
|
|
fixture: 'plugin',
|
|
command: 'print',
|
|
configExt: {
|
|
plugins: ['./plugin', './plugin'],
|
|
},
|
|
}),
|
|
).to.be.eventually.rejected.and.have.property(
|
|
'code',
|
|
'DUPLICATE_PLUGIN_DEFINITION',
|
|
)
|
|
})
|
|
|
|
it('should pass through an error when trying to load a plugin with error', async () => {
|
|
await expect(
|
|
runServerless({
|
|
fixture: 'plugin',
|
|
command: 'print',
|
|
configExt: {
|
|
plugins: ['./broken-plugin'],
|
|
},
|
|
}),
|
|
).to.be.eventually.rejectedWith(Error, 'failed to load plugin')
|
|
})
|
|
|
|
it('should load ESM plugins', async () => {
|
|
const { serverless } = await runServerless({
|
|
fixture: 'plugin',
|
|
command: 'print',
|
|
configExt: {
|
|
plugins: ['./local-esm-plugin', 'esm-plugin'],
|
|
},
|
|
})
|
|
|
|
const pluginNames = new Set(
|
|
serverless.pluginManager.plugins.map((plugin) => plugin.constructor.name),
|
|
)
|
|
expect(pluginNames).to.include('LocalESMPlugin')
|
|
expect(pluginNames).to.include('ESMPlugin')
|
|
})
|
|
})
|