diff --git a/.travis.yml b/.travis.yml index c85c567a..68276368 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ before_script: - git config credential.helper "store --file=.git/credentials" - echo "https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com" > .git/credentials script: -- npm run lint - npm test after_script: - npm run buildDocs && npm run pushDocs diff --git a/docs/Development/plugin creation.md b/docs/Development/plugin creation.md index 1ee6e9be..b15835e4 100644 --- a/docs/Development/plugin creation.md +++ b/docs/Development/plugin creation.md @@ -1,78 +1,3 @@ # Plugin Creation -SockBot incorporates a plugin architecture to add functionality, some plugins are built in but any number of -other plugins can be created and loaded into SockBot to add additional functionality. - -## Plugin Format - -SockBot plugins are node modules that export a certain set of functions. A minimal set would be: -``` -exports.prepare = function prepare(pluginConfig, sockBotConfig, events, browser) {}; - -exports.start = function start() {}; - -exports.stop = function stop() {}; -``` - -Plugins are activated by having their require path included as a key in the configuration `plugins` key. -Bundled plugins and plugins installed via NPM may omit the path as node.js will find them correctly on the -bare name, other plugins should be specified by absolute path. - -### Function `prepare(pluginConfig, sockBotConfig, events, browser)` - -This function is the initial entry point for plugins, it is called when SockBot has read the configuration -for the bot and before it has logged in to discourse. The provided parameters give the plugin the configuration -state of SockBot as well as the [EventEmitter] used for communication within the bot, and a [browser] for -communicating with discourse. - -This function is assumed to be synchronous and *should not* set up any periodic or delayed actions. Any such -actions desired should be initiated in `start()`. - -#### Parameter `pluginConfig` - -This parameter will be the value that was stored in the configuration file for the plugin. The format of this -object is to be determined by each individual plugin, however all plugins *should* accept the value `true` -to use the plugin with the plugins default configuration. - -#### Parameter `sockBotConfig` - -This parameter will be the complete [configuration] object for SockBot, including core and plugin -configuration. Once SockBot has logged into discourse this object will be populated with current user -information. - -#### Parameter `events` - -This parameter will be an [EventEmitter], augmented with several methods specific to SockBot. Events -originating within Discourse will be emitted by this object, allowing plugins to respond to Discourse -notifications and other events. - -#### Parameter `browser` - -This parameter will be a [browser] that is set up to communicate with Discourse. At the time of the -prepare() call the browser will not have yet authenticated with discourse. - -[EventEmitter]: ../api/external/events.md#module_SockEvents -[browser]: ../api/lib/browser.md#module_browser -[configuration]: ../api/lib/config.md - -### Function `start()` - -This function is called after SockBot has successfully authenticated to Discourse. At the point of this call -the current user information has been stored in the `sockBotConfig` provided earlier in the `prepare()` call. -Additionally the earlier provided browser object has been authenticated with discourse and is fully ready. - -Any periodic or automatic actions *should* be initiated by this function. These periodic or automatic actions -*must* be cancellable by a call to `stop()`. - -No arguments are provided when this function is called. - -### Function `stop()` - -This function is called before SockBot stops, either for a configuration reload or for termination. Any -periodic or deferred actions created by the plugin *must* be canceled by this function. The plugin *must* -assume that the bot is stopping for termination. - -If the bot is merely reloading configuration the new configuration will be communicated to the plugin by a -call to `prepare()` followed by a call to `start()` when the new configuration is ready to be used. - -No arguments are provided when this function is called. +Coming soon: Plugin creation instructions for Sockbot 3.0! \ No newline at end of file diff --git a/docs/api/lib/app.md b/docs/api/lib/app.md index 51d1a0e3..b475ddde 100644 --- a/docs/api/lib/app.md +++ b/docs/api/lib/app.md @@ -1,7 +1,7 @@ ## Functions
-
_buildMessage(...message)string
+
_buildMessage(args)string

Construct a stringified message to log

log(...message)
@@ -23,7 +23,7 @@ -## _buildMessage(...message) ⇒ string +## _buildMessage(args) ⇒ string Construct a stringified message to log **Kind**: global function @@ -31,7 +31,7 @@ Construct a stringified message to log | Param | Type | Description | | --- | --- | --- | -| ...message | \* | Item to stringify and log | +| args | Array.<\*> | Item to stringify and log | diff --git a/docs/api/plugins/summoner.md b/docs/api/plugins/summoner.md index 9f74a2c2..3d7161a3 100644 --- a/docs/api/plugins/summoner.md +++ b/docs/api/plugins/summoner.md @@ -7,14 +7,14 @@ Example plugin, replies to mentions with random quips. **License**: MIT * [summoner](#module_summoner) - * [module.exports(forum)](#exp_module_summoner--module.exports) ⇒ Plugin ⏏ + * [module.exports(forum, config)](#exp_module_summoner--module.exports) ⇒ Plugin ⏏ * [~handler(notification)](#module_summoner--module.exports..handler) ⇒ Promise * [~activate()](#module_summoner--module.exports..activate) * [~deactivate()](#module_summoner--module.exports..deactivate) -### module.exports(forum) ⇒ Plugin ⏏ +### module.exports(forum, config) ⇒ Plugin ⏏ Plugin generation function. Returns a plugin object bound to the provided forum provider @@ -25,6 +25,7 @@ Returns a plugin object bound to the provided forum provider | Param | Type | Description | | --- | --- | --- | | forum | Provider | Active forum Provider | +| config | object | Array | Plugin configuration | diff --git a/lib/app.js b/lib/app.js index 7d6bf711..63e63a20 100644 --- a/lib/app.js +++ b/lib/app.js @@ -13,13 +13,18 @@ const debug = require('debug')('sockbot'); /** * Construct a stringified message to log * - * @param {...*} message Item to stringify and log + * @param {Array<*>} args Item to stringify and log * @returns {string} stringified message */ -exports._buildMessage = function _buildMessage() { - const parts = Array.prototype.slice.call(arguments); - parts.unshift(`[${new Date().toISOString()}]`); - return parts +exports._buildMessage = function _buildMessage(args) { + if (!args || args.length < 1) { + return ''; + } + if (!Array.isArray(args)) { + args = Array.prototype.slice.apply(args); + } + args.unshift(`[${new Date().toISOString()}]`); + return args .map((part) => typeof part === 'string' ? part : JSON.stringify(part, null, '\t')) .join(' '); }; @@ -30,7 +35,7 @@ exports._buildMessage = function _buildMessage() { * @param {...*} message Message to log to stdout */ exports.log = function log() { - console.log(exports._buildMessage.apply(null, arguments)); // eslint-disable-line no-console + console.log(exports._buildMessage(arguments)); // eslint-disable-line no-console }; /** @@ -39,7 +44,7 @@ exports.log = function log() { * @param {...*} message Message to log to stderr */ exports.error = function error() { - console.error(exports._buildMessage.apply(null, arguments)); // eslint-disable-line no-console + console.error(exports._buildMessage(arguments)); // eslint-disable-line no-console }; /** @@ -75,14 +80,18 @@ exports.relativeRequire = function relativeRequire(relativePath, module, require * * @param {Provider} forumInstance Provider instance to load plugins into * @param {object} botConfig Bot configuration to load plugins with + * @returns {Promise} Resolves when plugins have been loaded */ exports.loadPlugins = function loadPlugins(forumInstance, botConfig) { - Object.keys(botConfig.plugins).map((name) => { + return Promise.all(Object.keys(botConfig.plugins).map((name) => { exports.log(`Loading plugin ${name} for ${botConfig.core.username}`); const plugin = exports.relativeRequire('plugins', name, require); const pluginConfig = botConfig.plugins[name]; - forumInstance.addPlugin(plugin, pluginConfig); - }); + return forumInstance.addPlugin(plugin, pluginConfig).catch((err) => { + exports.error(`Plugin ${name} failed to load with error: ${err}`); + throw err; + }); + })); }; /** @@ -99,9 +108,11 @@ exports.activateConfig = function activateConfig(botConfig) { instance.on('error', exports.error); instance.on('logExtended', utils.logExtended); instance.Commands = commands.bindCommands(instance); - exports.loadPlugins(instance, botConfig); - exports.log(`${botConfig.core.username} ready for login`); - return instance.login() + return exports.loadPlugins(instance, botConfig) + .then(() => { + exports.log(`${botConfig.core.username} ready for login`); + }) + .then(() => instance.login()) .then(() => { exports.log(`${botConfig.core.username} login successful`); return instance.activate(); diff --git a/lib/config.js b/lib/config.js index a5bdb9c6..ba4c9306 100644 --- a/lib/config.js +++ b/lib/config.js @@ -119,8 +119,8 @@ exports.load = function load(filePath) { }) .then((data) => data.map((cfg) => { exports.basePath = path.posix.resolve(filePath, '..'); + exports.validateConfig(cfg); const config = utils.mergeObjects(true, defaultConfig, cfg); - exports.validateConfig(config); return config; })); }; @@ -133,14 +133,25 @@ exports.load = function load(filePath) { */ exports.validateConfig = function validateConfig(config) { const errors = []; - const checkIt = (key) => { + const checkSection = (key) => { + if (typeof config[key] !== 'object') { + errors.push(`Missing configuration section: ${key}`); + } else if (Object.keys(config[key]).length === 0) { + errors.push(`Configuration section ${key} has no configuration items`); + } + }; + const checkCore = (key) => { if (typeof config.core[key] !== 'string' || config.core[key].length < 1) { errors.push(`A valid ${key} must be specified`); } }; - checkIt('username'); - checkIt('password'); - checkIt('owner'); + checkSection('core'); + checkSection('plugins'); + if (errors.length === 0) { + checkCore('username'); + checkCore('password'); + checkCore('owner'); + } if (errors.length > 0) { throw new Error(errors.join('\n')); } diff --git a/package.json b/package.json index d36bd0a6..d6daec13 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sockbot", - "version": "3.0.0", - "releaseName": "Alpha Alpaca", + "version": "3.1.0-RC1", + "releaseName": "Beta Badger", "description": "A sockpuppet bot to use on http://what.thedailywtf.com.", "repository": "https://github.com/SockDrawer/SockBot", "license": "MIT", @@ -47,7 +47,9 @@ "scripts": { "start": "node lib/app.js", "lint": "eslint .", - "pretest": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R spec", + "preistanbulspec": "npm run lint", + "istanbulspec": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R spec", + "pretest": "npm run istanbulspec", "test": "istanbul check-coverage coverage/coverage.json", "istanbul": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R dot", "mocha": "mocha --recursive -R dot", diff --git a/providers/nodebb/category.js b/providers/nodebb/category.js index 9e8e0ae7..5820618e 100644 --- a/providers/nodebb/category.js +++ b/providers/nodebb/category.js @@ -45,7 +45,7 @@ exports.bindCategory = function bindCategory(forum) { * This constructor is intended for private use only, if you need top construct a category from payload data use * `Category.parse()` instead. * - * @private + * @public * @class * * @param {*} payload Payload to construct the Category object out of diff --git a/providers/nodebb/index.js b/providers/nodebb/index.js index 346a45bd..cad04cd1 100644 --- a/providers/nodebb/index.js +++ b/providers/nodebb/index.js @@ -190,11 +190,15 @@ class Forum extends EventEmitter { remember: 'off', returnTo: this.url } - }, (loginError) => { + }, (loginError, response, reason) => { if (loginError) { debug(`Login failed for reason: ${loginError}`); return reject(loginError); } + if (response.statusCode >= 400) { + debug(`Login failed for reason: ${reason}`); + return reject(reason); + } debug('complete post login data'); return resolve(); }); diff --git a/providers/nodebb/notification.js b/providers/nodebb/notification.js index 5c6a8958..908cd8ba 100644 --- a/providers/nodebb/notification.js +++ b/providers/nodebb/notification.js @@ -16,6 +16,9 @@ const utils = require('../../lib/utils'); * @returns {Notification} A Notification class bound to the provided `forum` instance */ exports.bindNotification = function bindNotification(forum) { + + const mentionTester = new RegExp(`(^|\\s)@${forum.username}(\\s|$)`, 'i'); + /** * Notification types enum * @@ -35,18 +38,23 @@ exports.bindNotification = function bindNotification(forum) { * This constructor is intended to be private use only, if you need to construct a notification from payload * data use `Notification.parse()` instead * - * @private + * @public * @class * * @param {*} payload Payload to construct the Notification object out of */ constructor(payload) { payload = utils.parseJSON(payload); + const body = string(payload.bodyLong || '').unescapeHTML().s; let type = 'notification'; if (/^\[\[notifications:user_posted_to/i.test(payload.bodyShort)) { type = 'reply'; } else if (/^\[\[mentions:user_mentioned_you_in/i.test(payload.bodyShort)) { - type = 'mention'; + if (mentionTester.test(body)) { + type = 'mention'; + } else { + type = 'group_mention'; + } } const subtype = (/^\[\[\w+:(\w+)/.exec(payload.bodyShort) || [])[1] || ''; @@ -55,7 +63,7 @@ exports.bindNotification = function bindNotification(forum) { type: type, subtype: subtype, label: payload.bodyShort, - body: string(payload.bodyLong || '').unescapeHTML().s, + body: body, id: payload.nid, postId: payload.pid, topicId: payload.tid, diff --git a/providers/nodebb/post.js b/providers/nodebb/post.js index 7089901b..c945af22 100644 --- a/providers/nodebb/post.js +++ b/providers/nodebb/post.js @@ -22,7 +22,7 @@ exports.bindPost = function bindPost(forum) { * This constructor is intended to be private use only, if you need to construct a post from payload data use * `User.parse()` instead * - * @private + * @public * @class * * @param {*} payload Payload to construct the Post object out of diff --git a/providers/nodebb/topic.js b/providers/nodebb/topic.js index 419d6494..d30a28be 100644 --- a/providers/nodebb/topic.js +++ b/providers/nodebb/topic.js @@ -21,7 +21,7 @@ exports.bindTopic = function bindTopic(forum) { * This constructor is intended for private use only, if you need top construct a topic from payload data use * `Topic.parse()` instead. * - * @private + * @public * @class * * @param {*} payload Payload to construct the User object out of diff --git a/providers/nodebb/user.js b/providers/nodebb/user.js index 107d2876..2c80fb6f 100644 --- a/providers/nodebb/user.js +++ b/providers/nodebb/user.js @@ -23,7 +23,7 @@ exports.bindUser = function bindUser(forum) { * This constructor is intended to be private use only, if you need to construct a user from payload data use * `User.parse()` instead * - * @private + * @public * @class * * @param {*} payload Payload to construct the User object out of @@ -184,7 +184,6 @@ exports.bindUser = function bindUser(forum) { * @reject {Error} An Error that occured while processing */ follow() { - debug(`following user ${this.id}`); return forum._emit('user.follow', { uid: this.id }).then(() => this); @@ -202,7 +201,6 @@ exports.bindUser = function bindUser(forum) { * @reject {Error} An Error that occured while processing */ unfollow() { - debug(`unfollowing user ${this.id}`); return forum._emit('user.unfollow', { uid: this.id }).then(() => this); diff --git a/test/lib/appTest.js b/test/lib/appTest.js index eb5d8790..a088b6aa 100644 --- a/test/lib/appTest.js +++ b/test/lib/appTest.js @@ -63,16 +63,18 @@ describe('lib/app', () => { sandbox = sinon.sandbox.create(); sandbox.stub(testModule, 'relativeRequire'); sandbox.stub(testModule, 'log'); + sandbox.stub(testModule, 'error'); forum = { - addPlugin: sinon.stub() + addPlugin: sinon.stub().resolves() }; }); afterEach(() => sandbox.restore()); it('should allow zero plugins', () => { - testModule.loadPlugins(forum, { + return testModule.loadPlugins(forum, { plugins: {} + }).then(() => { + testModule.relativeRequire.called.should.be.false; }); - testModule.relativeRequire.called.should.be.false; }); it('should load listed plugins', () => { const name = `name${Math.random()}`; @@ -81,8 +83,9 @@ describe('lib/app', () => { plugins: {} }; cfg.plugins[name] = true; - testModule.loadPlugins(forum, cfg); - testModule.relativeRequire.calledWith('plugins', name, testModule.require).should.be.true; + return testModule.loadPlugins(forum, cfg).then(() => { + testModule.relativeRequire.calledWith('plugins', name, testModule.require).should.be.true; + }); }); it('should log message on plugin load', () => { const name = `name${Math.random()}`; @@ -94,20 +97,62 @@ describe('lib/app', () => { plugins: {} }; cfg.plugins[name] = true; - testModule.loadPlugins(forum, cfg); - testModule.log.calledWith(`Loading plugin ${name} for ${username}`).should.equal.true; + return testModule.loadPlugins(forum, cfg).then(() => { + testModule.log.calledWith(`Loading plugin ${name} for ${username}`).should.equal.true; + }); }); it('should add loaded plugin to forum', () => { const cfg = Math.random(); const plugin = Math.random(); testModule.relativeRequire.returns(plugin); - testModule.loadPlugins(forum, { + return testModule.loadPlugins(forum, { + core: {}, + plugins: { + alpha: cfg + } + }).then(() => { + forum.addPlugin.calledWith(plugin, cfg).should.be.true; + }); + }); + it('should reject when forum.addPlugin rejects', () => { + const cfg = Math.random(); + const plugin = Math.random(); + testModule.relativeRequire.returns(plugin); + forum.addPlugin.rejects('bad'); + return testModule.loadPlugins(forum, { + core: {}, + plugins: { + alpha: cfg + } + }).should.be.rejected; + }); + it('should log error when forum.addPlugin rejects', () => { + const cfg = Math.random(); + const plugin = Math.random(); + testModule.relativeRequire.returns(plugin); + forum.addPlugin.rejects('bad'); + return testModule.loadPlugins(forum, { core: {}, plugins: { alpha: cfg } + }).catch(() => { + testModule.error.calledWith('Plugin alpha failed to load with error: Error: bad').should.be.true; }); - forum.addPlugin.calledWith(plugin, cfg).should.be.true; + }); + + it('should reject with error from forum.addPlugin when rejecting', () => { + const cfg = Math.random(); + const plugin = Math.random(); + testModule.relativeRequire.returns(plugin); + const error = new Error('boogy boo!'); + forum.addPlugin.rejects(error); + return testModule.loadPlugins(forum, { + core: {}, + plugins: { + alpha: cfg + } + }).should.be.rejectedWith(error); }); }); describe('activateConfig()', () => { @@ -127,7 +172,7 @@ describe('lib/app', () => { sandbox = sinon.sandbox.create(); sandbox.stub(testModule, 'relativeRequire'); testModule.relativeRequire.returns(DummyForum); - sandbox.stub(testModule, 'loadPlugins'); + sandbox.stub(testModule, 'loadPlugins').resolves(); sandbox.stub(testModule, 'log'); sandbox.stub(commands, 'bindCommands'); basicConfig = { @@ -179,6 +224,10 @@ describe('lib/app', () => { instance.activate.called.should.be.true; }); }); + it('should reject when exports.loadPlugins rejects', () => { + testModule.loadPlugins.rejects('bad'); + return testModule.activateConfig(basicConfig).should.be.rejected; + }); it('should reject when insatnce.login rejects', () => { DummyForum.login.rejects('bad'); return testModule.activateConfig(basicConfig).should.be.rejected; @@ -268,17 +317,26 @@ describe('lib/app', () => { }); it('should prefix timestamp to message', () => { const prefix = `[${new Date(theTime).toISOString()}]`; - testModule._buildMessage('foo').should.startWith(prefix); + testModule._buildMessage(['foo']).should.startWith(prefix); }); it('should join multiple arguments together', () => { const contents = 'foo bar baz quux'; - testModule._buildMessage('foo', 'bar', 'baz', 'quux').should.endWith(contents); + testModule._buildMessage(['foo', 'bar', 'baz', 'quux']).should.endWith(contents); + }); + + it('should accept `arguments` object as input', () => { + let args = null; + (function argExtractor() { + args = arguments; + })('foo', 'bar', 'baz', 'xyzzy'); + const contents = 'foo bar baz xyzzy'; + testModule._buildMessage(args).should.endWith(contents); }); it('should serialize objects to JSON', () => { const contents = '{\n\t"alpha": "one"\n}'; - testModule._buildMessage({ + testModule._buildMessage([{ alpha: 'one' - }).should.endWith(contents); + }]).should.endWith(contents); }); }); describe('log()', () => { @@ -296,7 +354,8 @@ describe('lib/app', () => { four = 3, five = 5; testModule.log(one, two, three, four, five); - testModule._buildMessage.calledWith(one, two, three, four, five).should.be.true; + const args = Array.prototype.slice.apply(testModule._buildMessage.firstCall.args[0]); + args.should.eql([one, two, three, four, five]); }); it('should log message to console.log', () => { const message = `a${Math.random()}b`; @@ -320,7 +379,8 @@ describe('lib/app', () => { four = 3, five = 5; testModule.error(one, two, three, four, five); - testModule._buildMessage.calledWith(one, two, three, four, five).should.be.true; + const args = Array.prototype.slice.apply(testModule._buildMessage.firstCall.args[0]); + args.should.eql([one, two, three, four, five]); }); it('should log message to console.log', () => { const message = `a${Math.random()}b`; diff --git a/test/lib/configTest.js b/test/lib/configTest.js index f496a021..68e0388e 100644 --- a/test/lib/configTest.js +++ b/test/lib/configTest.js @@ -106,77 +106,107 @@ describe('lib/config', () => { }); describe('validateConfig()', () => { - it('should throw error for missing username', () => { - expect(() => config.validateConfig({ + let testConfig = null; + beforeEach(() => { + testConfig = { core: { - username: undefined, + username: 'username', password: 'password', owner: 'owner' + }, + plugins: { + foo: true } - })).to.throw('A valid username must be specified'); + }; + }); + it('should throw error for missing core configuration', () => { + delete testConfig.core; + expect(() => config.validateConfig(testConfig)) + .to.throw('Missing configuration section: core'); + }); + it('should throw error for empty core configuration', () => { + testConfig.core = {}; + expect(() => config.validateConfig(testConfig)) + .to.throw('Configuration section core has no configuration items'); + }); + it('should throw error for missing plugins configuration', () => { + delete testConfig.plugins; + expect(() => config.validateConfig(testConfig)) + .to.throw('Missing configuration section: plugins'); + }); + it('should throw error for empty plugins configuration', () => { + testConfig.plugins = {}; + expect(() => config.validateConfig(testConfig)) + .to.throw('Configuration section plugins has no configuration items'); + }); + it('should throw error for missing username', () => { + delete testConfig.core.username; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid username must be specified'); }); it('should throw error for empty username', () => { - expect(() => config.validateConfig({ - core: { - username: '', - password: 'password', - owner: 'owner' - } - })).to.throw('A valid username must be specified'); + testConfig.core.username = ''; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid username must be specified'); + }); + it('should throw error for wrong username type', () => { + testConfig.core.username = Math.random(); + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid username must be specified'); }); it('should throw error for missing password', () => { - expect(() => config.validateConfig({ - core: { - username: 'username', - password: undefined, - owner: 'owner' - } - })).to.throw('A valid password must be specified'); + delete testConfig.core.password; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid password must be specified'); }); it('should throw error for empty password', () => { - expect(() => config.validateConfig({ - core: { - username: 'username', - password: '', - owner: 'owner' - } - })).to.throw('A valid password must be specified'); + testConfig.core.password = ''; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid password must be specified'); + }); + it('should throw error for wrong password type', () => { + testConfig.core.password = Math.random(); + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid password must be specified'); }); it('should throw error for missing owner', () => { - expect(() => config.validateConfig({ - core: { - username: 'username', - password: 'password', - owner: undefined - } - })).to.throw('A valid owner must be specified'); + delete testConfig.core.owner; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid owner must be specified'); }); it('should throw error for empty owner', () => { - expect(() => config.validateConfig({ - core: { - username: 'username', - password: 'password', - owner: '' - } - })).to.throw('A valid owner must be specified'); + testConfig.core.owner = ''; + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid owner must be specified'); }); - it('should throw error for missing username', () => { - expect(() => config.validateConfig({ - core: { - username: undefined, - password: 'password', - owner: 'owner' - } - })).to.throw('A valid username must be specified'); + it('should throw error for wrong owner type', () => { + testConfig.core.owner = Math.random(); + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid owner must be specified'); + }); + it('should throw multiple messages for multiple errors', () => { + testConfig.core.username = Math.random(); + testConfig.core.password = Math.random(); + testConfig.core.owner = Math.random(); + expect(() => config.validateConfig(testConfig)) + .to.throw('A valid username must be specified\n' + + 'A valid password must be specified\nA valid owner must be specified'); + }); + it('should throw multiple messages for multiple missing sections', () => { + delete testConfig.core; + testConfig.plugins = {}; + expect(() => config.validateConfig(testConfig)) + .to.throw('Missing configuration section: core\n' + + 'Configuration section plugins has no configuration items'); + }); + it('should not validate core settings with invalid config section', () => { + delete testConfig.core.username; + testConfig.plugins = {}; + expect(() => config.validateConfig(testConfig)) + .to.throw('Configuration section plugins has no configuration items'); }); it('should accept valid config', () => { - expect(() => config.validateConfig({ - core: { - username: 'username', - password: 'password', - owner: 'owner' - } - })).to.not.throw(); + expect(() => config.validateConfig(testConfig)).to.not.throw(); }); }); describe('load()', () => { diff --git a/test/providers/nodebb/indexTest.js b/test/providers/nodebb/indexTest.js index b5657a5c..1da69c3a 100644 --- a/test/providers/nodebb/indexTest.js +++ b/test/providers/nodebb/indexTest.js @@ -285,7 +285,9 @@ describe('providers/nodebb', () => { sandbox = sinon.sandbox.create(); sandbox.stub(forum, '_verifyCookies'); sandbox.stub(forum, '_getConfig').resolves({}); - sandbox.stub(request, 'post').yields(null, null, '""'); + sandbox.stub(request, 'post').yields(null, { + statusCode: 200 + }, '""'); }); afterEach(() => sandbox.restore()); it('should retrieve config via _getConfig', () => { @@ -360,6 +362,12 @@ describe('providers/nodebb', () => { request.post.yields('bad'); return forum.login().should.be.rejected; }); + it('should reject when request.post yields 4xx status code', () => { + request.post.yields(null, { + statusCode: 403 + }, 'bad'); + return forum.login().should.be.rejected; + }); }); describe('connectWebsocket()', () => { let forum = null, diff --git a/test/providers/nodebb/notificationTest.js b/test/providers/nodebb/notificationTest.js index 0e9300b9..cefb45c0 100644 --- a/test/providers/nodebb/notificationTest.js +++ b/test/providers/nodebb/notificationTest.js @@ -20,7 +20,9 @@ describe('providers/nodebb/notification', () => { testModule.bindNotification({}).should.be.a('function'); }); describe('Post', () => { - const forum = {}; + const forum = { + username: `username${Math.random()}` + }; const Notification = testModule.bindNotification(forum); beforeEach(() => { forum._emit = sinon.stub().resolves(); @@ -91,16 +93,32 @@ describe('providers/nodebb/notification', () => { }); it('should switch to `mention` for mentions', () => { const notification = new Notification({ - bodyShort: '[[mentions:user_mentioned_you_in' + bodyShort: '[[mentions:user_mentioned_you_in', + bodyLong: `this is for @${forum.username} ` }); utils.mapGet(notification, 'type').should.equal('mention'); }); it('should switch to `mention` case insentitively', () => { const notification = new Notification({ - bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN' + bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN', + bodyLong: `this is for @${forum.username}` }); utils.mapGet(notification, 'type').should.equal('mention'); }); + it('should switch to `group_mention` for group mentions', () => { + const notification = new Notification({ + bodyShort: '[[mentions:user_mentioned_you_in', + bodyLong: 'this is for @bots' + }); + utils.mapGet(notification, 'type').should.equal('group_mention'); + }); + it('should switch to `group_mention` case insentitively', () => { + const notification = new Notification({ + bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN', + bodyLong: 'this is for @bots ' + }); + utils.mapGet(notification, 'type').should.equal('group_mention'); + }); }); describe('subtype', () => { it('should default to the empty string', () => { @@ -356,17 +374,33 @@ describe('providers/nodebb/notification', () => { utils.mapGet(notification, 'type').should.equal('reply'); }); it('should switch to `mention` for mentions', () => { - const notification = Notification.parse({ - bodyShort: '[[mentions:user_mentioned_you_in' + const notification = new Notification({ + bodyShort: '[[mentions:user_mentioned_you_in', + bodyLong: `this is for @${forum.username} ` }); utils.mapGet(notification, 'type').should.equal('mention'); }); it('should switch to `mention` case insentitively', () => { - const notification = Notification.parse({ - bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN' + const notification = new Notification({ + bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN', + bodyLong: `this is for @${forum.username}` }); utils.mapGet(notification, 'type').should.equal('mention'); }); + it('should switch to `group_mention` for group mentions', () => { + const notification = new Notification({ + bodyShort: '[[mentions:user_mentioned_you_in', + bodyLong: 'this is for @bots' + }); + utils.mapGet(notification, 'type').should.equal('group_mention'); + }); + it('should switch to `groupmention` case insentitively', () => { + const notification = new Notification({ + bodyShort: '[[mEnTiOnS:UsEr_mEnTiOnEd_yOu_iN', + bodyLong: 'this is for @bots ' + }); + utils.mapGet(notification, 'type').should.equal('group_mention'); + }); }); describe('subtype', () => { it('should default to the empty string', () => {