diff --git a/.babelrc b/.babelrc index ca2e2cead..41d27bf34 100644 --- a/.babelrc +++ b/.babelrc @@ -1,16 +1,15 @@ { "sourceMaps": true, "presets": [ - "stage-0", "es2015" ], "plugins": [ - ["transform-decorators-legacy"], - ["transform-react-jsx"], - ["transform-object-assign"], - ["transform-class-properties"], - ["transform-async-to-generator"], - ["transform-object-rest-spread"], - ["transform-class-properties"] + "add-module-exports", + "transform-async-to-generator", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-react-jsx" ] } diff --git a/README.md b/README.md index 0b77e95b7..3b258736f 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,49 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -A commenting platform from The Coral Project. [https://coralproject.net](https://coralproject.net) + +A commenting platform from [The Coral Project](https://coralproject.net). ## Contributing to Talk ### Product Roadmap -You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625 -You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk +You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625. -### Local Dependencies -Node +You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk. -Mongo - -### Getting Started -`npm install` -Run it once to install the dependencies. - -`npm start` -Runs Talk. +## Usage ### Configuration The Talk application requires specific configuration options to be available inside the environment in order to run, those variables are listed here: -- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to +- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database. +- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database. +- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to secure cookies. - `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook Login enabled app. - `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your Facebook Login enabled app. -- `TALK_ROOT_URL` (*required*) - root url of the installed application externally +- `TALK_ROOT_URL` (*required*) - root url of the installed application externally available in the format: `://` without the path. -- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name. +- `TALK_SMTP_EMAIL` (*required*) - the address to send emails from using the + SMTP provider. - `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using. - `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using. - `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`. - `TALK_SMTP_PORT` (*required*) - SMTP port. -### Running with Docker -Make sure you have Docker running first and then run `docker-compose up -d` - -### Testing -`npm test` - -### Lint -`npm run lint` - -### Helpful URLs -Comment stream: http://localhost:3000/ - -Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html - -Moderator view: http://localhost:3000/admin - -### Docs -`swagger.yaml` - ### License -**Apache-2.0** + + Copyright 2016 Mozilla Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + + See the License for the specific language governing permissions and limitations under the License. diff --git a/app.json b/app.json new file mode 100644 index 000000000..ba15f1ce4 --- /dev/null +++ b/app.json @@ -0,0 +1,32 @@ +{ + "name": "The Coral Project: Talk", + "env": { + "TALK_SESSION_SECRET": { + "description": "The session secret", + "generator": "secret" + }, + "TALK_FACEBOOK_APP_ID": { + "value": "", + "required": true + }, + "TALK_FACEBOOK_APP_SECRET": { + "value": "", + "required": true + }, + "NODE_ENV": "production", + "TALK_SMTP_PORT": "2525", + "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN" + }, + "addons": [{ + "plan": "mongolab:sandbox", + "as": "MONGO" + }, { + "plan": "rediscloud:30", + "as": "REDIS" + }, { + "plan": "postmark:10k", + "as": "POSTMARK" + }], + "image": "heroku/nodejs", + "success_url": "/admin/setup" +} diff --git a/bin/cli b/bin/cli index d8148282f..131614e86 100755 --- a/bin/cli +++ b/bin/cli @@ -1,10 +1,9 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; +// Perform rewrites to the runtime environment variables based on the contents +// of the process.env.REWRITE_ENV if it exists. This is done here as it is the +// entrypoint for the entire application. +require('env-rewrite').rewrite(); /** * Module dependencies. diff --git a/bin/cli-assets b/bin/cli-assets index 4b0d65c2a..008460f9e 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-jobs b/bin/cli-jobs index 91a0a6e59..60a7b8efb 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-serve b/bin/cli-serve index 5478b7c1e..05fe6efc7 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - const app = require('../app'); const debug = require('debug')('talk:server'); const http = require('http'); diff --git a/bin/cli-settings b/bin/cli-settings index e38249199..5fdfbc38f 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/bin/cli-users b/bin/cli-users index e212a477f..ae688e122 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -1,11 +1,5 @@ #!/usr/bin/env node -/** - * Setup the debug paramater. - */ - -process.env.DEBUG = process.env.TALK_DEBUG; - /** * Module dependencies. */ diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 6d55d7b18..bc9747f34 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -4,6 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router'; import ModerationQueue from 'containers/ModerationQueue/ModerationQueue'; import CommentStream from 'containers/CommentStream/CommentStream'; import Configure from 'containers/Configure/Configure'; +import Streams from 'containers/Streams/Streams'; import CommunityContainer from 'containers/Community/CommunityContainer'; import LayoutContainer from 'containers/LayoutContainer'; @@ -13,6 +14,7 @@ const routes = ( + ); diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js new file mode 100644 index 000000000..f431f1ad6 --- /dev/null +++ b/client/coral-admin/src/actions/assets.js @@ -0,0 +1,36 @@ +import { + FETCH_ASSETS_REQUEST, + FETCH_ASSETS_SUCCESS, + FETCH_ASSETS_FAILURE, + UPDATE_ASSET_STATE_REQUEST, + UPDATE_ASSET_STATE_SUCCESS, + UPDATE_ASSET_STATE_FAILURE +} from '../constants/assets'; +import coralApi from '../../../coral-framework/helpers/response'; + +/** + * Action disptacher related to assets + */ + +// Fetch a page of assets +// Get comments to fill each of the three lists on the mod queue +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { + dispatch({type: FETCH_ASSETS_REQUEST}); + return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + .then(({result, count}) => + dispatch({type: FETCH_ASSETS_SUCCESS, + assets: result, + count + })) + .catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error})); +}; + +// Update an asset state +// Get comments to fill each of the three lists on the mod queue +export const updateAssetState = (id, closedAt) => (dispatch) => { + dispatch({type: UPDATE_ASSET_STATE_REQUEST}); + return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + .then(() => + dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) + .catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error})); +}; diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index d4aee034e..e755f1ed1 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,30 +1,69 @@ +import coralApi from '../../../coral-framework/helpers/response'; +import * as commentActions from '../constants/comments'; + +// Get comments to fill each of the three lists on the mod queue +export const fetchModerationQueueComments = () => { + return dispatch => { + dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH}); + return Promise.all([ + coralApi('/queue/comments/pending'), + coralApi('/comments?status=rejected'), + coralApi('/comments?action_type=flag') + ]) + .then(([pending, rejected, flagged]) => { + + /* Combine seperate calls into a single object */ + flagged.comments.forEach(comment => comment.flagged = true); + return { + comments: [...pending.comments, ...rejected.comments, ...flagged.comments], + users: [...pending.users, ...rejected.users, ...flagged.users], + actions: [...pending.actions, ...rejected.actions, ...flagged.actions] + }; + }) + .then(({comments, users}) => { + + /* Post comments and users to redux store. Actions will be posted when they are needed. */ + dispatch({type: commentActions.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); + dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); + + }); + }; +}; + +// Create a new comment +export const createComment = (name, body) => { + return dispatch => { + const comment = {body, name}; + return coralApi('/comments', {method: 'POST', comment}) + .then(res => dispatch({type: commentActions.COMMENT_CREATE_SUCCESS, comment: res})) + .catch(error => dispatch({type: commentActions.COMMENT_CREATE_FAILED, error})); + }; +}; + /** * Action disptacher related to comments */ -export const updateStatus = (status, id) => (dispatch, getState) => { - dispatch({type: 'COMMENT_STATUS_UPDATE', id, status}); - dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); +// Update a comment. Now to update a comment we need to send back the whole object +export const updateStatus = (status, comment) => { + return dispatch => { + dispatch({type: commentActions.COMMENT_STATUS_UPDATE, id: comment.id, status}); + return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}}) + .then(res => dispatch({type: commentActions.COMMENT_UPDATE_SUCCESS, res})) + .catch(error => dispatch({type: commentActions.COMMENT_UPDATE_FAILED, error})); + }; }; export const flagComment = id => (dispatch, getState) => { - dispatch({type: 'COMMENT_FLAG', id}); + dispatch({type: commentActions.COMMENT_FLAG, id}); dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); }; -export const createComment = (name, body) => dispatch => { - dispatch({type: 'COMMENT_CREATE', name, body}); -}; - // Dialog Actions export const showBanUserDialog = (userId, userName, commentId) => { - return dispatch => { - dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId}); - }; + return {type: commentActions.SHOW_BANUSER_DIALOG, userId, userName, commentId}; }; export const hideBanUserDialog = (showDialog) => { - return dispatch => { - dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog}); - }; + return {type: commentActions.HIDE_BANUSER_DIALOG, showDialog}; }; diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index f2ff37cbd..bc42a7a4c 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,14 +1,15 @@ +import coralApi from '../../../coral-framework/helpers/response'; +import * as actions from '../constants/user'; /** * Action disptacher related to users */ -// -// export const banUser = (status, author_id) => (dispatch) => { -// dispatch({type: 'USER_STATUS_UPDATE', author_id, status}); -// }; -export const banUser = (status, userId, commentId) => { +// change status of a user +export const userStatusUpdate = (status, userId, commentId) => { return dispatch => { - dispatch({type: 'USER_BAN', status, userId, commentId}); - dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + dispatch({type: actions.UPDATE_STATUS_REQUEST}); + return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res})) + .catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error})); }; }; diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 4a21b0846..291825c71 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -30,7 +30,7 @@ export default props => {
{links ? Contains Link : null} -
+
{props.actions.map((action, i) => getActionButton(action, i, props))}
@@ -63,21 +63,23 @@ const getActionButton = (action, i, props) => { if (action === 'ban') { return ( ); } return ( props.onClickAction(props.actionsMap[action].status, comment.id)} + onClick={() => props.onClickAction(props.actionsMap[action].status, comment)} /> ); }; diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 597ea62a6..39de5121b 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -126,7 +126,10 @@ export default class CommentList extends React.Component { const {active} = this.state; return ( -
    +
      {commentIds.map((commentId, index) => { const comment = comments[commentId]; const author = users[comment.author_id]; diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index e4d151d30..4c76424cc 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -16,6 +16,8 @@ export default ({handleLogout}) => ( activeClassName={styles.active}>{lang.t('configure.community')} {lang.t('configure.configure')} + {lang.t('configure.streams')}
        diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js new file mode 100644 index 000000000..0a2ecf73c --- /dev/null +++ b/client/coral-admin/src/constants/assets.js @@ -0,0 +1,6 @@ +export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST'; +export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; +export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE'; +export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST'; +export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; +export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE'; diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index 856f619d0..c17aa1fd1 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,3 +1,12 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; -export const USER_BAN_SUCESS = 'USER_BAN_SUCESS'; +export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; +export const COMMENTS_MODERATION_QUEUE_FETCH = 'COMMENTS_MODERATION_QUEUE_FETCH'; +export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS'; +export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS'; +export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; +export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS'; +export const COMMENT_UPDATE_SUCCESS = 'COMMENT_UPDATE_SUCCESS'; +export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED'; +export const COMMENT_STATUS_UPDATE = 'COMMENT_STATUS_UPDATE'; +export const COMMENT_FLAG = 'COMMENT_FLAG'; diff --git a/client/coral-admin/src/constants/user.js b/client/coral-admin/src/constants/user.js new file mode 100644 index 000000000..8c0563237 --- /dev/null +++ b/client/coral-admin/src/constants/user.js @@ -0,0 +1,3 @@ +export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; +export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; +export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index e798266f0..286a7f364 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -7,7 +7,7 @@ import styles from './Community.css'; import Table from './Table'; import Loading from './Loading'; import NoResults from './NoResults'; -import Pager from './Pager'; +import Pager from 'coral-ui/components/Pager'; const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 050432743..6bcf1194b 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -71,6 +71,7 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => { const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => { if (fetchingSettings) { + /* maybe a spinner here at some point */ return

        Loading settings...

        ; } diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index a76652b18..5e0bfd3a3 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -1,14 +1,18 @@ import React from 'react'; import {connect} from 'react-redux'; -import {Icon} from 'react-mdl'; import key from 'keymaster'; import ModerationKeysModal from 'components/ModerationKeysModal'; import CommentList from 'components/CommentList'; import BanUserDialog from 'components/BanUserDialog'; -import {updateStatus, showBanUserDialog, hideBanUserDialog} from 'actions/comments'; -import {banUser} from 'actions/users'; +import { + updateStatus, + showBanUserDialog, + hideBanUserDialog, + fetchModerationQueueComments +} from 'actions/comments'; +import {userStatusUpdate} from 'actions/users'; import styles from './ModerationQueue.css'; import I18n from 'coral-framework/modules/i18n/i18n'; @@ -32,7 +36,7 @@ class ModerationQueue extends React.Component { // Fetch comments and bind singleView key before render componentWillMount () { - this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'}); + this.props.dispatch(fetchModerationQueueComments()); key('s', () => this.setState({singleView: !this.state.singleView})); key('shift+/', () => this.setState({modalOpen: true})); key('esc', () => this.setState({modalOpen: false})); @@ -55,10 +59,10 @@ class ModerationQueue extends React.Component { } // Dispatch the update status action - onCommentAction (action, id) { + onCommentAction (action, comment) { // If not banning then change the status to approved or flagged as action = status - this.props.dispatch(updateStatus(action, id)); + this.props.dispatch(updateStatus(action, comment)); } showBanUserDialog (userId, userName, commentId) { @@ -70,11 +74,10 @@ class ModerationQueue extends React.Component { } banUser (userId, commentId) { - this.props.dispatch(banUser('banned', userId, commentId)); - } - - showShortcuts = () => { - this.setState({modalOpen: true}); + this.props.dispatch(userStatusUpdate('banned', userId, commentId)) + .then(() => { + this.props.dispatch(fetchModerationQueueComments()); + }); } onTabClick (activeTab) { @@ -100,11 +103,6 @@ class ModerationQueue extends React.Component { className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')} this.onTabClick('flagged')} className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')} - - - {lang.t('modqueue.showshortcuts')} -
      this.onCommentAction(action, commentId)} + onClickAction={(action, comment) => this.onCommentAction(action, comment)} onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)} actions={['reject', 'approve', 'ban']} loading={comments.loading} /> @@ -122,7 +120,7 @@ class ModerationQueue extends React.Component { handleClose={() => this.hideBanUserDialog()} onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)} user={comments.banUser}/> -
      +
this.onCommentAction(action, id)} + onClickAction={(action, comment) => this.onCommentAction(action, comment)} actions={['approve']} loading={comments.loading} />
this.onCommentAction(action, id)} + onClickAction={(action, comment) => this.onCommentAction(action, comment)} actions={['reject', 'approve']} loading={comments.loading} />
-
- this.setState({modalOpen: false})} /> -
+ this.setState({modalOpen: false})} /> ); - } } diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Streams/Streams.css new file mode 100644 index 000000000..c64de7974 --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.css @@ -0,0 +1,83 @@ +.container { + padding: 10px; + display: flex; +} + +.leftColumn { + width: 200px; +} + +.mainContent { + width: calc(90% - 200px); +} + +.searchIcon { + vertical-align: middle; + font-size: 18px; + color: #ccc; +} + +.searchBox { + padding: 3px; + border: 1px solid #ccc; + border-radius: 3px; + width: 90%; + display: flex; +} + +.searchBoxInput { + border: none; + flex: 1; + font-size: 14px; +} + +.searchBoxInput:focus { + outline: none; +} + +.optionHeader { + font-size: 16px; + font-weight: 900; + margin-top: 15px; +} + +.optionDetail { + font-size: 16px; + margin-top: 3px; +} + +.streamsTable { + width: 100%; +} + +.radio { + display: block; +} + +.statusMenu { + border-radius: 3px; + width: 10em; + text-align: center; + float: right; + border: 1px solid #ccc; + color: #fff; + cursor: pointer; +} + +.statusMenuOpen { + padding: 10px; + background-color: #4caf50; +} + +.statusMenuIcon { + float: right; +} + +.statusMenuClosed { + padding: 10px; + background-color: #000; +} + +.hidden { + display: none; +} diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js new file mode 100644 index 000000000..a154516dd --- /dev/null +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -0,0 +1,180 @@ +import React, {Component} from 'react'; +import styles from './Streams.css'; +import {connect} from 'react-redux'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import {fetchAssets, updateAssetState} from '../../actions/assets'; +import translations from '../../translations.json'; +import { + RadioGroup, + Radio, + Icon, + DataTable, + TableHeader +} from 'react-mdl'; +import Pager from 'coral-ui/components/Pager'; + +const limit = 25; + +class Streams extends Component { + + state = { + search: '', + sort: 'desc', + filter: 'all', + statusMenus: {}, + timer: null, + page: 0 + } + + componentDidMount () { + this.props.fetchAssets(0, limit, '', this.state.sortBy); + } + + onSettingChange = (setting) => (e) => { + let options = this.state; + this.setState({[setting]: e.target.value}); + options[setting] = e.target.value; + this.props.fetchAssets(0, limit, options.search, options.sort, options.filter); + } + + onSearchChange = (e) => { + this.setState((prevState) => { + prevState.search = e.target.value; + clearTimeout(prevState.timer); + const fetchAssets = this.props.fetchAssets; + prevState.timer = setTimeout(() => { + fetchAssets(0, limit, e.target.value, this.state.sort, this.state.filter); + }, 350); + return prevState; + }); + } + + renderDate = (date) => { + const d = new Date(date); + return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; + } + + onStatusClick = (closeStream, id, statusMenuOpen) => () => { + if (statusMenuOpen) { + this.setState(prev => { + prev.statusMenus[id] = false; + return prev; + }); + this.props.updateAssetState(id, closeStream ? Date.now() : null) + .then(() => { + const {search, sort, filter, page} = this.state; + this.props.fetchAssets(page, limit, search, sort, filter); + }); + } else { + this.setState(prev => { + prev.statusMenus[id] = true; + return prev; + }); + } + } + + renderStatus = (closedAt, {id}) => { + const closed = closedAt && new Date(closedAt).getTime() < Date.now(); + const statusMenuOpen = this.state.statusMenus[id]; + return
+
+ {closed ? lang.t('streams.closed') : lang.t('streams.open')} + {!statusMenuOpen && } +
+ { + statusMenuOpen && +
+ {!closed ? lang.t('streams.closed') : lang.t('streams.open')} +
+ } +
; + } + + onPageClick = (page) => { + this.setState({page}); + const {search, sort, filter} = this.state; + this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter); + } + + render () { + const {search, sort, filter} = this.state; + const {assets} = this.props; + + return
+
+ +
+ + +
+ +
{lang.t('streams.filter-streams')}
+
{lang.t('streams.stream-status')}
+ + {lang.t('streams.all')} + {lang.t('streams.open')} + {lang.t('streams.closed')} + +
{lang.t('streams.sort-by')}
+ + {lang.t('streams.newest')} + {lang.t('streams.oldest')} + +
+ +
+ assets.byId[id])}> + {lang.t('streams.article')} + + {lang.t('streams.pubdate')} + + + {lang.t('streams.status')} + + + +
+
; + } +} + +const mapStateToProps = ({assets}) => { + return { + assets: assets.toJS() + }; +}; +const mapDispatchToProps = (dispatch) => { + return { + fetchAssets: (...args) => { + dispatch(fetchAssets.apply(this, args)); + }, + updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args)) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Streams); + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js new file mode 100644 index 000000000..77d0bf081 --- /dev/null +++ b/client/coral-admin/src/reducers/assets.js @@ -0,0 +1,24 @@ +import {Map, List, fromJS} from 'immutable'; +import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets'; + +const initialState = Map({ + byId: Map(), + ids: List() +}); + +export default (state = initialState, action) => { + switch (action.type) { + case FETCH_ASSETS_SUCCESS: + return replaceAssets(action, state); + case UPDATE_ASSET_STATE_REQUEST: + return state.setIn(['byId', action.id, 'closedAt'], action.closedAt); + default: return state; + } +}; + +const replaceAssets = (action, state) => { + const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); + return state.set('byId', assets) + .set('count', action.count) + .set('ids', List(assets.keys())); +}; diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js index dca726b5e..27dc79177 100644 --- a/client/coral-admin/src/reducers/comments.js +++ b/client/coral-admin/src/reducers/comments.js @@ -1,4 +1,5 @@ import * as actions from '../constants/comments'; +import * as userActions from '../constants/user'; import {Map, List, fromJS} from 'immutable'; /** @@ -23,16 +24,17 @@ const initialState = Map({ // Handle the comment actions export default (state = initialState, action) => { switch (action.type) { - case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true); - case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state); - case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false); - case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action); - case 'COMMENT_FLAG': return flag(state, action); - case 'COMMENT_CREATE_SUCCESS': return addComment(state, action); - case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state); + case actions.COMMENTS_MODERATION_QUEUE_FETCH: return state.set('loading', true); + case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state); + case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false); + case actions.COMMENT_STATUS_UPDATE: return updateStatus(state, action); + case actions.COMMENT_FLAG: return flag(state, action); + case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action); + case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state); case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action); case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action); - case actions.USER_BAN_SUCESS: return setBanUser(state, false, action); + case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action); + case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action); default: return state; } }; diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 1f1b444fc..fca4694d8 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -4,6 +4,7 @@ import settings from 'reducers/settings'; import community from 'reducers/community'; import users from 'reducers/users'; import auth from 'reducers/auth'; +import assets from 'reducers/assets'; // Combine all reducers into a main one export default combineReducers({ @@ -11,5 +12,6 @@ export default combineReducers({ comments, community, auth, - users + users, + assets }); diff --git a/client/coral-admin/src/services/store.js b/client/coral-admin/src/services/store.js index 88b2650e0..0700dc579 100644 --- a/client/coral-admin/src/services/store.js +++ b/client/coral-admin/src/services/store.js @@ -2,7 +2,6 @@ import {createStore, applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; import mainReducer from 'reducers'; -import talkAdapter from 'services/talk-adapter'; /** * Create the store by merging the app reducers with @@ -14,5 +13,5 @@ import talkAdapter from 'services/talk-adapter'; export default createStore( mainReducer, window.devToolsExtension && window.devToolsExtension(), - applyMiddleware(thunk, talkAdapter) + applyMiddleware(thunk) ); diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js deleted file mode 100644 index 7c9bc968f..000000000 --- a/client/coral-admin/src/services/talk-adapter.js +++ /dev/null @@ -1,96 +0,0 @@ -import coralApi from '../../../coral-framework/helpers/response'; - -/** - * The adapter is a redux middleware that interecepts the actions that need - * to interface with the backend, do the job and return the results. - * The idea is that if we expose the required actions to handle to devs, the - * moderation app can be platform agnostic. This same client could work not only - * for the coral but also for wordpress comments, disqus and many more. - */ - -// Intercept redux actions and act over the ones we are interested -export default store => next => action => { - - switch (action.type) { - case 'COMMENTS_MODERATION_QUEUE_FETCH': - fetchModerationQueueComments(store); - break; - case 'COMMENT_UPDATE': - updateComment(store, action.comment); - break; - case 'COMMENT_CREATE': - createComment(store, action.name, action.body); - break; - case 'USER_BAN': - userStatusUpdate(store, action.status, action.userId, action.commentId); - break; - } - - next(action); -}; - -// Get comments to fill each of the three lists on the mod queue -const fetchModerationQueueComments = store => - -Promise.all([ - coralApi('/queue/comments/pending'), - coralApi('/comments?status=rejected'), - coralApi('/comments?action_type=flag') -]) -.then(([pending, rejected, flagged]) => { - - /* Combine seperate calls into a single object */ - let all = {}; - all.comments = pending.comments - .concat(rejected.comments) - .concat(flagged.comments.map(comment => { - comment.flagged = true; - return comment; - })); - all.users = pending.users - .concat(rejected.users) - .concat(flagged.users); - all.actions = pending.actions - .concat(rejected.actions) - .concat(flagged.actions); - return all; -}) -.then(all => { - - /* Post comments and users to redux store. Actions will be posted when they are needed. */ - store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS', - users: all.users}); - store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', - comments: all.comments}); - -}); - -// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error})); - -// Update a comment. Now to update a comment we need to send back the whole object - -const updateComment = (store, comment) => { - coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}}) - .then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res})) - .catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error})); -}; - -// Create a new comment -const createComment = (store, name, comment) => { - const body = { - status: 'Untouched', - body: comment, - name: name, - createdAt: Date.now() - }; - return coralApi('/comments', {method: 'POST', body}) - .then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res})) - .catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error})); -}; - -// Ban a user -const userStatusUpdate = (store, status, userId, commentId) => { - return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) - .then(res => store.dispatch({type: 'USER_BAN_SUCESS', res})) - .catch(error => store.dispatch({type: 'USER_BAN_FAILED', error})); -}; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 6f8e76c4b..11218552f 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -56,6 +56,7 @@ "moderate": "Moderate", "configure": "Configure", "community": "Community", + "streams": "Streams", "closed-comments-desc": "Write a message for closed threads", "closed-comments-label": "Write a message...", "hours": "Hours", @@ -73,6 +74,22 @@ "note": "Note: Banning this user will also place this comment in the Rejected queue.", "cancel": "Cancel", "yes_ban_user": "Yes, Ban User" + }, + "streams": { + "search": "Search", + "filter-streams": "Filter Streams", + "stream-status": "Stream Status", + "all": "All", + "open": "Open", + "closed": "Closed", + "newest": "Newest", + "oldest": "Oldest", + "sort-by": "Sort By", + "open": "Open", + "closed": "Closed", + "article": "Article", + "pubdate": "Publication Date", + "status": "Status" } }, "es": { @@ -139,6 +156,22 @@ "note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.", "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" + }, + "streams": { + "search": "", + "filter-streams": "", + "stream-status": "", + "all": "", + "open": "", + "closed": "", + "newest": "", + "oldest": "", + "sort-by": "", + "open": "", + "closed": "", + "article": "", + "pubdate": "", + "status": "" } } } diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 9e7d06f33..fae30c8c7 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -2,7 +2,7 @@ import React from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; -const name = 'coral-plugin-flags'; +const name = 'coral-plugin-likes'; const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => { const liked = like && like.current_user; diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 68d96b910..966870293 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -9,10 +9,11 @@ import ForgotContent from './ForgotContent'; const SignDialog = ({open, view, handleClose, offset, ...props}) => ( × {view === 'SIGNIN' && } diff --git a/client/coral-sign-in/components/UserBox.js b/client/coral-sign-in/components/UserBox.js index 11badfaef..e99c0f88c 100644 --- a/client/coral-sign-in/components/UserBox.js +++ b/client/coral-sign-in/components/UserBox.js @@ -9,7 +9,9 @@ const UserBox = ({className, user, logout, ...props}) => ( className={`${styles.userBox} ${className ? className : ''}`} {...props} > - {lang.t('signIn.loggedInAs')} {user.displayName}. {lang.t('signIn.notYou')} {lang.t('signIn.logout')} + {lang.t('signIn.loggedInAs')} + {user.displayName}. {lang.t('signIn.notYou')} + {lang.t('signIn.logout')} ); diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 2bec52b45..f1a7bace3 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -113,6 +113,7 @@ input.error{ font-weight: bold; cursor: pointer; margin: 0px; + margin-left: 4px; padding-bottom: 2px; border-bottom: solid 1px black; } diff --git a/client/coral-admin/src/containers/Community/Pager.css b/client/coral-ui/components/Pager.css similarity index 100% rename from client/coral-admin/src/containers/Community/Pager.css rename to client/coral-ui/components/Pager.css diff --git a/client/coral-admin/src/containers/Community/Pager.js b/client/coral-ui/components/Pager.js similarity index 91% rename from client/coral-admin/src/containers/Community/Pager.js rename to client/coral-ui/components/Pager.js index 3184cfca0..fca20b0db 100644 --- a/client/coral-admin/src/containers/Community/Pager.js +++ b/client/coral-ui/components/Pager.js @@ -12,7 +12,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
    { - (totalPages > page) ? + (totalPages > page && totalPages > 1) ?
  • onNewPageHandler(page - 1)}> @@ -23,7 +23,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => ( } {Rows(page, totalPages, onNewPageHandler)} { - (page < totalPages) ? + (page < totalPages && totalPages > 1) ?
  • onNewPageHandler(page + 1)}> @@ -42,4 +42,3 @@ Pager.propTypes = { }; export default Pager; - diff --git a/nightwatch.conf.js b/nightwatch.conf.js index a39ab128f..fb6c5f9c7 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -19,6 +19,7 @@ module.exports = { }, 'test_settings': { 'default': { + 'launch_url' : 'http://localhost:3000', 'selenium_port': 6666, 'selenium_host': 'localhost', 'silent': true, @@ -26,19 +27,27 @@ module.exports = { 'browserName': 'chrome', 'javascriptEnabled': true, 'acceptSslCerts': true, - 'webStorageEnabled' : true, - 'databaseEnabled' : true, - 'applicationCacheEnabled' : false, - 'nativeEvents' : true + 'webStorageEnabled': true, + 'databaseEnabled': true, + 'applicationCacheEnabled': false, + 'nativeEvents': true }, 'screenshots' : { - 'enabled' : true, - 'on_failure' : true, - 'on_error' : true, - 'path' : './tests/e2e/reports' + 'enabled': true, + 'on_failure': true, + 'on_error': true, + 'path': './tests/e2e/reports' }, 'exclude': [ + './tests/e2e/tests/EmbedStreamTests.js' ] + }, + 'integration': { + 'launch_url': 'http://localhost:3000' } } }; + +// "chromeOptions" : { +// "args" : ["start-fullscreen"] +// } diff --git a/package.json b/package.json index 4ec24a27a..4d4aa01bb 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "main": "app.js", "scripts": { "start": "./bin/cli serve --jobs", - "build": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.js --bail", - "build-watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.config.dev.js --watch", - "lint": "./node_modules/.bin/eslint bin/* .", - "lint-fix": "./node_modules/.bin/eslint bin/* . --fix", - "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests", - "test-watch": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive -w tests", - "pree2e": "NODE_ENV=test ./scripts/pree2e.sh", - "e2e": "NODE_ENV=test ./node_modules/.bin/nightwatch", - "embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs" + "build": "NODE_ENV=production webpack --config webpack.config.js --bail", + "build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch", + "lint": "eslint bin/* .", + "lint-fix": "eslint bin/* . --fix", + "test": "NODE_ENV=test mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests", + "test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests", + "pree2e": "NODE_ENV=test scripts/pree2e.sh", + "e2e": "NODE_ENV=test nightwatch", + "embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs", + "postinstall": "npm run build" }, "config": { "pre-git": { @@ -53,6 +54,7 @@ "connect-redis": "^3.1.0", "debug": "^2.2.0", "ejs": "^2.5.2", + "env-rewrite": "^1.0.2", "express": "^4.14.0", "express-session": "^1.14.2", "helmet": "^3.1.0", @@ -74,10 +76,11 @@ }, "devDependencies": { "autoprefixer": "^6.5.2", - "babel-core": "^6.18.2", + "babel-core": "^6.21.0", "babel-eslint": "^7.1.0", "babel-jest": "^15.0.0", "babel-loader": "^6.2.7", + "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-async-to-generator": "^6.16.0", "babel-plugin-transform-class-properties": "^6.18.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", @@ -115,7 +118,7 @@ "material-design-lite": "^1.2.1", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", - "nightwatch": "^0.9.9", + "nightwatch": "^0.9.11", "node-fetch": "^1.6.3", "postcss-loader": "^1.1.0", "postcss-modules": "^0.5.2", @@ -136,7 +139,7 @@ "redux-mock-store": "^1.2.1", "redux-thunk": "^2.1.0", "regenerator": "^0.8.46", - "selenium-standalone": "^5.8.0", + "selenium-standalone": "latest", "style-loader": "^0.13.1", "supertest": "^2.0.1", "timeago.js": "^2.0.3", diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index 3be877e93..f5a4afe44 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -12,18 +12,47 @@ router.get('/', (req, res, next) => { skip = 0, sort = 'asc', field = 'created_at', + filter = 'all', search = '' } = req.query; + const FilterOpenAssets = (query, filter) => { + switch(filter) { + case 'open': + return query.merge({ + $or: [ + { + closedAt: null + }, + { + closedAt: { + $gt: Date.now() + } + } + ] + }); + case 'closed': + return query.merge({ + closedAt: { + $lt: Date.now() + } + }); + default: + return query; + } + }; + // Find all the assets. Promise.all([ - Asset - .search(search) + + // Find the actuall assets. + FilterOpenAssets(Asset.search(search), filter) .sort({[field]: (sort === 'asc') ? 1 : -1}) - .skip(skip) - .limit(limit), - Asset - .search(search) + .skip(parseInt(skip)) + .limit(parseInt(limit)), + + // Get the count of actual assets. + FilterOpenAssets(Asset.search(search), filter) .count() ]) .then(([result, count]) => { @@ -107,7 +136,6 @@ router.put('/:asset_id/status', (req, res, next) => { } }) .then(() => { - res.status(204).json(); }) .catch((err) => { diff --git a/scripts/pree2e.sh b/scripts/pree2e.sh index dc3266dd2..e79addc2e 100755 --- a/scripts/pree2e.sh +++ b/scripts/pree2e.sh @@ -1,7 +1,15 @@ #!/bin/bash # install selenium -../node_modules/selenium-standalone/bin/selenium-standalone install +selenium-standalone install -# start the app server -npm start & +# Creating Admin Test User +{ echo admin@test.com; echo test; echo test; echo Admin Test User; echo admin;} | dotenv ./bin/cli-users create + +# Creating Moderator Test User +{ echo moderator@test.com; echo test; echo test; echo Moderator Test User; echo moderator;} | dotenv ./bin/cli-users create + +# Creating Commenter Test User +{ echo commenter@test.com; echo test; echo test; echo Commenter Test User; echo ;} | dotenv ./bin/cli-users create + +npm start diff --git a/tests/client/coral-admin/actions/assets.js b/tests/client/coral-admin/actions/assets.js new file mode 100644 index 000000000..24a28daef --- /dev/null +++ b/tests/client/coral-admin/actions/assets.js @@ -0,0 +1,89 @@ +import 'react'; +import 'redux'; +import {expect} from 'chai'; +import fetchMock from 'fetch-mock'; +import * as actions from '../../../../client/coral-admin/src/actions/assets'; +import {Map} from 'immutable'; + +import configureStore from 'redux-mock-store'; + +const mockStore = configureStore(); + +describe('Asset actions', () => { + let store; + + const assets = [ + { + url: 'http://test.com', + id: '123', + status: 'closed' + }, + { + url: 'http://test.org', + id: '456', + status: 'open' + } + ]; + + beforeEach(() => { + store = mockStore(new Map({})); + fetchMock.restore(); + }); + + describe('FETCH_ASSETS_REQUEST', () => { + + it('should fetch a list of assets', () => { + + fetchMock.get('*', JSON.stringify({ + result: assets, + count: 2 + })); + + return actions.fetchAssets(2, 20)(store.dispatch) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_SUCCESS'); + expect(store.getActions()[1]).to.have.property('count', 2); + expect(store.getActions()[1]).to.have.property('assets'). + and.to.deep.equal(assets); + }); + }); + + it('should return an error appropriatly', () => { + + fetchMock.get('*', 404); + + return actions.fetchAssets(2, 20)(store.dispatch) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_FAILURE'); + }); + }); + }); + + describe('UPDATE_ASSET_STATE_REQUEST', () => { + + it('should update an asset', () => { + + fetchMock.put('*', JSON.stringify(assets[0])); + + return actions.updateAssetState('123', 'status', 'open')(store.dispatch) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_SUCCESS'); + }); + + }); + + it('should return an error appropriately', () => { + + fetchMock.put('*', 404); + + return actions.updateAssetState('123', 'status', 'open')(store.dispatch) + .then(() => { + expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST'); + expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_FAILURE'); + }); + }); + }); +}); diff --git a/tests/client/coral-admin/reducers/assets.js b/tests/client/coral-admin/reducers/assets.js new file mode 100644 index 000000000..753061612 --- /dev/null +++ b/tests/client/coral-admin/reducers/assets.js @@ -0,0 +1,64 @@ +import {Map, fromJS} from 'immutable'; +import {expect} from 'chai'; +import assetsReducer from '../../../../client/coral-admin/src/reducers/assets'; + +describe ('assetsReducer', () => { + describe('FETCH_ASSETS_SUCCESS', () => { + it('should replace the existing assets', () => { + const action = { + type: 'FETCH_ASSETS_SUCCESS', + count: 200, + assets: [ + { + id: '123', + url: 'http://test.com', + closedAt: 'tomorrow' + }, + { + id: '456', + url: 'http://test2.com', + closedAt: 'thursday' + }, + ] + }; + const store = new Map({}); + const result = assetsReducer(store, action); + expect(result.getIn(['byId', '123']).toJS()).to.deep.equal({ + url: 'http://test.com', + closedAt: 'tomorrow', + id: '123' + }); + expect(result.getIn(['ids']).toJS()).to.deep.equal([ + '123', + '456' + ]); + expect(result.getIn(['count'])).to.equal(200); + }); + }); + + describe('UPDATE_ASSET_STATE_REQUEST', () => { + it('should update the state of a particular asset', () => { + const action = { + type: 'UPDATE_ASSET_STATE_REQUEST', + id: '123', + closedAt: null + }; + const store = new fromJS({ + byId: { + '123': { + id: '123', + url: 'http://test.com', + closedAt: Date.now() + }, + '456': { + id: '456', + url: 'http://test2.com', + closedAt: 'thursday' + } + } + }); + const result = assetsReducer(store, action); + expect(result.getIn(['byId', '123', 'closedAt'])).to.equal.null; + }); + }); +}); diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.js similarity index 100% rename from tests/client/coral-framework/store/itemActions.spec.js rename to tests/client/coral-framework/store/itemActions.js diff --git a/tests/client/coral-framework/store/itemReducer.spec.js b/tests/client/coral-framework/store/itemReducer.js similarity index 100% rename from tests/client/coral-framework/store/itemReducer.spec.js rename to tests/client/coral-framework/store/itemReducer.js diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js index 147af2663..22ad3cb4b 100644 --- a/tests/e2e/globals.js +++ b/tests/e2e/globals.js @@ -1,4 +1,18 @@ module.exports = { - waitForConditionTimeout: 20000, - baseUrl: 'localhost:3011/' + waitForConditionTimeout: 8000, + baseUrl: 'http://localhost:3000', + users: { + admin: { + email: 'admin@test.com', + pass: 'test' + }, + moderator: { + email: 'moderator@test.com', + pass: 'test' + }, + commenter: { + email: 'commenter@test.com', + pass: 'test' + } + }, }; diff --git a/tests/e2e/pages/adminPage.js b/tests/e2e/pages/adminPage.js new file mode 100644 index 000000000..cb5371d53 --- /dev/null +++ b/tests/e2e/pages/adminPage.js @@ -0,0 +1,33 @@ +const embedStreamCommands = { + url: function () { + return `${this.api.launchUrl}/admin`; + }, + ready() { + return this + .waitForElementVisible('body', 2000); + }, + approveComment() { + return this + .waitForElementVisible('@commentList') + .waitForElementVisible('@approveButton') + .click('@approveButton'); + } +}; + +module.exports = { + commands: [embedStreamCommands], + elements: { + commentList: { + selector: '#commentList' + }, + banButton: { + selector: '#commentList .actions:first-child .ban' + }, + rejectButton: { + selector: '#commentList .actions:first-child .reject' + }, + approveButton: { + selector: '#commentList .actions:first-child .approve' + } + } +}; diff --git a/tests/e2e/pages/embedStream.js b/tests/e2e/pages/embedStream.js deleted file mode 100644 index cc584a656..000000000 --- a/tests/e2e/pages/embedStream.js +++ /dev/null @@ -1,45 +0,0 @@ -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/pages/embedStreamPage.js b/tests/e2e/pages/embedStreamPage.js new file mode 100644 index 000000000..164449ff0 --- /dev/null +++ b/tests/e2e/pages/embedStreamPage.js @@ -0,0 +1,160 @@ +const embedStreamCommands = { + url: function () { + return this + .api.launchUrl; + }, + ready() { + return this + .waitForElementVisible('body', 4000) + .waitForElementVisible('iframe#coralStreamIframe') + .api.frame('coralStreamIframe'); + }, + signUp(user) { + return this + .waitForElementVisible('@signInButton', 2000) + .click('@signInButton') + .waitForElementVisible('@signInDialog') + .waitForElementVisible('@registerButton') + .click('@registerButton') + .setValue('@signInDialogEmail', user.email) + .setValue('@signInDialogPassword', user.pass) + .setValue('@signUpDialogConfirmPassword', user.pass) + .setValue('@signUpDialogDisplayName', user.displayName) + .waitForElementVisible('@signUpButton') + .click('@signUpButton') + .waitForElementVisible('@logInButton') + .click('@logInButton') + .waitForElementVisible('@logoutButton', 5000); + }, + login(user) { + return this + .waitForElementVisible('@signInButton', 2000) + .click('@signInButton') + .waitForElementVisible('@signInDialog') + .waitForElementVisible('@signInDialogEmail') + .waitForElementVisible('@signInDialogPassword') + .setValue('@signInDialogEmail', user.email) + .setValue('@signInDialogPassword', user.pass) + .waitForElementVisible('@logInButton') + .click('@logInButton') + .waitForElementVisible('@logoutButton', 5000); + }, + logout() { + return this + .waitForElementVisible('@logoutButton') + .click('@logoutButton') + .waitForElementVisible('@signInButton', 2000); + }, + postComment(comment = 'Test Comment') { + return this + .waitForElementVisible('@commentBox', 2000) + .setValue('@commentBox', comment) + .click('@postButton'); + }, + likeComment() { + return this + .waitForElementVisible('@likeButton') + .click('@likeButton'); + }, + flagComment() { + return this + .waitForElementVisible('@flagButton') + .click('@flagButton'); + }, + flagUsername() { + return this + .waitForElementVisible('@flagButton') + .click('@flagButton'); + }, + getPermalink(fn) { + return this + .waitForElementVisible('@permalinkButton') + .click('@permalinkButton') + .waitForElementVisible('@permalinkPopUp') + .getValue('@permalinkInput', result => fn(result.value)); + } +}; + +module.exports = { + commands: [embedStreamCommands], + elements: { + signInButton: { + selector: '#coralSignInButton' + }, + signInDialog:{ + selector: '#signInDialog' + }, + signInDialogEmail: { + selector: '#signInDialog #email' + }, + signInDialogPassword: { + selector: '#signInDialog #password' + }, + signUpDialogConfirmPassword: { + selector: '#signInDialog #confirmPassword' + }, + signUpDialogDisplayName: { + selector: '#signInDialog #displayName' + }, + logInButton: { + selector: '#coralLogInButton' + }, + signUpButton: { + selector: '#coralSignUpButton' + }, + logoutButton: { + selector: '.commentStream #logout' + }, + commentBox: { + selector: '.coral-plugin-commentbox-textarea' + }, + postButton: { + selector: '#commentBox .coral-plugin-commentbox-button' + }, + likeButton: { + selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button' + }, + likeText: { + selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button .coral-plugin-likes-button-text' + }, + likesCount: { + selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button .coral-plugin-likes-like-count' + }, + flagButton: { + selector: '.comment .coral-plugin-flags-container .coral-plugin-flags-button' + }, + flagPopUp: { + selector: '.comment .coral-plugin-flags-popup' + }, + flagCommentOption: { + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="comments"]' + }, + flagUsernameOption: { + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="user"]' + }, + flagOtherOption: { + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="other"]' + }, + flagHeaderMessage: { + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-header' + }, + flagButtonText: { + selector: '.comment .coral-plugin-flags-button-text' + }, + flagDoneButton: { + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-button' + }, + permalinkButton: { + selector: '.comment .coral-plugin-permalinks-button' + }, + permalinkPopUp: { + selector: '.comment .coral-plugin-permalinks-popover.active' + }, + permalinkInput: { + selector: '.comment .coral-plugin-permalinks-popover.active input' + }, + registerButton: { + selector: '#signInDialog #coralRegister' + } + } +}; diff --git a/tests/e2e/tests/AppTest.js b/tests/e2e/tests/00_AppTest.js similarity index 84% rename from tests/e2e/tests/AppTest.js rename to tests/e2e/tests/00_AppTest.js index 36281ed3f..27b6dbe75 100644 --- a/tests/e2e/tests/AppTest.js +++ b/tests/e2e/tests/00_AppTest.js @@ -6,8 +6,5 @@ module.exports = { .url(baseUrl) .assert.title('Coral Talk') .waitForElementPresent('body', 1000); - }, - after: client => { - client.end(); } }; diff --git a/tests/e2e/tests/01_EmbedStreamTest.js b/tests/e2e/tests/01_EmbedStreamTest.js new file mode 100644 index 000000000..35f4171c7 --- /dev/null +++ b/tests/e2e/tests/01_EmbedStreamTest.js @@ -0,0 +1,44 @@ +module.exports = { + '@tags': ['embedStream'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .navigate() + .ready(); + }, + 'Login as commenter': client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + embedStreamPage + .login(users.commenter); + }, + 'Add test comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .postComment('Test Comment'); + }, + 'Logout': client => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .logout(); + }, + 'Login as admin': client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + embedStreamPage + .login(users.admin); + }, + 'Approve test comment': client => { + const adminPage = client.page.adminPage(); + + adminPage + .navigate() + .ready(); + + adminPage + .approveComment(); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Admin/LoginTest.js b/tests/e2e/tests/Admin/LoginTest.js new file mode 100644 index 000000000..e75d29903 --- /dev/null +++ b/tests/e2e/tests/Admin/LoginTest.js @@ -0,0 +1,23 @@ +module.exports = { + '@tags': ['login', 'admin'], + before(client) { + const embedStreamPage = client.page.embedStreamPage(); + const {launchUrl} = client; + + client + .url(launchUrl); + + embedStreamPage + .ready(); + }, + 'Admin logs in': client => { + const {users} = client.globals; + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .login(users.admin); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Commenter/FlagCommentTest.js b/tests/e2e/tests/Commenter/FlagCommentTest.js new file mode 100644 index 000000000..baff5fdf5 --- /dev/null +++ b/tests/e2e/tests/Commenter/FlagCommentTest.js @@ -0,0 +1,34 @@ +module.exports = { + '@tags': ['flag', 'comments', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .navigate() + .ready(); + + embedStreamPage + .login(users.commenter); + }, + 'Commenter flags a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .flagComment() + .waitForElementVisible('@flagPopUp') + .waitForElementVisible('@flagCommentOption') + .click('@flagCommentOption') + .waitForElementVisible('@flagDoneButton') + .click('@flagDoneButton') + .waitForElementVisible('@flagOtherOption') + .click('@flagOtherOption') + .waitForElementVisible('@flagDoneButton') + .click('@flagDoneButton') + .click('@flagDoneButton') + .expect.element('@flagButtonText').text.to.equal('Reported'); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Commenter/FlagUsernameTest.js b/tests/e2e/tests/Commenter/FlagUsernameTest.js new file mode 100644 index 000000000..7d9ab5a90 --- /dev/null +++ b/tests/e2e/tests/Commenter/FlagUsernameTest.js @@ -0,0 +1,33 @@ +module.exports = { + '@tags': ['flag', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .navigate() + .ready(); + + embedStreamPage + .login(users.commenter); + }, + 'Commenter flags a username': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .flagUsername() + .waitForElementVisible('@flagPopUp') + .waitForElementVisible('@flagUsernameOption') + .click('@flagUsernameOption') + .waitForElementVisible('@flagDoneButton') + .click('@flagDoneButton') + .waitForElementVisible('@flagOtherOption') + .click('@flagOtherOption') + .waitForElementVisible('@flagDoneButton') + .click('@flagDoneButton') + .click('@flagDoneButton'); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Commenter/LikeCommentTest.js b/tests/e2e/tests/Commenter/LikeCommentTest.js new file mode 100644 index 000000000..59e964969 --- /dev/null +++ b/tests/e2e/tests/Commenter/LikeCommentTest.js @@ -0,0 +1,26 @@ +module.exports = { + '@tags': ['like', 'comments', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .navigate() + .ready(); + + embedStreamPage + .login(users.commenter); + }, + 'Commenter likes a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .likeComment() + .waitForElementVisible('@likesCount', 2000) + .expect.element('@likeText').text.to.equal('Liked'); + + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Commenter/LoginTest.js b/tests/e2e/tests/Commenter/LoginTest.js new file mode 100644 index 000000000..15c830c74 --- /dev/null +++ b/tests/e2e/tests/Commenter/LoginTest.js @@ -0,0 +1,20 @@ +module.exports = { + '@tags': ['login', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .navigate() + .ready(); + }, + 'Commenter logs in': client => { + const {users} = client.globals; + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .login(users.commenter); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Commenter/PermalinkTest.js b/tests/e2e/tests/Commenter/PermalinkTest.js new file mode 100644 index 000000000..deed9ec90 --- /dev/null +++ b/tests/e2e/tests/Commenter/PermalinkTest.js @@ -0,0 +1,34 @@ +let permalink = ''; + +module.exports = { + '@tags': ['permalink', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .navigate() + .ready(); + + embedStreamPage + .login(users.commenter); + }, + 'Commenter gets the permalink of a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .getPermalink(value => { + permalink = value; + }); + }, + 'Commenter navigates to the permalink': client => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .navigate(permalink); + + client.assert.urlContains(permalink); + }, + after: client => { + client.end(); + } +}; + diff --git a/tests/e2e/tests/Commenter/PostComment.js b/tests/e2e/tests/Commenter/PostComment.js new file mode 100644 index 000000000..91e28415d --- /dev/null +++ b/tests/e2e/tests/Commenter/PostComment.js @@ -0,0 +1,38 @@ +module.exports = { + '@tags': ['write', 'commenter'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .navigate() + .ready(); + + embedStreamPage + .login(users.commenter); + }, + 'Commenter posts a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .postComment('I read the comments'); + }, + after: client => { + const adminPage = client.page.adminPage(); + const embedStreamPage = client.page.embedStreamPage(); + const {users} = client.globals; + + embedStreamPage + .logout() + .login(users.admin); + + adminPage + .navigate() + .ready(); + + adminPage + .approveComment(); + + client.end(); + } +}; diff --git a/tests/e2e/tests/Moderator/LoginTest.js b/tests/e2e/tests/Moderator/LoginTest.js new file mode 100644 index 000000000..3f1754a50 --- /dev/null +++ b/tests/e2e/tests/Moderator/LoginTest.js @@ -0,0 +1,23 @@ +module.exports = { + '@tags': ['login', 'moderator'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + const {launchUrl} = client; + + client + .url(launchUrl); + + embedStreamPage + .ready(); + }, + 'Moderator logs in': client => { + const {users} = client.globals; + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .login(users.moderator); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Visitor/FlagCommentTest.js b/tests/e2e/tests/Visitor/FlagCommentTest.js new file mode 100644 index 000000000..8fbab04ee --- /dev/null +++ b/tests/e2e/tests/Visitor/FlagCommentTest.js @@ -0,0 +1,20 @@ +module.exports = { + '@tags': ['flag', 'comments', 'visitor'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .navigate() + .ready(); + }, + 'Visitor tries to flag a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .flagComment() + .waitForElementVisible('@signInDialog', 2000); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Visitor/LikeCommentTest.js b/tests/e2e/tests/Visitor/LikeCommentTest.js new file mode 100644 index 000000000..6aaca5b49 --- /dev/null +++ b/tests/e2e/tests/Visitor/LikeCommentTest.js @@ -0,0 +1,20 @@ +module.exports = { + '@tags': ['like', 'comments', 'visitor'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .navigate() + .ready(); + }, + 'Visitor tries to like a comment': client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .likeComment() + .waitForElementVisible('@signInDialog', 2000); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/Visitor/SignUpTest.js b/tests/e2e/tests/Visitor/SignUpTest.js new file mode 100644 index 000000000..4c7650630 --- /dev/null +++ b/tests/e2e/tests/Visitor/SignUpTest.js @@ -0,0 +1,24 @@ +module.exports = { + '@tags': ['signup', 'visitor'], + before: client => { + const embedStreamPage = client.page.embedStreamPage(); + + embedStreamPage + .navigate() + .ready(); + }, + 'Visitor signs up': client => { + const embedStreamPage = client.page.embedStreamPage(); + const hash = Math.floor(Math.random() * (999 - 0)); + + embedStreamPage + .signUp({ + email: `visitor_${hash}@test.com`, + displayName: 'Visitor', + pass: 'testtest' + }); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/routes/api/assets/index.js b/tests/routes/api/assets/index.js index 8e94c81fc..3354f9671 100644 --- a/tests/routes/api/assets/index.js +++ b/tests/routes/api/assets/index.js @@ -18,12 +18,13 @@ describe('/api/v1/assets', () => { url: 'https://coralproject.net/news/asset1', title: 'Asset 1', description: 'term1', - id: '1' + closedAt: Date.now() }, { url: 'https://coralproject.net/news/asset2', title: 'Asset 2', - description: 'term2' + description: 'term2', + closedAt: null } ]); }); @@ -81,6 +82,38 @@ describe('/api/v1/assets', () => { }); }); + it('should return only closed assets', () => { + return chai.request(app) + .get('/api/v1/assets?filter=closed') + .set(passport.inject({roles: ['admin']})) + .then((res) => { + const body = res.body; + + expect(body).to.have.property('count', 1); + expect(body).to.have.property('result'); + + const assets = body.result; + + expect(assets[0]).to.have.property('title', 'Asset 1'); + }); + }); + + it('should return only opened assets', () => { + return chai.request(app) + .get('/api/v1/assets?filter=open') + .set(passport.inject({roles: ['admin']})) + .then((res) => { + const body = res.body; + + expect(body).to.have.property('count', 1); + expect(body).to.have.property('result'); + + const assets = body.result; + + expect(assets[0]).to.have.property('title', 'Asset 2'); + }); + }); + }); describe('#put', () => { diff --git a/views/article.ejs b/views/article.ejs index f4d7ef910..302d76572 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -36,7 +36,7 @@