From 500d7790e09287c2b0fd2e5a268157e0382af471 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 19 Jul 2019 11:16:36 -0700 Subject: [PATCH 1/5] [skip ci] Rename src/stackstorm.js to src/stackstorm_api.js --- src/{stackstorm.js => stackstorm_api.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{stackstorm.js => stackstorm_api.js} (100%) diff --git a/src/stackstorm.js b/src/stackstorm_api.js similarity index 100% rename from src/stackstorm.js rename to src/stackstorm_api.js From 50e9e329ad0cf307df833d824c4ee7b6d7844be0 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 19 Jul 2019 11:19:46 -0700 Subject: [PATCH 2/5] [skip ci] Refactor stackstorm_api.js to use JS (old) classes --- src/stackstorm.js | 42 ++++ src/stackstorm_api.js | 561 ++++++++++++++++++++++-------------------- 2 files changed, 335 insertions(+), 268 deletions(-) create mode 100644 src/stackstorm.js diff --git a/src/stackstorm.js b/src/stackstorm.js new file mode 100644 index 0000000..53eca57 --- /dev/null +++ b/src/stackstorm.js @@ -0,0 +1,42 @@ +// Copyright 2019 Extreme Networks, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Description: +// StackStorm hubot integration +// +// Dependencies: +// +// +// Configuration: +// ST2_API_URL - FQDN + port to StackStorm endpoint +// ST2_ROUTE - StackStorm notification route name +// ST2_COMMANDS_RELOAD_INTERVAL - Reload interval for commands +// +// Notes: +// Command list is automatically generated from StackStorm ChatOps metadata +// + +"use strict"; + +var env = process.env; +var StackStormApi = require('./stackstorm_api'); + +module.exports = function (robot) { + var stackstormApi = new StackStormApi(robot); + + return stackstormApi.authenticate().then(function () { + stackstormApi.start(); + return stackstormApi.stop.bind(stackstormApi); + }); +}; diff --git a/src/stackstorm_api.js b/src/stackstorm_api.js index 380e7db..fbe55da 100644 --- a/src/stackstorm_api.js +++ b/src/stackstorm_api.js @@ -94,7 +94,23 @@ var ERROR_MESSAGES = [ var TWOFACTOR_MESSAGE = "This action requires two-factor auth! Waiting for your confirmation."; -module.exports = function(robot) { +function StackStorm(robot) { + var self = this; + + self.robot = robot; + + // factory to manage commands + self.command_factory = new CommandFactory(self.robot); + + // adapter - specific to each chat provider + self.adapter = adapters.getAdapter(self.robot.adapterName, self.robot); + + self.commands_load_interval = null; + self.st2stream = null; + + self.two_factor_authorization_enabled = env.HUBOT_2FA || false; + + // Makes the script crash on unhandled rejections instead of ignoring them and keep running. // Usually happens when trying to connect to a nonexistent instances or similar unrecoverable issues. // In the future Node.js versions, promise rejections that are not handled will terminate the process with a non-zero exit code. @@ -103,9 +119,9 @@ module.exports = function(robot) { }); // Handle uncaught exceptions, log error and terminate hubot if one occurs - robot.error(function(err, res) { + self.robot.error(function(err, res) { if (err) { - robot.logger.error(err.stack || JSON.stringify(err)); + self.robot.logger.error(err.stack || JSON.stringify(err)); } if (res) { res.send(JSON.stringify({ @@ -114,20 +130,59 @@ module.exports = function(robot) { })); } - robot.logger.info('Hubot will shut down ...'); - robot.shutdown(); + self.robot.logger.info('Hubot will shut down ...'); + self.robot.shutdown(); }); - var self = this; + self.robot.respond(/([\s\S]+?)$/i, function(msg) { + var command, result; + + // Normalize the command and remove special handling provided by the chat service. + // e.g. slack replace quote marks with left double quote which would break behavior. + command = self.adapter.normalizeCommand(msg.match[1]); + + result = self.command_factory.getMatchingCommand(command); + + if (!result) { + // No command found + return; + } + + var [command_name, format_string, action_alias] = result; + + self.executeCommand(msg, command_name, format_string, command, action_alias); + }); + + self.robot.router.post('/hubot/st2', function(req, res) { + var data; + + try { + if (req.body.payload) { + data = JSON.parse(req.body.payload); + } else { + data = req.body; + } + self.adapter.postData(data); + + res.send('{"status": "completed", "msg": "Message posted successfully"}'); + } catch (e) { + self.robot.logger.error("Unable to decode JSON: " + e); + self.robot.logger.error(e.stack); + res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); + } + }); - var promise = Promise.resolve(); if (env.ST2_API) { - robot.logger.warning("ST2_API is deprecated and will be removed in a future releases. Instead, please use the ST2_API_URL environment variable."); + self.robot.logger.warning("ST2_API is deprecated and will be removed in a future releases. Instead, please use the ST2_API_URL environment variable."); } - var self = this, - authenticated = Promise.resolve(), - url = utils.parseUrl(env.ST2_API_URL), + + if (self.two_factor_authorization_enabled) { + self.twofactor = {}; + self.robot.logger.info('Two-factor auth is enabled'); + } + + var url = utils.parseUrl(env.ST2_API_URL), opts = { protocol: url.protocol, host: url.hostname, @@ -146,237 +201,255 @@ module.exports = function(robot) { }; } - var api_client = st2client(opts); + self.auth_client = null + self.api_client = st2client(opts); - function authenticate() { - api_client.removeListener('expiry', authenticate); + if (env.ST2_API_KEY) { + self.api_client.setKey({ key: env.ST2_API_KEY }); + } else if (env.ST2_AUTH_TOKEN) { + self.api_client.setToken({ token: env.ST2_AUTH_TOKEN }); + } - // API key gets precedence 1 - if (env.ST2_API_KEY) { - robot.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); - return Promise.resolve(); - } - // Auth token gets precedence 2 - if (env.ST2_AUTH_TOKEN) { - robot.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); - return Promise.resolve(); + if (env.ST2_API_KEY || env.ST2_AUTH_TOKEN || env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) { + // If using username and password then all are required. + if ((env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) && + !(env.ST2_AUTH_USERNAME && env.ST2_AUTH_PASSWORD && env.ST2_AUTH_URL)) { + throw new Error('Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together.'); } + } +}; - robot.logger.info('Requesting a token...'); - - var url = utils.parseUrl(env.ST2_AUTH_URL); - var auth_client = st2client({ - auth: { - protocol: url.protocol, - host: url.hostname, - port: url.port, - prefix: url.path - } - }); +// This is a privileged method, and is not shared amongst StackStorm objects. +// It is a method on every StackStorm _object_. Luckily, this should only be +// created once. +// For more information, see: +// https://stackoverflow.com/a/2294252 +StackStorm.prototype.authenticate = function () { + var self = this; - return auth_client.authenticate(env.ST2_AUTH_USERNAME, env.ST2_AUTH_PASSWORD) - .then(function (token) { - robot.logger.info('Token received. Expiring ' + token.expiry); - api_client.setToken(token); - auth_client.on('expiry', authenticate); - }) - .catch(function (err) { - robot.logger.error('Failed to authenticate: ' + err.message); + self.api_client.removeListener('expiry', self.authenticate.bind(self)); - throw err; - }); + // API key gets precedence 1 + if (env.ST2_API_KEY) { + self.robot.logger.info('Using ST2_API_KEY as authentication. Expiry will lead to bot exit.'); + return Promise.resolve(); + } + // Auth token gets precedence 2 + if (env.ST2_AUTH_TOKEN) { + self.robot.logger.info('Using ST2_AUTH_TOKEN as authentication. Expiry will lead to bot exit.'); + return Promise.resolve(); } - // factory to manage commands - var command_factory = new CommandFactory(robot); + self.robot.logger.info('Requesting a token...'); - // adapter - specific to each chat provider - var adapter = adapters.getAdapter(robot.adapterName, robot); - - var loadCommands = function() { - robot.logger.info('Loading commands....'); - - api_client.actionAlias.list({limit: -1}) - .then(function (aliases) { - // Remove all the existing commands - command_factory.removeCommands(); - - _.each(aliases, function (alias) { - var name = alias.name; - var formats = alias.formats; - var description = alias.description; - - if (alias.enabled === false) { - return; - } - - if (!formats || formats.length === 0) { - robot.logger.error('No formats specified for command: ' + name); - return; - } - - _.each(formats, function (format) { - var command = formatCommand(robot.logger, name, format.display || format, description); - command_factory.addCommand(command, name, format.display || format, alias, - format.display ? utils.DISPLAY : false); - - _.each(format.representation, function (representation) { - command = formatCommand(robot.logger, name, representation, description); - command_factory.addCommand(command, name, representation, alias, utils.REPRESENTATION); - }); - }); - }); + var url = utils.parseUrl(env.ST2_AUTH_URL); - robot.logger.info(command_factory.st2_hubot_commands.length + ' commands are loaded'); - }) - .catch(function (err) { - robot.logger.error(util.format('Failed to retrieve commands from "%s": %s', env.ST2_API_URL, err.message)); - if (err.status === 401 || err.message.includes('Unauthorized')) { - throw err; - } - }); - }; + self.auth_client = st2client({ + auth: { + protocol: url.protocol, + host: url.hostname, + port: url.port, + prefix: url.path + } + }); - var sendAck = function (msg, res) { - var history_url = utils.getExecutionHistoryUrl(res.execution); - var history = history_url ? util.format(' (details available at %s)', history_url) : ''; + return self.auth_client.authenticate(env.ST2_AUTH_USERNAME, env.ST2_AUTH_PASSWORD) + .then(function (token) { + self.robot.logger.info('Token received. Expiring ' + token.expiry); + self.api_client.setToken(token); + self.auth_client.on('expiry', self.authenticate.bind(self)); + }) + .catch(function (err) { + self.robot.logger.error('Failed to authenticate: ' + err.message); - if (res.actionalias && res.actionalias.ack) { - if (res.actionalias.ack.enabled === false) { - return; - } else if (res.actionalias.ack.append_url === false) { - history = ''; - } - } + throw err; + }); +}; - if (res.message) { - return msg.send(res.message + history); - } +StackStorm.prototype.loadCommands = function () { + var self = this; - var message = util.format(_.sample(START_MESSAGES), res.execution.id); - return msg.send(message + history); - }; + self.robot.logger.info('Loading commands....'); + + self.api_client.actionAlias.list({limit: -1}) + .then(function (aliases) { + // Remove all the existing commands + self.command_factory.removeCommands(); - var sendAliasExecutionRequest = function (msg, payload) { - robot.logger.debug('Sending command payload:', JSON.stringify(payload)); + _.each(aliases, function (alias) { + var name = alias.name; + var formats = alias.formats; + var description = alias.description; - api_client.aliasExecution.create(payload) - .then(function (res) { sendAck(msg, res); }) - .catch(function (err) { - // Compatibility with older StackStorm versions - if (err.status === 200) { - return sendAck(msg, { execution: { id: err.message } }); + if (alias.enabled === false) { + return; } - robot.logger.error('Failed to create an alias execution:', err); - var addressee = adapter.normalizeAddressee(msg); - var message = util.format(_.sample(ERROR_MESSAGES), err.message); - if (err.requestId) { - message = util.format( - message, - util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); + + if (!formats || formats.length === 0) { + self.robot.logger.error('No formats specified for command: ' + name); + return; } - adapter.postData({ - whisper: false, - user: addressee.name, - channel: addressee.room, - message: message, - extra: { - color: '#F35A00' - } + + _.each(formats, function (format) { + var command = formatCommand(self.robot.logger, name, format.display || format, description); + self.command_factory.addCommand(command, name, format.display || format, alias, + format.display ? utils.DISPLAY : false); + + _.each(format.representation, function (representation) { + command = formatCommand(self.robot.logger, name, representation, description); + self.command_factory.addCommand(command, name, representation, alias, utils.REPRESENTATION); + }); }); }); - }; - var executeCommand = function(msg, command_name, format_string, command, action_alias) { - var addressee = adapter.normalizeAddressee(msg); - var payload = { - 'name': command_name, - 'format': format_string, - 'command': command, - 'user': addressee.name, - 'source_channel': addressee.room, - 'source_context': msg.envelope, - 'notification_route': env.ST2_ROUTE || 'hubot' - }; + self.robot.logger.info(self.command_factory.st2_hubot_commands.length + ' commands are loaded'); + }) + .catch(function (err) { + self.robot.logger.error(util.format('Failed to retrieve commands from "%s": %s', env.ST2_API_URL, err.message)); + if (err.status === 401 || err.message.includes('Unauthorized')) { + throw err; + } + }); +}; + +StackStorm.prototype.sendAck = function (msg, res) { + var self = this; + + var history_url = utils.getExecutionHistoryUrl(res.execution); + var history = history_url ? util.format(' (details available at %s)', history_url) : ''; + + if (res.actionalias && res.actionalias.ack) { + if (res.actionalias.ack.enabled === false) { + return; + } else if (res.actionalias.ack.append_url === false) { + history = ''; + } + } - if (utils.enable2FA(action_alias)) { - var twofactor_id = uuid.v4(); - robot.logger.debug('Requested an action that requires 2FA. Guid: ' + twofactor_id); - msg.send(TWOFACTOR_MESSAGE); - api_client.executions.create({ - 'action': env.HUBOT_2FA, - 'parameters': { - 'uuid': twofactor_id, - 'user': addressee.name, - 'channel': addressee.room, - 'hint': action_alias.description + if (res.message) { + return msg.send(res.message + history); + } + + var message = util.format(_.sample(START_MESSAGES), res.execution.id); + return msg.send(message + history); +}; + +StackStorm.prototype.sendAliasExecutionRequest = function (msg, payload) { + var self = this; + + self.robot.logger.debug('Sending command payload:', JSON.stringify(payload)); + + self.api_client.aliasExecution.create(payload) + .then(function (res) { + self.sendAck(msg, res); + }) + .catch(function (err) { + // Compatibility with older StackStorm versions + if (err.status === 200) { + return self.sendAck(msg, { execution: { id: err.message } }); + } + self.robot.logger.error('Failed to create an alias execution:', err); + var addressee = self.adapter.normalizeAddressee(msg); + var message = util.format(_.sample(ERROR_MESSAGES), err.message); + if (err.requestId) { + message = util.format( + message, + util.format('; Use request ID %s to grep st2 api logs.', err.requestId)); + } + self.adapter.postData({ + whisper: false, + user: addressee.name, + channel: addressee.room, + message: message, + extra: { + color: '#F35A00' } }); - twofactor[twofactor_id] = { - 'msg': msg, - 'payload': payload - }; - } else { - sendAliasExecutionRequest(msg, payload); - } + }); +}; + +StackStorm.prototype.executeCommand = function (msg, command_name, format_string, command, action_alias) { + var self = this; + + var addressee = self.adapter.normalizeAddressee(msg); + var payload = { + 'name': command_name, + 'format': format_string, + 'command': command, + 'user': addressee.name, + 'source_channel': addressee.room, + 'source_context': msg.envelope, + 'notification_route': env.ST2_ROUTE || 'hubot' }; - robot.respond(/([\s\S]+?)$/i, function(msg) { - var command, result; + if (utils.enable2FA(action_alias)) { + var twofactor_id = uuid.v4(); + self.robot.logger.debug('Requested an action that requires 2FA. Guid: ' + twofactor_id); + msg.send(TWOFACTOR_MESSAGE); + self.api_client.executions.create({ + 'action': self.two_factor_authorization_enabled, + 'parameters': { + 'uuid': twofactor_id, + 'user': addressee.name, + 'channel': addressee.room, + 'hint': action_alias.description + } + }); + self.twofactor[twofactor_id] = { + 'msg': msg, + 'payload': payload + }; + } else { + self.sendAliasExecutionRequest(msg, payload); + } +}; - // Normalize the command and remove special handling provided by the chat service. - // e.g. slack replace quote marks with left double quote which would break behavior. - command = adapter.normalizeCommand(msg.match[1]); +StackStorm.prototype.install_sigusr2_handler = function () { + var self = this; - result = command_factory.getMatchingCommand(command); + process.on('SIGUSR2', function() { + self.robot.logger.debug("Caught SIGUSR2, reloading commands"); + self.loadCommands(); + }); +}; - if (!result) { - // No command found - return; - } +StackStorm.prototype.start = function () { + var self = this; - var [command_name, format_string, action_alias] = result; + self.api_client.stream.listen().catch(function (err) { + self.robot.logger.error('Unable to connect to stream:', err); + }).then(function (st2stream) { + // Save the connection stream object + self.st2stream = st2stream; - executeCommand(msg, command_name, format_string, command, action_alias); - }); + self.st2stream.onerror = function (err) { + // TODO: squeeze a little bit more info out of evensource.js + self.robot.logger.warning('Stream error:', err); + if (err.status === 401) { + throw err; + } + }; + self.st2stream.addEventListener('st2.announcement__chatops', function (e) { + var data; - robot.router.post('/hubot/st2', function(req, res) { - var data; + self.robot.logger.debug('Chatops message received:', e.data); - try { - if (req.body.payload) { - data = JSON.parse(req.body.payload); + if (e.data) { + data = JSON.parse(e.data).payload; } else { - data = req.body; + data = e.data; } - adapter.postData(data); - res.send('{"status": "completed", "msg": "Message posted successfully"}'); - } catch (e) { - robot.logger.error("Unable to decode JSON: " + e); - robot.logger.error(e.stack); - res.send('{"status": "failed", "msg": "An error occurred trying to post the message: ' + e + '"}'); - } - }); - var commands_load_interval; - - function start() { - api_client.stream.listen().catch(function (err) { - robot.logger.error('Unable to connect to stream:', err); - }).then(function (st2stream) { - st2stream.onerror = function (err) { - // TODO: squeeze a little bit more info out of evensource.js - robot.logger.warning('Stream error:', err); - if (err.status === 401) { - throw err; - } - }; - st2stream.addEventListener('st2.announcement__chatops', function (e) { + self.adapter.postData(data); + }); + + if (self.two_factor_authorization_enabled) { + st2stream.addEventListener('st2.announcement__2fa', function (e) { var data; - robot.logger.debug('Chatops message received:', e.data); + self.robot.logger.debug('Successfull two-factor auth:', e.data); if (e.data) { data = JSON.parse(e.data).payload; @@ -384,78 +457,30 @@ module.exports = function(robot) { data = e.data; } - adapter.postData(data); - }); - - if (env.HUBOT_2FA) { - st2stream.addEventListener('st2.announcement__2fa', function (e) { - var data; - - robot.logger.debug('Successfull two-factor auth:', e.data); - - if (e.data) { - data = JSON.parse(e.data).payload; - } else { - data = e.data; - } - - var executionData = twofactor[data.uuid]; - sendAliasExecutionRequest(executionData.msg, executionData.payload); - delete twofactor[data.uuid]; - }); - } - }); - - // Add an interval which tries to re-load the commands - commands_load_interval = setInterval(loadCommands.bind(self), (env.ST2_COMMANDS_RELOAD_INTERVAL * 1000)); - // Initial command loading - loadCommands(); + }); + } + }); - // Install SIGUSR2 handler which reloads the command - install_sigusr2_handler(); - } + // Add an interval which tries to re-load the commands + self.commands_load_interval = setInterval(self.loadCommands.bind(self), (env.ST2_COMMANDS_RELOAD_INTERVAL * 1000)); - function stop() { - clearInterval(commands_load_interval); - api_client.stream.listen().then(function (st2stream) { - st2stream.removeAllListeners(); - st2stream.close(); - }); - } + // Initial command loading + self.loadCommands(); - function install_sigusr2_handler() { - process.on('SIGUSR2', function() { - robot.logger.debug("Caught SIGUSR2, reloading commands"); - loadCommands(); - }); - } + // Install SIGUSR2 handler which reloads the command + self.install_sigusr2_handler(); +}; - if (env.ST2_API_KEY) { - api_client.setKey({ key: env.ST2_API_KEY }); - } else if (env.ST2_AUTH_TOKEN) { - api_client.setToken({ token: env.ST2_AUTH_TOKEN }); - } +StackStorm.prototype.stop = function () { + var self = this; - if (env.ST2_API_KEY || env.ST2_AUTH_TOKEN || env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) { - // If using username and password then all are required. - if ((env.ST2_AUTH_USERNAME || env.ST2_AUTH_PASSWORD) && - !(env.ST2_AUTH_USERNAME && env.ST2_AUTH_PASSWORD && env.ST2_AUTH_URL)) { - throw new Error('Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together.'); - } - authenticated = authenticate(); + clearInterval(self.commands_load_interval); + if (self.st2stream) { + self.st2stream.removeAllListeners(); + self.st2stream.close(); } +}; - // Pending 2-factor auth commands - if (env.HUBOT_2FA) { - var twofactor = {}; - robot.logger.info('Two-factor auth is enabled'); - } - // Authenticate with StackStorm backend and then call start. - // On a failure to authenticate log the error but do not quit. - return authenticated.then(function () { - start(); - return stop; - }); -}; +module.exports = StackStorm; From 032ea823c342f32a674eb5c938a3285c9e04fa7c Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 19 Jul 2019 11:20:33 -0700 Subject: [PATCH 3/5] Update tests to account for refactored stackstorm_api.js --- test/test-st2-invalid-auth.js | 34 ++++++++++++---------------------- test/test-st2-unauthorized.js | 5 +++++ test/test-st2bot-envvars.js | 27 +++++++++++++++++++++------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/test/test-st2-invalid-auth.js b/test/test-st2-invalid-auth.js index fd9a239..e456df8 100644 --- a/test/test-st2-invalid-auth.js +++ b/test/test-st2-invalid-auth.js @@ -51,6 +51,7 @@ describe("invalid st2 credential configuration", function() { // Remove stackstorm.js from the require cache // https://medium.com/@gattermeier/invalidate-node-js-require-cache-c2989af8f8b0 delete require.cache[require.resolve("../src/stackstorm.js")]; + delete require.cache[require.resolve("../src/stackstorm_api.js")]; }); it("should error out with missing auth URL", function(done) { @@ -63,13 +64,8 @@ describe("invalid st2 credential configuration", function() { // Load script under test var stackstorm = require("../src/stackstorm.js"); - try { - stackstorm(robot); - done(new Error("The previous code should have thrown an exception")) - } catch (err) { - expect(err.message).to.equal("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); - done(); - } + expect(stackstorm.bind(this, robot)).to.throw("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); + done(); }); it("should error out with missing auth username", function(done) { @@ -82,13 +78,8 @@ describe("invalid st2 credential configuration", function() { // Load script under test var stackstorm = require("../src/stackstorm.js"); - try { - stackstorm(robot); - done(new Error("The previous code should have thrown an exception")) - } catch (err) { - expect(err.message).to.equal("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); - done(); - } + expect(stackstorm.bind(this, robot)).to.throw("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); + done(); }); @@ -102,16 +93,13 @@ describe("invalid st2 credential configuration", function() { // Load script under test var stackstorm = require("../src/stackstorm.js"); - try { - stackstorm(robot); - done(new Error("The previous code should have thrown an exception")) - } catch (err) { - expect(err.message).to.equal("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); - done(); - } + expect(stackstorm.bind(this, robot)).to.throw("Env variables ST2_AUTH_USERNAME, ST2_AUTH_PASSWORD and ST2_AUTH_URL should only be used together."); + done(); }); it("should throw exception with bad auth URL", function(done) { + // Mock process.env for all modules + // https://glebbahmutov.com/blog/mocking-process-env/ restore_env = mockedEnv({ ST2_AUTH_URL: 'https://nonexistent-st2-auth-url:9101', ST2_AUTH_USERNAME: 'nonexistent-st2-auth-username', @@ -120,7 +108,9 @@ describe("invalid st2 credential configuration", function() { // Load script under test var i, stackstorm = require("../src/stackstorm.js"); - stackstorm(robot).catch(function (err) { + stackstorm(robot).then(function (result) { + done(new Error("The previous code should have thrown an exception")); + }).catch(function (err) { expect(error_spy.args).to.have.lengthOf(1); expect(error_spy.args[0][0]).to.be.a('string'); expect(error_spy.args[0][0]).to.startWith('Failed to authenticate'); diff --git a/test/test-st2-unauthorized.js b/test/test-st2-unauthorized.js index 85a4284..e355089 100644 --- a/test/test-st2-unauthorized.js +++ b/test/test-st2-unauthorized.js @@ -61,6 +61,10 @@ describe("auth with invalid st2 API key", function() { //done(); }); + // Remove stackstorm.js from the require cache + // https://medium.com/@gattermeier/invalidate-node-js-require-cache-c2989af8f8b0 + delete require.cache[require.resolve("../src/stackstorm.js")]; + delete require.cache[require.resolve("../src/stackstorm_api.js")]; var stackstorm = require("../src/stackstorm.js"); stackstorm(robot).then(function (result) { stop = result; @@ -76,6 +80,7 @@ describe("auth with invalid st2 API key", function() { // Remove stackstorm.js from the require cache // https://medium.com/@gattermeier/invalidate-node-js-require-cache-c2989af8f8b0 delete require.cache[require.resolve("../src/stackstorm.js")]; + delete require.cache[require.resolve("../src/stackstorm_api.js")]; }); // CAUTION: These tests are brittle - do not move them around, remove diff --git a/test/test-st2bot-envvars.js b/test/test-st2bot-envvars.js index c2f258f..9617450 100644 --- a/test/test-st2bot-envvars.js +++ b/test/test-st2bot-envvars.js @@ -33,16 +33,26 @@ describe("environment variable configuration", function () { var restore_env = null, debug_spy = sinon.spy(robot.logger, 'debug'), info_spy = sinon.spy(robot.logger, 'info'), + warning_spy = sinon.spy(robot.logger, 'warning'), error_spy = sinon.spy(robot.logger, 'error'); + before(function() { + // Remove stackstorm.js from the require cache + // https://medium.com/@gattermeier/invalidate-node-js-require-cache-c2989af8f8b0 + delete require.cache[require.resolve("../src/stackstorm.js")]; + delete require.cache[require.resolve("../src/stackstorm_api.js")]; + }); + afterEach(function() { restore_env && restore_env(); error_spy.resetHistory(); + warning_spy.resetHistory(); info_spy.resetHistory(); debug_spy.resetHistory(); // Remove stackstorm.js from the require cache // https://medium.com/@gattermeier/invalidate-node-js-require-cache-c2989af8f8b0 delete require.cache[require.resolve("../src/stackstorm.js")]; + delete require.cache[require.resolve("../src/stackstorm_api.js")]; if (robot) { robot.shutdown(); if (robot.server) { @@ -55,14 +65,17 @@ describe("environment variable configuration", function () { // Mock process.env for all modules // https://glebbahmutov.com/blog/mocking-process-env/ restore_env = mockedEnv({ - ST2_API: 'https://nonexistent-st2-auth-url:9101' + ST2_API: 'https://nonexistent-st2-auth-url:9101', + ST2_AUTH_URL: 'localhost:8000', + ST2_AUTH_USERNAME: 'user', + ST2_AUTH_PASSWORD: 'pass' }); // Load script under test var stackstorm = require("../src/stackstorm.js"); stackstorm(robot).then(function (stop) { - expect(robot.logger.logs.warning).to.have.length.above(0); - expect(robot.logger.logs.warning).to.contain( + expect(warning_spy.args).length.to.be.above(0); + expect(warning_spy).to.have.been.calledWith( 'ST2_API is deprecated and will be removed in a future releases. Instead, please use the '+ 'ST2_API_URL environment variable.'); @@ -70,7 +83,7 @@ describe("environment variable configuration", function () { done(); }).catch(function (err) { - console.log(err); + console.error(err); done(err); }); }); @@ -79,6 +92,9 @@ describe("environment variable configuration", function () { // Mock process.env for all modules // https://glebbahmutov.com/blog/mocking-process-env/ restore_env = mockedEnv({ + ST2_AUTH_URL: 'localhost:8000', + ST2_AUTH_USERNAME: 'user', + ST2_AUTH_PASSWORD: 'pass', HUBOT_2FA: 'true' }); @@ -87,13 +103,12 @@ describe("environment variable configuration", function () { stackstorm(robot).then(function (stop) { expect(info_spy.args).length.to.be.above(1); expect(info_spy).to.have.been.calledWith('Two-factor auth is enabled'); - expect(info_spy).to.have.been.calledWith('Loading commands....'); stop(); done(); }).catch(function (err) { - console.log(err); + console.error(err); done(err); }); }); From 2c4ceed845a6b3adaa42abc080a7eaa7c32cc53c Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 19 Jul 2019 12:48:18 -0700 Subject: [PATCH 4/5] [skip ci] Add a line to the changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 590faca..b814a46 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ in development * Small refactor and more tests (for ``scripts/stackstorm.js``) (improvement) * Refactor chat providers into their own modules (improvement) * Modernize directory structure to be more consistent with other hubot plugins (improvement) +* Split out the functionality of ``src/stackstorm.js`` into ``stackstorm_api.js`` and refactor it be a JS old style class with a wrapper (improvement) 0.9.6 ----- From 020daf36e6d32b7b3ef9bb336848a21fd7e56849 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 19 Jul 2019 16:15:55 -0700 Subject: [PATCH 5/5] Revert changes to stop() semantics --- src/stackstorm_api.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stackstorm_api.js b/src/stackstorm_api.js index fbe55da..8dda627 100644 --- a/src/stackstorm_api.js +++ b/src/stackstorm_api.js @@ -476,10 +476,10 @@ StackStorm.prototype.stop = function () { var self = this; clearInterval(self.commands_load_interval); - if (self.st2stream) { - self.st2stream.removeAllListeners(); - self.st2stream.close(); - } + self.api_client.stream.listen().then(function (second_st2stream) { + second_st2stream.removeAllListeners(); + second_st2stream.close(); + }); };