From e3d4e23c8a1f4e1234f06482bf62f4cd140ed1ef Mon Sep 17 00:00:00 2001 From: horike37 Date: Sat, 5 Aug 2017 18:12:00 +0900 Subject: [PATCH] initial commit for adding plugin command --- lib/plugins/Plugins.json | 1 + lib/plugins/plugin/plugin.js | 303 +++++++++++++ lib/plugins/plugin/plugin.test.js | 688 ++++++++++++++++++++++++++++++ 3 files changed, 992 insertions(+) create mode 100644 lib/plugins/plugin/plugin.js create mode 100644 lib/plugins/plugin/plugin.test.js diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json index 05b681737..0346b2ddb 100644 --- a/lib/plugins/Plugins.json +++ b/lib/plugins/Plugins.json @@ -14,6 +14,7 @@ "./remove/remove.js", "./rollback/index.js", "./slstats/slstats.js", + "./plugin/plugin.js", "./aws/configCredentials/awsConfigCredentials.js", "./aws/provider/awsProvider.js", "./aws/common/index.js", diff --git a/lib/plugins/plugin/plugin.js b/lib/plugins/plugin/plugin.js new file mode 100644 index 000000000..015fe2c7e --- /dev/null +++ b/lib/plugins/plugin/plugin.js @@ -0,0 +1,303 @@ + +'use strict'; + +const BbPromise = require('bluebird'); +const childProcess = require('child_process'); +const fetch = require('node-fetch'); +const chalk = require('chalk'); +const fs = require('fs'); +const fse = require('fs-extra'); +const path = require('path'); +const _ = require('lodash'); +const YAML = require('js-yaml'); + +class Plugin { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options; + + this.commands = { + plugin: { + usage: 'Plugin management for Serverless', + commands: { + install: { + usage: 'Install and add a plugin to your service', + lifecycleEvents: [ + 'install', + ], + options: { + name: { + usage: 'The plugin name', + required: true, + shortcut: 'n', + }, + }, + }, + uninstall: { + usage: 'Uninstall and remove a plugin from your service', + lifecycleEvents: [ + 'uninstall', + ], + options: { + name: { + usage: 'The plugin name', + required: true, + shortcut: 'n', + }, + }, + }, + list: { + usage: 'Lists all available plugins', + lifecycleEvents: [ + 'list', + ], + }, + search: { + usage: 'Search for plugins', + lifecycleEvents: [ + 'search', + ], + options: { + query: { + usage: 'Search query', + required: true, + shortcut: 'q', + }, + }, + }, + }, + }, + }; + + this.hooks = { + 'plugin:install:install': () => BbPromise.bind(this).then(this.install), + 'plugin:uninstall:uninstall': () => BbPromise.bind(this).then(this.uninstall), + 'plugin:list:list': () => BbPromise.bind(this).then(this.list), + 'plugin:search:search': () => BbPromise.bind(this).then(this.search), + }; + } + + install() { + return BbPromise.bind(this) + .then(this.validate) + .then(this.getPlugins) + .then((plugins) => { + const pluginName = this.options.name; + const plugin = plugins.find((item) => item.name === pluginName); + + if (plugin) { + const servicePath = this.serverless.config.servicePath; + const packageJsonFilePath = path.join(servicePath, 'package.json'); + + // check if package.json is already present. Otherwise create one + if (!fs.existsSync(packageJsonFilePath)) { + this.serverless.cli + .log('Creating an empty package.json file in your service directory'); + + const packageJsonFileContent = { + name: this.serverless.service.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + } + + // install the package through npm + this.serverless.cli + .log(`Installing plugin "${pluginName}" (this might take a few seconds...)`); + + childProcess + .execSync(`npm install --save-dev ${pluginName}`, { + stdio: 'ignore', + }); + + // check if plugin was installed correctly + const pluginInstalled = !!JSON.parse( + fs.readFileSync(packageJsonFilePath).toString() + ).devDependencies[pluginName]; + + if (pluginInstalled) { + const serverlessFilePath = this.getServerlessFilePath(); + let serverlessFileContent = fs.readFileSync(serverlessFilePath).toString(); + + const newPluginsArray = { + plugins: [], + }; + + // load the service and parse it to JSON + const parsedYaml = YAML.load(serverlessFileContent); + + if (parsedYaml.plugins) { + newPluginsArray.plugins = parsedYaml.plugins; + } + + newPluginsArray.plugins = _.union(newPluginsArray.plugins, [pluginName]); + + if (parsedYaml.plugins) { + // replace the plugins definition in the serverless file + serverlessFileContent = serverlessFileContent + .replace( + /(plugins:)+(\s)*(- .*\s*)*(\[\])*/, // eslint-disable-line no-useless-escape + YAML.dump(newPluginsArray) + ); + } else { + serverlessFileContent = serverlessFileContent + .concat(`\n${YAML.dump(newPluginsArray)}`); + } + + // write the file back to the disc + fs.writeFileSync(serverlessFilePath, serverlessFileContent); + this.serverless.cli.log(`Successfully installed "${pluginName}"`); + } else { + const message = 'An error occurred while installing your plugin. Please try again...'; + this.serverless.cli.log(message); + } + } else { + const message = `Plugin "${pluginName}" not found. Did you spell it correct?`; + this.serverless.cli.log(message); + } + }); + } + + uninstall() { + return BbPromise.bind(this) + .then(this.validate) + .then(() => { + const pluginName = this.options.name; + + const servicePath = this.serverless.config.servicePath; + const packageJsonFilePath = path.join(servicePath, 'package.json'); + + // uninstall the package through npm + this.serverless.cli + .log(`Uninstalling plugin "${pluginName}" (this might take a few seconds...)`); + + childProcess + .execSync(`npm uninstall --save-dev ${pluginName}`, { + stdio: 'ignore', + }); + + // check if plugin was uninstalled correctly + const pluginStillAvailable = !!JSON.parse( + fs.readFileSync(packageJsonFilePath).toString() + ).devDependencies[pluginName]; + + if (!pluginStillAvailable) { + const serverlessFilePath = this.getServerlessFilePath(); + let serverlessFileContent = fs.readFileSync(serverlessFilePath).toString(); + + // load the service and parse it to JSON + const parsedYaml = YAML.load(serverlessFileContent); + // remove the plugin from the array if the plugins array is available + if (parsedYaml.plugins) { + const newPluginsArray = { + plugins: [], + }; + + newPluginsArray.plugins = _.pull(parsedYaml.plugins, pluginName); + if (!_.isEmpty(newPluginsArray.plugins)) { + // replace the plugins definition in the serverless file + serverlessFileContent = serverlessFileContent + .replace(/(plugins:)+(\s)+(- .*\s*)+/, YAML.dump(newPluginsArray)); + } else { + // remove the plugins definition from the serverless file + serverlessFileContent = serverlessFileContent + .replace(/(plugins:)+(\s)+(- .*\s*)+/, ''); + } + + // write the file back to the disc + fs.writeFileSync(serverlessFilePath, serverlessFileContent); + this.serverless.cli.log(`Successfully uninstalled "${pluginName}"`); + } + } else { + const message = 'An error occurred while uninstalling your plugin. Please try again...'; + this.serverless.cli.log(message); + } + }); + } + + list() { + return BbPromise.bind(this) + .then(this.getPlugins) + .then((plugins) => this.display(plugins)); + } + + search() { + return BbPromise.bind(this) + .then(this.getPlugins) + .then((plugins) => { + // filter out plugins which match the query + const regex = new RegExp(this.options.query); + + const filteredPlugins = plugins.filter((plugin) => + (plugin.name.match(regex) || plugin.description.match(regex)) + ); + + // print a message with the search result + const pluginCount = filteredPlugins.length; + const query = this.options.query; + const message = `${pluginCount} plugin(s) found for your search query "${query}"\n`; + this.serverless.cli.consoleLog(chalk.yellow(message)); + + return filteredPlugins; + }) + .then((plugins) => { + this.display(plugins); + }); + } + + // helper methods + validate() { + if (!this.serverless.config.servicePath) { + throw new this.serverless.classes + .Error('This command can only be run inside a service directory'); + } + + return BbPromise.resolve(); + } + + getServerlessFilePath() { + const servicePath = this.serverless.config.servicePath; + const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); + const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml'); + + let serverlessFilePath; + if (fs.existsSync(serverlessYmlFilePath)) { + serverlessFilePath = serverlessYmlFilePath; + } else { + serverlessFilePath = serverlessYamlFilePath; + } + + return serverlessFilePath; + } + + getPlugins() { + const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; + + return fetch(endpoint).then((result) => result.json()).then((json) => json); + } + + display(plugins) { + let message = ''; + if (plugins && plugins.length) { + // order plugins by name + const orderedPlugins = _.orderBy(plugins, ['name'], ['asc']); + orderedPlugins.forEach((plugin) => { + message += `${chalk.yellow.underline(plugin.name)}\n`; + message += `${plugin.description}\n\n`; + }); + // remove last two newlines for a prettier output + message = message.slice(0, -2); + this.serverless.cli.consoleLog(message); + } else { + message = 'There are no plugins available to display'; + this.serverless.cli.consoleLog(message); + } + return BbPromise.resolve(message); + } +} + +module.exports = Plugin; diff --git a/lib/plugins/plugin/plugin.test.js b/lib/plugins/plugin/plugin.test.js new file mode 100644 index 000000000..fa5d79121 --- /dev/null +++ b/lib/plugins/plugin/plugin.test.js @@ -0,0 +1,688 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const BbPromise = require('bluebird'); +const YAML = require('js-yaml'); +const path = require('path'); +const childProcess = require('child_process'); +const fs = require('fs'); +const fse = require('fs-extra'); +const proxyquire = require('proxyquire'); +const chalk = require('chalk'); +const Plugin = require('./plugin'); +const Serverless = require('../../Serverless'); +const CLI = require('../../classes/CLI'); +const testUtils = require('../../../tests/utils'); + +describe('Plugin', () => { + let plugin; + let serverless; + let consoleLogStub; + const plugins = [ + { + name: 'serverless-plugin-1', + description: 'Serverless Plugin 1', + githubUrl: 'https://github.com/serverless/serverless-plugin-1', + }, + { + name: 'serverless-plugin-2', + description: 'Serverless Plugin 2', + githubUrl: 'https://github.com/serverless/serverless-plugin-2', + }, + ]; + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = new CLI(serverless); + const options = {}; + plugin = new Plugin(serverless, options); + consoleLogStub = sinon.stub(serverless.cli, 'consoleLog').returns(); + }); + + afterEach(() => { + serverless.cli.consoleLog.restore(); + }); + + describe('#constructor()', () => { + it('should have the command "plugin"', () => { + expect(plugin.commands.plugin).to.not.equal(undefined); + }); + + it('should have the sub-command "install"', () => { + expect(plugin.commands.plugin.commands.install).to.not.equal(undefined); + }); + + it('should have the sub-command "uninstall"', () => { + expect(plugin.commands.plugin.commands.uninstall).to.not.equal(undefined); + }); + + it('should have the sub-command "list"', () => { + expect(plugin.commands.plugin.commands.list).to.not.equal(undefined); + }); + + it('should have the sub-command "search"', () => { + expect(plugin.commands.plugin.commands.search).to.not.equal(undefined); + }); + + it('should have the lifecycle event "install" for the "install" sub-command', () => { + expect(plugin.commands.plugin.commands.install.lifecycleEvents).to.deep.equal([ + 'install', + ]); + }); + + it('should have the lifecycle event "uninstall" for the "uninstall" sub-command', () => { + expect(plugin.commands.plugin.commands.uninstall.lifecycleEvents).to.deep.equal([ + 'uninstall', + ]); + }); + + it('should have the lifecycle event "list" for the "list" sub-command', () => { + expect(plugin.commands.plugin.commands.list.lifecycleEvents).to.deep.equal([ + 'list', + ]); + }); + + it('should have the lifecycle event "search" for the "search" sub-command', () => { + expect(plugin.commands.plugin.commands.search.lifecycleEvents).to.deep.equal([ + 'search', + ]); + }); + + it('should have a required option "name" for the "install" sub-command', () => { + // eslint-disable-next-line no-unused-expressions + expect(plugin.commands.plugin.commands.install.options.name.required).to.be.true; + }); + + it('should have a required option "name" for the "uninstall" sub-command', () => { + // eslint-disable-next-line no-unused-expressions + expect(plugin.commands.plugin.commands.uninstall.options.name.required).to.be.true; + }); + + it('should have no option for the "list" sub-command', () => { + // eslint-disable-next-line no-unused-expressions + expect(plugin.commands.plugin.commands.list.options).to.equal(undefined); + }); + + it('should have a required option "query" for the "search" sub-command', () => { + // eslint-disable-next-line no-unused-expressions + expect(plugin.commands.plugin.commands.search.options.query.required).to.be.true; + }); + + it('should have a "plugin:install:install" hook', () => { + expect(plugin.hooks['plugin:install:install']).to.not.equal(undefined); + }); + + it('should have a "plugin:uninstall:uninstall" hook', () => { + expect(plugin.hooks['plugin:uninstall:uninstall']).to.not.equal(undefined); + }); + + it('should have a "plugin:list:list" hook', () => { + expect(plugin.hooks['plugin:list:list']).to.not.equal(undefined); + }); + + it('should have a "plugin:search:search" hook', () => { + expect(plugin.hooks['plugin:search:search']).to.not.equal(undefined); + }); + + it('should run promise chain in order for "plugin:install:install" hook', () => { + const installStub = sinon + .stub(plugin, 'install').returns(BbPromise.resolve()); + + return plugin.hooks['plugin:install:install']().then(() => { + expect(installStub.calledOnce).to.equal(true); + + plugin.install.restore(); + }); + }); + + it('should run promise chain in order for "plugin:uninstall:uninstall" hook', () => { + const uninstallStub = sinon + .stub(plugin, 'uninstall').returns(BbPromise.resolve()); + + return plugin.hooks['plugin:uninstall:uninstall']().then(() => { + expect(uninstallStub.calledOnce).to.equal(true); + + plugin.uninstall.restore(); + }); + }); + + it('should run promise chain in order for "plugin:list:list" hook', () => { + const listStub = sinon + .stub(plugin, 'list').returns(BbPromise.resolve()); + + return plugin.hooks['plugin:list:list']().then(() => { + expect(listStub.calledOnce).to.equal(true); + + plugin.list.restore(); + }); + }); + + it('should run promise chain in order for "plugin:search:search" hook', () => { + const searchStub = sinon + .stub(plugin, 'search').returns(BbPromise.resolve()); + + return plugin.hooks['plugin:search:search']().then(() => { + expect(searchStub.calledOnce).to.equal(true); + + plugin.search.restore(); + }); + }); + }); + + describe('#install()', () => { + let servicePath; + let serverlessYmlFilePath; + let packageJsonFilePath; + let validateStub; + let getPluginsStub; + let npmInstallStub; + let savedCwd; + + beforeEach(() => { + servicePath = testUtils.getTmpDirPath(); + fse.ensureDirSync(servicePath); + serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); + packageJsonFilePath = path.join(servicePath, 'package.json'); + plugin.serverless.config.servicePath = servicePath; + validateStub = sinon + .stub(plugin, 'validate') + .returns(BbPromise.resolve()); + getPluginsStub = sinon + .stub(plugin, 'getPlugins') + .returns(BbPromise.resolve(plugins)); + npmInstallStub = sinon + .stub(childProcess, 'execSync') + .withArgs('npm install --save-dev serverless-plugin-1') + .returns(); + // save the cwd so that we can restore it later + savedCwd = process.cwd(); + process.chdir(servicePath); + }); + + afterEach(() => { + plugin.validate.restore(); + plugin.getPlugins.restore(); + childProcess.execSync.restore(); + process.chdir(savedCwd); + }); + + it('should not install the plugin if it can not be found in the registry', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + + plugin.options.name = 'some-not-available-plugin'; // this plugin is not in the plugins mock + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + expect(npmInstallStub.called).to.equal(false); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins).to.equal(undefined); + }); + }); + + it('should log a message if a problem during installation happens', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: {}, // plugin was not added successfully + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + expect(npmInstallStub.calledWithExactly( + `npm install --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + }); + }); + + it('should generate a package.json file in the service directory if not present', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(npmInstallStub.calledWithExactly( + `npm install --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + expect(fs.existsSync(packageJsonFilePath)).to.equal(true); + }); + }); + + it('should add the plugin to the service file if plugins array is not present', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + // no plugins array here + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: { + 'serverless-plugin-1': '0.1.0', + }, + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(npmInstallStub.calledWithExactly( + `npm install --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins[0]).to.equal('serverless-plugin-1'); + }); + }); + + it('should push the plugin to the service files plugin array if present', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: ['serverless-existing-plugin'], // one plugin was already added + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: { + 'serverless-plugin-1': '0.1.0', + }, + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(npmInstallStub.calledWithExactly( + `npm install --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins.length).to.equal(2); + expect(serverlessFileContent.plugins).to.include('serverless-existing-plugin'); + expect(serverlessFileContent.plugins).to.include('serverless-plugin-1'); + }); + }); + + it('should push the plugin to the service files plugin array if it is empty', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: [], // empty plugins array + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: { + 'serverless-plugin-1': '0.1.0', + }, + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.install().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(npmInstallStub.calledWithExactly( + `npm install --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins.length).to.equal(1); + expect(serverlessFileContent.plugins).to.include('serverless-plugin-1'); + }); + }); + }); + + describe('#uninstall()', () => { + let servicePath; + let serverlessYmlFilePath; + let packageJsonFilePath; + let validateStub; + let npmUninstallStub; + let savedCwd; + + beforeEach(() => { + servicePath = testUtils.getTmpDirPath(); + fse.ensureDirSync(servicePath); + serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); + packageJsonFilePath = path.join(servicePath, 'package.json'); + plugin.serverless.config.servicePath = servicePath; + validateStub = sinon + .stub(plugin, 'validate').returns(BbPromise.resolve()); + npmUninstallStub = sinon + .stub(childProcess, 'execSync') + .withArgs('npm uninstall --save-dev serverless-plugin-1') + .returns(); + // save the cwd so that we can restore it later + savedCwd = process.cwd(); + process.chdir(servicePath); + }); + + afterEach(() => { + plugin.validate.restore(); + childProcess.execSync.restore(); + process.chdir(savedCwd); + }); + + it('should log a message if a problem during uninstallation happens', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: { + 'serverless-plugin-1': '0.1.0', // plugin is still available + }, + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.uninstall().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(npmUninstallStub.calledWithExactly( + `npm uninstall --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + }); + }); + + it('should only remove the given plugin from the service', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: [ + 'serverless-plugin-1', // only this should be removed + 'serverless-existing-plugin', + ], + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: {}, // plugin was removed via npm + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.uninstall().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(npmUninstallStub.calledWithExactly( + `npm uninstall --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins.length).to.equal(1); + expect(serverlessFileContent.plugins).to.not.contain('serverless-plugin-1'); + }); + }); + + it('should remove the plugin from the service if it is the only one', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: ['serverless-plugin-1'], // plugin is available + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: {}, // plugin was removed via npm + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.uninstall().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(npmUninstallStub.calledWithExactly( + `npm uninstall --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent).to.not.have.property('plugins'); + }); + }); + + it('should do nothing if plugins array is not present in service file', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + // no plugins array + }; + serverless.utils + .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml)); + // package.json file + const packageJsonFileContent = { + devDependencies: {}, // plugin was removed via npm + }; + fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent); + + plugin.options.name = 'serverless-plugin-1'; + + return plugin.uninstall().then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(npmUninstallStub.calledWithExactly( + `npm uninstall --save-dev ${plugin.options.name}`, + { stdio: 'ignore' } + )).to.equal(true); + expect(consoleLogStub.called).to.equal(true); + + // inspect the serverless.yml file + const serverlessFileContent = YAML.load(fs.readFileSync(serverlessYmlFilePath).toString()); + expect(serverlessFileContent.plugins).to.equal(undefined); + }); + }); + }); + + describe('#list()', () => { + let getPluginsStub; + let displayStub; + + beforeEach(() => { + getPluginsStub = sinon + .stub(plugin, 'getPlugins').returns(BbPromise.resolve()); + displayStub = sinon + .stub(plugin, 'display').returns(BbPromise.resolve()); + }); + + afterEach(() => { + plugin.getPlugins.restore(); + plugin.display.restore(); + }); + + it('should print a list with all available plugins', () => + plugin.list().then(() => { + expect(getPluginsStub.calledOnce).to.equal(true); + expect(displayStub.calledOnce).to.equal(true); + }) + ); + }); + + describe('#search()', () => { + let getPluginsStub; + let displayStub; + + beforeEach(() => { + getPluginsStub = sinon.stub(plugin, 'getPlugins').returns(BbPromise.resolve(plugins)); + displayStub = sinon.stub(plugin, 'display').returns(BbPromise.resolve()); + }); + + afterEach(() => { + plugin.getPlugins.restore(); + plugin.display.restore(); + }); + + it('should return a list of plugins based on the search query', () => { + plugin.options.query = 'serverless-plugin-1'; + + return plugin.search().then(() => { + expect(consoleLogStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(displayStub.calledOnce).to.equal(true); + }); + }); + }); + + describe('#validate()', () => { + it('should throw an error if the the cwd is not a Serverless service', () => { + plugin.serverless.config.servicePath = false; + + expect(() => { plugin.validate(); }).to.throw(Error); + }); + + it('should resolve if the cwd is a Serverless service', (done) => { + plugin.serverless.config.servicePath = true; + + plugin.validate().then(() => done()); + }); + }); + + describe('#getServerlessFilePath()', () => { + let servicePath; + + beforeEach(() => { + servicePath = testUtils.getTmpDirPath(); + plugin.serverless.config.servicePath = servicePath; + }); + + it('should return the correct serverless file path for a .yml file', () => { + const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml'); + fse.ensureFileSync(serverlessYmlFilePath); + + const serverlessFilePath = plugin.getServerlessFilePath(); + + expect(serverlessFilePath).to.equal(serverlessYmlFilePath); + }); + + it('should return the correct serverless file path for a .yaml file', () => { + const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml'); + fse.ensureFileSync(serverlessYamlFilePath); + + const serverlessFilePath = plugin.getServerlessFilePath(); + + expect(serverlessFilePath).to.equal(serverlessYamlFilePath); + }); + }); + + describe('#getPlugins()', () => { + let fetchStub; + let PluginWithFetchStub; + let pluginWithFetchStub; + + beforeEach(() => { + fetchStub = sinon.stub().returns( + BbPromise.resolve({ + json: sinon.stub().returns(BbPromise.resolve(plugins)), + }) + ); + PluginWithFetchStub = proxyquire('./plugin.js', { + 'node-fetch': fetchStub, + }); + pluginWithFetchStub = new PluginWithFetchStub(serverless); + }); + + it('should fetch and return the plugins from the plugins repository', () => { + const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; + + return pluginWithFetchStub.getPlugins().then((result) => { + expect(fetchStub.calledOnce).to.equal(true); + expect(fetchStub.args[0][0]).to.equal(endpoint); + expect(result).to.deep.equal(plugins); + }); + }); + }); + + describe('#display()', () => { + it('should display the plugins if present', () => { + let expectedMessage = ''; + expectedMessage += `${chalk.yellow.underline('serverless-plugin-1')}\n`; + expectedMessage += 'Serverless Plugin 1\n\n'; + expectedMessage += `${chalk.yellow.underline('serverless-plugin-2')}\n`; + expectedMessage += 'Serverless Plugin 2\n\n'; + expectedMessage = expectedMessage.slice(0, -2); + + return plugin.display(plugins).then((message) => { + expect(consoleLogStub.calledOnce).to.equal(true); + expect(message).to.equal(expectedMessage); + }); + }); + + it('should print a message when no plugins are available to display', () => { + const expectedMessage = 'There are no plugins available to display'; + + return plugin.display([]).then((message) => { + expect(consoleLogStub.calledOnce).to.equal(true); + expect(message).to.equal(expectedMessage); + }); + }); + }); +});