diff --git a/.gitignore b/.gitignore index 3b59ff2f5..5a42acb67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ node_modules -npm-debug.log +npm-debug.log* dist !dist/coral-admin dist/coral-admin/bundle.js +tests/e2e/reports .DS_Store *.iml +dump.rdb .env gaba.cfg .idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9fa7985c0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contribution Guide + +We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together. + + +## Doing the Work + +We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. + + +### Code Reviews + +One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. + +Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand. + +Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace. + +**Is the code clear, efficient and a pleasure to read?** + +Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code. + +Thinking like a computer, you could ask: + +* Is the code using memory efficiently? +* Is data being moved around unnecessarily? +* Are multiple network requests being made where fewer would do? +* Is there excess processing happening in a synchronous flow that may disrupt user experience? +* Are there large libraries included for small gains? + +Then, returning to your human roots... Is the code readable? + +* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? +* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? +* Are the files separated logically such that each one contains a clear concept of code? + + +**Is the API documentation up to date? Are all client calls written against the docs?** + +We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation. + +* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs. +* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled? + +**Is there sufficient test coverage?** + +Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests) + +* Can you a sense of the logic behind the code by reading the tests? +* Can you see both what should happen and what should _never, ever_ be allowed to happen? +* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)? + + +### Forking, Branching and Merging + +Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. + +Releases are _tagged_ off the master branch. + +Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution. + +* At the outset of a piece of work, a branch or fork is made from master. +* The work is done in that fork. +* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.) +* At least one other contributor to the project must review all code (see Code Reviews below.) +* If there are merge conflicts with master, merge master into the branch. +* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) +* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master. +* Merge to master, but _you're not quite done yet!_ +* Deploy master to staging (or have a core member do so.) +* Ensure that all your changes are working on staging. +* Have your reviewer verify the same. +* ... aaaand the work is delivered! + + +## Continuous Integration + +We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk) + +Our pipeline will _test_, _lint_, and _build_ all pushes to the repo. + +Any branch not passing CI will not be merged into master. + +If you're working in a fork, please run each of the steps locally before submitting a PR. + + +## Coding Style + +### API Design + +When building APIs, we follow these principles: + +* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations. +* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object. +* Put non-required, flexible variables into query params, required/identity based values in request params. diff --git a/bin/cli-serve b/bin/cli-serve index 14c1261d7..dd682f5ad 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -17,7 +17,8 @@ const util = require('../util'); /** * Get port from environment and store in Express. */ -const port = normalizePort(process.env.TALK_PORT || '3000'); + +const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000')); app.set('port', port); @@ -95,6 +96,10 @@ function startApp() { server.listen(port); server.on('error', onError); server.on('listening', onListening); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); }); } diff --git a/cache.js b/cache.js index 9345f8cde..1d090e46d 100644 --- a/cache.js +++ b/cache.js @@ -76,6 +76,21 @@ cache.get = (key) => new Promise((resolve, reject) => { }); }); +/** + * This invalidates a cached entry in the cache. + * @param {Mixed} key Either an array of items composing a key or a string + * @return {Promise} + */ +cache.invalidate = (key) => new Promise((resolve, reject) => { + cache.client.del(keyfunc(key), (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); +}); + /** * This sets a value on the key with the expiry and then resolves once it is * done. diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 26142a311..0fda9444d 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -7,6 +7,7 @@ import { Notification, notificationActions, authActions, + configActions } from '../../coral-framework'; import CommentBox from '../../coral-plugin-commentbox/CommentBox'; @@ -21,14 +22,17 @@ import LikeButton from '../../coral-plugin-likes/LikeButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; import UserBox from '../../coral-sign-in/components/UserBox'; + import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; import RestrictedContent from '../../coral-framework/components/RestrictedContent'; import SuspendedAccount from '../../coral-framework/components/SuspendedAccount'; +import CloseCommentsInfo from '../../coral-framework/components/CloseCommentsInfo'; const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; -const {logout} = authActions; +const {logout, showSignInDialog} = authActions; +const {updateOpenStatus} = configActions; class CommentStream extends Component { @@ -40,6 +44,7 @@ class CommentStream extends Component { }; this.changeTab = this.changeTab.bind(this); + this.toggleStatus = this.toggleStatus.bind(this); } changeTab (tab) { @@ -48,6 +53,10 @@ class CommentStream extends Component { }); } + toggleStatus () { + this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open'); + } + static propTypes = { items: PropTypes.object.isRequired, addItem: PropTypes.func.isRequired, @@ -56,31 +65,22 @@ class CommentStream extends Component { componentDidMount () { // Set up messaging between embedded Iframe an parent component - // Using recommended Pym init code which violates .eslint standards - const pym = new Pym.Child({polling: 100}); + this.pym = new Pym.Child({polling: 100}); - if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) { - this.props.getStream(pym.parentUrl); - } else { - this.props.getStream(window.location); - } + const path = /https?\:\/\/([^?#]+)/.exec(this.pym.parentUrl); + + this.props.getStream(path[1] || window.location); + this.path = path; + + this.pym.sendMessage('childReady'); } render () { - if (Object.keys(this.props.items).length === 0) { - // Loading mock asset - this.props.postItem({ - comments: [], - url: 'http://coralproject.net' - }, 'asset', 'assetTest'); - } - - // TODO: Replace teststream id with id from params - const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0]; const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; const {loggedIn, user, showSignInDialog} = this.props.auth; + const {status} = this.props.config; const {activeTab} = this.state; return
@@ -90,131 +90,145 @@ class CommentStream extends Component { Settings + Configure Stream - {loggedIn && } + {loggedIn && } {/* Add to the restricted param a boolean if the user is suspended*/} }> -
- - + { + status === 'open' + ?
+ + +
+ :

Comments are closed for this thread.

+ } {!loggedIn && } -
- { - rootItem.comments && rootItem.comments.map((commentId) => { - const comment = comments[commentId]; - return
-
- - - -
- { + const comment = comments[commentId]; + return
+
+ + + +
+ + +
+
+ + +
+ - -
-
- - -
- - { - comment.children && - comment.children.map((replyId) => { - let reply = this.props.items.comments[replyId]; - return
-
- - - -
- { + let reply = this.props.items.comments[replyId]; + return
+
+ + + +
+ + +
+
+ + +
+ - -
-
- - -
- -
; - }) +
; + }) }
; - }) - } + }) + } + - {!loggedIn && } + + +

{status === 'open' ? 'Close' : 'Open'} Comment Stream

+ + +
: - + }
; } @@ -253,11 +272,13 @@ const mapDispatchToProps = (dispatch) => ({ getStream: (rootId) => dispatch(getStream(rootId)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), + showSignInDialog: () => dispatch(showSignInDialog()), postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)), deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)), appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), handleSignInDialog: () => dispatch(authActions.showSignInDialog()), logout: () => dispatch(logout()), + updateStatus: status => dispatch(updateOpenStatus(status)) }); export default connect(mapStateToProps, mapDispatchToProps)(CommentStream); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index b6fe672cc..59efc3dc8 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -56,12 +56,14 @@ hr { /* Info Box Styles */ .coral-plugin-infobox-info { - position: fixed; top: 0; border: 0; background: rgb(105,105,105); color: white; - border-radius: 2px; + width: 100%; + text-align: center; + padding: 10px; + margin-bottom: 10px; font-weight: bold; display: block; } @@ -195,3 +197,42 @@ hr { float: right; margin: 8px; } + +/* Close comments */ + +.close-comments-intro-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +.close-comments-intro-wrapper button { + width: 300px; + margin-left: 20px; +} + +.close-comments-intro-wrapper button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.close-comments-message { + box-sizing: border-box; + width: 100%; + height: 100px; +} + +.close-comments-confirm-wrapper { + float: right; +} + +.close-comments-alert { + background-color: #d65344; + color: white; + font-size: 16px; + padding: 5px; +} + +.close-comments-alert i.material-icons { + font-size: 16px !important; +} diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js new file mode 100644 index 000000000..6dc880434 --- /dev/null +++ b/client/coral-framework/actions/config.js @@ -0,0 +1,18 @@ +import coralApi from '../helpers/response'; +/* Config Actions */ + +/** + * Action name constants + */ + +export const OPEN_COMMENTS = 'OPEN_COMMENTS'; +export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; +export const ADD_ITEM = 'ADD_ITEM'; + +export const updateOpenStatus = status => (dispatch, getState) => { + const assetId = getState().items.get('assets') + .keySeq() + .toArray()[0]; + return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'}) + .then(() => dispatch({type: status === 'open' ? OPEN_COMMENTS : CLOSE_COMMENTS})); +}; diff --git a/client/coral-framework/components/CloseCommentsInfo.js b/client/coral-framework/components/CloseCommentsInfo.js new file mode 100644 index 000000000..627328f9b --- /dev/null +++ b/client/coral-framework/components/CloseCommentsInfo.js @@ -0,0 +1,23 @@ +import React from 'react'; +import {Button} from 'coral-ui'; + +export default ({status, onClick}) => ( + status === 'open' ? ( +
+

+ This comment stream is currently open. By closing this comment stream, + no new comments may be submitted and all previous comments will still + be displayed. +

+ +
+ ) : ( +
+

+ This comment stream is currently closed. By opening this comment stream, + new comments may be submitted and displayed +

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