+
{lang.t('signIn.signUp')}
)}
diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js
index c201a3863..28dc021d2 100644
--- a/client/coral-sign-in/containers/SignInContainer.js
+++ b/client/coral-sign-in/containers/SignInContainer.js
@@ -132,7 +132,9 @@ class SignInContainer extends Component {
const {auth, showSignInDialog, noButton} = this.props;
return (
- {!noButton && Sign in to comment }
+ {!noButton &&
+ Sign in to comment
+ }
{
+
+ // Get the user id's from the author id's as a unique array that gets
+ // sorted.
+ let userIDs = _.uniq(comments.map((comment) => comment.author_id)).sort();
+
+ // Fetch the actions for pretty much everything at this point.
+ return Action.getActionSummaries(_.uniq([
+
+ // Actions can be on assets...
+ asset_id,
+
+ // Comments...
+ ...comments.map((comment) => comment.id),
+
+ // Or Authors...
+ ...userIDs
+ ].filter((e) => e)), current_user_id);
+};
+
/**
* Returns summaries of actions for an array of ids
* @param {String} ids array of user identifiers (uuid)
diff --git a/models/asset.js b/models/asset.js
index 00251985a..a0619576f 100644
--- a/models/asset.js
+++ b/models/asset.js
@@ -1,6 +1,8 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
+const Setting = require('./setting');
+
const uuid = require('uuid');
const AssetSchema = new Schema({
@@ -34,7 +36,11 @@ const AssetSchema = new Schema({
subsection: String,
author: String,
publication_date: Date,
- modified_date: Date
+ modified_date: Date,
+ status: {
+ type: String,
+ default: 'open'
+ }
}, {
versionKey: false,
timestamps: {
@@ -54,11 +60,6 @@ AssetSchema.index({
background: true
});
-/**
- * Search for assets. Currently only returns all.
- */
-AssetSchema.statics.search = (query) => Asset.find(query);
-
/**
* Finds an asset by its id.
* @param {String} id identifier of the asset (uuid).
@@ -71,6 +72,25 @@ AssetSchema.statics.findById = (id) => Asset.findOne({id});
*/
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
+/**
+ * Retrieves the settings given an asset query and rectifies it against the
+ * global settings.
+ * @param {Promise} assetQuery an asset query that returns a single asset.
+ * @return {Promise}
+ */
+AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
+ Setting.retrieve(),
+ assetQuery
+]).then(([settings, asset]) => {
+
+ // If the asset exists and has settings then return the merged object.
+ if (asset && asset.settings) {
+ return Object.assign({}, settings, asset.settings);
+ }
+
+ return settings;
+});
+
/**
* Finds a asset by its url.
*
diff --git a/models/comment.js b/models/comment.js
index c1a32cf1b..af9aa4cb1 100644
--- a/models/comment.js
+++ b/models/comment.js
@@ -1,9 +1,37 @@
const mongoose = require('../mongoose');
+const Schema = mongoose.Schema;
const uuid = require('uuid');
const Action = require('./action');
-const Schema = mongoose.Schema;
+/**
+ * The Mongo schema for a Comment Status.
+ * @type {Schema}
+ */
+const StatusSchema = new Schema({
+ type: {
+ type: String,
+ enum: [
+ 'accepted',
+ 'rejected',
+ 'premod',
+ ],
+ },
+ // The User ID of the user that assigned the status.
+ assigned_by: {
+ type: String,
+ default: null
+ },
+
+ created_at: Date
+}, {
+ _id: false
+});
+
+/**
+ * The Mongo schema for a Comment.
+ * @type {Schema}
+ */
const CommentSchema = new Schema({
id: {
type: String,
@@ -17,11 +45,7 @@ const CommentSchema = new Schema({
},
asset_id: String,
author_id: String,
- status: {
- type: String,
- enum: ['accepted', 'rejected', ''],
- default: ''
- },
+ status: [StatusSchema],
parent_id: String
}, {
timestamps: {
@@ -30,90 +54,168 @@ const CommentSchema = new Schema({
}
});
-//==============================================================================
-// Find Statics
-//==============================================================================
+/**
+ * toJSON overrides to remove fields from the json
+ * output.
+ */
+CommentSchema.options.toJSON = {};
+CommentSchema.options.toJSON.hide = '_id status';
+CommentSchema.options.toJSON.transform = (doc, ret, options) => {
+ if (options.hide) {
+ options.hide.split(' ').forEach((prop) => {
+ delete ret[prop];
+ });
+ }
+
+ return ret;
+};
+
+/**
+ * toJSON overrides to remove fields from the json
+ * output.
+ */
+CommentSchema.options.toJSON = {};
+CommentSchema.options.toJSON.hide = '_id';
+CommentSchema.options.toJSON.transform = (doc, ret, options) => {
+ if (options.hide) {
+ options.hide.split(' ').forEach((prop) => {
+ delete ret[prop];
+ });
+ }
+
+ return ret;
+};
+
+/**
+ * Sets up a virtual getter function on a comment such that when you try and
+ * access the `comment.last_status` it returns the last status in the array
+ * of status's on the comment, or `null` if there was no status.
+ */
+CommentSchema.virtual('last_status').get(function() {
+ if (this.status && this.status.length > 0) {
+ return this.status[this.status.length - 1].type;
+ }
+
+ return null;
+});
+
+/**
+ * Creates a new Comment that came from a public source.
+ * @param {Mixed} comment either a single comment or an array of comments.
+ * @return {Promise}
+ */
+CommentSchema.statics.publicCreate = (comment) => {
+
+ // Check to see if this is an array of comments, if so map it out.
+ if (Array.isArray(comment)) {
+ return Promise.all(comment.map(Comment.publicCreate));
+ }
+
+ const {
+ body,
+ asset_id,
+ parent_id,
+ status = false,
+ author_id
+ } = comment;
+
+ comment = new Comment({
+ body,
+ asset_id,
+ parent_id,
+ status: status ? [{
+ type: status,
+ created_at: new Date()
+ }] : [],
+ author_id
+ });
+
+ return comment.save();
+};
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
* @return {Promise}
*/
-CommentSchema.statics.findById = function(id) {
- return Comment.findOne({'id': id});
-};
+CommentSchema.statics.findById = (id) => Comment.findOne({id});
/**
* Finds ALL the comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
* @return {Promise}
*/
-CommentSchema.statics.findByAssetId = function(asset_id) {
- return Comment.find({asset_id});
-};
+CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
+ asset_id
+});
/**
- * Finds the accepted comments by the asset_id.
- * get the comments that are accepted.
+ * Finds the accepted comments by the asset_id get the comments that are
+ * accepted.
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
* @return {Promise}
*/
-CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
- return Comment.find({asset_id: asset_id, status:'accepted'});
-};
+CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
+ asset_id,
+ 'status.type': 'accepted'
+});
/**
* Finds the new and accepted comments by the asset_id.
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
* @return {Promise}
*/
-CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
- return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}});
-};
+CommentSchema.statics.findAcceptedAndNewByAssetId = (asset_id) => Comment.find({
+ asset_id,
+ $or: [
+ {
+ 'status.type': 'accepted'
+ },
+ {
+ status: {
+ $size: 0
+ }
+ }
+ ]
+});
/**
* Find comments by an action that was performed on them.
* @param {String} action_type the type of action that was performed on the comment
* @return {Promise}
*/
-CommentSchema.statics.findByActionType = function(action_type) {
- return Action
- .findCommentsIdByActionType(action_type, 'comment')
- .then((actions) => {
- return Comment.find({'id': {'$in': actions.map(function(a){
- return a.item_id;})}
- });
- });
-};
+CommentSchema.statics.findByActionType = (action_type) => Action
+ .findCommentsIdByActionType(action_type, 'comment')
+ .then((actions) => Comment.find({
+ id: {
+ $in: actions.map((a) => a.item_id)
+ }
+ }));
/**
- * Find not moderated comments by an action that was performed on them.
+ * Find comment id's where the action type matches the argument.
* @param {String} action_type the type of action that was performed on the comment
- * @param {String} status the status of the comment to search for
* @return {Promise}
*/
-CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
- return Action
- .findCommentsIdByActionType(action_type, 'comment')
- .then((actions) => {
- return Comment.find({
- status: status,
- id: {
- $in: actions.map(a => a.item_id)
- }
- });
- });
-};
+CommentSchema.statics.findIdsByActionType = (action_type) => Action
+ .findCommentsIdByActionType(action_type, 'comment')
+ .then((actions) => actions.map(a => a.item_id));
/**
* Find comments by their status.
* @param {String} status the status of the comment to search for
* @return {Promise}
*/
-CommentSchema.statics.findByStatus = function(status) {
- return Comment.find({
- status: status === 'new' ? '' : status
- });
+CommentSchema.statics.findByStatus = (status = false) => {
+ let q = {};
+
+ if (status) {
+ q['status.type'] = status;
+ } else {
+ q.status = {$size: 0};
+ }
+
+ return Comment.find(q);
};
/**
@@ -121,39 +223,59 @@ CommentSchema.statics.findByStatus = function(status) {
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
* @return {Promise}
*/
-CommentSchema.statics.moderationQueue = function(moderation) {
- switch(moderation){
+CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
- // Pre-moderation: New comments are shown in the moderator queues immediately.
- case 'pre':
- return Comment.findByStatus('').then((comments) => {
- return comments;
- });
+ /**
+ * This adds the asset_id requirement to the query if the asset_id is defined.
+ */
+ const assetIDWrap = (query) => {
+ if (asset_id) {
+ query = query.where('asset_id', asset_id);
+ }
- // Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
- case 'post':
- return Comment.findByStatusByActionType('', 'flag').then((comments) => {
- return comments;
- });
+ return query;
+ };
- default:
- return Promise.reject(Error('Moderation setting not found.'));
+ // Decide on whether or not we need to load extended options for the
+ // moderation based on the moderation options.
+ let comments;
+
+ if (moderation === 'pre') {
+
+ // Pre-moderation: New comments are shown in the moderator queues immediately.
+ comments = assetIDWrap(CommentSchema.statics.findByStatus('premod'));
+
+ } else {
+
+ // Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
+ comments = CommentSchema.statics.findIdsByActionType('flag')
+ .then((ids) => assetIDWrap(Comment.find({
+ id: {
+ $in: ids
+ }
+ })));
}
-};
-//==============================================================================
-// Update Statics
-//==============================================================================
+ return comments;
+};
/**
- * Change the status of a comment.
- * @param {String} id identifier of the comment (uuid)
- * @param {String} status the new status of the comment
+ * Pushes a new status in for the user.
+ * @param {String} id identifier of the comment (uuid)
+ * @param {String} status the new status of the comment
+ * @param {String} assigned_by the user id for the user who performed the
+ * moderation action
* @return {Promise}
*/
-CommentSchema.statics.changeStatus = function(id, status) {
- return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}});
-};
+CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
+ $push: {
+ status: {
+ type: status,
+ created_at: new Date(),
+ assigned_by
+ }
+ }
+});
/**
* Add an action to the comment.
@@ -169,19 +291,13 @@ CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.inse
action_type
});
-//==============================================================================
-// Remove Statics
-//==============================================================================
-
/**
* Change the status of a comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} status the new status of the comment
* @return {Promise}
*/
-CommentSchema.statics.removeById = function(id) {
- return Comment.remove({'id': id});
-};
+CommentSchema.statics.removeById = (id) => Comment.remove({id});
/**
* Remove an action from the comment.
@@ -190,22 +306,18 @@ CommentSchema.statics.removeById = function(id) {
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
-CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
- return Action.remove({
- action_type,
- item_type: 'comment',
- item_id,
- user_id
- });
-};
+CommentSchema.statics.removeAction = (item_id, user_id, action_type) => Action.remove({
+ action_type,
+ item_type: 'comment',
+ item_id,
+ user_id
+});
/**
* Returns all the comments in the collection.
* @return {Promise}
*/
-CommentSchema.statics.all = () => {
- return Comment.find();
-};
+CommentSchema.statics.all = () => Comment.find();
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
diff --git a/models/setting.js b/models/setting.js
index e23f654a7..22b2b103f 100644
--- a/models/setting.js
+++ b/models/setting.js
@@ -1,18 +1,33 @@
const mongoose = require('../mongoose');
const Schema = mongoose.Schema;
+const _ = require('lodash');
+const cache = require('../cache');
/**
- * this Schema manages application settings that get used on front and backend
- * NOTE: when you set a setting here, it will not automatically be exposed to
- * the front end. You must add it to the whitelist in the settings route
- * in /routes/api/settings/index.js
+ * SettingSchema manages application settings that get used on front and backend.
* @type {Schema}
*/
const SettingSchema = new Schema({
- id: {type: String, default: '1'},
- moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
- infoBoxEnable: {type: Boolean, default: false},
- infoBoxContent: {type: String, default: ''},
+ id: {
+ type: String,
+ default: '1'
+ },
+ moderation: {
+ type: String,
+ enum: [
+ 'pre',
+ 'post'
+ ],
+ default: 'pre'
+ },
+ infoBoxEnable: {
+ type: Boolean,
+ default: false
+ },
+ infoBoxContent: {
+ type: String,
+ default: ''
+ },
wordlist: [String]
}, {
timestamps: {
@@ -22,49 +37,70 @@ const SettingSchema = new Schema({
});
/**
- * this is run once when the app starts to ensure settings are populated
- * @return {Promise} null initialize the global settings object
+ * The Mongo Mongoose object.
*/
-SettingSchema.statics.init = function (defaults) {
- return this.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
-};
+const Setting = mongoose.model('Setting', SettingSchema);
+
+/**
+ * The Setting Service object exposing the Setting model.
+ * @type {Object}
+ */
+const SettingService = module.exports = {};
+
+/**
+ * The selector used to uniquely identify the settings document.
+ */
+const selector = {id: '1'};
+
+/**
+ * Cache expiry time in seconds for when the cached entry of the settings object
+ * expires. 2 minutes.
+ */
+const EXPIRY_TIME = 60 * 2;
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
-SettingSchema.statics.getSettings = function () {
- return this.findOne({id: '1'});
-};
-
-/**
- * Gets the moderation settings and sends it back
- * @return {Promise} moderation the settings for how to moderate comments
- */
-SettingSchema.statics.getModerationSetting = function () {
- console.log('Getting moderation setting');
- return this.findOne({id: '1'}).select('moderation');
-};
-
-/**
- * Gets the info box settings and sends it back
- * @return {Promise} content the content of the info Box
- */
-SettingSchema.statics.getInfoBoxSetting = function () {
- return this.findOne({id: '1'}).select('infoBoxEnable', 'infoBoxContent');
-};
+SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => Setting.findOne(selector));
/**
* This will update the settings object with whatever you pass in
* @param {object} setting a hash of whatever settings you want to update
* @return {Promise} settings Promise that resolves to the entire (updated) settings object.
*/
-SettingSchema.statics.updateSettings = function (setting) {
- // There should only ever be one record unless something has gone wrong.
- // In the future we may have multiple records for custom settings for objects/users.
- return this.findOneAndUpdate({id: '1'}, {$set: setting}, {new: true});
+SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
+ $set: settings
+}, {
+ upsert: true,
+ new: true,
+ setDefaultsOnInsert: true
+}).then((settings) => {
+
+ // Invalidate the settings cache.
+ return cache
+ .set('settings', settings, EXPIRY_TIME)
+ .then(() => settings);
+});
+
+/**
+ * Filters the document to ensure that the resulting document is indeed ready
+ * for non authenticated users.
+ * @param {Object} settings the source settings object
+ * @return {Object} the filtered settings object
+ */
+SettingService.public = (settings) => _.pick(settings, ['moderation', 'infoBoxEnable', 'infoBoxContent']);
+
+/**
+ * This is run once when the app starts to ensure settings are populated.
+ * @return {Promise} null initialize the global settings object
+ */
+SettingService.init = (defaults) => {
+
+ // Inject the defaults on top of the passed in defaults to ensure that the new
+ // settings conform to the required selector.
+ defaults = Object.assign({}, defaults, selector);
+
+ // Actually update the settings collection.
+ return SettingService.update(defaults);
};
-
-const Setting = mongoose.model('Setting', SettingSchema);
-
-module.exports = Setting;
diff --git a/models/user.js b/models/user.js
index 86908ab42..336486a54 100644
--- a/models/user.js
+++ b/models/user.js
@@ -9,7 +9,6 @@ const SALT_ROUNDS = 10;
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = [
- '',
'admin',
'moderator'
];
@@ -106,7 +105,7 @@ UserSchema.index({
* output.
*/
UserSchema.options.toJSON = {};
-UserSchema.options.toJSON.hide = 'password profiles roles disabled';
+UserSchema.options.toJSON.hide = '_id password profiles roles disabled';
UserSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
diff --git a/mongoose.js b/mongoose.js
index 25d1cf65a..0f728ef16 100644
--- a/mongoose.js
+++ b/mongoose.js
@@ -1,7 +1,13 @@
const mongoose = require('mongoose');
const debug = require('debug')('talk:db');
const enabled = require('debug').enabled;
-const url = process.env.TALK_MONGO_URL || (process.env.NODE_ENV === 'test' ? 'mongodb://localhost/test' : 'mongodb://localhost/talk');
+
+//Append '-test' to the db if node_env === 'test'
+let url = process.env.TALK_MONGO_URL || 'mongodb://localhost/coral-talk';
+
+if (process.env.NODE_ENV === 'test') {
+ url += '-test';
+}
// Use native promises
mongoose.Promise = global.Promise;
diff --git a/nightwatch.conf.js b/nightwatch.conf.js
new file mode 100644
index 000000000..a39ab128f
--- /dev/null
+++ b/nightwatch.conf.js
@@ -0,0 +1,44 @@
+require('babel-core/register');
+
+module.exports = {
+ 'src_folders': './tests/e2e/tests',
+ 'output_folder': './tests/e2e/reports',
+ 'page_objects_path': './tests/e2e/pages',
+ 'globals_path': './tests/e2e/globals',
+ 'custom_commands_path' : '',
+ 'custom_assertions_path' : '',
+ 'selenium': {
+ 'start_process': true,
+ 'server_path': 'node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar',
+ 'log_path': './tests/e2e/reports',
+ 'host': '127.0.0.1',
+ 'port': 6666,
+ 'cli_args': {
+ 'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver'
+ }
+ },
+ 'test_settings': {
+ 'default': {
+ 'selenium_port': 6666,
+ 'selenium_host': 'localhost',
+ 'silent': true,
+ 'desiredCapabilities': {
+ 'browserName': 'chrome',
+ 'javascriptEnabled': true,
+ 'acceptSslCerts': true,
+ 'webStorageEnabled' : true,
+ 'databaseEnabled' : true,
+ 'applicationCacheEnabled' : false,
+ 'nativeEvents' : true
+ },
+ 'screenshots' : {
+ 'enabled' : true,
+ 'on_failure' : true,
+ 'on_error' : true,
+ 'path' : './tests/e2e/reports'
+ },
+ 'exclude': [
+ ]
+ }
+ }
+};
diff --git a/package.json b/package.json
index 8d0256af2..c54bd6723 100644
--- a/package.json
+++ b/package.json
@@ -11,18 +11,15 @@
"lint-fix": "eslint . --fix",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive tests",
"test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
+ "pree2e": "NODE_ENV=test ./pree2e.sh",
+ "e2e": "NODE_ENV=test node_modules/.bin/nightwatch",
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs"
},
"config": {
"pre-git": {
"commit-msg": [],
- "pre-commit": [
- "npm run lint",
- "npm test"
- ],
- "pre-push": [
- "npm test"
- ],
+ "pre-commit": ["npm run lint", "npm test"],
+ "pre-push": ["npm test"],
"post-commit": [],
"post-merge": []
}
@@ -31,12 +28,7 @@
"type": "git",
"url": "git+https://github.com/coralproject/talk.git"
},
- "keywords": [
- "talk",
- "coral",
- "coralproject",
- "ask"
- ],
+ "keywords": ["talk", "coral", "coralproject", "ask"],
"author": "",
"license": "Apache-2.0",
"bugs": {
@@ -97,6 +89,7 @@
"eslint-config-standard": "^6.2.1",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-import": "^2.2.0",
+ "eslint-plugin-mocha": "^4.7.0",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^6.6.0",
"eslint-plugin-standard": "^2.0.1",
@@ -110,6 +103,8 @@
"material-design-lite": "^1.2.1",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
+ "nightwatch": "^0.9.9",
+ "node-fetch": "^1.6.3",
"postcss-loader": "^1.1.0",
"postcss-modules": "^0.5.2",
"postcss-smart-import": "^0.5.1",
@@ -127,11 +122,11 @@
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
+ "selenium-standalone": "^5.8.0",
"style-loader": "^0.13.1",
"supertest": "^2.0.1",
"timeago.js": "^2.0.3",
- "webpack": "^1.13.3",
- "whatwg-fetch": "^1.0.0"
+ "webpack": "^1.13.3"
},
"engines": {
"node": ">=7.0.0"
diff --git a/pree2e.sh b/pree2e.sh
new file mode 100755
index 000000000..8857e5745
--- /dev/null
+++ b/pree2e.sh
@@ -0,0 +1,2 @@
+node_modules/selenium-standalone/bin/selenium-standalone install
+npm start &
diff --git a/routes/api/asset/index.js b/routes/api/asset/index.js
index 96b83a969..05926bc15 100644
--- a/routes/api/asset/index.js
+++ b/routes/api/asset/index.js
@@ -96,4 +96,12 @@ router.put('/:asset_id/settings', (req, res, next) => {
});
+router.put('/:asset_id/status', (req, res, next) => {
+ // Update the asset status
+ Asset
+ .update({id: req.params.asset_id}, {status: req.query.status})
+ .then(() => res.status(204).end())
+ .catch((err) => next(err));
+});
+
module.exports = router;
diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js
index 3bdf4afc6..b82c1faf3 100644
--- a/routes/api/comments/index.js
+++ b/routes/api/comments/index.js
@@ -1,5 +1,6 @@
const express = require('express');
const Comment = require('../../../models/comment');
+const Asset = require('../../../models/asset');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const wordlist = require('../../../services/wordlist');
@@ -9,24 +10,45 @@ const _ = require('lodash');
const router = express.Router();
router.get('/', authorization.needed('admin'), (req, res, next) => {
+
+ const {
+ status = null,
+ action_type = null,
+ asset_id = null
+ } = req.query;
+
+ /**
+ * This adds the asset_id requirement to the query if the asset_id is defined.
+ */
+ const assetIDWrap = (query) => {
+ if (asset_id) {
+ query = query.where('asset_id', asset_id);
+ }
+
+ return query;
+ };
+
let query;
- if (req.query.status) {
- query = Comment.findByStatus(req.query.status);
- } else if (req.query.action_type) {
- query = Comment.findByActionType(req.query.action_type);
+ if (status) {
+ query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
+ } else if (action_type) {
+ query = Comment
+ .findIdsByActionType(action_type)
+ .then((ids) => assetIDWrap(Comment.find({
+ id: {
+ $in: ids
+ },
+ })));
} else {
- query = Comment.all();
+ query = assetIDWrap(Comment.all());
}
query.then((comments) => {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
- Action.getActionSummaries(_.uniq([
- ...comments.map((comment) => comment.id),
- ...comments.map((comment) => comment.author_id)
- ]))
+ Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
]);
})
.then(([comments, users, actions])=>
@@ -48,21 +70,38 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
parent_id
} = req.body;
- Comment
- .create({
- body,
- asset_id,
- parent_id,
- status: req.wordlist.matched ? 'rejected' : '',
- author_id: req.user.id
- })
- .then((comment) => {
+ // Decide the status based on whether or not the current asset/settings
+ // has pre-mod enabled or not. If the comment was rejected based on the
+ // wordlist, then reject it, otherwise if the moderation setting is
+ // premod, set it to `premod`.
+ let status;
- res.status(201).send(comment);
- })
- .catch((err) => {
- next(err);
- });
+ if (req.wordlist.matched) {
+ status = Promise.resolve('rejected');
+ } else {
+ status = Asset
+ .rectifySettings(Asset.findById(asset_id))
+
+ // Return `premod` if pre-moderation is enabled and an empty "new" status
+ // in the event that it is not in pre-moderation mode.
+ .then(({moderation}) => moderation === 'pre' ? 'premod' : '');
+ }
+
+ status.then((status) => Comment.publicCreate({
+ body,
+ asset_id,
+ parent_id,
+ status,
+ author_id: req.user.id
+ }))
+ .then((comment) => {
+
+ // The comment was created! Send back the created comment.
+ res.status(201).send(comment);
+ })
+ .catch((err) => {
+ next(err);
+ });
});
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
@@ -99,7 +138,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
} = req.body;
Comment
- .changeStatus(req.params.comment_id, status)
+ .pushStatus(req.params.comment_id, status, req.user.id)
.then(() => {
res.status(204).end();
})
diff --git a/routes/api/queue/index.js b/routes/api/queue/index.js
index 448149387..9c85fbe94 100644
--- a/routes/api/queue/index.js
+++ b/routes/api/queue/index.js
@@ -3,6 +3,7 @@ const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const Setting = require('../../../models/setting');
+const Asset = require('../../../models/asset');
const _ = require('lodash');
const router = express.Router();
@@ -16,27 +17,52 @@ const router = express.Router();
// Pre-moderation: New comments are shown in the moderator queues immediately.
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
router.get('/comments/pending', (req, res, next) => {
- Setting.getModerationSetting().then(({moderation}) =>
- Comment.moderationQueue(moderation))
- .then((comments) => {
- return Promise.all([
- comments,
- User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
- Action.getActionSummaries(_.uniq([
- ...comments.map((comment) => comment.id),
- ...comments.map((comment) => comment.author_id)
- ]))
- ]);
- })
- .then(([comments, users, actions])=>
- res.status(200).json({
- comments,
- users,
- actions
- }))
- .catch(error => {
- next(error);
- });
+
+ const {
+ asset_id
+ } = req.query;
+
+ let settings = Setting.retrieve();
+
+ if (asset_id) {
+
+ // In the event that we have an asset_id, we should fetch the asset settings
+ // in order to actually determine if there is additional comments to parse.
+ settings = Promise.all([
+ settings,
+ Asset.findById(asset_id).select('settings')
+ ]).then(([{moderation}, asset]) => {
+ if (asset.settings && asset.settings.moderation) {
+ return {moderation: asset.settings.moderation};
+ }
+
+ return {moderation};
+ });
+ }
+
+ settings
+ .then(({moderation}) => {
+ return Comment.moderationQueue(moderation);
+ }).then((comments) => {
+ return Promise.all([
+ comments,
+ User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
+ Action.getActionSummaries(_.uniq([
+ ...comments.map((comment) => comment.id),
+ ...comments.map((comment) => comment.author_id)
+ ]))
+ ]);
+ })
+ .then(([comments, users, actions]) => {
+ res.json({
+ comments,
+ users,
+ actions
+ });
+ })
+ .catch(error => {
+ next(error);
+ });
});
module.exports = router;
diff --git a/routes/api/settings/index.js b/routes/api/settings/index.js
index 910524fb2..76d16c7ec 100644
--- a/routes/api/settings/index.js
+++ b/routes/api/settings/index.js
@@ -4,19 +4,21 @@ const Setting = require('../../../models/setting');
const router = express.Router();
router.get('/', (req, res, next) => {
- Setting
- .getSettings()
- .then(settings => {
- res.json(settings);
- })
- .catch(next);
+ Setting.retrieve().then((settings) => {
+ res.json(settings);
+ })
+ .catch((err) => {
+ next(err);
+ });
});
router.put('/', (req, res, next) => {
- Setting
- .updateSettings(req.body)
- .then(() => res.status(204).end())
- .catch(next);
+ Setting.update(req.body).then(() => {
+ res.status(204).end();
+ })
+ .catch((err) => {
+ next(err);
+ });
});
module.exports = router;
diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js
index ce8871ab8..26947be09 100644
--- a/routes/api/stream/index.js
+++ b/routes/api/stream/index.js
@@ -27,13 +27,13 @@ router.get('/', (req, res, next) => {
}),
// Get the moderation setting from the settings.
- Setting.getModerationSetting()
+ Setting.retrieve()
])
.then(([asset, settings]) => {
// Merge the asset specific settings with the returned settings object in
// the event that the asset that was returned also had settings.
- if (asset.settings) {
+ if (asset && asset.settings) {
settings = Object.assign({}, settings, asset.settings);
}
@@ -70,17 +70,7 @@ router.get('/', (req, res, next) => {
let users = userIDs.length > 0 ? User.findByIdArray(userIDs) : [];
// Fetch the actions for pretty much everything at this point.
- let actions = Action.getActionSummaries(_.uniq([
-
- // Actions can be on assets...
- asset.id,
-
- // Comments...
- ...comments.map((comment) => comment.id),
-
- // Or Authors...
- ...userIDs
- ]), req.user ? req.user.id : false);
+ let actions = Action.getActionSummariesFromComments(asset.id, comments, req.user ? req.user.id : false);
return Promise.all([
@@ -108,7 +98,7 @@ router.get('/', (req, res, next) => {
comments,
users,
actions,
- settings
+ settings: Setting.public(settings)
});
})
.catch(error => {
diff --git a/services/wordlist.js b/services/wordlist.js
index e6f2ad668..59a842817 100644
--- a/services/wordlist.js
+++ b/services/wordlist.js
@@ -20,7 +20,7 @@ const wordlist = {
*/
wordlist.init = () => {
return Setting
- .getSettings()
+ .retrieve()
.then((settings) => {
// Insert the settings wordlist.
diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json
index 363ef5a28..b188e92cf 100644
--- a/tests/.eslintrc.json
+++ b/tests/.eslintrc.json
@@ -4,8 +4,12 @@
"node": true,
"mocha": true
},
+ "plugins": [
+ "mocha"
+ ],
"extends": "../.eslintrc.json",
"rules": {
- "no-undef": [0]
+ "no-undef": [0],
+ "mocha/no-exclusive-tests": "warn"
}
}
diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js
new file mode 100644
index 000000000..147af2663
--- /dev/null
+++ b/tests/e2e/globals.js
@@ -0,0 +1,4 @@
+module.exports = {
+ waitForConditionTimeout: 20000,
+ baseUrl: 'localhost:3011/'
+};
diff --git a/tests/e2e/mocks.js b/tests/e2e/mocks.js
new file mode 100644
index 000000000..a4f430cd5
--- /dev/null
+++ b/tests/e2e/mocks.js
@@ -0,0 +1,25 @@
+const Comments = require('../../models/comment');
+const Users = require('../../models/user');
+const Actions = require('../../models/action');
+const Assets = require('../../models/asset');
+const Settings = require('../../models/setting');
+const globals = require('./globals');
+
+/* Create an array of comments */
+module.exports.comments = (comments) => Assets.findOrCreateByUrl(globals.baseUrl)
+ .then((asset) => {
+ comments = comments.map((comment) => {
+ comment.asset_id = asset.id;
+ return comment;
+ });
+ return Comments.create(comments);
+ });
+
+/* Create an array of users */
+module.exports.users = (users) => Users.createLocalUsers(users);
+
+/* Create an array of actions */
+module.exports.actions = (actions) => Actions.create(actions);
+
+/* Update a setting */
+module.exports.settings = (setting) => Settings.init().then(() => Settings.updateSettings(setting));
diff --git a/tests/e2e/pages/embedStream.js b/tests/e2e/pages/embedStream.js
new file mode 100644
index 000000000..cc584a656
--- /dev/null
+++ b/tests/e2e/pages/embedStream.js
@@ -0,0 +1,45 @@
+const fetch = require('node-fetch');
+
+const embedCommands = {
+ ready() {
+ return this.resizeWindow(1200, 800)
+ .url(client.globals.baseUrl)
+ .waitForElementVisible('body', 2000)
+ .frame('coralStreamIframe');
+ },
+ setConfig(config, baseUrl) {
+ return fetch(`${baseUrl}/api/v1/settings`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify(config)
+ });
+ },
+ enterComment() {
+ const comment = 'This is a test comment';
+ return this
+ .waitForElementVisible('@commentBox')
+ .setValue('@commentBox', comment)
+ .click('@postButton')
+ .waitForElementVisible('.comment', 1000);
+ },
+ validateComment(comment) {
+ return this
+ .assert.equal(comment, '.comment');
+ }
+};
+
+module.exports = {
+ commands: [embedCommands],
+ elements: {
+
+ commentBox: {
+ selector: '#commentBox'
+ },
+ postButton: {
+ selector: '#commentBox .coral-plugin-commentbox-button'
+ }
+ }
+};
diff --git a/tests/e2e/tests/AppTest.js b/tests/e2e/tests/AppTest.js
new file mode 100644
index 000000000..36281ed3f
--- /dev/null
+++ b/tests/e2e/tests/AppTest.js
@@ -0,0 +1,13 @@
+module.exports = {
+ '@tags': ['app'],
+ 'Base url and Hostname': browser => {
+ const {baseUrl} = browser.globals;
+ browser
+ .url(baseUrl)
+ .assert.title('Coral Talk')
+ .waitForElementPresent('body', 1000);
+ },
+ after: client => {
+ client.end();
+ }
+};
diff --git a/tests/e2e/tests/EmbedStreamTests.js b/tests/e2e/tests/EmbedStreamTests.js
new file mode 100644
index 000000000..65ea9fa76
--- /dev/null
+++ b/tests/e2e/tests/EmbedStreamTests.js
@@ -0,0 +1,173 @@
+const utils = require('../../utils/e2e-mongoose');
+const mocks = require('../mocks');
+
+const mockComment = 'This is a test comment.';
+const mockReply = 'This is a test reply';
+const mockUser = {
+ email: `${new Date().getTime()}@test.com`,
+ name: 'Test User',
+ pw: 'testtesttest'
+};
+
+module.exports = {
+ '@tags': ['embed-stream', 'comment', 'premodoff', 'premodon'],
+ before: () => {
+ utils.before();
+ },
+ 'User registers and posts a comment with premod off': client => {
+ client.perform((client, done) => {
+ mocks.settings({moderation: 'post'})
+ .then(() => {
+ //Load Page
+ client.resizeWindow(1200, 800)
+ .url(client.globals.baseUrl)
+ .frame('coralStreamIframe')
+
+ //Register and Log In
+ .waitForElementVisible('#commentBox', 1000)
+ .waitForElementVisible('#coralSignInButton', 2000)
+ .click('#coralSignInButton')
+ .waitForElementVisible('#coralRegister', 1000)
+ .click('#coralRegister')
+ .waitForElementVisible('#email', 1000)
+ .setValue('#email', mockUser.email)
+ .setValue('#displayName', mockUser.name)
+ .setValue('#password', mockUser.pw)
+ .setValue('#confirmPassword', mockUser.pw)
+ .click('#coralSignUpButton')
+ .waitForElementVisible('#coralLogInButton', 10000)
+ .click('#coralLogInButton')
+ .waitForElementVisible('.coral-plugin-commentbox-button', 4000)
+
+ // Post a comment
+ .setValue('.coral-plugin-commentbox-textarea', mockComment)
+ .click('.coral-plugin-commentbox-button')
+ .waitForElementVisible('.comment', 1000)
+
+ //Verify that it appears
+ .assert.containsText('.comment', mockComment);
+ done();
+ })
+ .catch((err) => {
+ console.log(err);
+ done();
+ });
+ });
+ },
+ 'User posts a comment with premod on': client => {
+ client.perform((client, done) => {
+ mocks.settings({moderation: 'pre'})
+ .then(() => {
+ //Load Page
+ client.url(client.globals.baseUrl)
+ .frame('coralStreamIframe');
+
+ // Post a comment
+ client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
+ .setValue('.coral-plugin-commentbox-textarea', mockComment)
+ .click('.coral-plugin-commentbox-button')
+ .waitForElementVisible('#coral-notif', 1000)
+
+ //Verify that it appears
+ .assert.containsText('#coral-notif', 'moderation team');
+ done();
+ })
+ .catch((err) => {
+ console.log(err);
+ done();
+ });
+ });
+ },
+ 'User replies to a comment with premod off': client => {
+ client.perform((client, done) => {
+ mocks.settings({moderation: 'post'})
+ .then(() => {
+ //Load Page
+ client.resizeWindow(1200, 800)
+ .url(client.globals.baseUrl)
+ .frame('coralStreamIframe');
+
+ // Post a comment
+ client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
+ .setValue('.coral-plugin-commentbox-textarea', mockComment)
+ .click('.coral-plugin-commentbox-button')
+
+ // Post a reply
+ .waitForElementVisible('.coral-plugin-replies-reply-button', 5000)
+ .click('.coral-plugin-replies-reply-button')
+ .waitForElementVisible('#replyText')
+ .setValue('#replyText', mockReply)
+ .click('.coral-plugin-replies-textarea button')
+ .waitForElementVisible('.reply', 2000)
+
+ //Verify that it appears
+ .assert.containsText('.reply', mockReply);
+ done();
+ })
+ .catch((err) => {
+ console.log(err);
+ done();
+ });
+ });
+ },
+ 'User replies to a comment with premod on': client => {
+ client.perform((client, done) => {
+ mocks.settings({moderation: 'pre'})
+
+ // Add a mock user
+ .then(() => {
+ return mocks.users([{
+ displayName: 'Baby Blue',
+ email: 'whale@tale.sea',
+ password: 'krill'
+ }]);
+ })
+
+ // Add a mock preapproved comment by that user
+ .then((user) => {
+ return mocks.comments([{
+ body: 'Whales are not fish.',
+ status: 'accepted',
+ author_id: user.id
+ }]);
+ })
+ .then(() => {
+ //Load Page
+ client.resizeWindow(1200, 800)
+ .url(client.globals.baseUrl)
+ .frame('coralStreamIframe');
+
+ // Post a reply
+ client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000)
+ .click('.coral-plugin-replies-reply-button')
+ .waitForElementVisible('#replyText')
+ .setValue('#replyText', mockReply)
+ .click('.coral-plugin-replies-textarea button')
+ .waitForElementVisible('#coral-notif', 1000)
+
+ //Verify that it appears
+ .assert.containsText('#coral-notif', 'moderation team');
+ done();
+ })
+ .catch((err) => {
+ console.log(err);
+ done();
+ });
+ });
+ },
+ 'Total comment count premod on': client => {
+ client.perform((client, done) => {
+ client.url(client.globals.baseUrl)
+ .frame('coralStreamIframe');
+
+ // Verify that comment count is correct
+ client.waitForElementVisible('.coral-plugin-comment-count-text', 2000)
+ .assert.containsText('.coral-plugin-comment-count-text', '1 Comment');
+ done();
+ });
+ },
+ after: client => {
+ utils.after();
+ client.end();
+ }
+};
diff --git a/tests/models/action.js b/tests/models/action.js
index c354b8f83..5f05dea71 100644
--- a/tests/models/action.js
+++ b/tests/models/action.js
@@ -1,35 +1,34 @@
const Action = require('../../models/action');
const expect = require('chai').expect;
-describe('Action: models', () => {
- let mockActions;
+describe('models.Action', () => {
+ let mockActions = [];
- beforeEach(() => {
- return Action.create([{
- action_type: 'flag',
- item_id: '123',
- item_type: 'comment',
- user_id: 'flagginguserid'
- }, {
- action_type: 'flag',
- item_id: '456',
- item_type: 'comment'
- }, {
- action_type: 'flag',
- item_id: '123',
- item_type: 'comment'
- }, {
- action_type: 'like',
- item_id: '123',
- item_type: 'comment'
- }]).then((actions) => {
- mockActions = actions;
- });
- });
+ beforeEach(() => Action.create([{
+ action_type: 'flag',
+ item_id: '123',
+ item_type: 'comment',
+ user_id: 'flagginguserid'
+ }, {
+ action_type: 'flag',
+ item_id: '456',
+ item_type: 'comment'
+ }, {
+ action_type: 'flag',
+ item_id: '123',
+ item_type: 'comment'
+ }, {
+ action_type: 'like',
+ item_id: '123',
+ item_type: 'comment'
+ }]).then((actions) => {
+ mockActions = actions;
+ }));
describe('#findById()', () => {
it('should find an action by id', () => {
return Action.findById(mockActions[0].id).then((result) => {
+ expect(result).to.not.be.null;
expect(result).to.have.property('action_type', 'flag');
});
});
diff --git a/tests/models/asset.js b/tests/models/asset.js
index 69bf99f9d..c9eff819e 100644
--- a/tests/models/asset.js
+++ b/tests/models/asset.js
@@ -6,7 +6,7 @@ const expect = chai.expect;
// Use the chai should.
chai.should();
-describe('Asset: model', () => {
+describe('models.Asset', () => {
beforeEach(() => {
const defaults = {url:'http://test.com'};
diff --git a/tests/models/comment.js b/tests/models/comment.js
index 07834a186..a9c2f4787 100644
--- a/tests/models/comment.js
+++ b/tests/models/comment.js
@@ -7,35 +7,57 @@ const settings = {id: '1', moderation: 'pre'};
const expect = require('chai').expect;
-describe('Comment: models', () => {
+describe('models.Comment', () => {
const comments = [{
body: 'comment 10',
asset_id: '123',
- status: '',
+ status: [],
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
- status: 'accepted',
+ status: [{
+ type: 'accepted'
+ }],
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
- status: '',
+ status: [],
parent_id: '',
author_id: '456',
id: '3'
}, {
body: 'comment 40',
asset_id: '123',
- status: 'rejected',
+ status: [{
+ type: 'rejected'
+ }],
parent_id: '',
author_id: '456',
id: '4'
+ }, {
+ body: 'comment 50',
+ asset_id: '1234',
+ status: [{
+ type: 'premod'
+ }],
+ parent_id: '',
+ author_id: '456',
+ id: '5'
+ }, {
+ body: 'comment 60',
+ asset_id: '1234',
+ status: [{
+ type: 'premod'
+ }],
+ parent_id: '',
+ author_id: '456',
+ id: '6'
}];
const users = [{
@@ -60,25 +82,69 @@ describe('Comment: models', () => {
user_id: '456'
}];
- beforeEach(() => {
- return Promise.all([
- Setting.create(settings),
- Comment.create(comments),
- User.createLocalUsers(users),
- Action.create(actions)
- ]);
+ beforeEach(() => Promise.all([
+ Setting.init(settings),
+ Comment.create(comments),
+ User.createLocalUsers(users),
+ Action.create(actions)
+ ]));
+
+ describe('#publicCreate()', () => {
+
+ it('creates a new comment', () => {
+ return Comment.publicCreate({
+ body: 'This is a comment!',
+ status: 'accepted'
+ }).then((c) => {
+ expect(c).to.not.be.null;
+ expect(c.id).to.not.be.null;
+ expect(c.id).to.be.uuid;
+ expect(c.status).to.have.length(1);
+ expect(c.status[0]).to.have.property('type', 'accepted');
+ });
+ });
+
+ it('creates many new comments', () => {
+ return Comment.publicCreate([{
+ body: 'This is a comment!',
+ status: 'accepted'
+ }, {
+ body: 'This is another comment!'
+ }, {
+ body: 'This is a rejected comment!',
+ status: 'rejected'
+ }]).then(([c1, c2, c3]) => {
+ expect(c1).to.not.be.null;
+ expect(c1.id).to.be.uuid;
+ expect(c1.status).to.have.length(1);
+ expect(c1.status[0]).to.have.property('type', 'accepted');
+
+ expect(c2).to.not.be.null;
+ expect(c2.id).to.be.uuid;
+ expect(c2.status).to.have.length(0);
+
+ expect(c3).to.not.be.null;
+ expect(c3.id).to.be.uuid;
+ expect(c3.status).to.have.length(1);
+ expect(c3.status[0]).to.have.property('type', 'rejected');
+ });
+ });
+
});
describe('#findById()', () => {
+
it('should find a comment by id', () => {
return Comment.findById('1').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('body', 'comment 10');
});
});
+
});
describe('#findByAssetId()', () => {
+
it('should find an array of all comments by asset id', () => {
return Comment.findByAssetId('123').then((result) => {
expect(result).to.have.length(3);
@@ -91,6 +157,7 @@ describe('Comment: models', () => {
expect(result[2]).to.have.property('body', 'comment 40');
});
});
+
it('should find an array of accepted comments by asset id', () => {
return Comment.findAcceptedByAssetId('123').then((result) => {
expect(result).to.have.length(1);
@@ -101,6 +168,7 @@ describe('Comment: models', () => {
expect(result[0]).to.have.property('body', 'comment 20');
});
});
+
it('should find an array of new and accepted comments by asset id', () => {
return Comment.findAcceptedAndNewByAssetId('123').then((result) => {
expect(result).to.have.length(2);
@@ -112,13 +180,16 @@ describe('Comment: models', () => {
});
});
});
+
describe('#moderationQueue()', () => {
+
it('should find an array of new comments to moderate when pre-moderation', () => {
return Comment.moderationQueue('pre').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.lengthOf(2);
});
});
+
it('should find an array of new comments to moderate when post-moderation', () => {
return Comment.moderationQueue('post').then((result) => {
expect(result).to.not.be.null;
@@ -126,21 +197,56 @@ describe('Comment: models', () => {
expect(result[0]).to.have.property('body', 'comment 30');
});
});
- // it('should fail when the moderation is not pre or post', () => {
- // return Comment.moderationQueue('any').catch(function(error) {
- // expect(error).to.not.be.null;
- // });
- // });
+
});
describe('#removeAction', () => {
+
it('should remove an action', () => {
- return Comment.removeAction('3', '123', 'flag').then(() => {
- return Action.findByItemIdArray(['123']);
- })
- .then((actions) => {
- expect(actions.length).to.equal(0);
- });
+ return Comment.removeAction('3', '123', 'flag')
+ .then(() => {
+ return Action.findByItemIdArray(['123']);
+ })
+ .then((actions) => {
+ expect(actions.length).to.equal(0);
+ });
});
});
+
+ describe('#changeStatus', () => {
+
+ it('should change the status of a comment from no status', () => {
+ let comment_id = comments[0].id;
+
+ return Comment.findById(comment_id)
+ .then((c) => {
+ expect(c).to.have.property('status');
+ expect(c.status).to.have.length(0);
+
+ return Comment.pushStatus(comment_id, 'rejected', '123');
+ })
+ .then(() => Comment.findById(comment_id))
+ .then((c) => {
+ expect(c).to.have.property('status');
+ expect(c.status).to.have.length(1);
+ expect(c.status[0]).to.have.property('type', 'rejected');
+ expect(c.status[0]).to.have.property('assigned_by', '123');
+ });
+ });
+
+ it('should change the status of a comment from accepted', () => {
+ return Comment.pushStatus(comments[1].id, 'rejected', '123')
+ .then(() => Comment.findById(comments[1].id))
+ .then((c) => {
+ expect(c).to.have.property('status');
+ expect(c.status).to.have.length(2);
+ expect(c.status[0]).to.have.property('type', 'accepted');
+ expect(c.status[0]).to.have.property('assigned_by', null);
+
+ expect(c.status[1]).to.have.property('type', 'rejected');
+ expect(c.status[1]).to.have.property('assigned_by', '123');
+ });
+ });
+
+ });
});
diff --git a/tests/models/setting.js b/tests/models/setting.js
index 08a47d7a1..3e77297fc 100644
--- a/tests/models/setting.js
+++ b/tests/models/setting.js
@@ -1,32 +1,29 @@
const Setting = require('../../models/setting');
const expect = require('chai').expect;
-describe('Setting: model', () => {
+describe('models.Setting', () => {
- beforeEach(() => {
- const defaults = {id: 1};
- return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
- });
+ beforeEach(() => Setting.init({moderation: 'pre'}));
- describe('#getSettings()', () => {
+ describe('#retrieve()', () => {
it('should have a moderation field defined', () => {
- return Setting.getSettings().then(settings => {
+ return Setting.retrieve().then(settings => {
expect(settings).to.have.property('moderation').and.to.equal('pre');
});
});
it('should have two infoBox fields defined', () => {
- return Setting.getSettings().then(settings => {
+ return Setting.retrieve().then(settings => {
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
});
});
});
- describe('#updateSettings()', () => {
+ describe('#update()', () => {
it('should update the settings with a passed object', () => {
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
- return Setting.updateSettings(mockSettings).then(updatedSettings => {
+ return Setting.update(mockSettings).then(updatedSettings => {
expect(updatedSettings).to.be.an('object');
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
expect(updatedSettings).to.have.property('infoBoxEnable', true);
@@ -35,9 +32,9 @@ describe('Setting: model', () => {
});
});
- describe('#getModerationSetting', () => {
+ describe('#get', () => {
it('should return the moderation settings', () => {
- return Setting.getModerationSetting().then(({moderation}) => {
+ return Setting.retrieve().then(({moderation}) => {
expect(moderation).not.to.be.null;
});
});
diff --git a/tests/models/user.js b/tests/models/user.js
index c8af92054..5e1677016 100644
--- a/tests/models/user.js
+++ b/tests/models/user.js
@@ -1,7 +1,7 @@
const User = require('../../models/user');
const expect = require('chai').expect;
-describe('User: models', () => {
+describe('models.User', () => {
let mockUsers;
beforeEach(() => {
return User.createLocalUsers([{
diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js
index bc8b67bc1..bf7a06ff6 100644
--- a/tests/routes/api/comments/index.js
+++ b/tests/routes/api/comments/index.js
@@ -10,6 +10,7 @@ chai.use(require('chai-http'));
const wordlist = require('../../../../services/wordlist');
const Comment = require('../../../../models/comment');
+const Asset = require('../../../../models/asset');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
@@ -17,63 +18,71 @@ const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
describe('/api/v1/comments', () => {
- const comments = [{
- id: 'abc',
- body: 'comment 10',
- asset_id: 'asset',
- author_id: '123'
- }, {
- id: 'def',
- body: 'comment 20',
- asset_id: 'asset',
- author_id: '456'
- }, {
- id: 'def-rejected',
- body: 'comment 20',
- asset_id: 'asset',
- author_id: '456',
- status: 'rejected'
- }, {
- id: 'hij',
- body: 'comment 30',
- asset_id: '456',
- author_id: '456',
- status: 'accepted'
- }];
-
- const users = [{
- displayName: 'Ana',
- email: 'ana@gmail.com',
- password: '123'
- }, {
- displayName: 'Maria',
- email: 'maria@gmail.com',
- password: '123'
- }];
-
- const actions = [{
- action_type: 'flag',
- item_id: 'abc',
- item_type: 'comment'
- }, {
- action_type: 'like',
- item_id: 'hij',
- item_type: 'comment'
- }];
-
- beforeEach(() => {
- return Promise.all([
- Comment.create(comments),
- User.createLocalUsers(users),
- Action.create(actions),
- wordlist.insert([
- 'bad words'
- ]),
- Setting.create(settings)
- ]);
- });
describe('#get', () => {
+ const comments = [{
+ body: 'comment 10',
+ asset_id: 'asset',
+ author_id: '123'
+ }, {
+ body: 'comment 20',
+ asset_id: 'asset',
+ author_id: '456'
+ }, {
+ body: 'comment 20',
+ asset_id: 'asset',
+ author_id: '456',
+ status: [{
+ type: 'rejected'
+ }]
+ }, {
+ body: 'comment 30',
+ asset_id: '456',
+ status: [{
+ type: 'accepted'
+ }]
+ }];
+
+ const users = [{
+ displayName: 'Ana',
+ email: 'ana@gmail.com',
+ password: '123'
+ }, {
+ displayName: 'Maria',
+ email: 'maria@gmail.com',
+ password: '123'
+ }];
+
+ const actions = [{
+ action_type: 'flag',
+ item_id: 'abc',
+ item_type: 'comment'
+ }, {
+ action_type: 'like',
+ item_id: 'hij',
+ item_type: 'comment'
+ }];
+
+ beforeEach(() => {
+ return Promise.all([
+ Comment.create(comments).then((newComments) => {
+ newComments.forEach((comment, i) => {
+ comments[i].id = comment.id;
+ });
+
+ actions[0].item_id = comments[0].id;
+ actions[1].item_id = comments[1].id;
+
+ return Action.create(actions);
+ }),
+ User.createLocalUsers(users),
+ wordlist.insert([
+ 'bad words'
+ ]),
+ Setting.init(settings)
+ ]);
+ });
+
it('should return all the comments', () => {
return chai.request(app)
.get('/api/v1/comments')
@@ -91,7 +100,9 @@ describe('/api/v1/comments', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
- expect(res.body.comments[0]).to.have.property('id', 'def-rejected');
+ expect(res.body).to.have.property('comments');
+ expect(res.body.comments).to.have.length(1);
+ expect(res.body.comments[0]).to.have.property('id', comments[2].id);
});
});
@@ -102,7 +113,7 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
- expect(res.body.comments[0]).to.have.property('id', 'hij');
+ expect(res.body.comments[0]).to.have.property('id', comments[3].id);
});
});
@@ -124,8 +135,7 @@ describe('/api/v1/comments', () => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(1);
- expect(res.body.comments[0]).to.have.property('id', 'abc');
-
+ expect(res.body.comments[0]).to.have.property('id', comments[0].id);
});
});
});
@@ -151,7 +161,31 @@ describe('/api/v1/comments', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
- expect(res.body).to.have.property('status', 'rejected');
+ expect(res.body).to.have.property('status').and.to.have.length(1);
+ expect(res.body.status[0]).to.have.property('type', 'rejected');
+ });
+ });
+
+ it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => {
+ return Asset
+ .findOrCreateByUrl('https://coralproject.net/article1')
+ .then((asset) => {
+ return Asset
+ .overrideSettings(asset.id, {moderation: 'pre'})
+ .then(() => asset);
+ })
+ .then((asset) => {
+ return chai.request(app)
+ .post('/api/v1/comments')
+ .set(passport.inject({roles: []}))
+ .send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
+ })
+ .then((res) => {
+ expect(res).to.have.status(201);
+ expect(res.body).to.have.property('id');
+ expect(res.body).to.have.property('asset_id');
+ expect(res.body).to.have.property('status').and.to.have.length(1);
+ expect(res.body.status[0]).to.have.property('type', 'premod');
});
});
});
@@ -267,18 +301,22 @@ describe('/api/v1/comments/:comment_id/actions', () => {
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
- status: ''
+ status: []
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
- status: 'rejected'
+ status: [{
+ type: 'rejected'
+ }]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
- status: 'accepted'
+ status: [{
+ type: 'accepted'
+ }]
}];
const users = [{
@@ -316,10 +354,8 @@ describe('/api/v1/comments/:comment_id/actions', () => {
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
- expect(res.body).to.have.property('item_type', 'comment');
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('item_id', 'abc');
- expect(res.body).to.have.property('user_id', '456');
});
});
});
diff --git a/tests/routes/api/queue/index.js b/tests/routes/api/queue/index.js
index 1e99ae61e..d64423013 100644
--- a/tests/routes/api/queue/index.js
+++ b/tests/routes/api/queue/index.js
@@ -15,56 +15,54 @@ const User = require('../../../../models/user');
const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre'};
-beforeEach(() => {
- return Setting.create(settings);
-});
+describe('/api/v1/queue', () => {
+ const comments = [{
+ id: 'abc',
+ body: 'comment 10',
+ asset_id: 'asset',
+ author_id: '123',
+ status: [{
+ type: 'rejected'
+ }]
+ }, {
+ id: 'def',
+ body: 'comment 20',
+ asset_id: 'asset',
+ author_id: '456',
+ status: [{
+ type: 'premod'
+ }]
+ }, {
+ id: 'hij',
+ body: 'comment 30',
+ asset_id: '456',
+ status: [{
+ type: 'accepted'
+ }]
+ }];
-describe('Get moderation queues rejected, pending, flags', () => {
+ const users = [{
+ displayName: 'Ana',
+ email: 'ana@gmail.com',
+ password: '123'
+ }, {
+ displayName: 'Maria',
+ email: 'maria@gmail.com',
+ password: '123'
+ }];
- describe('/api/v1/queue', () => {
- let comments;
+ const actions = [{
+ action_type: 'flag',
+ item_id: 'abc',
+ item_type: 'comment'
+ }, {
+ action_type: 'like',
+ item_id: 'hij',
+ item_type: 'comment'
+ }];
- const users = [{
- id: '456',
- displayName: 'Ana',
- email: 'ana@gmail.com',
- password: '123'
- }, {
- id: '123',
- displayName: 'Maria',
- email: 'maria@gmail.com',
- password: '123'
- }];
-
- let actions;
-
- beforeEach(() => {
-
- comments = [{
- id: 'abc',
- body: 'comment 10',
- asset_id: 'asset',
- status: 'rejected'
- }, {
- id: 'def',
- body: 'comment 20',
- asset_id: 'asset'
- }, {
- id: 'hij',
- body: 'comment 30',
- asset_id: '456',
- status: 'accepted'
- }];
-
- actions = [{
- action_type: 'flag',
- item_type: 'comment'
- }, {
- action_type: 'like',
- item_type: 'comment'
- }];
-
- return User.createLocalUsers(users)
+ beforeEach(() => {
+ return User.createLocalUsers(users)
.then((u) => {
comments[0].author_id = u[0].id;
comments[1].author_id = u[1].id;
@@ -76,22 +74,24 @@ describe('Get moderation queues rejected, pending, flags', () => {
actions[0].item_id = c[0].id;
actions[1].item_id = c[1].id;
- return Action.create(actions);
+ return Promise.all([
+ Action.create(actions),
+ Setting.init(settings)
+ ]);
});
- });
+ });
- it('should return all the pending comments, users and actions', function(done){
- chai.request(app)
- .get('/api/v1/queue/comments/pending')
- .set(passport.inject({roles: ['admin']}))
- .end(function(err, res){
- expect(err).to.be.null;
- expect(res).to.have.status(200);
- expect(res.body.comments[0]).to.have.property('body');
- expect(res.body.users[0]).to.have.property('displayName');
- expect(res.body.actions[0]).to.have.property('action_type');
- done();
- });
- });
+ it('should return all the pending comments, users and actions', function(done){
+ chai.request(app)
+ .get('/api/v1/queue/comments/pending')
+ .set(passport.inject({roles: ['admin']}))
+ .end(function(err, res){
+ expect(err).to.be.null;
+ expect(res).to.have.status(200);
+ expect(res.body.comments[0]).to.have.property('body');
+ expect(res.body.users[0]).to.have.property('displayName');
+ expect(res.body.actions[0]).to.have.property('action_type');
+ done();
+ });
});
});
diff --git a/tests/routes/api/settings/index.js b/tests/routes/api/settings/index.js
index d1a7ba81b..318af81f4 100644
--- a/tests/routes/api/settings/index.js
+++ b/tests/routes/api/settings/index.js
@@ -12,7 +12,7 @@ const defaults = {id: '1', moderation: 'pre'};
describe('/api/v1/settings', () => {
- beforeEach(() => Setting.create(defaults));
+ beforeEach(() => Setting.init(defaults));
describe('#get', () => {
@@ -40,7 +40,7 @@ describe('/api/v1/settings', () => {
.then((res) => {
expect(res).to.have.status(204);
- return Setting.getSettings();
+ return Setting.retrieve();
})
.then((settings) => {
expect(settings).to.have.property('moderation', 'post');
diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js
index 6484fdc12..a128938ba 100644
--- a/tests/routes/api/stream/index.js
+++ b/tests/routes/api/stream/index.js
@@ -20,7 +20,37 @@ describe('/api/v1/stream', () => {
moderation: 'post'
};
- let comments;
+ const comments = [{
+ id: 'abc',
+ body: 'comment 10',
+ author_id: '',
+ parent_id: '',
+ status: [{
+ type: 'accepted'
+ }]
+ }, {
+ id: 'def',
+ body: 'comment 20',
+ author_id: '',
+ parent_id: '',
+ status: []
+ }, {
+ id: 'uio',
+ body: 'comment 30',
+ asset_id: 'asset',
+ author_id: '456',
+ parent_id: '',
+ status: [{
+ type: 'accepted'
+ }]
+ }, {
+ id: 'hij',
+ body: 'comment 40',
+ asset_id: '456',
+ status: [{
+ type: 'rejected'
+ }]
+ }];
const users = [{
displayName: 'Ana',
@@ -41,33 +71,6 @@ describe('/api/v1/stream', () => {
}];
beforeEach(() => {
-
- comments = [{
- id: 'abc',
- body: 'comment 10',
- author_id: '',
- parent_id: '',
- status: 'accepted'
- }, {
- id: 'def',
- body: 'comment 20',
- author_id: '',
- parent_id: '',
- status: ''
- }, {
- id: 'uio',
- body: 'comment 30',
- asset_id: 'asset',
- author_id: '456',
- parent_id: '',
- status: 'accepted'
- }, {
- id: 'hij',
- body: 'comment 40',
- asset_id: '456',
- status: 'rejected'
- }];
-
return Promise.all([
User.createLocalUsers(users),
Asset.findOrCreateByUrl('http://test.com'),
@@ -94,10 +97,11 @@ describe('/api/v1/stream', () => {
return Promise.all([
Comment.create(comments),
Action.create(actions),
- Setting.init().then(() => Setting.updateSettings(settings))
+ Setting.init(settings)
]);
});
});
+
it('should return a stream with comments, users and actions for an existing asset', () => {
return chai.request(app)
.get('/api/v1/stream')
diff --git a/tests/utils/e2e-mongoose.js b/tests/utils/e2e-mongoose.js
new file mode 100644
index 000000000..1bb7e695b
--- /dev/null
+++ b/tests/utils/e2e-mongoose.js
@@ -0,0 +1,34 @@
+const mongoose = require('../../mongoose');
+
+// Ensure the NODE_ENV is set to 'test',
+// this is helpful when you would like to change behavior when testing.
+function clearDB() {
+ // console.log('Clearing DB', mongoose.connection);
+ for (let i in mongoose.connection.collections) {
+ // console.log('Clearing', i);
+ mongoose.connection.collections[i].remove(function() {});
+ }
+}
+
+module.exports = {
+ before: () => {
+ clearDB();
+ },
+ beforeEach: () => {
+ if (mongoose.connection.readyState === 0) {
+ mongoose.on('open', function() {
+ if (err) {
+ throw err;
+ }
+
+ return clearDB();
+ });
+ } else {
+ return clearDB();
+ }
+ },
+ after: () => {
+ clearDB();
+ mongoose.disconnect();
+ }
+};
diff --git a/views/article.ejs b/views/article.ejs
index 6fd420f82..f4d7ef910 100644
--- a/views/article.ejs
+++ b/views/article.ejs
@@ -15,6 +15,7 @@
width:500px;
}
+ <%= title %>
@@ -35,7 +36,7 @@