Merge branch 'graphql' of github.com:coralproject/talk into graphql

This commit is contained in:
David Jay
2017-01-24 14:19:58 -05:00
60 changed files with 1845 additions and 2512 deletions
+3 -3
View File
@@ -8,7 +8,7 @@ const program = require('commander');
const pkg = require('../package.json');
const parseDuration = require('parse-duration');
const Table = require('cli-table');
const Asset = require('../models/asset');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
@@ -22,7 +22,7 @@ util.onshutdown([
* Lists all the assets registered in the database.
*/
function listAssets() {
Asset
AssetModel
.find({})
.sort({'created_at': 1})
.then((asset) => {
@@ -56,7 +56,7 @@ function refreshAssets(ageString) {
const ageMs = parseDuration(ageString);
const age = new Date(now - ageMs);
Asset.find({
AssetModel.find({
$or: [
{
scraped: {
+3 -3
View File
@@ -6,7 +6,7 @@
const program = require('commander');
const mongoose = require('../services/mongoose');
const Setting = require('../models/setting');
const SettingsService = require('../services/settings');
const util = require('../util');
// Register the shutdown criteria.
@@ -24,8 +24,8 @@ program
.action(() => {
const defaults = {id: '1', moderation: 'pre'};
Setting
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
SettingsService
.init(defaults)
.then(() => {
console.log('Created settings object.');
util.shutdown();
+22 -19
View File
@@ -7,7 +7,8 @@
const program = require('commander');
const pkg = require('../package.json');
const prompt = require('prompt');
const User = require('../models/user');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const mongoose = require('../services/mongoose');
const util = require('../util');
const Table = require('cli-table');
@@ -76,13 +77,15 @@ function createUser(options) {
});
})
.then((result) => {
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
return UsersService.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
if (result.role && result.role.length > 0) {
return User
.addRoleToUser(user.id, result.role.trim())
let role = result.role ? result.role.trim() : null;
if (role && role.length > 0) {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
util.shutdown();
@@ -102,7 +105,7 @@ function createUser(options) {
* Deletes a user.
*/
function deleteUser(userID) {
User
UserModel
.findOneAndRemove({
id: userID
})
@@ -148,7 +151,7 @@ function passwd(userID) {
return;
}
User
UsersService
.changePassword(userID, result.password)
.then(() => {
console.log('Password changed.');
@@ -168,7 +171,7 @@ function updateUser(userID, options) {
const updates = [];
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID,
'profiles.provider': 'local'
}, {
@@ -181,7 +184,7 @@ function updateUser(userID, options) {
}
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID
}, {
$set: {
@@ -208,7 +211,7 @@ function updateUser(userID, options) {
* Lists all the users registered in the database.
*/
function listUsers() {
User
UsersService
.all()
.then((users) => {
let table = new Table({
@@ -248,7 +251,7 @@ function listUsers() {
* @param {String} srcUserID id of the user to which is the source of the merge
*/
function mergeUsers(dstUserID, srcUserID) {
User
UsersService
.mergeUsers(dstUserID, srcUserID)
.then(() => {
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
@@ -266,7 +269,7 @@ function mergeUsers(dstUserID, srcUserID) {
* @param {String} role the role to add
*/
function addRole(userID, role) {
User
UsersService
.addRoleToUser(userID, role)
.then(() => {
console.log(`Added the ${role} role to User ${userID}.`);
@@ -284,7 +287,7 @@ function addRole(userID, role) {
* @param {String} role the role to remove
*/
function removeRole(userID, role) {
User
UsersService
.removeRoleFromUser(userID, role)
.then(() => {
console.log(`Removed the ${role} role from User ${userID}.`);
@@ -301,8 +304,8 @@ function removeRole(userID, role) {
* @param {String} userID id of the user to ban
*/
function ban(userID) {
User
.setStatus(userID, 'banned', '')
UsersService
.setStatus(userID, 'BANNED')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
@@ -318,8 +321,8 @@ function ban(userID) {
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
User
.setStatus(userID, 'active', '')
UsersService
.setStatus(userID, 'ACTIVE')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
@@ -335,7 +338,7 @@ function unban(userID) {
* @param {String} userID the ID of a user to disable
*/
function disableUser(userID) {
User
UsersService
.disableUser(userID)
.then(() => {
console.log(`User ${userID} was disabled.`);
@@ -352,7 +355,7 @@ function disableUser(userID) {
* @param {String} userID the ID of a user to enable
*/
function enableUser(userID) {
User
UsersService
.enableUser(userID)
.then(() => {
console.log(`User ${userID} was enabled.`);
+1 -1
View File
@@ -11,7 +11,7 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
+1 -1
View File
@@ -132,7 +132,7 @@ class Comment extends React.Component {
{
this.state.replyBoxVisible
? <ReplyBox
replyPostedHandler={() => {
commentPostedHandler={() => {
console.log('replyPostedHandler');
this.setState({replyBoxVisible: false});
refetch();
+1 -1
View File
@@ -120,7 +120,7 @@ class Embed extends Component {
{
user
? <CommentBox
refetch={refetch}
commentPostedHandler={refetch}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
@@ -1,3 +1,3 @@
import Pym from 'pym.js';
import Pym from '../../../node_modules/pym.js';
export default new Pym.Child({polling: 100});
+2 -2
View File
@@ -27,7 +27,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
@@ -132,7 +132,7 @@ export const checkLogin = () => dispatch => {
throw new Error('Not logged in');
}
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
+4 -4
View File
@@ -11,7 +11,7 @@ class CommentBox extends Component {
// updateItem: PropTypes.func,
// comments: PropTypes.array,
replyPostedHandler: PropTypes.func,
commentPostedHandler: PropTypes.func,
postItem: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
parentId: PropTypes.string,
@@ -32,7 +32,7 @@ class CommentBox extends Component {
// child_id,
// updateItem,
// appendItemArray,
replyPostedHandler,
commentPostedHandler,
postItem,
assetId,
parentId,
@@ -79,8 +79,8 @@ class CommentBox extends Component {
addNotification('success', 'Your comment has been posted.');
}
if (replyPostedHandler) {
replyPostedHandler();
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => console.error(err));
+3 -3
View File
@@ -3,10 +3,10 @@ import CommentBox from '../coral-plugin-commentbox/CommentBox';
const name = 'coral-plugin-replies';
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, replyPostedHandler}) => (
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler}) => (
<div className={`${name}-textarea`} style={styles && styles.container}>
<CommentBox
replyPostedHandler={replyPostedHandler}
commentPostedHandler={commentPostedHandler}
parentId={parentId}
addNotification={addNotification}
authorId={authorId}
@@ -17,7 +17,7 @@ const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentI
);
ReplyBox.propTypes = {
replyPostedHandler: PropTypes.func,
commentPostedHandler: PropTypes.func,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
authorId: PropTypes.string.isRequired,
+3 -2
View File
@@ -2,7 +2,7 @@ const DataLoader = require('dataloader');
const util = require('./util');
const Action = require('../../models/action');
const ActionsService = require('../../services/actions');
/**
* Looks up actions based on the requested id's all bounded by the user.
@@ -11,7 +11,8 @@ const Action = require('../../models/action');
* @return {Promise} resolves to the promises of the requested actions
*/
const genActionSummariessByItemID = ({user = {}}, item_ids) => {
return Action.getActionSummaries(item_ids, user.id)
return ActionsService
.getActionSummaries(item_ids, user.id)
.then(util.arrayJoinBy(item_ids, 'item_id'));
};
+5 -4
View File
@@ -5,14 +5,15 @@ const errors = require('../../errors');
const scraper = require('../../services/scraper');
const util = require('./util');
const Asset = require('../../models/asset');
const AssetModel = require('../../models/asset');
const AssetsService = require('../../services/assets');
/**
* Retrieves assets by an array of ids.
* @param {Object} context the context of the request
* @param {Array} ids array of ids to lookup
*/
const genAssetsByID = (context, ids) => Asset.find({
const genAssetsByID = (context, ids) => AssetModel.find({
id: {
$in: ids
}
@@ -32,7 +33,7 @@ const findOrCreateAssetByURL = (context, asset_url) => {
return Promise.reject(errors.ErrInvalidAssetURL);
}
return Asset.findOrCreateByUrl(asset_url)
return AssetsService.findOrCreateByUrl(asset_url)
.then((asset) => {
// If the asset wasn't scraped before, scrape it! Otherwise just return
@@ -58,6 +59,6 @@ module.exports = (context) => ({
getByURL: (url) => findOrCreateAssetByURL(context, url),
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
getAll: new util.SingletonResolver(() => Asset.find({}))
getAll: new util.SingletonResolver(() => AssetModel.find({}))
}
});
+9 -8
View File
@@ -2,14 +2,15 @@ const DataLoader = require('dataloader');
const util = require('./util');
const Action = require('../../models/action');
const Comment = require('../../models/comment');
const ActionModel = require('../../models/action');
const CommentModel = require('../../models/comment');
const CommentsService = require('../../services/comments');
/**
* Retrieves comments by an array of asset id's.
* @param {Array} ids array of ids to lookup
*/
const genCommentsByAssetID = (context, ids) => Comment.find({
const genCommentsByAssetID = (context, ids) => CommentModel.find({
asset_id: {
$in: ids
},
@@ -23,7 +24,7 @@ const genCommentsByAssetID = (context, ids) => Comment.find({
* Retrieves comments by an array of parent ids.
* @param {Array} ids array of ids to lookup
*/
const genCommentsByParentID = (context, ids) => Comment.find({
const genCommentsByParentID = (context, ids) => CommentModel.find({
parent_id: {
$in: ids
},
@@ -39,7 +40,7 @@ const getCommentsByStatusAndAssetID = (context, {status = null, asset_id = null}
status = status.toLowerCase();
}
return Comment.moderationQueue(status, asset_id);
return CommentsService.moderationQueue(status, asset_id);
};
const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = null}) => {
@@ -49,13 +50,13 @@ const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = nul
action_type = action_type.toLowerCase();
}
return Action.find({
return ActionModel.find({
action_type,
// TODO: remove when we move the enum over to the uppercase.
item_type: 'comments'
}).then((actions) => {
let comments = Comment.find({
let comments = CommentModel.find({
id: {
$in: actions.map((action) => action.item_id)
}
@@ -69,7 +70,7 @@ const getCommentsByActionTypeAndAssetID = (context, {action_type, asset_id = nul
});
};
const genCommentsByAuthorID = (context, authorIDs) => Comment.find({
const genCommentsByAuthorID = (context, authorIDs) => CommentModel.find({
author_id: {
$in: authorIDs
}
+2 -2
View File
@@ -1,4 +1,4 @@
const Settings = require('../../models/setting');
const SettingsService = require('../../services/settings');
const util = require('./util');
@@ -8,5 +8,5 @@ const util = require('./util');
* @return {Object} object of loaders
*/
module.exports = () => ({
Settings: new util.SingletonResolver(() => Settings.retrieve())
Settings: new util.SingletonResolver(() => SettingsService.retrieve())
});
+2 -2
View File
@@ -1,8 +1,8 @@
const DataLoader = require('dataloader');
const User = require('../../models/user');
const UsersService = require('../../services/users');
const genUserByIDs = (context, ids) => User.findByIdArray(ids);
const genUserByIDs = (context, ids) => UsersService.findByIdArray(ids);
/**
* Creates a set of loaders based on a GraphQL context.
+4 -3
View File
@@ -1,4 +1,5 @@
const Action = require('../../models/action');
const ActionModel = require('../../models/action');
const ActionsService = require('../../services/actions');
/**
* Creates an action on a item.
@@ -9,7 +10,7 @@ const Action = require('../../models/action');
* @return {Promise} resolves to the action created
*/
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
return Action.insertUserAction({
return ActionsService.insertUserAction({
item_id,
item_type,
user_id: user.id,
@@ -25,7 +26,7 @@ const createAction = ({user = {}}, {item_id, item_type, action_type, metadata =
* @return {Promise} resolves when the action is deleted
*/
const deleteAction = ({user}, {id}) => {
return Action.remove({
return ActionModel.remove({
id,
user_id: user.id
});
+6 -5
View File
@@ -1,6 +1,7 @@
const errors = require('../../errors');
const Asset = require('../../models/asset');
const Comment = require('../../models/comment');
const AssetsService = require('../../services/assets');
const CommentsService = require('../../services/comments');
const Wordlist = require('../../services/wordlist');
@@ -14,7 +15,7 @@ const Wordlist = require('../../services/wordlist');
* @return {Promise} resolves to the created comment
*/
const createComment = ({user}, {body, asset_id, parent_id = null}, status = null) => {
return Comment.publicCreate({
return CommentsService.publicCreate({
body,
asset_id,
parent_id,
@@ -58,8 +59,8 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
if (wordlist.banned) {
status = Promise.resolve('rejected');
} else {
status = Asset
.rectifySettings(Asset.findById(asset_id).then((asset) => {
status = AssetsService
.rectifySettings(AssetsService.findById(asset_id).then((asset) => {
if (!asset) {
return Promise.reject(errors.ErrNotFound);
}
+2 -2
View File
@@ -1,4 +1,4 @@
const User = require('../../models/user');
const UsersService = require('../../services/users');
/**
* Updates a users settings.
@@ -7,7 +7,7 @@ const User = require('../../models/user');
* @return {Promise}
*/
const updateUserSettings = ({user}, {bio}) => {
return User.updateSettings(user.id, {bio});
return UsersService.updateSettings(user.id, {bio});
};
module.exports = (context) => {
+9 -2
View File
@@ -1,7 +1,14 @@
const Setting = require('./models/setting');
const SettingsService = require('./services/settings');
module.exports = () => Promise.all([
// Upsert the settings object.
Setting.init({id: '1', moderation: 'pre', wordlist: {banned: [], suspect: []}})
SettingsService.init({
id: '1',
moderation: 'pre',
wordlist: {
banned: [],
suspect: []
}
})
]);
+5 -1
View File
@@ -17,7 +17,11 @@ const ErrNotAuthorized = require('../errors').ErrNotAuthorized;
* @return {Boolean} true if the user has all the roles required, false
* otherwise
*/
authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf(role) >= 0);
authorization.has = (user, ...roles) => roles.every((role) => {
// TODO: remove toUpperCase once we've migrated over the roles.
return user.roles.indexOf(role.toUpperCase()) >= 0;
});
/**
* needed is a connect middleware layer that ensures that all requests coming
-74
View File
@@ -1,74 +0,0 @@
// The maximum depth to recurse into nested objects checking for mongoose
// objects.
const maxRecursion = 3;
/**
* Middleware to wrap the `res.json` function to ensure that we can filter the
* payload response first based on user and role.
*/
module.exports = (req, res, next) => {
/**
* Updates the original document based on filtering out for roles.
* @param {Mixed} o original object to be modified
* @param {Integer} l current level of depth in the first object
* @return {Mixed} (possibly) modified object
*/
const wrap = (o, d = 0) => {
if (d > maxRecursion) {
return o;
}
// If this is an array, we need to walk over all the object's elements.
if (Array.isArray(o)) {
// Map each of the array elements.
return o.map((ob) => wrap(ob, d + 1));
} else if (o && o.constructor && o.constructor.name === 'model') {
// The object here is definitly a mongoose model.
// Check to see if it has a `filterForUser` method.
if (typeof o.filterForUser === 'function') {
// The object here actually has the `filterForUser` function, so filter
// the object!
o = o.filterForUser(req.user);
}
} else if (typeof o === 'object') {
// Iterate over the props, find only properties owned by the object.
for (let prop in o) {
// If and only if the object owns the property do we actually pull the
// property out.
if (typeof o.hasOwnProperty === 'function' && o.hasOwnProperty(prop)) {
// Wrap the property with one more layer down.
o[prop] = wrap(o[prop], d + 1);
}
}
}
return o;
};
// Save a reference to the original json function.
const json = res.json;
// Override the original json function.
res.json = (payload) => {
// Restore the old pointer.
res.json = json;
// Send it down the pipe after we've filtered it.
res.json(wrap(payload));
};
// Now that we've overridden the `res.json`, let's hand it down.
next();
};
-30
View File
@@ -1,30 +0,0 @@
These are some settings we're planning on implementing in the future.
I'm keeping them in this file for reference
```javascript
anonymous_users: {type: Boolean, default: false},
block_mute_enabled: {type: Boolean, default: false},
comment_count: {type: Boolean, default: false},
comment_editing_enabled: {type: Boolean, default: false},
comments_hidden: {type: Boolean, default: false},
community_guidelines: {type: Boolean, default: false},
detailed_flags: {type: Boolean, default: false},
emojis_enabled: {type: Boolean, default: false},
following: {type: Boolean, default: false},
likes_enabled: {type: Boolean, default: false},
mentions: {type: Boolean, default: false},
nested_replies: {type: Boolean, default: false},
notification_timeout: {type: Number, default: 4500},
permalinks: {type: Boolean, default: false},
post_button_text: {type: String, default: 'Post'},
pseudonyms: {type: Boolean, default: false},
public_profile: {type: Boolean, default: false},
reactions_enabled: {type: Boolean, default: false},
reply_button_text: {type: String, default: 'Reply'},
rich_content: {type: Boolean, default: false},
show_staff_picks: {type: Boolean, default: false},
up_down_voting: {type: Boolean, default: false},
user_badges: {type: Boolean, default: false},
user_mods_enabled: {type: Boolean, default: false},
user_stats_enabled: {type: Boolean, default: false}
```
+19 -175
View File
@@ -1,16 +1,32 @@
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const Schema = mongoose.Schema;
const ACTION_TYPES = [
'LIKE',
'FLAG'
];
const ITEM_TYPES = [
'ASSETS',
'COMMENTS',
'USERS'
];
const ActionSchema = new Schema({
id: {
type: String,
default: uuid.v4,
unique: true
},
action_type: String,
item_type: String,
action_type: {
type: String,
enum: ACTION_TYPES
},
item_type: {
type: String,
enum: ITEM_TYPES
},
item_id: String,
user_id: String,
metadata: Schema.Types.Mixed
@@ -21,178 +37,6 @@ const ActionSchema = new Schema({
}
});
/**
* Finds an action by the id.
* @param {String} id identifier of the action (uuid)
*/
ActionSchema.statics.findById = function(id) {
return Action.findOne({id});
};
/**
* Add an action.
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
ActionSchema.statics.insertUserAction = (action) => {
// Actions are made unique by using a query that can be reproducable, i.e.,
// not containing user inputable values.
let query = {
action_type: action.action_type,
item_type: action.item_type,
item_id: action.item_id,
user_id: action.user_id
};
// Create/Update the action.
return Action.findOneAndUpdate(query, action, {
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
};
/**
* Finds actions in an array of ids.
* @param {String} ids array of user identifiers (uuid)
*/
ActionSchema.statics.findByItemIdArray = function(item_ids) {
return Action.find({
'item_id': {$in: item_ids}
});
};
/**
* Fetches the action summaries for the given asset, and comments around the
* given user id.
* @param {[type]} asset_id [description]
* @param {[type]} comments [description]
* @param {String} [current_user_id=''] [description]
* @return {[type]} [description]
*/
ActionSchema.statics.getActionSummariesFromComments = (asset_id = '', comments, current_user_id = '') => {
// 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)
*/
ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = '') {
return Action.aggregate([
{
// only grab items that match the specified item id's
$match: {
item_id: {
$in: item_ids
}
}
},
{
$group: {
// group unique documents by these properties, we are leveraging the
// fact that each uuid is completly unique.
_id: {
item_id: '$item_id',
action_type: '$action_type'
},
// and sum up all actions matching the above grouping criteria
count: {
$sum: 1
},
// we are leveraging the fact that each uuid is completly unique and
// just grabbing the last instance of the item type here.
item_type: {
$last: '$item_type'
},
current_user: {
$max: {
$cond: {
if: {
$eq: ['$user_id', current_user_id],
},
then: '$$CURRENT',
else: null
}
}
}
}
},
{
$project: {
// suppress the _id field
_id: false,
// map the fields from the _id grouping down a level
item_id: '$_id.item_id',
action_type: '$_id.action_type',
// map the field directly
count: '$count',
item_type: '$item_type',
// set the current user to false here
current_user: '$current_user'
}
}
]);
};
/*
* Finds all comments for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
ActionSchema.statics.findByType = function(action_type, item_type) {
return Action.find({
'action_type': action_type,
'item_type': item_type
});
};
/**
* Finds all comments ids for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_type) {
return Action.find({
'action_type': action_type,
'item_type': item_type
}, 'item_id');
};
const Action = mongoose.model('Action', ActionSchema);
module.exports = Action;
-94
View File
@@ -1,8 +1,5 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const Setting = require('./setting');
const uuid = require('uuid');
const AssetSchema = new Schema({
@@ -68,18 +65,6 @@ AssetSchema.index({
background: true
});
/**
* Finds an asset by its id.
* @param {String} id identifier of the asset (uuid).
*/
AssetSchema.statics.findById = (id) => Asset.findOne({id});
/**
* Finds a asset by its url.
* @param {String} url identifier of the asset (uuid).
*/
AssetSchema.statics.findByUrl = (url) => Asset.findOne({url});
/**
* Returns true if the asset is closed, false else.
*/
@@ -87,85 +72,6 @@ AssetSchema.virtual('isClosed').get(function() {
return this.closedAt && this.closedAt.getTime() <= new Date().getTime();
});
/**
* 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) {
settings.merge(asset.settings);
}
return settings;
});
/**
* Finds a asset by its url.
*
* NOTE: This function has scalability concerns regarding mongoose's decision
* always write {updated_at: new Date()} on every call to findOneAndUpdate
* even though the update document exactly matches the query document... In
* the future this function should never update, only findOneAndCreate but this
* is not possible with the mongoose driver.
*
* @param {String} url identifier of the asset (uuid).
* @return {Promise}
*/
AssetSchema.statics.findOrCreateByUrl = (url) => Asset.findOneAndUpdate({url}, {url}, {
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
/**
* Updates the settings for the asset.
* @param {[type]} id [description]
* @param {[type]} settings [description]
* @return {[type]} [description]
*/
AssetSchema.statics.overrideSettings = (id, settings) => Asset.findOneAndUpdate({id}, {
$set: {
settings
}
}, {
new: true
});
/**
* Finds assets matching keywords on the model. If `value` is an empty string,
* then it will not even perform a text search query.
* @param {String} value string to search by.
* @return {Promise}
*/
AssetSchema.statics.search = (value) => value.length === 0 ? Asset.find({}) : Asset.find({
$text: {
$search: value
}
});
/**
* Finds multiple assets with matching ids
* @param {Array} ids an array of Strings of asset_id
* @return {Promise} resolves to list of Assets
*/
AssetSchema.statics.findMultipleById = function (ids) {
const query = ids.map(id => ({id}));
return Asset.find(query);
};
const Asset = mongoose.model('Asset', AssetSchema);
module.exports = Asset;
+7 -229
View File
@@ -1,8 +1,12 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const uuid = require('uuid');
const Action = require('./action');
const STATUSES = [
'ACCEPTED',
'REJECTED',
'PREMOD',
];
/**
* The Mongo schema for a Comment Status.
@@ -11,11 +15,7 @@ const Action = require('./action');
const StatusSchema = new Schema({
type: {
type: String,
enum: [
'accepted',
'rejected',
'premod',
],
enum: STATUSES,
},
// The User ID of the user that assigned the status.
@@ -56,228 +56,6 @@ const CommentSchema = new Schema({
}
});
/**
* toJSON overrides to remove fields from the json
* output.
*/
CommentSchema.options.toJSON = {};
CommentSchema.options.toJSON.virtuals = true;
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;
};
/**
* Filters the object for the given user only allowing those with the allowed
* roles/permissions to access particular parameters.
*/
CommentSchema.method('filterForUser', function(user = false) {
if (!user || !user.roles.includes('admin')) {
return _.pick(this.toJSON(), ['id', 'body', 'asset_id', 'author_id', 'parent_id', 'status']);
}
return this.toJSON();
});
/**
* 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 = null,
author_id
} = comment;
comment = new Comment({
body,
asset_id,
parent_id,
status_history: status ? [{
type: status,
created_at: new Date()
}] : [],
status,
author_id
});
return comment.save();
};
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
* @return {Promise}
*/
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 = (asset_id) => Comment.find({
asset_id
});
/**
* findByAssetIdWithStatuses finds all the comments where the asset id matches
* what's provided and the status is one of the ones listed in the statuses
* array.
* @param {String} asset_id the asset id to search by
* @param {Array} [statuses=[]] the array of statuses to search by
* @return {Promise} resolves to an array of comments
*/
CommentSchema.statics.findByAssetIdWithStatuses = (asset_id, statuses = []) => Comment.find({
asset_id,
status: {
$in: statuses
}
});
/**
* 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 = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comment')
.then((actions) => Comment.find({
id: {
$in: actions.map((a) => a.item_id)
}
}));
/**
* 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
* @return {Promise}
*/
CommentSchema.statics.findIdsByActionType = (action_type) => Action
.findCommentsIdByActionType(action_type, 'comments')
.then((actions) => actions.map(a => a.item_id));
/**
* Find comments by current status
* @param {String} status status of the comment to search for
* @return {Promise} resovles to comment array
*/
CommentSchema.statics.findByStatus = (status = null) => {
return Comment.find({status});
};
/**
* Find comments that need to be moderated (aka moderation queue).
* @param {String} asset_id
* @return {Promise}
*/
CommentSchema.statics.moderationQueue = (status, asset_id = null) => {
// Fetch the comments with statuses.
let comments = Comment.findByStatus(status);
if (asset_id) {
comments = comments.where('asset_id', asset_id);
}
return comments;
};
/**
* 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.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
$push: {
status_history: {
type: status,
created_at: new Date(),
assigned_by
}
},
$set: {status}
});
/**
* Add an action to the comment.
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
CommentSchema.statics.addAction = (item_id, user_id, action_type, metadata) => Action.insertUserAction({
item_id,
item_type: 'comments',
user_id,
action_type,
metadata
});
/**
* 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 = (id) => Comment.remove({id});
/**
* Remove an action from the comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} action_type the type of the action to be removed
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
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 = () => Comment.find();
/**
* Returns all the comments by user
* probably to be paginated at some point in the future
* @return {Promise} array resolves to an array of comments by that user
*/
CommentSchema.statics.findByUserId = function (author_id, admin = false) {
// do not return un-published comments for non-admins
let query = {author_id};
if (!admin) {
query.$nor = [{status: 'premod'}, {status: 'rejected'}];
}
return Comment.find(query);
};
// Comment model.
const Comment = mongoose.model('Comment', CommentSchema);
+1 -71
View File
@@ -1,6 +1,5 @@
const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const _ = require('lodash');
const WordlistSchema = new Schema({
banned: [String],
@@ -64,12 +63,6 @@ const SettingSchema = new Schema({
}
});
/**
* toJSON provides settings overrides to this object's serialization methods.
*/
SettingSchema.options.toJSON = {};
SettingSchema.options.toJSON.virtuals = true;
/**
* Merges two settings objects.
*/
@@ -88,72 +81,9 @@ SettingSchema.method('merge', function(src) {
});
});
/**
* Filters the object for the given user only allowing those with the allowed
* roles/permissions to access particular parameters.
*/
SettingSchema.method('filterForUser', function(user = false) {
if (!user || !user.roles.includes('admin')) {
return _.pick(this.toJSON(), [
'moderation',
'infoBoxEnable',
'infoBoxContent',
'closeTimeout',
'closedMessage',
'charCountEnable',
'charCount',
'requireEmailConfirmation'
]);
}
return this.toJSON();
});
/**
* The Mongo Mongoose object.
*/
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'};
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
SettingService.retrieve = () => 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.
*/
SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
$set: settings
}, {
upsert: true,
new: true,
setDefaultsOnInsert: true
});
/**
* 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);
};
module.exports = Setting;
+22 -644
View File
@@ -1,40 +1,18 @@
const mongoose = require('../services/mongoose');
const uuid = require('uuid');
const _ = require('lodash');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Action = require('./action');
const Comment = require('./comment');
const Wordlist = require('../services/wordlist');
const errors = require('../errors');
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
// through during the salting process.
const SALT_ROUNDS = 10;
// USER_ROLES is the array of roles that is permissible as a user role.
const USER_ROLES = [
'admin',
'moderator'
'ADMIN',
'MODERATOR'
];
// USER_STATUSES is the list of statuses that are permitted for the user status.
// USER_STATUS is the list of statuses that are permitted for the user status.
const USER_STATUS = [
'active',
'banned'
'ACTIVE',
'BANNED'
];
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
// set the process.env.TALK_SESSION_SECRET.
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
process.env.TALK_SESSION_SECRET = 'keyboard cat';
} else if (!process.env.TALK_SESSION_SECRET) {
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
}
// ProfileSchema is the mongoose schema defined as the representation of a
// User's profile stored in MongoDB.
const ProfileSchema = new mongoose.Schema({
@@ -103,11 +81,18 @@ const UserSchema = new mongoose.Schema({
// Roles provides an array of roles (as strings) that is associated with a
// user.
roles: [String],
roles: [{
type: String,
enum: USER_ROLES
}],
// Status provides a string that says in which state the account is.
// When the account is banned, the user login is disabled.
status: {type: String, enum: USER_STATUS, default: 'active'},
status: {
type: String,
enum: USER_STATUS,
default: 'ACTIVE'
},
// User's settings
settings: {
@@ -134,627 +119,20 @@ UserSchema.index({
background: false
});
/**
* toJSON overrides to remove the password field from the json
* output.
*/
UserSchema.options.toJSON = {};
UserSchema.options.toJSON.hide = '_id password';
UserSchema.options.toJSON.virtuals = true;
UserSchema.options.toJSON.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* Filters the object for the given user only allowing those with the allowed
* roles/permissions to access particular parameters.
*/
UserSchema.method('filterForUser', function(user = false) {
if (!user || !user.roles.includes('admin')) {
let allowed = ['id', 'displayName', 'settings', 'created_at', 'updated_at'];
if (user && user.id === this.id) {
allowed.push('roles');
}
return _.pick(this.toJSON(), allowed);
}
return this.toJSON();
});
/**
* Returns true if the user has all the roles specified.
*/
UserSchema.method('hasRoles', function(...roles) {
return roles.every((role) => this.roles.indexOf(role) >= 0);
return roles.every((role) => {
// TODO: remove toUpperCase() once we've migrated usage.
return this.roles.indexOf(role.toUpperCase()) >= 0;
});
});
// Create the User model.
const UserModel = mongoose.model('User', UserSchema);
// UserService is the interface for the application to interact with the
// UserModel through.
const UserService = module.exports = {};
/**
* Finds a user given their email address that we have for them in the system
* and ensures that the retuned user matches the password passed in as well.
* @param {string} email - email to look up the user by
* @param {string} password - password to match against the found user
* @param {Function} done [description]
*/
UserService.findLocalUser = (email, password) => {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required for findLocalUser');
}
return UserModel.findOne({
profiles: {
$elemMatch: {
id: email.toLowerCase(),
provider: 'local'
}
}
})
.then((user) => {
if (!user) {
return false;
}
return new Promise((resolve, reject) => {
bcrypt.compare(password, user.password, (err, res) => {
if (err) {
return reject(err);
}
if (!res) {
return resolve(false);
}
return resolve(user);
});
});
});
};
/**
* Merges two users together by taking all the profiles on a given user and
* pushing them into the source user followed by deleting the destination user's
* user account. This will not merge the roles associated with the source user.
* @param {String} dstUserID id of the user to which is the target of the merge
* @param {String} srcUserID id of the user to which is the source of the merge
* @return {Promise} resolves when the users are merged
*/
UserService.mergeUsers = (dstUserID, srcUserID) => {
let srcUser, dstUser;
return Promise
.all([
UserModel.findOne({id: dstUserID}).exec(),
UserModel.findOne({id: srcUserID}).exec()
])
.then((users) => {
dstUser = users[0];
srcUser = users[1];
srcUser.profiles.forEach((profile) => {
dstUser.profiles.push(profile);
});
return srcUser.remove();
})
.then(() => dstUser.save());
};
/**
* Finds a user given a social profile and if the user does not exist, creates
* them.
* @param {Object} profile - User social/external profile
* @param {Function} done [description]
*/
UserService.findOrCreateExternalUser = (profile) => {
return UserModel
.findOne({
profiles: {
$elemMatch: {
id: profile.id,
provider: profile.provider
}
}
})
.then((user) => {
if (user) {
return user;
}
// The user was not found, lets create them!
user = new UserModel({
displayName: profile.displayName,
roles: [],
profiles: [
{
id: profile.id,
provider: profile.provider
}
]
});
return user.save();
});
};
UserService.changePassword = (id, password) => {
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
resolve(hashedPassword);
});
})
.then((hashedPassword) => {
return UserModel.update({id}, {
$inc: {__v: 1},
$set: {
password: hashedPassword
}
});
});
};
/**
* Creates local users.
* @param {Array} users Users to create
* @return {Promise} Resolves with the users that were created
*/
UserService.createLocalUsers = (users) => {
return Promise.all(users.map((user) => {
return UserService
.createLocalUser(user.email, user.password, user.displayName);
}));
};
/**
* Check the requested displayname for naughty words (currently in English) and special chars
* @param {String} displayName word to be checked for profanity
* @return {Promise} rejected if the machine's sensibilites are offended
*/
const isValidDisplayName = (displayName) => {
const onlyLettersNumbersUnderscore = /^[a-z0-9_]+$/;
if (!displayName) {
return Promise.reject(errors.ErrMissingDisplay);
}
if (!onlyLettersNumbersUnderscore.test(displayName)) {
return Promise.reject(errors.ErrSpecialChars);
}
// check for profanity
return Wordlist.displayNameCheck(displayName);
};
/**
* Creates the local user with a given email, password, and name.
* @param {String} email email of the new user
* @param {String} password plaintext password of the new user
* @param {String} displayName name of the display user
* @param {Function} done callback
*/
UserService.createLocalUser = (email, password, displayName) => {
if (!email) {
return Promise.reject(errors.ErrMissingEmail);
}
email = email.toLowerCase().trim();
displayName = displayName.toLowerCase().trim();
if (!password) {
return Promise.reject(errors.ErrMissingPassword);
}
if (password.length < 8) {
return Promise.reject(errors.ErrPasswordTooShort);
}
return isValidDisplayName(displayName)
.then(() => { // displayName is valid
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
let user = new UserModel({
displayName: displayName,
password: hashedPassword,
roles: [],
profiles: [
{
id: email,
provider: 'local'
}
]
});
user.save((err) => {
if (err) {
if (err.code === 11000) {
if (err.message.match('displayName')) {
return reject(errors.ErrDisplayTaken);
}
return reject(errors.ErrEmailTaken);
}
return reject(err);
}
return resolve(user);
});
});
});
});
};
/**
* Disables a given user account.
* @param {String} id id of a user
* @param {Function} done callback after the operation is complete
*/
UserService.disableUser = (id) => {
return UserModel.update({
id: id
}, {
$set: {
disabled: true
}
});
};
/**
* Enables a given user account.
* @param {String} id id of a user
* @param {Function} done callback after the operation is complete
*/
UserService.enableUser = (id) => {
return UserModel.update({
id: id
}, {
$set: {
disabled: false
}
});
};
/**
* Adds a role to a user.
* @param {String} id id of a user
* @param {String} role role to add
* @param {Function} done callback after the operation is complete
*/
UserService.addRoleToUser = (id, role) => {
// Check to see if the user role is in the allowable set of roles.
if (USER_ROLES.indexOf(role) === -1) {
// User role is not supported! Error out here.
return Promise.reject(new Error(`role ${role} is not supported`));
}
return UserModel.update({
id: id
}, {
$addToSet: {
roles: role
}
});
};
/**
* Removes a role from a user.
* @param {String} id id of a user
* @param {String} role role to remove
* @param {Function} done callback after the operation is complete
*/
UserService.removeRoleFromUser = (id, role) => {
return UserModel.update({
id: id
}, {
$pull: {
roles: role
}
});
};
/**
* Set status of a user.
* @param {String} id id of a user
* @param {String} status status to set
* @param {String} comment_id id of the comment that the user was ban for.
* @param {Function} done callback after the operation is complete
*/
UserService.setStatus = (id, status, comment_id) => {
// Check to see if the user status is in the allowable set of roles.
if (USER_STATUS.indexOf(status) === -1) {
// User status is not supported! Error out here.
return Promise.reject(new Error(`status ${status} is not supported`));
}
// If ban then reject the comment and update status
if (status === 'banned') {
return UserModel.update({
id: id
}, {
$set: {
status: status
}
})
.then(() => {
return Comment.pushStatus(comment_id, 'rejected', id);
});
}
if (status === 'active') {
return UserModel.update({
id: id
}, {
$set: {
status: status
}
});
}
};
/**
* Finds a user with the id.
* @param {String} id user id (uuid)
*/
UserService.findById = (id) => {
return UserModel.findOne({id});
};
/**
* Finds users in an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findByIdArray = (ids) => {
return UserModel.find({
'id': {$in: ids}
});
};
/**
* Finds public user information by an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findPublicByIdArray = (ids) => {
return UserModel.find({
'id': {$in: ids}
}, 'id displayName');
};
/**
* Creates a JWT from a user email. Only works for local accounts.
* @param {String} email of the local user
*/
UserService.createPasswordResetToken = function (email) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
.then((user) => {
if (!user) {
// Since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the
// endpoint.
return;
}
const payload = {
jti: uuid.v4(),
email,
userId: user.id,
version: user.__v
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
algorithm: 'HS256',
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
});
});
};
/**
* Verifies that the token was indeed signed by the session secret.
* @param {String} token JWT token from the client
* @return {Promise}
*/
UserService.verifyToken = (token, options = {}) => {
return new Promise((resolve, reject) => {
// Set the allowed algorithms.
options.algorithms = ['HS256'];
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
});
};
/**
* Verifies a jwt and returns the associated user.
* @param {String} token the JSON Web Token to verify
*/
UserService.verifyPasswordResetToken = (token) => {
return UserService
.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT
})
.then((decoded) => UserService.findById(decoded.userId));
};
/**
* Finds a user using a value which gets compared using a prefix match against
* the user's email address and/or their display name.
* @param {String} value value to search by
* @return {Promise}
*/
UserService.search = (value) => {
return UserModel.find({
$or: [
// Search by a prefix match on the displayName.
{
'displayName': {
$regex: new RegExp(`^${value}`),
$options: 'i'
}
},
// Search by a prefix match on the email address.
{
'profiles': {
$elemMatch: {
id: {
$regex: new RegExp(`^${value}`),
$options: 'i'
},
provider: 'local'
}
}
}
]
});
};
/**
* Returns a count of the current users.
* @return {Promise}
*/
UserService.count = () => UserModel.count();
/**
* Returns all the users.
* @return {Promise}
*/
UserService.all = () => UserModel.find();
/**
* Updates the user's settings.
* @return {Promise}
*/
UserService.updateSettings = (id, settings) => UserModel.update({
id
}, {
$set: {
settings
}
});
/**
* Add an action to the user.
* @param {String} item_id identifier of the user (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the user
* @return {Promise}
*/
UserService.addAction = (item_id, user_id, action_type, metadata) => Action.insertUserAction({
item_id,
item_type: 'users',
user_id,
action_type,
metadata
});
/**
* This creates a token based around confirming the local profile.
* @param {String} userID The user id for the user that we are creating the
* token for.
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
UserService.createEmailConfirmToken = (userID, email) => {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return UserService
.findById(userID)
.then((user) => {
if (!user) {
return Promise.reject(new Error('user not found'));
}
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
return Promise.reject(new Error('email address already confirmed'));
}
const payload = {
email,
userID
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
});
};
/**
* This verifies that a given token was for the email confirmation and updates
* that user's profile with a 'confirmed_at' parameter with the current date.
* @param {String} token the token containing the email confirmation details
* signed with our secret.
* @return {Promise}
*/
UserService.verifyEmailConfirmation = (token) => {
return UserService
.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email}) => {
return UserModel
.update({
id: userID,
profiles: {
$elemMatch: {
id: email,
provider: 'local'
}
}
}, {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
});
});
};
module.exports = UserModel;
module.exports.USER_ROLES = USER_ROLES;
module.exports.USER_STATUS = USER_STATUS;
+1 -1
View File
@@ -11,7 +11,7 @@ router.get('/password-reset', (req, res) => {
});
router.get('/login', (req, res, next) => {
res.render('login');
res.render('admin/login');
});
router.get('*', (req, res) => {
+7 -6
View File
@@ -1,6 +1,6 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const UsersService = require('../../../services/users');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
@@ -26,7 +26,7 @@ router.post('/email/confirm', (req, res, next) => {
return next(errors.ErrMissingToken);
}
User
UsersService
.verifyEmailConfirmation(token)
.then(() => {
res.status(204).end();
@@ -47,7 +47,7 @@ router.post('/password/reset', (req, res, next) => {
return next('you must submit an email when requesting a password.');
}
User
UsersService
.createPasswordResetToken(email)
.then((token) => {
@@ -100,9 +100,10 @@ router.put('/password/reset', (req, res, next) => {
return next(errors.ErrPasswordTooShort);
}
User.verifyPasswordResetToken(token)
UsersService
.verifyPasswordResetToken(token)
.then(user => {
return User.changePassword(user.id, password);
return UsersService.changePassword(user.id, password);
})
.then(() => {
res.status(204).end();
@@ -120,7 +121,7 @@ router.put('/settings', authorization.needed(), (req, res, next) => {
bio
} = req.body;
User
UsersService
.updateSettings(req.user.id, {bio})
.then(() => {
res.status(204).end();
+2 -2
View File
@@ -1,10 +1,10 @@
const express = require('express');
const Action = require('../../../models/action');
const ActionsService = require('../../../services/actions');
const router = express.Router();
router.delete('/:action_id', (req, res, next) => {
Action
ActionsService
.findOneAndRemove({
id: req.params.action_id,
user_id: req.user.id
+9 -7
View File
@@ -1,8 +1,10 @@
const express = require('express');
const router = express.Router();
const Asset = require('../../../models/asset');
const scraper = require('../../../services/scraper');
const AssetsService = require('../../../services/assets');
const AssetModel = require('../../../models/asset');
// List assets.
router.get('/', (req, res, next) => {
@@ -46,13 +48,13 @@ router.get('/', (req, res, next) => {
Promise.all([
// Find the actuall assets.
FilterOpenAssets(Asset.search(search), filter)
FilterOpenAssets(AssetsService.search(search), filter)
.sort({[field]: (sort === 'asc') ? 1 : -1})
.skip(parseInt(skip))
.limit(parseInt(limit)),
// Get the count of actual assets.
FilterOpenAssets(Asset.search(search), filter)
FilterOpenAssets(AssetsService.search(search), filter)
.count()
])
.then(([result, count]) => {
@@ -73,7 +75,7 @@ router.get('/', (req, res, next) => {
router.get('/:asset_id', (req, res, next) => {
// Send back the asset.
Asset
AssetsService
.findById(req.params.asset_id)
.then((asset) => {
if (!asset) {
@@ -91,7 +93,7 @@ router.get('/:asset_id', (req, res, next) => {
router.post('/:asset_id/scrape', (req, res, next) => {
// Create a new asset scrape job.
Asset
AssetsService
.findById(req.params.asset_id)
.then((asset) => {
if (!asset) {
@@ -113,7 +115,7 @@ router.post('/:asset_id/scrape', (req, res, next) => {
router.put('/:asset_id/settings', (req, res, next) => {
// Override the settings for the asset.
Asset
AssetsService
.overrideSettings(req.params.asset_id, req.body)
.then(() => res.status(204).end())
.catch((err) => next(err));
@@ -128,7 +130,7 @@ router.put('/:asset_id/status', (req, res, next) => {
closedMessage
} = req.body;
Asset
AssetModel
.update({id}, {
$set: {
closedAt,
+19 -88
View File
@@ -1,10 +1,12 @@
const express = require('express');
const errors = require('../../../errors');
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');
const CommentModel = require('../../../models/comment');
const CommentsService = require('../../../services/comments');
const AssetsService = require('../../../services/assets');
const UsersService = require('../../../services/users');
const ActionsService = require('../../../services/actions');
const authorization = require('../../../middleware/authorization');
const _ = require('lodash');
@@ -48,27 +50,27 @@ router.get('/', (req, res, next) => {
// otherwise this will be a vulnerability if you pass user_id and something else,
// the app will return admin-level data without the proper checks
if (user_id) {
query = Comment.findByUserId(user_id, authorization.has(req.user, 'admin'));
query = CommentsService.findByUserId(user_id, authorization.has(req.user, 'admin'));
} else if (status) {
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? null : status));
} else if (action_type) {
query = Comment
query = CommentsService
.findIdsByActionType(action_type)
.then((ids) => assetIDWrap(Comment.find({
.then((ids) => assetIDWrap(CommentModel.find({
id: {
$in: ids
}
})));
} else {
query = assetIDWrap(Comment.all());
query = assetIDWrap(CommentsService.all());
}
query.then((comments) => {
return Promise.all([
comments,
Asset.findMultipleById(comments.map(comment => comment.asset_id)),
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
AssetsService.findMultipleById(comments.map(comment => comment.asset_id)),
UsersService.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
ActionsService.getActionSummariesFromComments(asset_id, comments, req.user ? req.user.id : false)
]);
})
.then(([comments, assets, users, actions]) =>
@@ -83,79 +85,8 @@ router.get('/', (req, res, next) => {
});
});
router.post('/', wordlist.filter('body'), (req, res, next) => {
const {
body,
asset_id,
parent_id
} = req.body;
// 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;
if (req.wordlist.banned) {
status = Promise.resolve('rejected');
} else {
status = Asset
.rectifySettings(Asset.findById(asset_id).then((asset) => {
if (!asset) {
return Promise.reject(errors.ErrNotFound);
}
// Check to see if the asset has closed commenting...
if (asset.isClosed) {
// They have, ensure that we send back an error.
return Promise.reject(new errors.ErrAssetCommentingClosed(asset.closedMessage));
}
return asset;
}))
// 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, charCountEnable, charCount}) => {
// Reject if the comment is too long
if (charCountEnable && body.length > charCount) {
return 'rejected';
}
return moderation === 'pre' ? 'premod' : null;
});
}
status.then((status) => Comment.publicCreate({
body,
asset_id,
parent_id,
status,
author_id: req.user.id
}))
.then((comment) => {
if (req.wordlist.suspect) {
return Comment
.addAction(comment.id, null, 'flag', {field: 'body', details: 'Matched suspect word filters.'})
.then(() => comment);
}
return comment;
})
.then((comment) => {
// The comment was created! Send back the created comment.
res.status(201).json(comment);
})
.catch((err) => {
next(err);
});
});
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
Comment
CommentsService
.findById(req.params.comment_id)
.then(comment => {
if (!comment) {
@@ -171,7 +102,7 @@ router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
});
router.delete('/:comment_id', authorization.needed('admin'), (req, res, next) => {
Comment
CommentsService
.removeById(req.params.comment_id)
.then(() => {
res.status(204).end();
@@ -186,7 +117,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
status
} = req.body;
Comment
CommentsService
.pushStatus(req.params.comment_id, status, req.user.id)
.then(() => {
res.status(204).end();
@@ -203,7 +134,7 @@ router.post('/:comment_id/actions', (req, res, next) => {
metadata
} = req.body;
Comment
CommentsService
.addAction(req.params.comment_id, req.user.id, action_type, metadata)
.then((action) => {
res.status(201).json(action);
-5
View File
@@ -1,12 +1,8 @@
const express = require('express');
const authorization = require('../../middleware/authorization');
const payloadFilter = require('../../middleware/payload-filter');
const router = express.Router();
// Filter all content going down the pipe based on user roles.
router.use(payloadFilter);
router.use('/assets', authorization.needed('admin'), require('./assets'));
router.use('/settings', authorization.needed('admin'), require('./settings'));
router.use('/queue', authorization.needed('admin'), require('./queue'));
@@ -15,7 +11,6 @@ router.use('/comments', authorization.needed(), require('./comments'));
router.use('/actions', authorization.needed(), require('./actions'));
router.use('/auth', require('./auth'));
router.use('/stream', require('./stream'));
router.use('/users', require('./users'));
router.use('/account', require('./account'));
+10 -9
View File
@@ -1,7 +1,8 @@
const express = require('express');
const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const CommentsService = require('../../../services/comments');
const CommentModel = require('../../../models/comment');
const UsersService = require('../../../services/users');
const ActionsService = require('../../../services/actions');
const authorization = require('../../../middleware/authorization');
const _ = require('lodash');
@@ -10,8 +11,8 @@ const router = express.Router();
function gatherActionsAndUsers (comments) {
return Promise.all([
comments,
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
Action.getActionSummaries(_.uniq([
UsersService.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
ActionsService.getActionSummaries(_.uniq([
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
@@ -30,7 +31,7 @@ router.get('/comments/pending', authorization.needed('admin'), (req, res, next)
const {asset_id} = req.query;
Comment.moderationQueue('premod', asset_id)
CommentsService.moderationQueue('PREMOD', asset_id)
.then(gatherActionsAndUsers)
.then(([comments, users, actions]) => {
res.json({comments, users, actions});
@@ -43,7 +44,7 @@ router.get('/comments/pending', authorization.needed('admin'), (req, res, next)
router.get('/comments/rejected', authorization.needed('admin'), (req, res, next) => {
const {asset_id} = req.query;
Comment.moderationQueue('rejected', asset_id)
CommentsService.moderationQueue('REJECTED', asset_id)
.then(gatherActionsAndUsers)
.then(([comments, users, actions]) => {
res.json({comments, users, actions});
@@ -64,8 +65,8 @@ router.get('/comments/flagged', authorization.needed('admin'), (req, res, next)
return query;
};
Comment.findIdsByActionType('flag')
.then(ids => assetIDWrap(Comment.find({
CommentsService.findIdsByActionType('FLAG')
.then(ids => assetIDWrap(CommentModel.find({
id: {$in: ids}
})))
.then(gatherActionsAndUsers)
+15 -13
View File
@@ -1,24 +1,26 @@
const express = require('express');
const Setting = require('../../../models/setting');
const SettingsService = require('../../../services/settings');
const router = express.Router();
router.get('/', (req, res, next) => {
Setting.retrieve().then((settings) => {
res.json(settings);
})
.catch((err) => {
next(err);
});
SettingsService
.retrieve().then((settings) => {
res.json(settings);
})
.catch((err) => {
next(err);
});
});
router.put('/', (req, res, next) => {
Setting.update(req.body).then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
SettingsService
.update(req.body).then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
-112
View File
@@ -1,112 +0,0 @@
const express = require('express');
const _ = require('lodash');
const scraper = require('../../../services/scraper');
const errors = require('../../../errors');
const url = require('url');
const Comment = require('../../../models/comment');
const User = require('../../../models/user');
const Action = require('../../../models/action');
const Asset = require('../../../models/asset');
const Setting = require('../../../models/setting');
const router = express.Router();
router.get('/', (req, res, next) => {
let asset_url = decodeURIComponent(req.query.asset_url);
// Verify that the asset_url is parsable.
let parsed_asset_url = url.parse(asset_url);
if (!parsed_asset_url.protocol) {
return next(errors.ErrInvalidAssetURL);
}
// Get the asset_id for this url (or create it if it doesn't exist)
Promise.all([
// Find or create the asset by url.
Asset.findOrCreateByUrl(asset_url)
// Add the found asset to the scraper if it's not already scraped.
.then((asset) => {
if (!asset.scraped) {
return scraper.create(asset).then(() => asset);
}
return asset;
}),
// Get the moderation setting from the settings.
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 && asset.settings) {
settings.merge(asset.settings);
}
return Promise.all([
// This is the promised component... Fetch the comments based on the
// moderation settings.
Comment.findByAssetIdWithStatuses(asset.id, [null, 'accepted']),
// Send back the reference to the asset.
asset,
// Send back the settings to the stream.
settings
]);
})
// Get all the users and actions for those comments.
.then(([comments, asset, settings]) => {
// 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 users for which there is a comment available for them.
let users = userIDs.length > 0 ? User.findByIdArray(userIDs) : [];
// Fetch the actions for pretty much everything at this point.
let actions = Action.getActionSummariesFromComments(asset.id, comments, req.user ? req.user.id : false);
return Promise.all([
// Pass back the asset that we loaded...
asset,
// It's comments...
comments,
// The users who wrote those comments
users,
// And all actions about the asset, comments, and users.
actions,
// Pass back the settings that we loaded.
settings
]);
})
.then(([asset, comments, users, actions, settings]) => {
// Send back the payload containing all this data.
res.json({
assets: [asset],
comments,
users,
actions,
settings
});
})
.catch(error => {
next(error);
});
});
module.exports = router;
+18 -13
View File
@@ -1,7 +1,8 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const Setting = require('../../../models/setting');
const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
@@ -15,12 +16,12 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
} = req.query;
Promise.all([
User
UsersService
.search(value)
.sort({[field]: (asc === 'true') ? 1 : -1})
.skip((page - 1) * limit)
.limit(limit),
User.count()
UsersService.count()
])
.then(([result, count]) => {
res.json({
@@ -35,7 +36,7 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
});
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
User
UsersService
.addRoleToUser(req.params.user_id, req.body.role)
.then(() => {
res.status(204).end();
@@ -43,11 +44,15 @@ router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) =>
.catch(next);
});
router.post('/:user_id/status', (req, res, next) => {
User
.setStatus(req.params.user_id, req.body.status, req.body.comment_id)
router.post('/:user_id/status', authorization.needed('admin'), (req, res, next) => {
UsersService
.setStatus(req.params.user_id, req.body.status)
.then((status) => {
res.status(201).json(status);
if (status === 'BANNED' && req.body.comment_id) {
return CommentsService.pushStatus(req.body.comment_id, 'rejected', req.params.user_id);
}
})
.catch(next);
});
@@ -64,7 +69,7 @@ router.post('/:user_id/status', (req, res, next) => {
* @param {String} userID the id for the user to send the email to
* @param {String} email the email for the user to send the email to
*/
const SendEmailConfirmation = (app, userID, email) => User
const SendEmailConfirmation = (app, userID, email) => UsersService
.createEmailConfirmToken(userID, email)
.then((token) => {
return mailer.sendSimple({
@@ -87,14 +92,14 @@ router.post('/', (req, res, next) => {
displayName
} = req.body;
User
UsersService
.createLocalUser(email, password, displayName)
.then((user) => {
// Get the settings from the database to find out if we need to send an
// email confirmation. The Front end will know about the
// requireEmailConfirmation as it's included in the settings get endpoint.
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
if (requireEmailConfirmation) {
@@ -123,7 +128,7 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
metadata
} = req.body;
User
UsersService
.addAction(req.params.user_id, req.user.id, action_type, metadata)
.then((action) => {
res.status(201).json(action);
@@ -138,7 +143,7 @@ router.post('/:user_id/email/confirm', authorization.needed('admin'), (req, res,
user_id
} = req.params;
User
UsersService
.findById(user_id)
.then((user) => {
if (!user) {
+177
View File
@@ -0,0 +1,177 @@
const ActionModel = require('../models/action');
const _ = require('lodash');
module.exports = class ActionsService {
/**
* Finds an action by the id.
* @param {String} id identifier of the action (uuid)
*/
static findById(id) {
return ActionModel.findOne({id});
}
/**
* Add an action.
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
static insertUserAction(action) {
// Actions are made unique by using a query that can be reproducable, i.e.,
// not containing user inputable values.
let query = {
action_type: action.action_type,
item_type: action.item_type,
item_id: action.item_id,
user_id: action.user_id
};
// Create/Update the action.
return ActionModel.findOneAndUpdate(query, action, {
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
}
/**
* Finds actions in an array of ids.
* @param {String} ids array of user identifiers (uuid)
*/
static findByItemIdArray(item_ids) {
return ActionModel.find({
'item_id': {$in: item_ids}
});
}
/**
* Fetches the action summaries for the given asset, and comments around the
* given user id.
* @param {[type]} asset_id [description]
* @param {[type]} comments [description]
* @param {String} [current_user_id=''] [description]
* @return {[type]} [description]
*/
static getActionSummariesFromComments(asset_id = '', comments, current_user_id = '') {
// 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 ActionsService.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)
*/
static getActionSummaries(item_ids, current_user_id = '') {
return ActionModel.aggregate([
{
// only grab items that match the specified item id's
$match: {
item_id: {
$in: item_ids
}
}
},
{
$group: {
// group unique documents by these properties, we are leveraging the
// fact that each uuid is completly unique.
_id: {
item_id: '$item_id',
action_type: '$action_type'
},
// and sum up all actions matching the above grouping criteria
count: {
$sum: 1
},
// we are leveraging the fact that each uuid is completly unique and
// just grabbing the last instance of the item type here.
item_type: {
$last: '$item_type'
},
current_user: {
$max: {
$cond: {
if: {
$eq: ['$user_id', current_user_id],
},
then: '$$CURRENT',
else: null
}
}
}
}
},
{
$project: {
// suppress the _id field
_id: false,
// map the fields from the _id grouping down a level
item_id: '$_id.item_id',
action_type: '$_id.action_type',
// map the field directly
count: '$count',
item_type: '$item_type',
// set the current user to false here
current_user: '$current_user'
}
}
]);
}
/*
* Finds all comments for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
static findByType(action_type, item_type) {
return ActionModel.find({
'action_type': action_type,
'item_type': item_type
});
}
/**
* Finds all comments ids for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
*/
static findCommentsIdByActionType(action_type, item_type) {
return ActionModel.find({
'action_type': action_type,
'item_type': item_type
}, 'item_id');
}
};
+116
View File
@@ -0,0 +1,116 @@
const AssetModel = require('../models/asset');
const SettingsService = require('./settings');
module.exports = class AssetsService {
/**
* Finds an asset by its id.
* @param {String} id identifier of the asset (uuid).
*/
static findById(id) {
return AssetModel.findOne({id});
}
/**
* Finds a asset by its url.
* @param {String} url identifier of the asset (uuid).
*/
static findByUrl(url) {
return AssetModel.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}
*/
static rectifySettings(assetQuery) {
return Promise.all([
SettingsService.retrieve(),
assetQuery
]).then(([settings, asset]) => {
// If the asset exists and has settings then return the merged object.
if (asset && asset.settings) {
settings.merge(asset.settings);
}
return settings;
});
}
/**
* Finds a asset by its url.
*
* NOTE: This function has scalability concerns regarding mongoose's decision
* always write {updated_at: new Date()} on every call to findOneAndUpdate
* even though the update document exactly matches the query document... In
* the future this function should never update, only findOneAndCreate but this
* is not possible with the mongoose driver.
*
* @param {String} url identifier of the asset (uuid).
* @return {Promise}
*/
static findOrCreateByUrl(url) {
return AssetModel.findOneAndUpdate({url}, {url}, {
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
}
/**
* Updates the settings for the asset.
* @param {[type]} id [description]
* @param {[type]} settings [description]
* @return {[type]} [description]
*/
static overrideSettings(id, settings) {
return AssetModel.findOneAndUpdate({id}, {
$set: {
settings
}
}, {
new: true
});
}
/**
* Finds assets matching keywords on the model. If `value` is an empty string,
* then it will not even perform a text search query.
* @param {String} value string to search by.
* @return {Promise}
*/
static search(value) {
if (value.length === 0) {
return AssetsService.all();
} else {
return AssetModel.find({
$text: {
$search: value
}
});
}
}
/**
* Finds multiple assets with matching ids
* @param {Array} ids an array of Strings of asset_id
* @return {Promise} resolves to list of Assets
*/
static findMultipleById(ids) {
const query = ids.map(id => ({id}));
return AssetModel.find(query);
}
static all() {
return AssetModel.find({});
}
};
+220
View File
@@ -0,0 +1,220 @@
const CommentModel = require('../models/comment');
const ActionModel = require('../models/action');
const ActionsService = require('./actions');
module.exports = class CommentsService {
/**
* Creates a new Comment that came from a public source.
* @param {Mixed} comment either a single comment or an array of comments.
* @return {Promise}
*/
static 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(CommentsService.publicCreate));
}
const {
body,
asset_id,
parent_id,
status = null,
author_id
} = comment;
comment = new CommentModel({
body,
asset_id,
parent_id,
status_history: status ? [{
type: status,
created_at: new Date()
}] : [],
status,
author_id
});
return comment.save();
}
/**
* Finds a comment by the id.
* @param {String} id identifier of comment (uuid)
* @return {Promise}
*/
static findById(id) {
return CommentModel.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}
*/
static findByAssetId(asset_id) {
return CommentModel.find({
asset_id
});
}
/**
* findByAssetIdWithStatuses finds all the comments where the asset id matches
* what's provided and the status is one of the ones listed in the statuses
* array.
* @param {String} asset_id the asset id to search by
* @param {Array} [statuses=[]] the array of statuses to search by
* @return {Promise} resolves to an array of comments
*/
static findByAssetIdWithStatuses(asset_id, statuses = []) {
return CommentModel.find({
asset_id,
status: {
$in: statuses
}
});
}
/**
* 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}
*/
static findByActionType(action_type) {
return ActionsService
.findCommentsIdByActionType(action_type, 'COMMENTS')
.then((actions) => CommentModel.find({
id: {
$in: actions.map((a) => a.item_id)
}
}));
}
/**
* 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
* @return {Promise}
*/
static findIdsByActionType(action_type) {
return ActionsService
.findCommentsIdByActionType(action_type, 'COMMENTS')
.then((actions) => actions.map(a => a.item_id));
}
/**
* Find comments by current status
* @param {String} status status of the comment to search for
* @return {Promise} resovles to comment array
*/
static findByStatus(status = null) {
return CommentModel.find({status});
}
/**
* Find comments that need to be moderated (aka moderation queue).
* @param {String} asset_id
* @return {Promise}
*/
static moderationQueue(status = null, asset_id = null) {
// Fetch the comments with statuses.
let comments = CommentModel.find({status});
if (asset_id) {
comments = comments.where({asset_id});
}
return comments;
}
/**
* 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}
*/
static pushStatus(id, status, assigned_by = null) {
return CommentModel.update({id}, {
$push: {
status_history: {
type: status,
created_at: new Date(),
assigned_by
}
},
$set: {status}
});
}
/**
* Add an action to the comment.
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
static addAction(item_id, user_id, action_type, metadata) {
return ActionsService.insertUserAction({
item_id,
item_type: 'COMMENTS',
user_id,
action_type,
metadata
});
}
/**
* 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}
*/
static removeById(id) {
return CommentModel.remove({id});
}
/**
* Remove an action from the comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} action_type the type of the action to be removed
* @param {String} user_id the id of the user performing the action
* @return {Promise}
*/
static removeAction(item_id, user_id, action_type) {
return ActionModel.remove({
action_type,
item_type: 'COMMENTS',
item_id,
user_id
});
}
/**
* Returns all the comments in the collection.
* @return {Promise}
*/
static all() {
return CommentModel.find({});
}
/**
* Returns all the comments by user
* probably to be paginated at some point in the future
* @return {Promise} array resolves to an array of comments by that user
*/
static findByUserId(author_id, admin = false) {
// do not return un-published comments for non-admins
let query = {author_id};
if (!admin) {
query.$nor = [{status: 'PREMOD'}, {status: 'REJECTED'}];
}
return CommentModel.find(query);
}
};
+6 -6
View File
@@ -1,6 +1,6 @@
const passport = require('passport');
const User = require('../models/user');
const Setting = require('../models/setting');
const UsersService = require('./users');
const SettingsService = require('./settings');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
@@ -13,7 +13,7 @@ passport.serializeUser((user, done) => {
});
passport.deserializeUser((id, done) => {
User
UsersService
.findById(id)
.then((user) => {
done(null, user);
@@ -43,7 +43,7 @@ function ValidateUserLogin(loginProfile, user, done) {
}
// The user is a local user, check if we need email confirmation.
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
// If we have the requirement of checking that emails for users are
// verified, then we need to check the email address to ensure that it has
@@ -77,7 +77,7 @@ passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (email, password, done) => {
User
UsersService
.findLocalUser(email, password)
.then((user) => {
if (!user) {
@@ -103,7 +103,7 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET &&
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, (accessToken, refreshToken, profile, done) => {
User
UsersService
.findOrCreateExternalUser(profile)
.then((user) => {
return ValidateUserLogin(profile, user, done);
+4 -3
View File
@@ -1,6 +1,7 @@
const kue = require('./kue');
const debug = require('debug')('talk:services:scraper');
const Asset = require('../models/asset');
const AssetModel = require('../models/asset');
const AssetsService = require('./assets');
const metascraper = require('metascraper');
@@ -49,7 +50,7 @@ const scraper = {
* Updates an Asset based on scraped asset metadata.
*/
update(id, meta) {
return Asset.update({id}, {
return AssetModel.update({id}, {
$set: {
title: meta.title || '',
description: meta.description || '',
@@ -74,7 +75,7 @@ const scraper = {
debug(`Starting on Job[${job.id}] for Asset[${job.data.asset_id}]`);
Asset
AssetsService
// Find the asset, or complain that it doesn't exist.
.findById(job.data.asset_id)
+49
View File
@@ -0,0 +1,49 @@
const SettingModel = require('../models/setting');
/**
* The selector used to uniquely identify the settings document.
*/
const selector = {id: '1'};
/**
* The Setting Service object exposing the Setting model.
*/
module.exports = class SettingsService {
/**
* Gets the entire settings record and sends it back
* @return {Promise} settings the whole settings record
*/
static retrieve() {
return SettingModel.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.
*/
static update(settings) {
return SettingModel.findOneAndUpdate(selector, {
$set: settings
}, {
upsert: true,
new: true,
setDefaultsOnInsert: true
});
}
/**
* This is run once when the app starts to ensure settings are populated.
* @return {Promise} null initialize the global settings object
*/
static 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 SettingsService.update(defaults);
}
};
+601
View File
@@ -0,0 +1,601 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Wordlist = require('./wordlist');
const errors = require('../errors');
const uuid = require('uuid');
const UserModel = require('../models/user');
const USER_STATUS = require('../models/user').USER_STATUS;
const USER_ROLES = require('../models/user').USER_ROLES;
const ActionsService = require('./actions');
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
// set the process.env.TALK_SESSION_SECRET.
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
process.env.TALK_SESSION_SECRET = 'keyboard cat';
} else if (!process.env.TALK_SESSION_SECRET) {
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
}
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
// through during the salting process.
const SALT_ROUNDS = 10;
// UsersService is the interface for the application to interact with the
// UserModel through.
module.exports = class UsersService {
/**
* Finds a user given their email address that we have for them in the system
* and ensures that the retuned user matches the password passed in as well.
* @param {string} email - email to look up the user by
* @param {string} password - password to match against the found user
* @param {Function} done [description]
*/
static findLocalUser(email, password) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required for findLocalUser');
}
return UserModel.findOne({
profiles: {
$elemMatch: {
id: email.toLowerCase(),
provider: 'local'
}
}
})
.then((user) => {
if (!user) {
return false;
}
return new Promise((resolve, reject) => {
bcrypt.compare(password, user.password, (err, res) => {
if (err) {
return reject(err);
}
if (!res) {
return resolve(false);
}
return resolve(user);
});
});
});
}
/**
* Merges two users together by taking all the profiles on a given user and
* pushing them into the source user followed by deleting the destination user's
* user account. This will not merge the roles associated with the source user.
* @param {String} dstUserID id of the user to which is the target of the merge
* @param {String} srcUserID id of the user to which is the source of the merge
* @return {Promise} resolves when the users are merged
*/
static mergeUsers(dstUserID, srcUserID) {
let srcUser, dstUser;
return Promise
.all([
UserModel.findOne({id: dstUserID}).exec(),
UserModel.findOne({id: srcUserID}).exec()
])
.then((users) => {
dstUser = users[0];
srcUser = users[1];
srcUser.profiles.forEach((profile) => {
dstUser.profiles.push(profile);
});
return srcUser.remove();
})
.then(() => dstUser.save());
}
/**
* Finds a user given a social profile and if the user does not exist, creates
* them.
* @param {Object} profile - User social/external profile
* @param {Function} done [description]
*/
static findOrCreateExternalUser(profile) {
return UserModel
.findOne({
profiles: {
$elemMatch: {
id: profile.id,
provider: profile.provider
}
}
})
.then((user) => {
if (user) {
return user;
}
// The user was not found, lets create them!
user = new UserModel({
displayName: profile.displayName,
roles: [],
profiles: [
{
id: profile.id,
provider: profile.provider
}
]
});
return user.save();
});
}
static changePassword(id, password) {
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
resolve(hashedPassword);
});
})
.then((hashedPassword) => {
return UserModel.update({id}, {
$inc: {__v: 1},
$set: {
password: hashedPassword
}
});
});
}
/**
* Creates local users.
* @param {Array} users Users to create
* @return {Promise} Resolves with the users that were created
*/
static createLocalUsers(users) {
return Promise.all(users.map((user) => {
return UsersService
.createLocalUser(user.email, user.password, user.displayName);
}));
}
/**
* Check the requested displayname for naughty words (currently in English) and special chars
* @param {String} displayName word to be checked for profanity
* @return {Promise} rejected if the machine's sensibilites are offended
*/
static isValidDisplayName(displayName) {
const onlyLettersNumbersUnderscore = /^[a-z0-9_]+$/;
if (!displayName) {
return Promise.reject(errors.ErrMissingDisplay);
}
if (!onlyLettersNumbersUnderscore.test(displayName)) {
return Promise.reject(errors.ErrSpecialChars);
}
// check for profanity
return Wordlist.displayNameCheck(displayName);
}
/**
* Creates the local user with a given email, password, and name.
* @param {String} email email of the new user
* @param {String} password plaintext password of the new user
* @param {String} displayName name of the display user
* @param {Function} done callback
*/
static createLocalUser(email, password, displayName) {
if (!email) {
return Promise.reject(errors.ErrMissingEmail);
}
email = email.toLowerCase().trim();
displayName = displayName.toLowerCase().trim();
if (!password) {
return Promise.reject(errors.ErrMissingPassword);
}
if (password.length < 8) {
return Promise.reject(errors.ErrPasswordTooShort);
}
return UsersService.isValidDisplayName(displayName)
.then(() => { // displayName is valid
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
let user = new UserModel({
displayName: displayName,
password: hashedPassword,
roles: [],
profiles: [
{
id: email,
provider: 'local'
}
]
});
user.save((err) => {
if (err) {
if (err.code === 11000) {
if (err.message.match('displayName')) {
return reject(errors.ErrDisplayTaken);
}
return reject(errors.ErrEmailTaken);
}
return reject(err);
}
return resolve(user);
});
});
});
});
}
/**
* Disables a given user account.
* @param {String} id id of a user
* @param {Function} done callback after the operation is complete
*/
static disableUser(id) {
return UserModel.update({
id: id
}, {
$set: {
disabled: true
}
});
}
/**
* Enables a given user account.
* @param {String} id id of a user
* @param {Function} done callback after the operation is complete
*/
static enableUser(id) {
return UserModel.update({
id: id
}, {
$set: {
disabled: false
}
});
}
/**
* Adds a role to a user.
* @param {String} id id of a user
* @param {String} role role to add
* @param {Function} done callback after the operation is complete
*/
static addRoleToUser(id, role) {
// Check to see if the user role is in the allowable set of roles.
if (USER_ROLES.indexOf(role) === -1) {
// User role is not supported! Error out here.
return Promise.reject(new Error(`role ${role} is not supported`));
}
return UserModel.update({
id: id
}, {
$addToSet: {
roles: role
}
});
}
/**
* Removes a role from a user.
* @param {String} id id of a user
* @param {String} role role to remove
* @param {Function} done callback after the operation is complete
*/
static removeRoleFromUser(id, role) {
// Check to see if the user role is in the allowable set of roles.
if (USER_ROLES.indexOf(role) === -1) {
// User role is not supported! Error out here.
return Promise.reject(new Error(`role ${role} is not supported`));
}
return UserModel.update({
id: id
}, {
$pull: {
roles: role
}
});
}
/**
* Set status of a user.
* @param {String} id id of a user
* @param {String} status status to set
* @param {Function} done callback after the operation is complete
*/
static setStatus(id, status) {
// Check to see if the user status is in the allowable set of roles.
if (USER_STATUS.indexOf(status) === -1) {
// User status is not supported! Error out here.
return Promise.reject(new Error(`status ${status} is not supported`));
}
return UserModel.update({id}, {$set: {status}});
}
/**
* Finds a user with the id.
* @param {String} id user id (uuid)
*/
static findById(id) {
return UserModel.findOne({id});
}
/**
* Finds users in an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
static findByIdArray(ids) {
return UserModel.find({
id: {$in: ids}
});
}
/**
* Finds public user information by an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
static findPublicByIdArray(ids) {
return UserModel.find({
id: {$in: ids}
}, 'id displayName');
}
/**
* Creates a JWT from a user email. Only works for local accounts.
* @param {String} email of the local user
*/
static createPasswordResetToken(email) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
.then((user) => {
if (!user) {
// Since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the
// endpoint.
return;
}
const payload = {
jti: uuid.v4(),
email,
userId: user.id,
version: user.__v
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
algorithm: 'HS256',
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
});
});
}
/**
* Verifies that the token was indeed signed by the session secret.
* @param {String} token JWT token from the client
* @return {Promise}
*/
static verifyToken(token, options = {}) {
return new Promise((resolve, reject) => {
// Set the allowed algorithms.
options.algorithms = ['HS256'];
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
});
}
/**
* Verifies a jwt and returns the associated user.
* @param {String} token the JSON Web Token to verify
*/
static verifyPasswordResetToken(token) {
return UsersService
.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT
})
.then((decoded) => UsersService.findById(decoded.userId));
}
/**
* Finds a user using a value which gets compared using a prefix match against
* the user's email address and/or their display name.
* @param {String} value value to search by
* @return {Promise}
*/
static search(value) {
return UserModel.find({
$or: [
// Search by a prefix match on the displayName.
{
'displayName': {
$regex: new RegExp(`^${value}`),
$options: 'i'
}
},
// Search by a prefix match on the email address.
{
'profiles': {
$elemMatch: {
id: {
$regex: new RegExp(`^${value}`),
$options: 'i'
},
provider: 'local'
}
}
}
]
});
}
/**
* Returns a count of the current users.
* @return {Promise}
*/
static count() {
return UserModel.count();
}
/**
* Returns all the users.
* @return {Promise}
*/
static all() {
UserModel.find();
}
/**
* Updates the user's settings.
* @return {Promise}
*/
static updateSettings(id, settings) {
return UserModel.update({
id
}, {
$set: {
settings
}
});
}
/**
* Add an action to the user.
* @param {String} item_id identifier of the user (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the user
* @return {Promise}
*/
static addAction(item_id, user_id, action_type, metadata) {
return ActionsService.insertUserAction({
item_id,
item_type: 'users',
user_id,
action_type,
metadata
});
}
/**
* This creates a token based around confirming the local profile.
* @param {String} userID The user id for the user that we are creating the
* token for.
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
static createEmailConfirmToken(userID, email) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return UsersService
.findById(userID)
.then((user) => {
if (!user) {
return Promise.reject(new Error('user not found'));
}
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
// Ensure that the user email hasn't already been verified.
if (profile && profile.metadata && profile.metadata.confirmed_at) {
return Promise.reject(new Error('email address already confirmed'));
}
const payload = {
email,
userID
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
});
}
/**
* This verifies that a given token was for the email confirmation and updates
* that user's profile with a 'confirmed_at' parameter with the current date.
* @param {String} token the token containing the email confirmation details
* signed with our secret.
* @return {Promise}
*/
static verifyEmailConfirmation(token) {
return UsersService
.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email}) => {
return UserModel
.update({
id: userID,
profiles: {
$elemMatch: {
id: email,
provider: 'local'
}
}
}, {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
});
});
}
};
+2 -2
View File
@@ -2,7 +2,7 @@ const debug = require('debug')('talk:services:wordlist');
const _ = require('lodash');
const natural = require('natural');
const tokenizer = new natural.WordTokenizer();
const Setting = require('../models/setting');
const SettingsService = require('./settings');
const Errors = require('../errors');
/**
@@ -22,7 +22,7 @@ class Wordlist {
* Loads wordlists in from the database
*/
load() {
return Setting
return SettingsService
.retrieve()
.then((settings) => {
-245
View File
@@ -1,245 +0,0 @@
const Comment = require('../../models/comment');
const User = require('../../models/user');
const Action = require('../../models/action');
const Setting = require('../../models/setting');
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
const expect = require('chai').expect;
describe('models.Comment', () => {
const comments = [{
body: 'comment 10',
asset_id: '123',
status_history: [],
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
status_history: [{
type: 'accepted'
}],
status: 'accepted',
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
status_history: [],
parent_id: '',
author_id: '456',
id: '3'
}, {
body: 'comment 40',
asset_id: '123',
status_history: [{
type: 'rejected'
}],
status: 'rejected',
parent_id: '',
author_id: '456',
id: '4'
}, {
body: 'comment 50',
asset_id: '1234',
status_history: [{
type: 'premod'
}],
status: 'premod',
parent_id: '',
author_id: '456',
id: '5'
}, {
body: 'comment 60',
asset_id: '1234',
status_history: [{
type: 'premod'
}],
status: 'premod',
parent_id: '',
author_id: '456',
id: '6'
}];
const users = [{
email: 'stampi@gmail.com',
displayName: 'Stampi',
password: '1Coral!!'
}, {
email: 'sockmonster@gmail.com',
displayName: 'Sockmonster',
password: '2Coral!!'
}];
const actions = [{
action_type: 'flag',
item_id: '3',
item_type: 'comments',
user_id: '123'
}, {
action_type: 'like',
item_id: '1',
item_type: 'comments',
user_id: '456'
}];
beforeEach(() => {
return Setting.init(settings).then(() => {
return Promise.all([
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.be.equal('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.be.equal('accepted');
expect(c2).to.not.be.null;
expect(c2.id).to.be.uuid;
expect(c2.status).to.be.null;
expect(c3).to.not.be.null;
expect(c3.id).to.be.uuid;
expect(c3.status).to.be.equal('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);
result.sort((a, b) => {
if (a.body < b.body) {return -1;}
else {return 1;}
});
expect(result[0]).to.have.property('body', 'comment 10');
expect(result[1]).to.have.property('body', 'comment 20');
expect(result[2]).to.have.property('body', 'comment 40');
});
});
});
describe('#moderationQueue()', () => {
it('should find an array of new comments to moderate when pre-moderation', () => {
return Comment.moderationQueue('premod').then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.lengthOf(2);
});
});
});
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);
});
});
});
describe('#findByUserId', () => {
it('should return all comments if admin', () => {
return Comment.findByUserId('456', true)
.then(comments => {
expect(comments).to.have.length(4);
});
});
it('should not return premod and rejected comments if not admin', () => {
return Comment.findByUserId('456')
.then(comments => {
expect(comments).to.have.length(1);
});
});
});
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.status).to.be.null;
return Comment.pushStatus(comment_id, 'rejected', '123');
})
.then(() => Comment.findById(comment_id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.equal('rejected');
expect(c.status_history).to.have.length(1);
expect(c.status_history[0]).to.have.property('type', 'rejected');
expect(c.status_history[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_history');
expect(c).to.have.property('status');
expect(c.status).to.equal('rejected');
expect(c.status_history).to.have.length(2);
expect(c.status_history[0]).to.have.property('type', 'accepted');
expect(c.status_history[0]).to.have.property('assigned_by', null);
expect(c.status_history[1]).to.have.property('type', 'rejected');
expect(c.status_history[1]).to.have.property('assigned_by', '123');
});
});
});
});
+5 -4
View File
@@ -8,12 +8,13 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
const Asset = require('../../../../models/asset');
const AssetModel = require('../../../../models/asset');
const AssetsService = require('../../../../services/assets');
describe('/api/v1/assets', () => {
beforeEach(() => {
return Asset.create([
return AssetModel.create([
{
url: 'https://coralproject.net/news/asset1',
title: 'Asset 1',
@@ -121,7 +122,7 @@ describe('/api/v1/assets', () => {
const today = Date.now();
return Asset.findOrCreateByUrl('http://test.com')
return AssetsService.findOrCreateByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('isClosed', null);
expect(asset).to.have.property('closedAt', null);
@@ -135,7 +136,7 @@ describe('/api/v1/assets', () => {
expect(res).to.have.status(204);
return Asset.findByUrl('http://test.com');
return AssetsService.findByUrl('http://test.com');
})
.then((asset) => {
expect(asset).to.have.property('isClosed', true);
+7 -7
View File
@@ -4,7 +4,7 @@ const expect = chai.expect;
chai.use(require('chai-http'));
const User = require('../../../../models/user');
const UsersService = require('../../../../services/users');
describe('/api/v1/auth', () => {
describe('#get', () => {
@@ -19,15 +19,15 @@ describe('/api/v1/auth', () => {
});
});
const Setting = require('../../../../models/setting');
const SettingsService = require('../../../../services/settings');
describe('/api/v1/auth/local', () => {
let mockUser;
beforeEach(() => {
const settings = {requireEmailConfirmation: false, wordlist: {banned: ['bad'], suspect: ['naughty']}};
return Setting.init(settings).then(() => {
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria')
return SettingsService.init(settings).then(() => {
return UsersService.createLocalUser('maria@gmail.com', 'password!', 'Maria')
.then((user) => {
mockUser = user;
});
@@ -66,7 +66,7 @@ describe('/api/v1/auth/local', () => {
describe('email confirmation enabled', () => {
beforeEach(() => Setting.init({requireEmailConfirmation: true}));
beforeEach(() => SettingsService.init({requireEmailConfirmation: true}));
describe('#post', () => {
it('should not allow a login from a user that is not confirmed', () => {
@@ -76,9 +76,9 @@ describe('/api/v1/auth/local', () => {
.catch((err) => {
err.response.should.have.status(401);
return User.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
})
.then(User.verifyEmailConfirmation)
.then(UsersService.verifyEmailConfirmation)
.then(() => {
return chai.request(app)
.post('/api/v1/auth/local')
+43 -206
View File
@@ -8,18 +8,19 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
const Comment = require('../../../../models/comment');
const Asset = require('../../../../models/asset');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const CommentModel = require('../../../../models/comment');
const ActionModel = require('../../../../models/action');
const CommentsService = require('../../../../services/comments');
const UsersService = require('../../../../services/users');
const SettingsService = require('../../../../services/settings');
const Setting = require('../../../../models/setting');
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
describe('/api/v1/comments', () => {
// Ensure that the settings are always available.
beforeEach(() => Setting.init(settings));
beforeEach(() => SettingsService.init(settings));
describe('#get', () => {
const comments = [{
@@ -34,16 +35,16 @@ describe('/api/v1/comments', () => {
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'rejected',
status: 'REJECTED',
status_history: [{
type: 'rejected'
type: 'REJECTED'
}]
}, {
body: 'comment 30',
asset_id: '456',
status: 'accepted',
status: 'ACCEPTED',
status_history: [{
type: 'accepted'
type: 'ACCEPTED'
}]
}];
@@ -58,18 +59,18 @@ describe('/api/v1/comments', () => {
}];
const actions = [{
action_type: 'flag',
action_type: 'FLAG',
item_id: 'abc',
item_type: 'comments'
item_type: 'COMMENTS'
}, {
action_type: 'like',
action_type: 'LIKE',
item_id: 'hij',
item_type: 'comments'
item_type: 'COMMENTS'
}];
beforeEach(() => {
return Promise.all([
Comment.create(comments).then((newComments) => {
CommentModel.create(comments).then((newComments) => {
newComments.forEach((comment, i) => {
comments[i].id = comment.id;
});
@@ -77,9 +78,9 @@ describe('/api/v1/comments', () => {
actions[0].item_id = comments[0].id;
actions[1].item_id = comments[1].id;
return Action.create(actions);
return ActionModel.create(actions);
}),
User.createLocalUsers(users)
UsersService.createLocalUsers(users)
]);
});
@@ -119,7 +120,7 @@ describe('/api/v1/comments', () => {
it('should return all the rejected comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=rejected')
.get('/api/v1/comments?status=REJECTED')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -131,7 +132,7 @@ describe('/api/v1/comments', () => {
it('should return all the approved comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=accepted')
.get('/api/v1/comments?status=ACCEPTED')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -142,7 +143,7 @@ describe('/api/v1/comments', () => {
it('should return all the new comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=new')
.get('/api/v1/comments?status=NEW')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -152,7 +153,7 @@ describe('/api/v1/comments', () => {
it('should return all the flagged comments', () => {
return chai.request(app)
.get('/api/v1/comments?action_type=flag')
.get('/api/v1/comments?action_type=FLAG')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -162,174 +163,6 @@ describe('/api/v1/comments', () => {
});
});
});
describe('#post', () => {
let asset_id;
let postmod_asset_id;
beforeEach(() => Promise.all([
Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
// Update the asset id.
asset_id = asset.id;
}),
Asset.findOrCreateByUrl('https://coralproject.net/section/postmod-article-is-the-best').then((asset) => {
// Update the asset id.
postmod_asset_id = asset.id;
return Asset.overrideSettings(postmod_asset_id, {moderation: 'post'});
}),
]));
it('should create a comment', () => {
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('status', 'premod');
});
});
it('should create a comment with a rejected status if it contains a bad word', () => {
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'bad words are the baddest', '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('status', 'rejected');
});
});
it('should create a comment with no status and a flag if it contains a suspected word', () => {
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_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('status', null);
return Promise.all([
res.body,
Action.findByType('flag', 'comments')
]);
})
.then(([comment, actions]) => {
expect(actions).to.have.length(1);
let action = actions[0];
expect(action).to.have.property('item_id', comment.id);
expect(action).to.have.property('metadata');
expect(action.metadata).to.have.property('field', 'body');
expect(action.metadata).to.have.property('details', 'Matched suspect word filters.');
});
});
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', 'premod');
});
});
it('should create a comment with null status if it\'s asset is has post-moderation enabled', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {moderation: 'post'})
.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', null);
});
});
it('should create a rejected comment if the body is above the character count', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
.then((asset) => {
return Asset
.overrideSettings(asset.id, {charCountEnable: true, charCount: 10})
.then(() => asset);
})
.then((asset) => {
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'This is way way way way way too long.', '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', 'rejected');
});
});
it('shouldn\'t create a comment when the asset has expired commenting', () => {
return Asset.create({
closedAt: new Date().setDate(0),
closedMessage: 'tests said expired!'
})
.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(500);
})
.catch((err) => {
expect(err.response.body).to.not.be.null;
expect(err.response.body).to.have.property('message');
expect(err.response.body.error.metadata.closedMessage).to.be.equal('tests said expired!');
});
});
it('should create a comment when the asset has not expired yet', () => {
return Asset.create({
closedAt: new Date().setDate(32),
closedMessage: 'tests said expired!'
})
.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);
});
});
});
});
describe('/api/v1/comments/:comment_id', () => {
@@ -360,21 +193,21 @@ describe('/api/v1/comments/:comment_id', () => {
}];
const actions = [{
action_type: 'flag',
action_type: 'FLAG',
item_id: 'abc',
item_type: 'comment'
item_type: 'COMMENTS'
}, {
action_type: 'like',
action_type: 'LIKE',
item_id: 'hij',
item_type: 'comment'
item_type: 'COMMENTS'
}];
beforeEach(() => {
return Setting.init(settings).then(() => {
return SettingsService.init(settings).then(() => {
return Promise.all([
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
CommentModel.create(comments),
UsersService.createLocalUsers(users),
ActionModel.create(actions)
]);
});
});
@@ -400,7 +233,7 @@ describe('/api/v1/comments/:comment_id', () => {
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(204);
return Comment.findById('abc');
return CommentsService.findById('abc');
})
.then((comment) => {
expect(comment).to.be.null;
@@ -448,15 +281,17 @@ describe('/api/v1/comments/:comment_id/actions', () => {
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'REJECTED',
status_history: [{
type: 'rejected'
type: 'REJECTED'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'ACCEPTED',
status_history: [{
type: 'accepted'
type: 'ACCEPTED'
}]
}];
@@ -471,19 +306,21 @@ describe('/api/v1/comments/:comment_id/actions', () => {
}];
const actions = [{
action_type: 'flag',
action_type: 'FLAG',
item_type: 'COMMENTS',
item_id: 'abc'
}, {
action_type: 'like',
action_type: 'LIKE',
item_type: 'COMMENTS',
item_id: 'hij'
}];
beforeEach(() => {
return Setting.init(settings).then(() => {
return SettingsService.init(settings).then(() => {
return Promise.all([
Comment.create(comments),
User.createLocalUsers(users),
Action.create(actions)
CommentModel.create(comments),
UsersService.createLocalUsers(users),
ActionModel.create(actions)
]);
});
});
+15 -15
View File
@@ -10,9 +10,9 @@ chai.use(require('chai-http'));
const Comment = require('../../../../models/comment');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const UsersService = require('../../../../services/users');
const Setting = require('../../../../models/setting');
const SettingsService = require('../../../../services/settings');
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['banned'], suspect: ['suspect']}};
describe('/api/v1/queue', () => {
@@ -21,26 +21,26 @@ describe('/api/v1/queue', () => {
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
status: 'rejected',
status: 'REJECTED',
status_history: [{
type: 'rejected'
type: 'REJECTED'
}]
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
status: 'premod',
status: 'PREMOD',
status_history: [{
type: 'premod'
type: 'PREMOD'
}]
}, {
id: 'hij',
body: 'comment 30',
asset_id: '456',
status: 'accepted',
status: 'ACCEPTED',
status_history: [{
type: 'accepted'
type: 'ACCEPTED'
}]
}];
@@ -55,18 +55,18 @@ describe('/api/v1/queue', () => {
}];
const actions = [{
action_type: 'flag',
action_type: 'FLAG',
item_id: 'abc',
item_type: 'comment'
item_type: 'COMMENTS'
}, {
action_type: 'like',
action_type: 'LIKE',
item_id: 'hij',
item_type: 'comment'
item_type: 'COMMENTS'
}];
beforeEach(() => {
return Setting.init(settings).then(() => {
return User.createLocalUsers(users)
return SettingsService.init(settings).then(() => {
return UsersService.createLocalUsers(users)
.then((u) => {
comments[0].author_id = u[0].id;
comments[1].author_id = u[1].id;
@@ -80,7 +80,7 @@ describe('/api/v1/queue', () => {
return Promise.all([
Action.create(actions),
Setting.init(settings)
SettingsService.init(settings)
]);
});
});
+3 -3
View File
@@ -7,12 +7,12 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
const Setting = require('../../../../models/setting');
const SettingsService = require('../../../../services/settings');
const defaults = {id: '1', moderation: 'pre'};
describe('/api/v1/settings', () => {
beforeEach(() => Setting.init(defaults));
beforeEach(() => SettingsService.init(defaults));
describe('#get', () => {
@@ -40,7 +40,7 @@ describe('/api/v1/settings', () => {
.then((res) => {
expect(res).to.have.status(204);
return Setting.retrieve();
return SettingsService.retrieve();
})
.then((settings) => {
expect(settings).to.have.property('moderation', 'post');
-225
View File
@@ -1,225 +0,0 @@
const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
// Setup chai.
chai.should();
chai.use(require('chai-http'));
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const Comment = require('../../../../models/comment');
const Asset = require('../../../../models/asset');
const Setting = require('../../../../models/setting');
describe('/api/v1/stream', () => {
describe('#get', () => {
const settings = {
id: '1',
moderation: 'post',
wordlist: {
banned: ['banned'],
suspect: ['suspect']
}
};
const assets = [
{
url: 'https://example.com/article/1'
},
{
url: 'https://example.com/article/2',
settings: {
moderation: 'pre'
}
},
{
url: 'https://example.com/article/3'
}
];
const comments = [{
id: 'abc',
body: 'comment 10',
author_id: '',
parent_id: '',
status: 'accepted',
status_history: [{
type: 'accepted'
}]
}, {
id: 'def',
body: 'comment 20',
author_id: '',
parent_id: '',
status: null,
status_history: []
}, {
id: 'uio',
body: 'comment 30',
asset_id: 'asset',
author_id: '456',
parent_id: '',
status: 'accepted',
status_history: [{
type: 'accepted'
}]
}, {
id: 'hij',
body: 'comment 40',
asset_id: '456',
status: 'rejected',
status_history: [{
type: 'rejected'
}]
}, {
body: 'comment 50',
status: 'premod',
status_history: [{
type: 'premod'
}]
}, {
body: 'comment 60',
status: 'accepted',
status_history: [{
type: 'accepted'
}]
}, {
body: 'comment 70',
status: 'rejected',
status_history: [{
type: 'rejected'
}]
}, {
body: 'comment 70',
status: null,
status_history: []
}];
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123456789'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123456789'
}];
const actions = [{
action_type: 'flag',
item_id: 'abc'
}, {
action_type: 'like',
item_id: 'hij'
}];
beforeEach(() => {
return Setting.init(settings)
.then(() => Promise.all([
User.createLocalUsers(users),
Promise.all(assets.map((asset) => Asset.create(asset)))
]))
.then(([mockUsers, mockAssets]) => {
// Map the id's over.
mockAssets.forEach((asset, i) => {
assets[i].id = asset.id;
});
mockUsers.forEach((user, i) => {
users[i].id = user.id;
});
comments.forEach((comment, i) => {
comments[i].author_id = users[(i % 2) === 0 ? 0 : 1].id;
});
comments[0].asset_id = assets[0].id;
comments[1].asset_id = assets[0].id;
comments[2].asset_id = assets[1].id;
comments[3].asset_id = assets[1].id;
comments[4].asset_id = assets[2].id;
comments[5].asset_id = assets[2].id;
comments[6].asset_id = assets[2].id;
comments[7].asset_id = assets[2].id;
return Promise.all([
Comment.create(comments),
Action.create(actions)
]);
});
});
it('should return a stream with comments, users and actions for an existing asset', () => {
return chai.request(app)
.get('/api/v1/stream')
.query({asset_url: assets[0].url})
.then(res => {
expect(res).to.have.status(200);
expect(res.body.assets.length).to.equal(1);
expect(res.body.comments.length).to.equal(2);
expect(res.body.users.length).to.equal(2);
expect(res.body.actions.length).to.equal(1);
expect(res.body.settings).to.have.property('moderation', 'post');
});
});
it('should reject requests without a scheme in the asset_url', () => {
return chai.request(app)
.get('/api/v1/stream')
.query({asset_url: 'test.com'})
.catch((err) => {
expect(err).to.have.status(400);
expect(err.response.body.message).to.contain('asset_url is invalid');
});
});
it('should merge the settings when the asset contains settings to override it with', () => {
return chai.request(app)
.get('/api/v1/stream')
.query({asset_url: assets[1].url})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.assets).to.have.length(1);
expect(res.body.comments).to.have.length(1);
expect(res.body.users).to.have.length(1);
expect(res.body.settings).to.have.property('moderation', 'pre');
expect(res.body.settings).to.not.have.property('wordlist');
});
});
it('should not change the previously displayed comments based on moderation state changes', () => {
let preComments, postComments;
return chai.request(app)
.get('/api/v1/stream')
.query({asset_url: assets[2].url})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments.length).to.equal(2);
expect(res.body.settings).to.have.property('moderation', 'post');
preComments = res.body.comments;
return Asset.overrideSettings(assets[2].id, {moderation: 'pre'});
})
.then(() => {
return chai.request(app)
.get('/api/v1/stream')
.query({asset_url: assets[2].url});
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments.length).to.equal(2);
expect(res.body.settings).to.have.property('moderation', 'pre');
postComments = res.body.comments;
expect(preComments).to.deep.equal(postComments);
});
});
});
});
+6 -6
View File
@@ -5,21 +5,21 @@ const mailer = require('../../../../services/mailer');
const chai = require('chai');
const expect = chai.expect;
const Setting = require('../../../../models/setting');
const SettingsService = require('../../../../services/settings');
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
// Setup chai.
chai.should();
chai.use(require('chai-http'));
const User = require('../../../../models/user');
const UsersService = require('../../../../services/users');
describe('/api/v1/users/:user_id/email/confirm', () => {
let mockUser;
beforeEach(() => Setting.init(settings).then(() => {
return User.createLocalUser('ana@gmail.com', '123321123', 'Ana');
beforeEach(() => SettingsService.init(settings).then(() => {
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
})
.then((user) => {
mockUser = user;
@@ -63,8 +63,8 @@ describe('/api/v1/users/:user_id/actions', () => {
}];
beforeEach(() => {
return Setting.init(settings).then(() => {
return User.createLocalUsers(users);
return SettingsService.init(settings).then(() => {
return UsersService.createLocalUsers(users);
});
});
@@ -1,30 +1,32 @@
const Action = require('../../models/action');
const ActionModel = require('../../models/action');
const ActionsService = require('../../services/actions');
const expect = require('chai').expect;
describe('models.Action', () => {
describe('services.ActionsService', () => {
let mockActions = [];
beforeEach(() => Action.create([
beforeEach(() => ActionModel.create([
{
action_type: 'flag',
action_type: 'FLAG',
item_id: '123',
item_type: 'comment',
item_type: 'COMMENTS',
user_id: 'flagginguserid'
},
{
action_type: 'flag',
action_type: 'FLAG',
item_id: '456',
item_type: 'comment'
item_type: 'COMMENTS'
},
{
action_type: 'flag',
action_type: 'FLAG',
item_id: '123',
item_type: 'comment'
item_type: 'COMMENTS'
},
{
action_type: 'like',
action_type: 'LIKE',
item_id: '123',
item_type: 'comment'
item_type: 'COMMENTS'
}
]).then((actions) => {
mockActions = actions;
@@ -32,16 +34,16 @@ describe('models.Action', () => {
describe('#findById()', () => {
it('should find an action by id', () => {
return Action.findById(mockActions[0].id).then((result) => {
return ActionsService.findById(mockActions[0].id).then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.property('action_type', 'flag');
expect(result).to.have.property('action_type', 'FLAG');
});
});
});
describe('#findByItemIdArray()', () => {
it('should find an array of actions from an array of item_ids', () => {
return Action.findByItemIdArray(['123', '456']).then((result) => {
return ActionsService.findByItemIdArray(['123', '456']).then((result) => {
expect(result).to.have.length(4);
});
});
@@ -49,46 +51,48 @@ describe('models.Action', () => {
describe('#getActionSummaries()', () => {
it('should return properly formatted summaries from an array of item_ids', () => {
return Action.getActionSummaries(['123', '789']).then((summaries) => {
expect(summaries).to.have.length(2);
return ActionsService
.getActionSummaries(['123', '789'])
.then((summaries) => {
expect(summaries).to.have.length(2);
expect(summaries).to.deep.include({
action_type: 'like',
count: 1,
item_id: '123',
item_type: 'comment',
current_user: null
});
expect(summaries).to.deep.include({
action_type: 'LIKE',
count: 1,
item_id: '123',
item_type: 'COMMENTS',
current_user: null
});
expect(summaries).to.deep.include({
action_type: 'flag',
count: 2,
item_id: '123',
item_type: 'comment',
current_user: null
expect(summaries).to.deep.include({
action_type: 'FLAG',
count: 2,
item_id: '123',
item_type: 'COMMENTS',
current_user: null
});
});
});
});
it('should include a current user when one is passed', () => {
return Action
return ActionsService
.getActionSummaries(['123'], 'flagginguserid')
.then((summaries) => {
expect(summaries).to.have.length(2);
let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'flag');
let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'FLAG');
expect(summary).to.not.be.undefined;
expect(summary.current_user).to.not.be.null;
expect(summary.current_user).to.have.property('item_id', '123');
expect(summary.current_user).to.have.property('item_type', 'comment');
expect(summary.current_user).to.have.property('item_type', 'COMMENTS');
expect(summary.current_user).to.have.property('user_id', 'flagginguserid');
expect(summary.current_user).to.have.property('action_type', 'flag');
expect(summary.current_user).to.have.property('action_type', 'FLAG');
});
});
it('should not include a current user when one is passed for a user that doesn\'t have an action', () => {
return Action
return ActionsService
.getActionSummaries(['123'], 'flagginguserid2')
.then((summaries) => {
expect(summaries).to.have.length(2);
@@ -1,4 +1,5 @@
const Asset = require('../../models/asset');
const AssetModel = require('../../models/asset');
const AssetsService = require('../../services/assets');
const chai = require('chai');
const expect = chai.expect;
@@ -6,16 +7,16 @@ const expect = chai.expect;
// Use the chai should.
chai.should();
describe('models.Asset', () => {
describe('services.AssetsService', () => {
beforeEach(() => {
const defaults = {url:'http://test.com'};
return Asset.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
describe('#findById', ()=> {
it('should find an asset by the id', () => {
return Asset.findById(1)
return AssetsService.findById(1)
.then((asset) => {
expect(asset).to.have.property('url')
.and.to.equal('http://test.com');
@@ -24,17 +25,19 @@ describe('models.Asset', () => {
});
describe('#findByUrl', ()=> {
beforeEach(() => Asset.findOrCreateByUrl('http://test.com'));
beforeEach(() => AssetsService.findOrCreateByUrl('http://test.com'));
it('should find an asset by a url', () => {
return Asset.findByUrl('http://test.com')
return AssetsService
.findByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('url', 'http://test.com');
});
});
it('should return null when a url does not exist', () => {
return Asset.findByUrl('http://new.test.com')
return AssetsService
.findByUrl('http://new.test.com')
.then((asset) => {
expect(asset).to.be.null;
});
@@ -43,7 +46,8 @@ describe('models.Asset', () => {
describe('#findOrCreateByUrl', ()=> {
it('should find an asset by a url', () => {
return Asset.findOrCreateByUrl('http://test.com')
return AssetsService
.findOrCreateByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('url')
.and.to.equal('http://test.com');
@@ -51,7 +55,8 @@ describe('models.Asset', () => {
});
it('should return a new asset when the url does not exist', () => {
return Asset.findOrCreateByUrl('http://new.test.com')
return AssetsService
.findOrCreateByUrl('http://new.test.com')
.then((asset) => {
expect(asset).to.have.property('id')
.and.to.not.equal(1);
@@ -61,16 +66,16 @@ describe('models.Asset', () => {
describe('#overrideSettings', () => {
it('should update the settings', () => {
return Asset
return AssetsService
.findOrCreateByUrl('https://override.test.com/asset')
.then((asset) => {
expect(asset).to.have.property('settings');
expect(asset.settings).to.be.null;
return Asset.overrideSettings(asset.id, {moderation: 'pre'});
return AssetsService.overrideSettings(asset.id, {moderation: 'pre'});
})
.then(() => {
return Asset.findOrCreateByUrl('https://override.test.com/asset');
return AssetsService.findOrCreateByUrl('https://override.test.com/asset');
})
.then((asset) => {
expect(asset).to.have.property('settings');
@@ -82,7 +87,8 @@ describe('models.Asset', () => {
describe('#findOrCreateByUrl', ()=> {
it('should find an asset by a url', () => {
return Asset.findOrCreateByUrl('http://test.com')
return AssetsService
.findOrCreateByUrl('http://test.com')
.then((asset) => {
expect(asset).to.have.property('url')
.and.to.equal('http://test.com');
@@ -90,7 +96,8 @@ describe('models.Asset', () => {
});
it('should return a new asset when the url does not exist', () => {
return Asset.findOrCreateByUrl('http://new.test.com')
return AssetsService
.findOrCreateByUrl('http://new.test.com')
.then((asset) => {
expect(asset).to.have.property('id')
.and.to.not.equal(1);
+259
View File
@@ -0,0 +1,259 @@
const CommentModel = require('../../models/comment');
const ActionModel = require('../../models/action');
const ActionsService = require('../../services/actions');
const UsersService = require('../../services/users');
const SettingsService = require('../../services/settings');
const CommentsService = require('../../services/comments');
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
const expect = require('chai').expect;
describe('services.CommentsService', () => {
const comments = [{
body: 'comment 10',
asset_id: '123',
status_history: [],
parent_id: '',
author_id: '123',
id: '1'
}, {
body: 'comment 20',
asset_id: '123',
status_history: [{
type: 'ACCEPTED'
}],
status: 'ACCEPTED',
parent_id: '',
author_id: '123',
id: '2'
}, {
body: 'comment 30',
asset_id: '456',
status_history: [],
parent_id: '',
author_id: '456',
id: '3'
}, {
body: 'comment 40',
asset_id: '123',
status_history: [{
type: 'REJECTED'
}],
status: 'REJECTED',
parent_id: '',
author_id: '456',
id: '4'
}, {
body: 'comment 50',
asset_id: '1234',
status_history: [{
type: 'PREMOD'
}],
status: 'PREMOD',
parent_id: '',
author_id: '456',
id: '5'
}, {
body: 'comment 60',
asset_id: '1234',
status_history: [{
type: 'PREMOD'
}],
status: 'PREMOD',
parent_id: '',
author_id: '456',
id: '6'
}];
const users = [{
email: 'stampi@gmail.com',
displayName: 'Stampi',
password: '1Coral!!'
}, {
email: 'sockmonster@gmail.com',
displayName: 'Sockmonster',
password: '2Coral!!'
}];
const actions = [{
action_type: 'FLAG',
item_id: '3',
item_type: 'COMMENTS',
user_id: '123'
}, {
action_type: 'LIKE',
item_id: '1',
item_type: 'COMMENTS',
user_id: '456'
}];
beforeEach(() => {
return SettingsService.init(settings).then(() => {
return Promise.all([
CommentModel.create(comments),
UsersService.createLocalUsers(users),
ActionModel.create(actions)
]);
});
});
describe('#publicCreate()', () => {
it('creates a new comment', () => {
return CommentsService
.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.be.equal('ACCEPTED');
});
});
it('creates many new comments', () => {
return CommentsService
.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.be.equal('ACCEPTED');
expect(c2).to.not.be.null;
expect(c2.id).to.be.uuid;
expect(c2.status).to.be.null;
expect(c3).to.not.be.null;
expect(c3.id).to.be.uuid;
expect(c3.status).to.be.equal('REJECTED');
});
});
});
describe('#findById()', () => {
it('should find a comment by id', () => {
return CommentsService
.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 CommentsService
.findByAssetId('123')
.then((result) => {
expect(result).to.have.length(3);
result.sort((a, b) => {
if (a.body < b.body) {return -1;}
else {return 1;}
});
expect(result[0]).to.have.property('body', 'comment 10');
expect(result[1]).to.have.property('body', 'comment 20');
expect(result[2]).to.have.property('body', 'comment 40');
});
});
});
describe('#moderationQueue()', () => {
it('should find an array of new comments to moderate when pre-moderation', () => {
return CommentsService
.moderationQueue('PREMOD')
.then((result) => {
expect(result).to.not.be.null;
expect(result).to.have.lengthOf(2);
});
});
});
describe('#removeAction', () => {
it('should remove an action', () => {
return CommentsService
.removeAction('3', '123', 'flag')
.then(() => {
return ActionsService.findByItemIdArray(['123']);
})
.then((actions) => {
expect(actions.length).to.equal(0);
});
});
});
describe('#findByUserId', () => {
it('should return all comments if admin', () => {
return CommentsService
.findByUserId('456', true)
.then(comments => {
expect(comments).to.have.length(4);
});
});
it('should not return premod and rejected comments if not admin', () => {
return CommentsService
.findByUserId('456')
.then(comments => {
expect(comments).to.have.length(1);
});
});
});
describe('#changeStatus', () => {
it('should change the status of a comment from no status', () => {
let comment_id = comments[0].id;
return CommentsService.findById(comment_id)
.then((c) => {
expect(c.status).to.be.null;
return CommentsService.pushStatus(comment_id, 'REJECTED', '123');
})
.then(() => CommentsService.findById(comment_id))
.then((c) => {
expect(c).to.have.property('status');
expect(c.status).to.equal('REJECTED');
expect(c.status_history).to.have.length(1);
expect(c.status_history[0]).to.have.property('type', 'REJECTED');
expect(c.status_history[0]).to.have.property('assigned_by', '123');
});
});
it('should change the status of a comment from accepted', () => {
return CommentsService.pushStatus(comments[1].id, 'REJECTED', '123')
.then(() => CommentsService.findById(comments[1].id))
.then((c) => {
expect(c).to.have.property('status_history');
expect(c).to.have.property('status');
expect(c.status).to.equal('REJECTED');
expect(c.status_history).to.have.length(2);
expect(c.status_history[0]).to.have.property('type', 'ACCEPTED');
expect(c.status_history[0]).to.have.property('assigned_by', null);
expect(c.status_history[1]).to.have.property('type', 'REJECTED');
expect(c.status_history[1]).to.have.property('assigned_by', '123');
});
});
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
describe('scraper: services', () => {
describe('services.scraper', () => {
describe('#create', () => {
it('should create a new kue job');
});
@@ -1,19 +1,19 @@
const Setting = require('../../models/setting');
const SettingsService = require('../../services/settings');
const expect = require('chai').expect;
describe('models.Setting', () => {
describe('services.SettingsService', () => {
beforeEach(() => Setting.init({moderation: 'pre', wordlist: ['donut']}));
beforeEach(() => SettingsService.init({moderation: 'pre', wordlist: ['donut']}));
describe('#retrieve()', () => {
it('should have a moderation field defined', () => {
return Setting.retrieve().then(settings => {
return SettingsService.retrieve().then(settings => {
expect(settings).to.have.property('moderation').and.to.equal('pre');
});
});
it('should have two infoBox fields defined', () => {
return Setting.retrieve().then(settings => {
return SettingsService.retrieve().then(settings => {
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
});
@@ -23,7 +23,7 @@ describe('models.Setting', () => {
describe('#update()', () => {
it('should update the settings with a passed object', () => {
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
return Setting.update(mockSettings).then(updatedSettings => {
return SettingsService.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);
@@ -34,7 +34,7 @@ describe('models.Setting', () => {
describe('#get', () => {
it('should return the moderation settings', () => {
return Setting.retrieve().then(({moderation}) => {
return SettingsService.retrieve().then(({moderation}) => {
expect(moderation).not.to.be.null;
});
});
@@ -42,7 +42,7 @@ describe('models.Setting', () => {
describe('#merge', () => {
it('should merge a settings object and its overrides', () => {
return Setting
return SettingsService
.retrieve()
.then((settings) => {
let ovrSett = {moderation: 'post'};
+42 -87
View File
@@ -1,16 +1,16 @@
const User = require('../../models/user');
const Comment = require('../../models/comment');
const Setting = require('../../models/setting');
const UsersService = require('../../services/users');
const SettingsService = require('../../services/settings');
const expect = require('chai').expect;
describe('models.User', () => {
describe('services.UsersService', () => {
let mockUsers;
beforeEach(() => {
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
return Setting.init(settings).then(() => {
return User.createLocalUsers([{
return SettingsService.init(settings).then(() => {
return UsersService.createLocalUsers([{
email: 'stampi@gmail.com',
displayName: 'Stampi',
password: '1Coral!-'
@@ -30,11 +30,10 @@ describe('models.User', () => {
describe('#findById()', () => {
it('should find a user by id', () => {
return User
return UsersService
.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('displayName')
.and.to.equal('stampi');
expect(user).to.have.property('displayName', 'stampi');
});
});
});
@@ -42,7 +41,7 @@ describe('models.User', () => {
describe('#findByIdArray()', () => {
it('should find an array of users from an array of ids', () => {
const ids = mockUsers.map((user) => user.id);
return User.findByIdArray(ids).then((result) => {
return UsersService.findByIdArray(ids).then((result) => {
expect(result).to.have.length(3);
});
});
@@ -51,15 +50,14 @@ describe('models.User', () => {
describe('#findPublicByIdArray()', () => {
it('should find an array of users from an array of ids', () => {
const ids = mockUsers.map((user) => user.id);
return User.findPublicByIdArray(ids).then((result) => {
return UsersService.findPublicByIdArray(ids).then((result) => {
expect(result).to.have.length(3);
const sorted = result.sort((a, b) => {
if(a.displayName < b.displayName) {return -1;}
if(a.displayName > b.displayName) {return 1;}
return 0;
});
expect(sorted[0]).to.have.property('displayName')
.and.to.equal('marvel');
expect(sorted[0]).to.have.property('displayName', 'marvel');
});
});
});
@@ -67,7 +65,7 @@ describe('models.User', () => {
describe('#findLocalUser', () => {
it('should find a user when we give the right credentials', () => {
return User
return UsersService
.findLocalUser(mockUsers[0].profiles[0].id, '1Coral!-')
.then((user) => {
expect(user).to.have.property('displayName')
@@ -76,7 +74,7 @@ describe('models.User', () => {
});
it('should not find the user when we give the wrong credentials', () => {
return User
return UsersService
.findLocalUser(mockUsers[0].profiles[0].id, '1Coral!-<nope>')
.then((user) => {
expect(user).to.equal(false);
@@ -87,9 +85,9 @@ describe('models.User', () => {
describe('#createLocalUser', () => {
it('should not create a user with duplicate display name', () => {
return User.createLocalUsers([{
return UsersService.createLocalUsers([{
email: 'otrostampi@gmail.com',
displayName: 'Stampi',
displayName: 'StampiTheSecond',
password: '1Coralito!'
}])
.then((user) => {
@@ -104,7 +102,7 @@ describe('models.User', () => {
describe('#createEmailConfirmToken', () => {
it('should create a token for a valid user', () => {
return User
return UsersService
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
@@ -112,15 +110,15 @@ describe('models.User', () => {
});
it('should not create a token for a user already verified', () => {
return User
return UsersService
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
return UsersService.verifyEmailConfirmation(token);
})
.then(() => {
return User.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
return UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
})
.catch((err) => {
expect(err).to.have.property('message', 'email address already confirmed');
@@ -132,17 +130,17 @@ describe('models.User', () => {
describe('#verifyEmailConfirmation', () => {
it('should correctly validate a valid token', () => {
return User
return UsersService
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
return UsersService.verifyEmailConfirmation(token);
});
});
it('should correctly reject an invalid token', () => {
return User
return UsersService
.verifyEmailConfirmation('cats')
.catch((err) => {
expect(err).to.not.be.null;
@@ -150,15 +148,15 @@ describe('models.User', () => {
});
it('should update the user model when verification is complete', () => {
return User
return UsersService
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
return UsersService.verifyEmailConfirmation(token);
})
.then(() => {
return User.findById(mockUsers[0].id);
return UsersService.findById(mockUsers[0].id);
})
.then((user) => {
expect(user.profiles[0]).to.have.property('metadata');
@@ -171,85 +169,42 @@ describe('models.User', () => {
describe('#setStatus', () => {
it('should set the status to active', () => {
return User
.setStatus(mockUsers[0].id, 'active')
.then(() => {
User.findById(mockUsers[0].id)
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('active');
});
return UsersService
.setStatus(mockUsers[0].id, 'ACTIVE')
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'ACTIVE');
});
});
});
describe('#ban', () => {
let mockComment;
beforeEach(() => {
return Comment.create([
{
body: 'testing the comment for that user if it is rejected.',
id: mockUsers[0].id
}
])
.then(([comment]) => {
mockComment = comment;
});
});
it('should set the status to banned', () => {
return User
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
.then(() => {
return User.findById(mockUsers[0].id);
})
return UsersService
.setStatus(mockUsers[0].id, 'BANNED')
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status')
.and.to.equal('banned');
});
});
it('should set the comment to rejected', () => {
return User
.setStatus(mockUsers[0].id, 'banned', mockComment.id)
.then(() => {
return Comment.findById(mockComment.id);
})
.then((comment) => {
expect(comment).to.have.property('status')
.and.to.equal('rejected');
expect(user).to.have.property('status', 'BANNED');
});
});
it('should still disable and ban the user if there is no comment', () => {
return User
.setStatus(mockUsers[0].id, 'banned', '')
.then(() => User.findById(mockUsers[0].id))
return UsersService
.setStatus(mockUsers[0].id, 'BANNED')
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'banned');
expect(user).to.have.property('status', 'BANNED');
});
});
});
describe('#unban', () => {
let mockComment;
beforeEach(() => {
return Promise.all([
Comment.create([{body: 'testing the comment for that user if it is rejected.', id: mockUsers[0].id}])
])
.then((comment) => {
mockComment = comment;
});
});
it('should set the status to active', () => {
return User
.setStatus(mockUsers[0].id, 'active', mockComment.id)
.then(() => User.findById(mockUsers[0].id))
return UsersService
.setStatus(mockUsers[0].id, 'ACTIVE')
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'active');
expect(user).to.have.property('status', 'ACTIVE');
});
});
});
+3 -3
View File
@@ -1,9 +1,9 @@
const expect = require('chai').expect;
const Errors = require('../../errors');
const Wordlist = require('../../services/wordlist');
const Setting = require('../../models/setting');
const SettingsService = require('../../services/settings');
describe('wordlist: services', () => {
describe('services.Wordlist', () => {
const wordlists = {
banned: [
@@ -19,7 +19,7 @@ describe('wordlist: services', () => {
let wordlist = new Wordlist();
const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
beforeEach(() => Setting.init(settings));
beforeEach(() => SettingsService.init(settings));
describe('#init', () => {