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
string
string
Construct a stringified message to log
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', () => {