diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 88a20e445..013c159a8 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -47,7 +47,7 @@ "comment-settings": "Comment Settings", "embed-comment-stream": "Embed Comment Stream", "banned-word-header": "Write the bannned words list", - "banned-word-text": "Comments which contain these words or phrases, not seperated by commas and not case sensitive, will be automatically removed from the comment stream.", + "banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.", "wordlist": "Banned words list", "save-changes": "Save Changes", "copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles", diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index bb59ffa8e..7ec042ea1 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -16,7 +16,7 @@ import PubDate from '../../coral-plugin-pubdate/PubDate'; import Count from '../../coral-plugin-comment-count/CommentCount'; import AuthorName from '../../coral-plugin-author-name/AuthorName'; import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'; -import FlagButton from '../../coral-plugin-flags/FlagButton'; +import FlagComment from '../../coral-plugin-flags/FlagComment'; import LikeButton from '../../coral-plugin-likes/LikeButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; @@ -90,8 +90,8 @@ class CommentStream extends Component { 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 {status, moderation, closedMessage} = this.props.config; const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth; - const {status, closedMessage} = this.props.config; const {activeTab} = this.state; const banned = (this.props.userData.status === 'banned'); @@ -123,7 +123,7 @@ class CommentStream extends Component { appendItemArray={this.props.appendItemArray} updateItem={this.props.updateItem} id={rootItemId} - premod={this.props.config.moderation} + premod={moderation} reply={false} currentUser={this.props.auth.user} banned={banned} @@ -139,7 +139,17 @@ class CommentStream extends Component { const comment = comments[commentId]; return

- +
@@ -162,9 +172,10 @@ class CommentStream extends Component { banned={banned}/>
- { comment.children && @@ -217,10 +228,11 @@ class CommentStream extends Component { banned={banned}/>
-
; }) @@ -295,14 +307,14 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = (dispatch) => ({ - addItem: (item, itemType) => dispatch(addItem(item, itemType)), + addItem: (item, item_id) => dispatch(addItem(item, item_id)), updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)), postItem: (data, type, id) => dispatch(postItem(data, type, id)), getStream: (rootId) => dispatch(getStream(rootId)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), + postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)), showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), - 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()), diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 766ad763a..c27baf406 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -127,6 +127,10 @@ hr { font-weight: bolder; } +.coral-plugin-author-name-bio-flag { + float: right; +} + /* Reply styles */ @@ -197,6 +201,54 @@ hr { margin: 8px; } +/* Flag Styles */ + +.coral-plugin-flags-container { + position: relative; +} + +.coral-plugin-flags-popup span { + min-width: 280px; + bottom: 36px; + left: -190px; + position: absolute; +} + +.coral-plugin-flags-popup-form { + margin-bottom: 10px; +} + +.coral-plugin-flags-popup-header { + font-weight: bolder; + font-size: 16px; + margin-bottom: 10px; +} + +.coral-plugin-flags-popup-radio { + margin:5px; +} + +.coral-plugin-flags-popup-radio-label { + margin:5px; + font-size: 14px; +} + +.coral-plugin-flags-popup-counter { + float: left; + margin-top: 21px; + color: #999; +} + +.coral-plugin-flags-popup-button { + float: right; + margin-top: 10px; +} + +.coral-plugin-flags-other-text { + margin-left: 20px; + width: 75%; +} + /* Close comments */ .close-comments-intro-wrapper { diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index c1ee14924..8f0e789af 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -212,13 +212,8 @@ export function postItem (item, type, id) { * */ -export function postAction (item_id, action_type, user_id, item_type) { +export function postAction (item_id, item_type, action) { return () => { - const action = { - action_type, - user_id - }; - return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}); }; } diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js index 56b868726..bf970b3c4 100644 --- a/client/coral-plugin-author-name/AuthorName.js +++ b/client/coral-plugin-author-name/AuthorName.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import {Tooltip} from 'coral-ui'; +import FlagBio from '../coral-plugin-flags/FlagBio'; const packagename = 'coral-plugin-author-name'; export default class AuthorName extends Component { @@ -36,7 +37,15 @@ export default class AuthorName extends Component { onMouseLeave={this.handleMouseLeave} > {author && author.displayName} - { showTooltip && {author.settings.bio}} + { showTooltip && +
+ {author.settings.bio} +
+
+ +
+
+ }
); } diff --git a/client/coral-plugin-flags/FlagBio.js b/client/coral-plugin-flags/FlagBio.js new file mode 100644 index 000000000..90cc11a7a --- /dev/null +++ b/client/coral-plugin-flags/FlagBio.js @@ -0,0 +1,35 @@ +import React from 'react'; +import FlagButton from './FlagButton'; +import {I18n} from '../coral-framework'; +import translations from './translations.json'; + +const FlagBio = (props) => ; + +const getPopupMenu = [ + () => { + return { + header: lang.t('step-2-header'), + itemType: 'user', + field: 'bio', + options: [ + {val: 'This bio is offensive', text: lang.t('bio-offensive')}, + {val: 'I don\'t like this bio', text: lang.t('no-like-bio')}, + {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, + {val: 'other', text: lang.t('other')} + ], + button: lang.t('continue'), + sets: 'detail' + }; + }, + () => { + return { + header: lang.t('step-3-header'), + text: lang.t('thank-you'), + button: lang.t('done'), + }; + } +]; + +export default FlagBio; + +const lang = new I18n(translations); diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 92e0769fe..d0b9cb6b4 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -1,52 +1,178 @@ -import React from 'react'; +import React, {Component} from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; +import {PopupMenu, Button} from 'coral-ui'; +import onClickOutside from 'react-onclickoutside'; const name = 'coral-plugin-flags'; -const FlagButton = ({flag, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, addNotification, currentUser, banned}) => { - const flagged = flag && flag.current_user; - const onFlagClick = () => { - if (!currentUser) { - const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75; - showSignInDialog(offset); +class FlagButton extends Component { + + state = { + showMenu: false, + showOther: false, + itemType: '', + detail: '', + otherText: '', + step: 0, + posted: false + } + + // When the "report" button is clicked expand the menu + onReportClick = () => { + if (!this.props.currentUser) { + const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75; + this.props.showSignInDialog(offset); return; } - if (banned) { - return; + this.setState({showMenu: !this.state.showMenu}); + } + + onPopupContinue = () => { + const {postAction, addItem, updateItem, flag, id, author_id} = this.props; + const {itemType, field, detail, step, otherText, posted} = this.state; + + //Proceed to the next step or close the menu if we've reached the end + if (step + 1 >= this.props.getPopupMenu.length) { + this.setState({showMenu: false}); + } else { + this.setState({step: step + 1}); } - if (!flagged) { - postAction(id, 'flag', currentUser.id, 'comments') + + // If itemType and detail are both set, post the action + if (itemType && detail && !posted) { + // Set the text from the "other" field if it exists. + const updatedDetail = otherText || detail; + let item_id; + switch(itemType) { + case 'comments': + item_id = id; + break; + case 'user': + item_id = author_id; + break; + } + const action = { + action_type: 'flag', + field, + detail: updatedDetail + }; + postAction(item_id, itemType, action) .then((action) => { let id = `${action.action_type}_${action.item_id}`; addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions'); - updateItem(action.item_id, action.action_type, id, 'comments'); + updateItem(action.item_id, action.action_type, id, action.item_type); + this.setState({posted: true}); }); - addNotification('success', lang.t('flag-notif')); - } else { - deleteAction(flagged.id) - .then(() => { - updateItem(id, 'flag', '', 'comments'); - }); - addNotification('success', lang.t('flag-notif-remove')); } - }; + } - return
- { - flagged - ? {lang.t('flagged')} - : {lang.t('flag')} + this.state.showMenu && +
+ +
{popupMenu.header}
+ { + popupMenu.text && +
{popupMenu.text}
+ } + { + popupMenu.options &&
+ { + popupMenu.options.map((option) => +
+ +
+
+ ) + } + { + this.state.showOther &&
+ +
+
+ } +
+ } +
+ {this.state.step + 1} of {getPopupMenu.length} +
+ { + popupMenu.button && + } +
+
} - flag - -
; -}; + ; + } +} -export default FlagButton; +export default onClickOutside(FlagButton); const styles = { flaggedIcon: { diff --git a/client/coral-plugin-flags/FlagComment.js b/client/coral-plugin-flags/FlagComment.js new file mode 100644 index 000000000..2d5b33039 --- /dev/null +++ b/client/coral-plugin-flags/FlagComment.js @@ -0,0 +1,52 @@ +import React from 'react'; +import FlagButton from './FlagButton'; +import {I18n} from '../coral-framework'; +import translations from './translations.json'; + +const FlagComment = (props) => ; + +const getPopupMenu = [ + () => { + return { + header: lang.t('step-1-header'), + options: [ + {val: 'user', text: lang.t('flag-username')}, + {val: 'comments', text: lang.t('flag-comment')} + ], + button: lang.t('continue'), + sets: 'itemType' + }; + }, + (itemType) => { + const options = itemType === 'comments' ? + [ + {val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')}, + {val: 'This comment is offensive', text: lang.t('comment-offensive')}, + {val: 'This comment reveals personally identifiable infomration', text: lang.t('personal-info')}, + {val: 'other', text: lang.t('other')} + ] + : [ + {val: 'This username is offensive', text: lang.t('username-offensive')}, + {val: 'I don\'t like this username', text: lang.t('no-like-username')}, + {val: 'This looks like an ad/marketing', text: lang.t('marketing')}, + {val: 'other', text: lang.t('other')} + ]; + return { + header: lang.t('step-2-header'), + options, + button: lang.t('continue'), + sets: 'detail' + }; + }, + () => { + return { + header: lang.t('step-3-header'), + text: lang.t('thank-you'), + button: lang.t('done'), + }; + } +]; + +export default FlagComment; + +const lang = new I18n(translations); diff --git a/client/coral-plugin-flags/translations.json b/client/coral-plugin-flags/translations.json index 633b85282..0c2ce594d 100644 --- a/client/coral-plugin-flags/translations.json +++ b/client/coral-plugin-flags/translations.json @@ -1,14 +1,50 @@ { "en": { - "flag": "Flag", - "flagged": "Flagged", - "flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.", - "flag-notif-remove": "Your flag has been removed." + "report": "Report", + "reported": "Reported", + "report-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.", + "report-notif-remove": "Your report has been removed.", + "step-1-header": "Report an issue", + "step-2-header": "Help us understand", + "step-3-header": "Thank you for your input", + "flag-username": "Flag username", + "flag-comment": "Flag comment", + "continue": "Continue", + "done": "Done", + "no-agree-comment": "I don't agree with this comment", + "comment-offensive": "This comment is offensive", + "personal-info": "This comment reveals personally identifiable information", + "username-offensive": "This username is offensive", + "no-like-username": "I don't like this username", + "bio-offensive": "This bio is offensive", + "no-like-bio": "I don't like this bio", + "marketing": "This looks like an ad/marketing", + "thank-you": "We value your safety and feedback. A moderator will review your flag.", + "flag-reason": "Reason for flag", + "other": "Other" }, "es": { - "flag": "Marcar", - "flagged": "Marcado", - "flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.", - "flag-notif-remove": "Tu marca ha sido eliminada." + "report": "Informe", + "reported": "Informado", + "report-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.", + "report-notif-remove": "Tu marca ha sido eliminada.", + "step-1-header": "Reportar un problema", + "step-2-header": "Ayudanos a entender", + "step-3-header": "Gracias por tu participación", + "flag-username": "Marcar el nombre de usuario", + "flag-comment": "Marcar el comentario", + "continue": "Continuar", + "done": "hecho", + "no-agree-comment": "No estoy de acuerdo con este comentario", + "comment-offensive": "Este comentario es ofensivo", + "personal-info": "Este comentario muestra información personal", + "username-offensive": "Este nombre de usuario es ofensivo", + "no-like-username": "No me gusta ese nombre de usuario", + "bio-offensive": "Esta bio es ofensiva", + "no-like-bio": "No me gusta esta bio", + "marketing": "Esto parece una publicidad/marketing", + "thank-you": "Nos interesa tu protección y comentarios. Un moderador va a mirar tu marca.", + "flag-reason": "Razón por la que marcar", + "other": "Otro" } } diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 73a945c7e..9e7d06f33 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -16,7 +16,10 @@ const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDial return; } if (!liked) { - postAction(id, 'like', currentUser.id, 'comments') + const action = { + action_type: 'like' + }; + postAction(id, 'comments', action) .then((action) => { let id = `${action.action_type}_${action.item_id}`; addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions'); diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css index e36f55886..6cf13c739 100644 --- a/client/coral-ui/components/Button.css +++ b/client/coral-ui/components/Button.css @@ -21,7 +21,7 @@ cursor: pointer; text-decoration: none; text-align: center; - line-height: 36px; + line-height: 28px; vertical-align: middle; margin: 2px; } diff --git a/client/coral-ui/components/PopupMenu.css b/client/coral-ui/components/PopupMenu.css new file mode 100644 index 000000000..fe57ba8b2 --- /dev/null +++ b/client/coral-ui/components/PopupMenu.css @@ -0,0 +1,31 @@ +.popupMenu { + display: inline-block; + width: inherit; + border: solid 1px #999; + box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3); + box-sizing: border-box; + background: white; + border-radius: 3px; + padding: 20px 10px; + z-index: 3; +} + +.popupMenu:before{ + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + right: 3em; + bottom: -20px; + z-index: 2; +} + +.popupMenu:after{ + content: ''; + border: 10px solid transparent; + border-top-color: #999; + position: absolute; + right: 3em; + bottom: -21px; + z-index: 1; +} diff --git a/client/coral-ui/components/PopupMenu.js b/client/coral-ui/components/PopupMenu.js new file mode 100644 index 000000000..dfc81c3a1 --- /dev/null +++ b/client/coral-ui/components/PopupMenu.js @@ -0,0 +1,6 @@ +import React from 'react'; +import styles from './PopupMenu.css'; + +export default ({children}) => ( + {children} +); diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index afcce7354..71b0d1d7e 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -7,4 +7,5 @@ export {default as TabContent} from './components/TabContent'; export {default as Button} from './components/Button'; export {default as Spinner} from './components/Spinner'; export {default as Tooltip} from './components/Tooltip'; +export {default as PopupMenu} from './components/PopupMenu'; export {default as Checkbox} from './components/Checkbox'; diff --git a/models/action.js b/models/action.js index b5d2ab8b6..ed18e48c5 100644 --- a/models/action.js +++ b/models/action.js @@ -12,7 +12,9 @@ const ActionSchema = new Schema({ action_type: String, item_type: String, item_id: String, - user_id: String + user_id: String, + field: String, // Used when an action references a particular field of an object. (e.g. a flag on a username or bio) + detail: String, // Describes the reason for an action (e.g. 'Username is offensive') }, { timestamps: { createdAt: 'created_at', @@ -35,13 +37,7 @@ ActionSchema.statics.findById = function(id) { * @param {String} action the new action to the comment * @return {Promise} */ -ActionSchema.statics.insertUserAction = ({item_id, item_type, user_id, action_type}) => { - const action = { - item_id, - item_type, - user_id, - action_type - }; +ActionSchema.statics.insertUserAction = (action) => { // Create/Update the action. return Action.findOneAndUpdate(action, action, { diff --git a/models/comment.js b/models/comment.js index 53e8bbfb0..9cfee2ee7 100644 --- a/models/comment.js +++ b/models/comment.js @@ -287,11 +287,13 @@ CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.u * @param {String} action the new action to the comment * @return {Promise} */ -CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.insertUserAction({ +CommentSchema.statics.addAction = (item_id, user_id, action_type, field, detail) => Action.insertUserAction({ item_id, - item_type: 'comment', + item_type: 'comments', user_id, - action_type + action_type, + field, + detail }); /** diff --git a/models/user.js b/models/user.js index b28ad3251..ac6328179 100644 --- a/models/user.js +++ b/models/user.js @@ -3,6 +3,7 @@ const uuid = require('uuid'); const _ = require('lodash'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); +const Action = require('./action'); const Comment = require('./comment'); @@ -596,3 +597,19 @@ UserService.addBio = (id, bio) => ( new: true }) ); + +/** + * 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, field, detail) => Action.insertUserAction({ + item_id, + item_type: 'user', + user_id, + action_type, + field, + detail +}); diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 5806c534b..168b89fc3 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -161,11 +161,13 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next router.post('/:comment_id/actions', (req, res, next) => { const { - action_type + action_type, + field, + detail } = req.body; Comment - .addAction(req.params.comment_id, req.user.id, action_type) + .addAction(req.params.comment_id, req.user.id, action_type, field, detail) .then((action) => { res.status(201).json(action); }) diff --git a/routes/api/user/index.js b/routes/api/user/index.js index ca84af361..5b0c48de8 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -158,4 +158,21 @@ router.put('/:user_id/bio', (req, res, next) => { }); }); +router.post('/:user_id/actions', authorization.needed(), (req, res, next) => { + const { + action_type, + field, + detail + } = req.body; + + User + .addAction(req.params.user_id, req.user.id, action_type, field, detail) + .then((action) => { + res.status(201).json(action); + }) + .catch((err) => { + next(err); + }); +}); + module.exports = router; diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.spec.js index 6a9b2aeb2..fb7044b7b 100644 --- a/tests/client/coral-framework/store/itemActions.spec.js +++ b/tests/client/coral-framework/store/itemActions.spec.js @@ -155,7 +155,11 @@ describe('itemActions', () => { describe('postAction', () => { it ('should post an action', () => { fetchMock.post('*', {id: '456'}); - return actions.postAction('abc', 'flag', '123', 'comments')(store.dispatch) + const action = { + action_type: 'flag', + detail: 'Comment smells funny' + }; + return actions.postAction('abc', 'comments', action)(store.dispatch) .then(response => { expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/comments/abc/actions'); expect(response).to.deep.equal({id:'456'}); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index 37713e899..b3e4e2675 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -395,11 +395,12 @@ describe('/api/v1/comments/:comment_id/actions', () => { return chai.request(app) .post('/api/v1/comments/abc/actions') .set(passport.inject({id: '456', roles: ['admin']})) - .send({'user_id': '456', 'action_type': 'flag'}) + .send({'action_type': 'flag', 'detail': 'Comment is too awesome.'}) .then((res) => { expect(res).to.have.status(201); expect(res).to.have.body; expect(res.body).to.have.property('action_type', 'flag'); + expect(res.body).to.have.property('detail', 'Comment is too awesome.'); expect(res.body).to.have.property('item_id', 'abc'); }); }); diff --git a/tests/routes/api/user/index.js b/tests/routes/api/user/index.js new file mode 100644 index 000000000..78030049b --- /dev/null +++ b/tests/routes/api/user/index.js @@ -0,0 +1,44 @@ +const passport = require('../../../passport'); + +const app = require('../../../../app'); +const chai = require('chai'); +const expect = chai.expect; + +// Setup chai. +chai.should(); +chai.use(require('chai-http')); + +const User = require('../../../../models/user'); + +describe('/api/v1/user/:user_id/actions', () => { + + const users = [{ + displayName: 'Ana', + email: 'ana@gmail.com', + password: '123' + }, { + displayName: 'Maria', + email: 'maria@gmail.com', + password: '123' + }]; + + beforeEach(() => { + return User.createLocalUsers(users); + }); + + describe('#post', () => { + it('it should update actions', () => { + return chai.request(app) + .post('/api/v1/user/abc/actions') + .set(passport.inject({id: '456', roles: ['admin']})) + .send({'action_type': 'flag', 'detail': 'Bio is too awesome.'}) + .then((res) => { + expect(res).to.have.status(201); + expect(res).to.have.body; + expect(res.body).to.have.property('action_type', 'flag'); + expect(res.body).to.have.property('detail', 'Bio is too awesome.'); + expect(res.body).to.have.property('item_id', 'abc'); + }); + }); + }); +});