diff --git a/app.js b/app.js index 839067a..e73f1cb 100644 --- a/app.js +++ b/app.js @@ -71,13 +71,17 @@ app.use('/messages', require('./routes/messages')); app.use('/account', require('./routes/account')); app.use('/contacts', require('./routes/contacts')); app.use('/ideas', require('./routes/ideas')); +app.use('/challenges', require('./routes/challenges')); // vote for ideas, ... app.use('/ideas', require('./routes/votes')); app.use('/comments', require('./routes/votes')); +app.use('/challenges', require('./routes/votes')); // following are route factories // they need to know what is the primary object (i.e. idea, comment, etc.) app.use('/ideas', require('./routes/primary-comments')('idea')); +app.use('/challenges', require('./routes/primary-comments')('challenge')); + app.use('/comments', require('./routes/comments')('')); app.use('/comments', require('./routes/primary-comments')('comment')); app.use('/reactions', require('./routes/comments')('comment')); diff --git a/collections.js b/collections.js index b460560..016bebf 100644 --- a/collections.js +++ b/collections.js @@ -78,6 +78,10 @@ module.exports = { type: 'document' }, + challenges: { + type: 'document' + }, + ideaTags: { type: 'edge', from: ['ideas'], @@ -91,6 +95,19 @@ module.exports = { ] }, + challengeTags: { + type: 'edge', + from: ['challenges'], + to: ['tags'], + indexes: [ + { + type: 'hash', + fields: ['_from', '_to'], + unique: true + } + ] + }, + comments: { type: 'document', indexes: [ diff --git a/controllers/authorize.js b/controllers/authorize.js index f49597d..e2fb04b 100644 --- a/controllers/authorize.js +++ b/controllers/authorize.js @@ -8,7 +8,6 @@ // Authorize only logged user function onlyLogged(req, res, next) { if (req.auth.logged === true) return next(); - return res.status(403).json({ errors: ['Not Authorized'] }); // TODO improve the error } diff --git a/controllers/dit-tags.js b/controllers/dit-tags.js new file mode 100644 index 0000000..d2325bf --- /dev/null +++ b/controllers/dit-tags.js @@ -0,0 +1,139 @@ +'use strict'; + +const path = require('path'); + +const models = require(path.resolve('./models')), + serialize = require(path.resolve('./serializers')).serialize; + +/** + * Controller for POST /dits/:id/tags + * Adds a tag to a dit + */ +async function post(req, res, next) { + let ditType; + try { + // gather data from request + const { tagname } = req.body.tag; + const ditId = req.params.id; + const username = req.auth.username; + + ditType = req.baseUrl.slice(1,-1); + // save new dit-tag to database + const newDitTag = await models.ditTag.create(ditType, ditId, tagname, { }, username); + // serialize response body + let responseBody; + switch(ditType){ + case 'idea': { + responseBody = serialize.ideaTag(newDitTag); + break; + } + case 'challenge': { + responseBody = serialize.challengeTag(newDitTag); + break; + } + } + + // respond + return res.status(201).json(responseBody); + } catch (e) { + // handle errors + switch (e.code) { + // duplicate dit-tag + case 409: { + return res.status(409).end(); + } + // missing dit or tag or creator + case 404: { + const errors = e.missing.map(miss => ({ status: 404, detail: `${miss} not found`})); + return res.status(404).json({ errors }); + } + // dit creator is not me + case 403: { + return res.status(403).json({ errors: [ + { status: 403, detail: `not logged in as ${ditType} creator` } + ]}); + } + // unexpected error + default: { + return next(e); + } + } + + } +} + +/** + * Read list of tags of dit + * GET /dits/:id/tags + */ +async function get(req, res, next) { + let ditType; + try { + // read dit id + const { id } = req.params; + ditType = req.baseUrl.slice(1, -1); + + // read ditTags from database + const newDitTags = await models.ditTag.readTagsOfDit(ditType, id); + + // serialize response body + let responseBody; + switch(ditType){ + case 'idea': { + responseBody = serialize.ideaTag(newDitTags); + break; + } + case 'challenge': { + responseBody = serialize.challengeTag(newDitTags); + break; + } + } + + // respond + return res.status(200).json(responseBody); + } catch (e) { + // error when idea doesn't exist + if (e.code === 404) { + return res.status(404).json({ errors: [{ + status: 404, + detail: `${ditType} not found` + }] }); + } + + // handle unexpected error + return next(e); + } +} + +/** + * Remove tag from idea + * DELETE /dits/:id/tags/:tagname + */ +async function del(req, res, next) { + let ditType; + try { + const { id, tagname } = req.params; + const { username } = req.auth; + ditType = req.baseUrl.slice(1, -1); + + await models.ditTag.remove(ditType, id, tagname, username); + + return res.status(204).end(); + } catch (e) { + switch (e.code) { + case 404: { + return res.status(404).end(); + } + case 403: { + return res.status(403).json({ errors: [ + { status: 403, detail: `not logged in as ${ditType} creator` } + ] }); + } + default: { + return next(e); + } + } + } +} + +module.exports = { post, get, del }; diff --git a/controllers/dits.js b/controllers/dits.js new file mode 100644 index 0000000..ededf84 --- /dev/null +++ b/controllers/dits.js @@ -0,0 +1,428 @@ +'use strict'; + +const path = require('path'), + models = require(path.resolve('./models')), + serialize = require(path.resolve('./serializers')).serialize; + +/** + * Create dit + */ +async function post(req, res, next) { + try { + // gather data + const { title, detail } = req.body; + const creator = req.auth.username; + const ditType = req.baseUrl.slice(1,-1); + + // save the dit to database + const newDit = await models.dit.create(ditType, { title, detail, creator }); + + // serialize the dit (JSON API) + let serializedDit; + switch(ditType){ + case 'idea': { + serializedDit = serialize.idea(newDit); + break; + } + case 'challenge': { + serializedDit = serialize.challenge(newDit); + break; + } + } + // respond + return res.status(201).json(serializedDit); + + } catch (e) { + return next(e); + } +} + +/** + * Read dit by id + */ +async function get(req, res, next) { + try { + // gather data + const { id } = req.params; + const { username } = req.auth; + const ditType = req.baseUrl.slice(1,-1); + + // read the dit from database + const dit = await models.dit.read(ditType, id); + + if (!dit) return res.status(404).json({ }); + + // see how many votes were given to dit and if/how logged user voted (0, -1, 1) + dit.votes = await models.vote.readVotesTo({ type: ditType+'s', id }); + dit.myVote = await models.vote.read({ from: username, to: { type: ditType+'s', id } }); + + // serialize the dit (JSON API) + let serializedDit; + switch(ditType){ + case 'idea': { + serializedDit = serialize.idea(dit); + break; + } + case 'challenge': { + serializedDit = serialize.challenge(dit); + break; + } + } + + // respond + return res.status(200).json(serializedDit); + + } catch (e) { + return next(e); + } +} + +/** + * Update dit's title or detail + * PATCH /dits/:id + */ +async function patch(req, res, next) { + let ditType; + try { + // gather data + const { title, detail } = req.body; + const { id } = req.params; + ditType = req.baseUrl.slice(1,-1); + const { username } = req.auth; + + // update dit in database + const dit = await models.dit.update(ditType, id, { title, detail }, username); + + // serialize the dit (JSON API) + let serializedDit; + switch(ditType){ + case 'idea': { + serializedDit = serialize.idea(dit); + break; + } + case 'challenge': { + serializedDit = serialize.challenge(dit); + break; + } + } + + // respond + return res.status(200).json(serializedDit); + } catch (e) { + // handle errors + switch (e.code) { + case 403: { + return res.status(403).json({ + errors: [{ status: 403, detail: 'only creator can update' }] + }); + } + case 404: { + return res.status(404).json({ + errors: [{ status: 404, detail: `${ditType} not found` }] + }); + } + default: { + return next(e); + } + } + } +} + +/** + * Get dits with my tags + */ +async function getDitsWithMyTags(req, res, next) { + let ditType; + try { + // gather data + const { username } = req.auth; + const { page: { offset = 0, limit = 10 } = { } } = req.query; + ditType = req.baseUrl.slice(1,-1); + + // read the dits from database + const foundDits = await models.dit.withMyTags(ditType, username, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + + } catch (e) { + return next(e); + } +} + +/** + * Get dits with specified tags + */ +async function getDitsWithTags(req, res, next) { + let ditType; + try { + + // gather data + const { page: { offset = 0, limit = 10 } = { } } = req.query; + const { withTags: tagnames } = req.query.filter; + ditType = req.baseUrl.slice(1,-1); + + + // read the dits from database + const foundDits = await models.dit.withTags(ditType, tagnames, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get new dits + */ +async function getNewDits(req, res, next) { + let ditType; + try { + const { page: { offset = 0, limit = 5 } = { } } = req.query; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.findNew(ditType, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + + } catch (e) { + return next(e); + } +} + +/** + * Get random dits + */ +async function getRandomDits(req, res, next) { + let ditType; + try { + const { page: { limit = 1 } = { } } = req.query; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.random(ditType, { limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get dits with specified creators + */ +async function getDitsWithCreators(req, res, next) { + let ditType; + try { + // gather data + const { page: { offset = 0, limit = 10 } = { } } = req.query; + const { creators } = req.query.filter; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.findWithCreators(ditType, creators, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get dits commented by specified users + */ +async function getDitsCommentedBy(req, res, next) { + let ditType; + try { + // gather data + const { page: { offset = 0, limit = 10 } = { } } = req.query; + const { commentedBy } = req.query.filter; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.findCommentedBy(ditType, commentedBy, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get highly voted dits with an optional parameter of minimum votes + */ +async function getDitsHighlyVoted(req, res, next) { + let ditType; + try { + // gather data + const { page: { offset = 0, limit = 5 } = { } } = req.query; + const { highlyVoted } = req.query.filter; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.findHighlyVoted(ditType, highlyVoted, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get trending dits + */ +async function getDitsTrending(req, res, next) { + let ditType; + try { + // gather data + const { page: { offset = 0, limit = 5 } = { } } = req.query; + ditType = req.baseUrl.slice(1,-1); + + // read dits from database + const foundDits = await models.dit.findTrending(ditType, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + } catch (e) { + return next(e); + } +} + +/** + * Get dits with any of specified keywords in title + */ +async function getDitsSearchTitle(req, res, next) { + let ditType; + try { + // gather data + const { page: { offset = 0, limit = 10 } = { } } = req.query; + const { like: keywords } = req.query.filter.title; + ditType = req.baseUrl.slice(1,-1); + + // read ideas from database + const foundDits = await models.dit.findWithTitleKeywords(ditType, keywords, { offset, limit }); + + // serialize the dit (JSON API) + let serializedDits; + switch(ditType){ + case 'idea': { + serializedDits = serialize.idea(foundDits); + break; + } + case 'challenge': { + serializedDits = serialize.challenge(foundDits); + break; + } + } + // respond + return res.status(200).json(serializedDits); + + } catch (e) { + return next(e); + } +} + +module.exports = { get, getDitsCommentedBy, getDitsHighlyVoted, getDitsSearchTitle, getDitsTrending, getDitsWithCreators, getDitsWithMyTags, getDitsWithTags, getNewDits, getRandomDits, patch, post }; diff --git a/controllers/goto/challenges.js b/controllers/goto/challenges.js new file mode 100644 index 0000000..33d63ff --- /dev/null +++ b/controllers/goto/challenges.js @@ -0,0 +1,17 @@ +'use strict'; + +const route = require('./goto'); + +module.exports = { + get: { + withMyTags: route(['query.filter.withMyTags']), + withTags: route(['query.filter.withTags']), + new: route(['query.sort'], 'newQuery'), + random: route(['query.filter.random']), + withCreators: route(['query.filter.creators']), + commentedBy: route(['query.filter.commentedBy']), + highlyVoted: route(['query.filter.highlyVoted']), + trending: route(['query.filter.trending']), + searchTitle: route(['query.filter.title.like']) + }, +}; diff --git a/controllers/ideas.js b/controllers/ideas.js index ef5e1de..29bb4b8 100644 --- a/controllers/ideas.js +++ b/controllers/ideas.js @@ -16,10 +16,8 @@ async function post(req, res, next) { // save the idea to database const newIdea = await models.idea.create({ title, detail, creator }); - // serialize the idea (JSON API) const serializedIdea = serialize.idea(newIdea); - // respond return res.status(201).json(serializedIdea); diff --git a/controllers/validators/challenge-tags.js b/controllers/validators/challenge-tags.js new file mode 100644 index 0000000..c61fbcc --- /dev/null +++ b/controllers/validators/challenge-tags.js @@ -0,0 +1,9 @@ +'use strict'; + +const validate = require('./validate-by-schema'); + +const del = validate('deleteChallengeTag'); +const get = validate('getChallengeTags'); +const post = validate('postChallengeTags'); + +module.exports = { del, get, post }; diff --git a/controllers/validators/challenges.js b/controllers/validators/challenges.js new file mode 100644 index 0000000..def0440 --- /dev/null +++ b/controllers/validators/challenges.js @@ -0,0 +1,19 @@ + +'use strict'; + +const validate = require('./validate-by-schema'); + +module.exports = { + get: validate('getChallenge'), + getChallengesCommentedBy: validate('getChallengesCommentedBy'), + getChallengesHighlyVoted: validate('getChallengesHighlyVoted'), + getChallengesSearchTitle: validate('getChallengesSearchTitle'), + getChallengesTrending: validate('getChallengesTrending'), + getChallengesWithCreators: validate('getChallengesWithCreators'), + getChallengesWithMyTags: validate('getChallengesWithMyTags'), + getChallengesWithTags: validate('getChallengesWithTags'), + getNewChallenges: validate('getNewChallenges'), + getRandomChallenges: validate('getRandomChallenges'), + patch: validate('patchChallenge', [['params.id', 'body.id']]), + post: validate('postChallenges') +}; diff --git a/controllers/validators/dit-tags.js b/controllers/validators/dit-tags.js new file mode 100644 index 0000000..e69de29 diff --git a/controllers/validators/schema/challenge-tags.js b/controllers/validators/schema/challenge-tags.js new file mode 100644 index 0000000..38f5fad --- /dev/null +++ b/controllers/validators/schema/challenge-tags.js @@ -0,0 +1,52 @@ +'use strict'; + +const { id, tagname } = require('./paths'); + +const getChallengeTags = { + properties: { + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + }, + }, + required: ['params'] +}; + +const postChallengeTags = { + properties: { + body: { + properties: { + tag: { + properties: { tagname }, + required: ['tagname'], + additionalProperties: false + } + }, + required: ['tag'], + additionalProperties: false + }, + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + }, + }, + required: ['body', 'params'] +}; + +const deleteChallengeTag = { + properties: { + params: { + properties: { + id, + tagname + }, + required: ['id', 'tagname'], + additionalProperties: false + }, + }, + required: ['params'] +}; + +module.exports = { deleteChallengeTag, getChallengeTags, postChallengeTags }; diff --git a/controllers/validators/schema/challenges.js b/controllers/validators/schema/challenges.js new file mode 100644 index 0000000..7961111 --- /dev/null +++ b/controllers/validators/schema/challenges.js @@ -0,0 +1,248 @@ +'use strict'; + +const { title, detail, ditType, id, keywordsList, page, pageOffset0, random, tagsList, usersList } = require('./paths'); + +const postChallenges = { + properties: { + body: { + properties: { + title, + detail, + ditType + }, + required: ['title', 'detail', 'ditType'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const getChallenge = { + properties: { + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + } + }, + required: ['params'] +}; + +const patchChallenge = { + properties: { + params: { + properties: { id }, + required: ['id'], + additionalProperties: false + }, + body: { + oneOf: [ + { + properties: { title, detail, id }, + required: ['title', 'detail', 'id'], + additionalProperties: false + }, + { + properties: { title, id }, + required: ['title', 'id'], + additionalProperties: false + }, + { + properties: { detail, id }, + required: ['detail', 'id'], + additionalProperties: false + } + ] + } + }, + required: ['body', 'params'] +}; + +const getChallengesWithMyTags = { + properties: { + query: { + properties: { + filter: { + properties: { + withMyTags: { + enum: [''] + } + }, + required: ['withMyTags'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesWithTags = { + properties: { + query: { + properties: { + filter: { + properties: { + withTags: tagsList + }, + required: ['withTags'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + } + }, + required: ['query'] +}; + +const getNewChallenges = { + properties: { + query: { + properties: { + sort: { + enum: ['-created'] + }, + page + }, + required: ['sort'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getRandomChallenges = { + properties: { + query: { + properties: { + filter: { + properties: { random }, + required: ['random'], + additionalProperties: false + }, + page: pageOffset0 + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesWithCreators = { + properties: { + query: { + properties: { + filter: { + properties: { + creators: usersList + }, + required: ['creators'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesCommentedBy = { + properties: { + query: { + properties: { + filter: { + properties: { + commentedBy: usersList + }, + required: ['commentedBy'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesHighlyVoted = { + properties: { + query: { + properties: { + filter: { + properties: { + highlyVoted: { + type: 'number' + } + }, + required: ['highlyVoted'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesTrending = { + properties: { + query: { + properties: { + filter: { + properties: { + trending: { + type: 'string', + enum: [''] + } + }, + required: ['trending'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +const getChallengesSearchTitle = { + properties: { + query: { + properties: { + filter: { + properties: { + title: { + properties: { + like: keywordsList + }, + required: ['like'], + additionalProperties: false + } + }, + required: ['title'], + additionalProperties: false + }, + page + }, + required: ['filter'], + additionalProperties: false + }, + }, + required: ['query'] +}; + +module.exports = { getChallenge, getChallengesCommentedBy, getChallengesHighlyVoted, getChallengesSearchTitle, getChallengesTrending, getChallengesWithCreators, getChallengesWithMyTags, getChallengesWithTags, getNewChallenges, getRandomChallenges, patchChallenge, postChallenges }; diff --git a/controllers/validators/schema/definitions.js b/controllers/validators/schema/definitions.js index ea820bd..016310d 100644 --- a/controllers/validators/schema/definitions.js +++ b/controllers/validators/schema/definitions.js @@ -118,6 +118,12 @@ module.exports = { maxLength: 2048 } }, + dit: { + ditType: { + type: 'string', + enum: ['idea', 'challenge'] + } + }, message: { body: { type: 'string', diff --git a/controllers/validators/schema/ideas.js b/controllers/validators/schema/ideas.js index 0699934..2f34022 100644 --- a/controllers/validators/schema/ideas.js +++ b/controllers/validators/schema/ideas.js @@ -1,14 +1,16 @@ 'use strict'; -const { title, detail, id, keywordsList, page, pageOffset0, random, tagsList, usersList } = require('./paths'); +const { title, detail, ditType, id, keywordsList, page, pageOffset0, random, tagsList, usersList } = require('./paths'); + const postIdeas = { properties: { body: { properties: { title, - detail + detail, + ditType }, - required: ['title', 'detail'], + required: ['title', 'detail', 'ditType'], additionalProperties: false } }, diff --git a/controllers/validators/schema/index.js b/controllers/validators/schema/index.js index a681495..d00150a 100644 --- a/controllers/validators/schema/index.js +++ b/controllers/validators/schema/index.js @@ -3,6 +3,8 @@ const account = require('./account'), authenticate = require('./authenticate'), avatar = require('./avatar'), + challenges = require('./challenges'), + challengeTags = require('./challenge-tags'), comments = require('./comments'), contacts = require('./contacts'), definitions = require('./definitions'), @@ -16,5 +18,5 @@ const account = require('./account'), votes = require('./votes'); -module.exports = Object.assign({ definitions }, account, authenticate, avatar, +module.exports = Object.assign({ definitions }, account, authenticate, avatar, challenges, challengeTags, comments, contacts, ideas, ideaTags, messages, params, tags, users, userTags, votes); diff --git a/controllers/validators/schema/paths.js b/controllers/validators/schema/paths.js index 2919065..fbc59fa 100644 --- a/controllers/validators/schema/paths.js +++ b/controllers/validators/schema/paths.js @@ -24,6 +24,7 @@ module.exports = { keywordsList: { $ref: 'sch#/definitions/query/keywordsList' }, ideaId: { $ref : 'sch#/definitions/idea/ideaId' }, title: { $ref: 'sch#/definitions/idea/titl' }, + ditType: { $ref: 'sch#/definitions/dit/ditType'}, detail: { $ref: 'sch#/definitions/idea/detail' }, content: { $ref: 'sch#/definitions/comment/content' }, id: { $ref: 'sch#/definitions/shared/objectId' }, diff --git a/models/dit-tag/index.js b/models/dit-tag/index.js new file mode 100644 index 0000000..ef17989 --- /dev/null +++ b/models/dit-tag/index.js @@ -0,0 +1,218 @@ +'use strict'; + +const path = require('path'); + +const Model = require(path.resolve('./models/model')), + schema = require('./schema'); +const ditsDictionary = { challenge: 'challenge', idea: 'idea' }; + + +class DitTag extends Model { + /** + * Create ditTag in database + */ + static async create(ditType, ditId, tagname, ditTagInput, creatorUsername) { + // allow just particular strings for a ditType + ditType = ditsDictionary[ditType]; + + // generate standard ditTag + const ditTag = await schema(ditTagInput); + // / STOPPED + const query = ` + // array of dits (1 or 0) + LET ds = (FOR d IN ${ditType}s FILTER d._key == @ditId RETURN d) + // array of dits (1 or 0) + LET ts = (FOR t IN tags FILTER t.tagname == @tagname RETURN t) + // array of users (1 or 0) + LET us = (FOR u IN users FILTER u.username == @creatorUsername RETURN u) + // create the ditTag (if dit, tag and creator exist) + LET ${ditType}Tag = (FOR d IN ds FOR t IN ts FOR u IN us FILTER u._id == d.creator + INSERT MERGE({ _from: d._id, _to: t._id, creator: u._id }, @ditTag) IN ${ditType}Tags RETURN KEEP(NEW, 'created'))[0] || { } + // if ditTag was not created, default to empty object (to be able to merge later) + // gather needed data + LET creator = MERGE(KEEP(us[0], 'username'), us[0].profile) + LET tag = KEEP(ts[0], 'tagname') + LET ${ditType} = MERGE(KEEP(ds[0], 'title', 'detail'), { id: ds[0]._key }) + // return data + RETURN MERGE(${ditType}Tag, { creator, tag, ${ditType} })`; + + const params = { ditId, tagname, ditTag, creatorUsername }; + + const cursor = await this.db.query(query, params); + + const response = (await cursor.all())[0]; + + switch (cursor.extra.stats.writesExecuted) { + // ditTag was created + case 1: { + return response; + } + // ditTag was not created + case 0: { + throw generateError(response); + } + } + + function generateError(response) { + let e; + // check that dit, tag and creator exist + // some of them don't exist, then ditTag was not created + if (!(response[`${ditType}`] && response['tag'] && response['creator'])) { + e = new Error('Not Found'); + e.code = 404; + e.missing = []; + + [`${ditType}`, 'tag', 'creator'].forEach((potentialMissing) => { + if (!response[potentialMissing]){ + e.missing.push(potentialMissing); + } + }); + } else { + // if all exist, then dit creator !== ditTag creator, not authorized + e = new Error('Not Authorized'); + e.code = 403; + } + + return e; + } + + } + + /** + * Read ditTag from database + */ + static async read(ditType, ditId, tagname) { + const ditCollection = ditType + 's'; + const ditTags = ditType + 'Tags'; + // allow just particular strings for a ditType + ditType = ditsDictionary[ditType]; + + const query = ` + FOR t IN tags FILTER t.tagname == @tagname + FOR d IN @@ditCollection FILTER d._key == @ditId + FOR dt IN @@ditTags FILTER dt._from == d._id AND dt._to == t._id + LET creator = (FOR u IN users FILTER u._id == dt.creator + RETURN MERGE(KEEP(u, 'username'), u.profile))[0] + LET ${ditType}Tag = KEEP(dt, 'created') + LET tag = KEEP(t, 'tagname') + LET ${ditType} = MERGE(KEEP(d, 'title', 'detail'), { id: d._key }) + RETURN MERGE(${ditType}Tag, { creator, tag, ${ditType} })`; + const params = { ditId, tagname, '@ditCollection': ditCollection, '@ditTags': ditTags }; + const cursor = await this.db.query(query, params); + + return (await cursor.all())[0]; + } + + /** + * Read tags of dit + */ + static async readTagsOfDit(ditType, ditId) { + const ditCollection = ditType + 's'; + const ditTag = ditType + 'Tags'; + // allow just particular strings for a ditType + ditType = ditsDictionary[ditType]; + + const query = ` + // read dit into array (length 1 or 0) + LET ds = (FOR d IN @@ditCollection FILTER d._key == @ditId RETURN d) + // read ditTags + LET dts = (FOR d IN ds + FOR dt IN @@ditTag FILTER dt._from == d._id + FOR t IN tags FILTER dt._to == t._id + SORT t.tagname + LET ${ditType}Tag = KEEP(dt, 'created') + LET tag = KEEP(t, 'tagname') + LET ${ditType} = MERGE(KEEP(d, 'title', 'detail'), { id: d._key }) + RETURN MERGE(${ditType}Tag, { tag, ${ditType} }) + ) + RETURN { ${ditType}Tags: dts, ${ditType}: ds[0] }`; + const params = { ditId, '@ditCollection': ditCollection, '@ditTag':ditTag }; + + const cursor = await this.db.query(query, params); + + // const [{ dit, ditTags }] = await cursor.all(); + const ditTagsData = await cursor.all(); + // when dit not found, error + if (!ditTagsData[0][`${ditType}`]) { + const e = new Error(`${ditType} not found`); + e.code = 404; + throw e; + } + + return ditTagsData[0][`${ditType}Tags`]; + } + + /** + * Remove ditTag from database + */ + static async remove(ditType, ditId, tagname, username) { + const ditCollection = ditType + 's'; + const ditTags = ditType + 'Tags'; + // allow just particular strings for a ditType + ditType = ditsDictionary[ditType]; + + const query = ` + // find users (1 or 0) + LET us = (FOR u IN users FILTER u.username == @username RETURN u) + // find dits (1 or 0) + LET ds = (FOR i IN @@ditCollection FILTER i._key == @ditId RETURN i) + // find [ditTag] between dit and tag specified (1 or 0) + LET dts = (FOR i IN ds + FOR t IN tags FILTER t.tagname == @tagname + FOR dt IN @@ditTags FILTER dt._from == i._id AND dt._to == t._id + RETURN dt) + // find and remove [ditTag] if and only if user is creator of dit + // is user authorized to remove the ditTag in question? + LET dtsdel = (FOR u IN us FOR d IN ds FILTER u._id == d.creator + FOR dt IN dts + REMOVE dt IN @@ditTags + RETURN dt) + // return [ditTag] between dit and tag + RETURN dts`; + + const params = { ditId, tagname, username, '@ditTags': ditTags, '@ditCollection': ditCollection}; + + // execute query and gather database response + const cursor = await this.db.query(query, params); + const [matchedDitTags] = await cursor.all(); + + // return or error + switch (cursor.extra.stats.writesExecuted) { + // ditTag was removed: ok + case 1: { + return; + } + // ditTag was not removed: error + case 0: { + throw generateError(matchedDitTags); + } + // unexpected error + default: { + throw new Error('unexpected error'); + } + } + + /** + * When no ditTag was removed, it can have 2 reasons: + * 1. ditTag was not found + * 2. ditTag was found, but the user is not creator of the dit + * therefore is not authorized to do so + */ + function generateError(response) { + let e; + if (response.length === 0) { + // ditTag was not found + e = new Error('not found'); + e.code = 404; + } else { + // ditTag was found, but user is not dit's creator + e = new Error('not authorized'); + e.code = 403; + } + + return e; + } + } +} + +module.exports = DitTag; diff --git a/models/dit-tag/schema.js b/models/dit-tag/schema.js new file mode 100644 index 0000000..d963502 --- /dev/null +++ b/models/dit-tag/schema.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function ({ created = Date.now() }) { + return { created }; +}; diff --git a/models/dit/index.js b/models/dit/index.js new file mode 100644 index 0000000..6547135 --- /dev/null +++ b/models/dit/index.js @@ -0,0 +1,398 @@ +const _ = require('lodash'), + path = require('path'); + +const Model = require(path.resolve('./models/model')), + schema = require('./schema'); + +class Dit extends Model { + + /** + * Create an dit + */ + static async create(ditType, { title, detail, created, creator }) { + // create the dit + const dit = schema({ title, detail, created }); + const ditCollection = ditType + 's'; + const query = ` + FOR u IN users FILTER u.username == @creator + INSERT MERGE(@dit, { creator: u._id }) IN @@ditCollection + LET creator = MERGE(KEEP(u, 'username'), u.profile) + LET savedDit = MERGE(KEEP(NEW, 'title', 'detail', 'created'), { id: NEW._key }, { creator }) + RETURN savedDit`; + const params = { creator, dit, '@ditCollection': ditCollection }; + + const cursor = await this.db.query(query, params); + + const out = await cursor.all(); + + if (out.length !== 1) return null; + + return out[0]; + } + + /** + * Read the dit by id (_key in db). + */ + static async read(ditType, id) { + const ditCollection = ditType + 's'; + + const query = ` + FOR i IN @@ditCollection FILTER i._key == @id + LET creator = (FOR u IN users FILTER u._id == i.creator + RETURN MERGE(KEEP(u, 'username'), u.profile))[0] + RETURN MERGE(KEEP(i, 'title', 'detail', 'created'), { id: i._key}, { creator })`; + const params = { id, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + const out = await cursor.all(); + return out[0]; + + } + + /** + * Update an dit + */ + static async update(ditType, id, newData, username) { + const dit = _.pick(newData, ['title', 'detail']); + const ditCollection = ditType + 's'; + + const query = ` + // read [user] + LET us = (FOR u IN users FILTER u.username == @username RETURN u) + // read [dit] + LET is = (FOR i IN @@ditCollection FILTER i._key == @id RETURN i) + // update dit if and only if user matches dit creator + LET newis = ( + FOR i IN is FOR u IN us FILTER u._id == i.creator + UPDATE i WITH @dit IN @@ditCollection + LET creator = MERGE(KEEP(u, 'username'), u.profile) + RETURN MERGE(KEEP(NEW, 'title', 'detail', 'created'), { id: NEW._key }, { creator }) + ) + // return old and new dit (to decide what is the error) + RETURN [is[0], newis[0]]`; + const params = { id, dit, username, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + const [[oldDit, newDit]] = await cursor.all(); + + // if nothing was updated, throw error + if (!newDit) { + const e = new Error('not updated'); + // if old dit was found, then user doesn't have sufficient writing rights, + // otherwise dit not found + e.code = (oldDit) ? 403 : 404; + throw e; + } + + return newDit; + } + + /** + * Read dits with my tags + */ + static async withMyTags(ditType, username, { offset, limit }) { + const ditTags = ditType + 'Tags'; + const query = ` + // gather the dits related to me + FOR me IN users FILTER me.username == @username + FOR t, ut IN 1..1 ANY me OUTBOUND userTag + FOR i IN 1..1 ANY t INBOUND @@ditTags + LET relevance = ut.relevance + LET tg = KEEP(t, 'tagname') + SORT relevance DESC + // collect found tags together + COLLECT ${ditType}=i INTO collected KEEP relevance, tg + LET c = (DOCUMENT(${ditType}.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + // sort dits by sum of relevances of related userTags + LET relSum = SUM(collected[*].relevance) + SORT relSum DESC + // format for output + LET ${ditType}Out = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }) + LET tagsOut = collected[*].tg + // limit + LIMIT @offset, @limit + // respond + RETURN { dit: ${ditType}Out, tags: tagsOut }`; + const params = { username, offset, limit, '@ditTags': ditTags }; + const cursor = await this.db.query(query, params); + const out = await cursor.all(); + + // generate dit-tags ids, and add them as attributes to each dit + // and return array of the dits + return out.map(({ dit, tags }) => { + dit[`${ditType}Tags`] = tags.map(({ tagname }) => ({ + id: `${dit.id}--${tagname}`, + [`${ditType}`]: dit, + tag: { tagname } + })); + return dit; + }); + } + + /** + * Read dits with tags + * @param {string[]} tagnames - list of tagnames to search with + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async withTags(ditType, tagnames, { offset, limit }) { + const ditTags = ditType + 'Tags'; + const query = ` + // find the provided tags + FOR t IN tags FILTER t.tagname IN @tagnames + SORT t.tagname + LET tg = KEEP(t, 'tagname') + // find the related dits + FOR i IN 1..1 ANY t INBOUND @@ditTags + // collect tags of each dit together + COLLECT ${ditType}=i INTO collected KEEP tg + // sort dits by amount of matched tags, and from oldest + SORT LENGTH(collected) DESC, ${ditType}.created ASC + // read and format creator + LET c = (DOCUMENT(${ditType}.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + // format for output + LET ${ditType}Out = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }) + LET tagsOut = collected[*].tg + // limit + LIMIT @offset, @limit + // respond + RETURN { dit: ${ditType}Out, tags: tagsOut }`; + const params = { tagnames, offset, limit, '@ditTags': ditTags }; + const cursor = await this.db.query(query, params); + const out = await cursor.all(); + + // generate dit-tags ids, and add them as attributes to each dit + // and return array of the dits + return out.map(({ dit, tags }) => { + dit[`${ditType}Tags`] = tags.map(({ tagname }) => ({ + id: `${dit.id}--${tagname}`, + [`${ditType}`]: dit, + tag: { tagname } + })); + return dit; + }); + } + + /** + * Read new dits + */ + static async findNew(ditType, { offset, limit }) { + const ditCollection = ditType + 's'; + const query = ` + FOR ${ditType} IN @@ditCollection + // sort from newest + SORT ${ditType}.created DESC + LIMIT @offset, @limit + // find creator + LET c = (DOCUMENT(${ditType}.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + // format for output + LET ditOut = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }) + // limit + // respond + RETURN ditOut`; + const params = { offset, limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + /** + * Read random dits + * @param {number} [limit] - max amount of random dits to return + */ + static async random(ditType, { limit }) { + const ditCollection = ditType + 's'; + const query = ` + FOR ${ditType} IN @@ditCollection + // sort from newest + SORT RAND() + LIMIT @limit + // find creator + LET c = (DOCUMENT(${ditType}.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + // format for output + LET ditOut = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }) + // limit + // respond + RETURN ditOut`; + const params = { limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + /** + * Read dits with specified creators + * @param {string[]} usernames - list of usernames to search with + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async findWithCreators(ditType, creators, { offset, limit }) { + // TODO to be checked for query optimization or unnecessary things + const ditCollection = ditType + 's'; + const query = ` + LET creators = (FOR u IN users FILTER u.username IN @creators RETURN u) + FOR ${ditType} IN @@ditCollection FILTER ${ditType}.creator IN creators[*]._id + // find creator + LET c = (DOCUMENT(${ditType}.creator)) + // format for output + LET creator = MERGE(KEEP(c, 'username'), c.profile) + LET ditOut = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }) + // sort from newest + SORT ${ditType}.created DESC + // limit + LIMIT @offset, @limit + // respond + RETURN ditOut`; + + const params = { offset, limit , creators, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + + /** + * Read dits commented by specified users + * @param {string[]} usernames - list of usernames to search with + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async findCommentedBy(ditType, commentedBy, { offset, limit }) { + const ditCollection = ditType + 's'; + const query = ` + FOR user IN users + FILTER user.username IN @commentedBy + FOR comment IN comments + FILTER comment.creator == user._id + AND IS_SAME_COLLECTION('${ditType}s', comment.primary) + FOR ${ditType} IN @@ditCollection + FILTER ${ditType}._id == comment.primary + COLLECT i = ${ditType} + // sort from newest + SORT i.created DESC + LIMIT @offset, @limit + RETURN i`; + + const params = { commentedBy, offset, limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + + /** + * Read highly voted dits + * @param {string[]} voteSumBottomLimit - minimal query voteSum + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async findHighlyVoted(ditType, voteSumBottomLimit, { offset, limit }) { + const ditCollection = ditType + 's'; + const query = ` + FOR ${ditType} IN @@ditCollection + LET ${ditType}Votes = (FOR vote IN votes FILTER ${ditType}._id == vote._to RETURN vote) + // get sum of each dit's votes values + LET voteSum = SUM(${ditType}Votes[*].value) + // set bottom limit of voteSum + FILTER voteSum >= @voteSumBottomLimit + // find creator + LET c = (DOCUMENT(${ditType}.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + LET ditOut = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }, { voteSum }) + + // sort by amount of votes + SORT ditOut.voteSum DESC, ditOut.created DESC + LIMIT @offset, @limit + RETURN ditOut`; + + const params = { voteSumBottomLimit, offset, limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + + /** + * Read trending dits + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async findTrending(ditType, { offset, limit }) { + const ditCollection = ditType + 's'; + + const now = Date.now(); + const oneWeek = 604800000; // 1000 * 60 * 60 * 24 * 7 + const threeWeeks = 1814400000; // 1000 * 60 * 60 * 24 * 21 + const threeMonths = 7776000000; // 1000 * 60 * 60 * 24 * 90 + const weekAgo = now - oneWeek; + const threeWeeksAgo = now - threeWeeks; + const threeMonthsAgo = now - threeMonths; + + // for each dit we are counting 'rate' + // rate is the sum of votes/day in the last three months + // votes/day from last week are taken with wage 3 + // votes/day from two weeks before last week are taken with wage 2 + // votes/day from the rest of days are taken with wage 1 + const query = ` + FOR ${ditType} IN @@ditCollection + FOR vote IN votes + FILTER ${ditType}._id == vote._to + // group by dit id + COLLECT d = ${ditType} + // get sum of each dit's votes values from last week, last three weeks and last three months + AGGREGATE rateWeek = SUM((vote.value * TO_NUMBER( @weekAgo <= vote.created))/7), + rateThreeWeeks = SUM((vote.value * TO_NUMBER( @threeWeeksAgo <= vote.created && vote.created <= @weekAgo))/14), + rateThreeMonths = SUM((vote.value * TO_NUMBER( @threeMonthsAgo <= vote.created && vote.created <= @threeWeeksAgo))/69) + // find creator + LET c = (DOCUMENT(d.creator)) + LET creator = MERGE(KEEP(c, 'username'), c.profile) + LET ditOut = MERGE(KEEP(d, 'title', 'detail', 'created'), { id: d._key}, { creator }) + LET rates = 3*rateWeek + 2*rateThreeWeeks + rateThreeMonths + FILTER rates > 0 + // sort by sum of rates + SORT rates DESC + LIMIT @offset, @limit + RETURN ditOut`; + + const params = { weekAgo, threeWeeksAgo, threeMonthsAgo, offset, limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } + + /** + * Read dits with any of specified keywords in the title + * @param {string[]} keywords - list of keywords to search with + * @param {integer} offset - pagination offset + * @param {integer} limit - pagination limit + * @returns {Promise} - list of found dits + */ + static async findWithTitleKeywords(ditType, keywords, { offset, limit }) { + const ditCollection = ditType + 's'; + const query = ` + FOR ${ditType} IN @@ditCollection + LET search = ( FOR keyword in @keywords + RETURN TO_NUMBER(CONTAINS(${ditType}.title, keyword))) + LET fit = SUM(search) + FILTER fit > 0 + // find creator + LET c = (DOCUMENT(${ditType}.creator)) + // format for output + LET creator = MERGE(KEEP(c, 'username'), c.profile) + LET ditOut = MERGE(KEEP(${ditType}, 'title', 'detail', 'created'), { id: ${ditType}._key}, { creator }, {fit}) + // sort from newest + SORT fit DESC, ditOut.title + // limit + LIMIT @offset, @limit + // respond + RETURN ditOut`; + + const params = { 'keywords': keywords, offset, limit, '@ditCollection': ditCollection }; + const cursor = await this.db.query(query, params); + return await cursor.all(); + } +} + + +module.exports = Dit; diff --git a/models/dit/schema.js b/models/dit/schema.js new file mode 100644 index 0000000..fbcd484 --- /dev/null +++ b/models/dit/schema.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = function ({ title, detail, created = Date.now() }) { + return { title, detail, created }; +}; + diff --git a/models/index.js b/models/index.js index 58013cc..27746af 100644 --- a/models/index.js +++ b/models/index.js @@ -2,6 +2,8 @@ const comment = require('./comment'), contact = require('./contact'), + dit = require('./dit'), + ditTag = require('./dit-tag'), idea = require('./idea'), ideaTag = require('./idea-tag'), message = require('./message'), @@ -22,4 +24,4 @@ const models = { } }; -module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote }); +module.exports = Object.assign(models, { comment, contact, dit, ditTag, idea, ideaTag, message, model, tag, user, userTag, vote }); diff --git a/routes/challenges.js b/routes/challenges.js new file mode 100644 index 0000000..03d7fe5 --- /dev/null +++ b/routes/challenges.js @@ -0,0 +1,69 @@ +'use strict'; + +const express = require('express'), + path = require('path'), + router = express.Router(); + +const authorize = require(path.resolve('./controllers/authorize')), + ditControllers = require(path.resolve('./controllers/dits')), + ditTagControllers = require(path.resolve('./controllers/dit-tags')), + challengeValidators = require(path.resolve('./controllers/validators/challenges')), + challengeTagValidators = require(path.resolve('./controllers/validators/challenge-tags')), + { parse } = require(path.resolve('./controllers/validators/parser')), + // TODO seems quite hard to deal with it right now + go = require(path.resolve('./controllers/goto/challenges')); + +router.route('/') + // post a new challenge + .post(authorize.onlyLogged, challengeValidators.post, ditControllers.post); + +// get challenges with my tags +router.route('/') + .get(go.get.withMyTags, authorize.onlyLogged, parse, challengeValidators.getChallengesWithMyTags, ditControllers.getDitsWithMyTags); + +// get new challenges +router.route('/') + .get(go.get.new, authorize.onlyLogged, parse, challengeValidators.getNewChallenges, ditControllers.getNewDits); + +// get challenges with specified tags +router.route('/') + .get(go.get.withTags, authorize.onlyLogged, parse, challengeValidators.getChallengesWithTags, ditControllers.getDitsWithTags); + +// get random challenges +router.route('/') + .get(go.get.random, authorize.onlyLogged, parse, challengeValidators.getRandomChallenges, ditControllers.getRandomDits); + +// get challenges with creators +router.route('/') + .get(go.get.withCreators, authorize.onlyLogged, parse, challengeValidators.getChallengesWithCreators, ditControllers.getDitsWithCreators); + +// get challenges commented by specified users +router.route('/') + .get(go.get.commentedBy, authorize.onlyLogged, parse, challengeValidators.getChallengesCommentedBy, ditControllers.getDitsCommentedBy); + +// get challenges commented by specified users +router.route('/') + .get(go.get.highlyVoted, authorize.onlyLogged, parse, challengeValidators.getChallengesHighlyVoted, ditControllers.getDitsHighlyVoted); + +// get trending challenges +router.route('/') + .get(go.get.trending, authorize.onlyLogged, parse, challengeValidators.getChallengesTrending, ditControllers.getDitsTrending); + +// get challenges with keywords +router.route('/') + .get(go.get.searchTitle, authorize.onlyLogged, parse, challengeValidators.getChallengesSearchTitle, ditControllers.getDitsSearchTitle); + + +router.route('/:id') + // read challenge by id + .get(authorize.onlyLogged, challengeValidators.get, ditControllers.get) + .patch(authorize.onlyLogged, challengeValidators.patch, ditControllers.patch); + +router.route('/:id/tags') + .post(authorize.onlyLogged, challengeTagValidators.post, ditTagControllers.post) + .get(authorize.onlyLogged, challengeTagValidators.get, ditTagControllers.get); + +router.route('/:id/tags/:tagname') + .delete(authorize.onlyLogged, challengeTagValidators.del, ditTagControllers.del); + +module.exports = router; diff --git a/serializers/challenge-tags.js b/serializers/challenge-tags.js new file mode 100644 index 0000000..3ed19fd --- /dev/null +++ b/serializers/challenge-tags.js @@ -0,0 +1,70 @@ +'use strict'; + +const path = require('path'); +const Serializer = require('jsonapi-serializer').Serializer; + +const config = require(path.resolve('./config')); + +/** + * Serializer for challengeTags + */ +const challengeTagsSerializer = new Serializer('challenge-tags', { + attributes: ['challenge', 'tag', 'creator'], + typeForAttribute(attribute) { + if (attribute === 'creator') { + return 'users'; + } + }, + // relationships + challenge: { + ref: 'id', + attributes: ['title', 'detail', 'created'], + includedLinks: { + self: (data, { id }) => `${config.url.all}/challenges/${id}` + } + }, + tag: { + ref: 'tagname', + attributes: ['tagname'], + includedLinks: { + self: (data, { tagname }) => `${config.url.all}/tags/${tagname}` + }, + relationshipLinks: { } + }, + creator: { + ref: 'username', + attributes: ['username', 'givenName', 'familyName', 'description'], + includedLinks: { + self: (data, { username }) => `${config.url.all}/users/${username}` + }, + relationshipLinks: { + related: (data, { username }) => `${config.url.all}/users/${username}` + } + } +}); + +/** + * Given challengeTag, we generate id and add it to the challengeTag + * This method mutates the parameter + */ +function createChallengeTagId(challengeTag) { + const { challenge: { id }, tag: { tagname } } = challengeTag; + challengeTag.id = `${id}--${tagname}`; +} + +/** + * Function to serialize either a userTag or array of userTags + */ +function challengeTag(data) { + // generate ids for challengeTags + if (Array.isArray(data)) { + data.forEach(createChallengeTagId); + } else { + createChallengeTagId(data); + } + + // serialize + return challengeTagsSerializer.serialize(data); +} + +module.exports = { challengeTag }; diff --git a/serializers/challenges.js b/serializers/challenges.js new file mode 100644 index 0000000..c46526d --- /dev/null +++ b/serializers/challenges.js @@ -0,0 +1,71 @@ +'use strict'; + +const path = require('path'), + Serializer = require('jsonapi-serializer').Serializer; + +const config = require(path.resolve('./config')); + +const challengeSerializer = new Serializer('challenges', { + id: 'id', + attributes: ['title', 'detail', 'created', 'creator', 'challengeTags'], + keyForAttribute: 'camelCase', + typeForAttribute(attribute) { + if (attribute === 'creator') { + return 'users'; + } + if (attribute === 'challengeTags') return 'challenge-tags'; + }, + creator: { + ref: 'username', + type: 'users', + attributes: ['username', 'givenName', 'familyName', 'description'], + includedLinks: { + self: (data, { username }) => `${config.url.all}/users/${username}` + }, + relationshipLinks: { + related: (data, { username }) => `${config.url.all}/users/${username}` + } + }, + // when we want to have challengeTags as relationships + challengeTags: { + ref: 'id', + attributes: ['challenge', 'tag', ''], + typeForAttribute(attribute) { + if (attribute === 'creator') { + return 'users'; + } + }, + relationshipLinks: { }, + includedLinks: { }, + // relationships + challenge: { + ref: 'id' + }, + tag: { + ref: 'tagname' + } + }, + dataMeta: { + votesUp(record, current) { + if (!current.votes) return; + return current.votes.filter(vote => vote.value === 1).length; + }, + votesDown(record, current) { + if (!current.votes) return; + return current.votes.filter(vote => vote.value === -1).length; + }, + myVote(record, current) { + if (!current.hasOwnProperty('myVote')) return; + return (current.myVote) ? current.myVote.value : 0; + }, + voteSum(record, current) { + return current.voteSum; + } + } +}); + +function challenge(data) { + return challengeSerializer.serialize(data); +} + +module.exports = { challenge }; diff --git a/serializers/index.js b/serializers/index.js index dc8fdd8..5a17220 100644 --- a/serializers/index.js +++ b/serializers/index.js @@ -2,7 +2,9 @@ const Deserializer = require('jsonapi-serializer').Deserializer; -const comments = require('./comments'), +const challenges = require('./challenges'), + challengeTags = require('./challenge-tags'), + comments = require('./comments'), contacts = require('./contacts'), ideas = require('./ideas'), ideaTags = require('./idea-tags'), @@ -44,6 +46,6 @@ function deserialize(req, res, next) { } module.exports = { - serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users, votes), + serialize: Object.assign({ }, challenges, challengeTags, comments, contacts, ideas, ideaTags, messages, tags, users, votes), deserialize }; diff --git a/test/comments.js b/test/comments.js index b5c14f2..97f93db 100644 --- a/test/comments.js +++ b/test/comments.js @@ -9,6 +9,7 @@ const agentFactory = require('./agent'), models = require(path.resolve('./models')); commentTestsFactory('idea'); +commentTestsFactory('challenge'); commentTestsFactory('comment'); /** @@ -26,7 +27,6 @@ function commentTestsFactory(primary, only=false) { const ds = (only) ? describe.only : describe; - ds(`${comments} of ${primary}`, () => { // declare some variables @@ -71,13 +71,13 @@ function commentTestsFactory(primary, only=false) { const data = { users: 3, verifiedUsers: [0, 1], - ideas: Array(1).fill([]), - ideaComments: [[0, 0]] + ['ideas']: Array(1).fill([]), + ['ideaComments']: [[0, 0]] }; dbData = await dbHandle.fill(data); - existentPrimary = dbData.ideaComments[0]; + existentPrimary = dbData['ideaComments'][0]; loggedUser = dbData.users[0]; }); } else { @@ -86,7 +86,7 @@ function commentTestsFactory(primary, only=false) { const data = { users: 3, verifiedUsers: [0, 1], - ideas: Array(1).fill([]) + [`${primarys}`]: Array(1).fill([]) }; dbData = await dbHandle.fill(data); @@ -256,8 +256,8 @@ function commentTestsFactory(primary, only=false) { const data = { users: 4, verifiedUsers: [0, 1, 2, 3], - ideas: Array(3).fill([]), - ideaComments: [ + [`${primarys}`]: Array(3).fill([]), + [`${primary}Comments`]: [ [0, 0], [0, 1], [0, 1], [0, 2], [0, 1], [0, 1], [0, 0], [0, 3], [1, 0], [1, 0], [1, 2], [2, 0], [2, 1], [2, 1], [2, 2], [2, 1], [2, 1], [2, 0], [2, 3], @@ -268,7 +268,7 @@ function commentTestsFactory(primary, only=false) { dbData = await dbHandle.fill(data); - [primary0,, primary2] = dbData.ideas; + [primary0,, primary2] = dbData[`${primarys}`]; loggedUser = dbData.users[0]; }); @@ -427,8 +427,8 @@ function commentTestsFactory(primary, only=false) { const data = { users: 2, verifiedUsers: [0, 1], - ideas: Array(1).fill([]), - ideaComments: [[0, 0]], + ['ideas']: Array(1).fill([]), + ['ideaComments']: [[0, 0]], reactions: [[0, 0], [0, 1]] }; @@ -444,17 +444,17 @@ function commentTestsFactory(primary, only=false) { const data = { users: 2, verifiedUsers: [0, 1], - ideas: Array(1).fill([]), - ideaComments: [ + [`${primarys}`]: Array(1).fill([]), + [`${primary}Comments`]: [ [0, 0], [0, 1], ] }; dbData = await dbHandle.fill(data); - primary0 = dbData.ideas[0]; + primary0 = dbData[`${primarys}`][0]; [user0] = dbData.users; - [comment00, comment01] = dbData.ideaComments; + [comment00, comment01] = dbData[`${primary}Comments`]; }); } @@ -625,8 +625,8 @@ function commentTestsFactory(primary, only=false) { const data = { users: 2, verifiedUsers: [0, 1], - ideas: Array(1).fill([]), - ideaComments: [[0, 0]], + ['ideas']: Array(1).fill([]), + ['ideaComments']: [[0, 0]], reactions: [[0, 0], [0, 1]] }; @@ -641,8 +641,8 @@ function commentTestsFactory(primary, only=false) { const data = { users: 2, verifiedUsers: [0, 1], - ideas: Array(1).fill([]), - ideaComments: [ + [`${primarys}`]: Array(1).fill([]), + [`${primary}Comments`]: [ [0, 0], [0, 1], ], reactions: [[0, 0], [0, 1], [1, 0]] @@ -651,7 +651,7 @@ function commentTestsFactory(primary, only=false) { dbData = await dbHandle.fill(data); [user0] = dbData.users; - [comment00, comment01] = dbData.ideaComments; + [comment00, comment01] = dbData[`${primary}Comments`]; }); } diff --git a/test/dit-tags.js b/test/dit-tags.js new file mode 100644 index 0000000..ecf0946 --- /dev/null +++ b/test/dit-tags.js @@ -0,0 +1,339 @@ +'use strict'; + +const path = require('path'), + should = require('should'); + +const dbHandle = require('./handle-database'); +const agentFactory = require('./agent'); +const models = require(path.resolve('./models')); + +testDitsTags('idea'); +testDitsTags('challenge'); + +function testDitsTags(dit){ + describe(`tags of ${dit}`, () => { + + let agent, + dbData, + existentDit, + loggedUser, + otherUser, + tag0, + tag1; + + beforeEach(() => { + agent = agentFactory(); + }); + + beforeEach(async () => { + const data = { + users: 3, // how many users to make + verifiedUsers: [0, 1], // which users to make verified + tags: 5, + [`${dit}s`]: [ + [{ }, 0], + [{ }, 1] + ], + // ditTag [0, 0] shouldn't be created here; is created in tests for POST + [`${dit}Tags`]: [[0, 1], [0, 2], [0, 3], [0, 4], [1, 0], [1, 4]] + }; + // create data in database + dbData = await dbHandle.fill(data); + + [loggedUser, otherUser] = dbData.users; + [existentDit] = dbData[`${dit}s`]; + [tag0, tag1] = dbData.tags; + }); + + afterEach(async () => { + await dbHandle.clear(); + }); + + describe(`POST /${dit}s/:id/tags`, () => { + let postBody; + + beforeEach(() => { + postBody = { data: { + type: `${dit}-tags`, + relationships: { + tag: { data: { type: 'tags', id: tag0.tagname } } + } + } }; + }); + + context(`logged as ${dit} creator`, () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid data', () => { + it(`[${dit} and tag exist and ${dit}Tag doesn't] 201`, async () => { + const response = await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(201); + const ditTagDb = await models.ditTag.read(dit, existentDit.id, tag0.tagname); + should(ditTagDb).match({ + [`${dit}`]: { id: existentDit.id }, + tag: { tagname: tag0.tagname }, + creator: { username: loggedUser.username } + }); + should(response.body).match({ + data: { + type: `${dit}-tags`, + id: `${existentDit.id}--${tag0.tagname}`, + relationships: { + [`${dit}`]: { data: { type: `${dit}s`, id: existentDit.id } }, + tag: { data: { type: 'tags', id: tag0.tagname } }, + creator: { data: { type: 'users', id: loggedUser.username } } + } + } + }); + }); + + it(`[duplicate ${dit}Tag] 409`, async () => { + // first it's ok + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(201); + + // duplicate request should error + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(409); + }); + + it(`[${dit} doesn't exist] 404`, async () => { + const response = await agent + .post(`/${dit}s/00000000/tags`) + .send(postBody) + .expect(404); + + should(response.body).deepEqual({ + errors: [{ + status: 404, + detail: `${dit} not found` + }] + }); + }); + + it('[tag doesn\'t exist] 404', async () => { + // set nonexistent tag in body + postBody.data.relationships.tag.data.id = 'nonexistent-tag'; + + const response = await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(404); + + should(response.body).deepEqual({ + errors: [{ + status: 404, + detail: 'tag not found' + }] + }); + }); + + }); + + context('invalid data', () => { + it('[invalid id] 400', async () => { + await agent + .post(`/${dit}s/invalid-id/tags`) + .send(postBody) + .expect(400); + }); + + it('[invalid tagname] 400', async () => { + // invalidate tagname + postBody.data.relationships.tag.data.id = 'invalidTagname'; + + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(400); + }); + + it('[missing tagname] 400', async () => { + // invalidate tagname + delete postBody.data.relationships.tag; + + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(400); + }); + + it('[additional properties in body] 400', async () => { + // add some attributes (or relationships) + postBody.data.attributes = { foo: 'bar' }; + + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(400); + }); + }); + + }); + + context(`logged, not ${dit} creator`, () => { + beforeEach(() => { + agent = agentFactory.logged(otherUser); + }); + + it('403', async () => { + const response = await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(403); + + should(response.body).deepEqual({ + errors: [{ status: 403, detail: `not logged in as ${dit} creator` }] + }); + }); + }); + + context('not logged', () => { + it('403', async () => { + await agent + .post(`/${dit}s/${existentDit.id}/tags`) + .send(postBody) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s/:id/tags`, () => { + context('logged', () => { + + beforeEach(() => { + agent = agentFactory.logged(); + }); + + context('valid data', () => { + it(`[${dit} exists] 200 and list of ${dit}-tags`, async () => { + const response = await agent + .get(`/${dit}s/${existentDit.id}/tags`) + .expect(200); + + const responseData = response.body.data; + + should(responseData).Array().length(4); + }); + + it(`[${dit} doesn't exist] 404`, async () => { + const response = await agent + .get(`/${dit}s/00000001/tags`) + .expect(404); + + should(response.body).match({ errors: [{ + status: 404, + detail: `${dit} not found` + }] }); + }); + }); + + context('invalid data', () => { + it('[invalid id] 400', async () => { + await agent + .get(`/${dit}s/invalidId/tags`) + .expect(400); + }); + }); + + }); + + context('not logged', () => { + it('403', async () => { + await agent + .get(`/${dit}s/${existentDit.id}/tags`) + .expect(403); + }); + }); + + }); + + describe(`DELETE /${dit}s/:id/tags/:tagname`, () => { + + context(`logged as ${dit} creator`, () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid data', () => { + it(`[${dit}-tag exists] 204`, async () => { + const ditTag = await models.ditTag.read(dit, existentDit.id, tag1.tagname); + + // first ditTag exists + should(ditTag).Object(); + + await agent + .delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`) + .expect(204); + + const ditTagAfter = await models.ditTag.read(dit, existentDit.id, tag1.tagname); + // the ditTag doesn't exist + should(ditTagAfter).be.undefined(); + + }); + + it(`[${dit}-tag doesn't exist] 404`, async () => { + await agent + .delete(`/${dit}s/${existentDit.id}/tags/${tag0.tagname}`) + .expect(404); + }); + }); + + context('invalid data', () => { + it('[invalid id] 400', async () => { + await agent + .delete(`/${dit}s/invalid-id/tags/${tag1.tagname}`) + .expect(400); + }); + + it('[invalid tagname] 400', async () => { + await agent + .delete(`/${dit}s/${existentDit.id}/tags/invalid--tagname`) + .expect(400); + }); + }); + + }); + + context(`logged, not ${dit} creator`, () => { + + beforeEach(() => { + agent = agentFactory.logged(otherUser); + }); + + it('403', async () => { + const response = await agent + .delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`) + .expect(403); + + should(response.body).deepEqual({ + errors: [{ status: 403, detail: `not logged in as ${dit} creator` }] + }); + }); + }); + + context('not logged', () => { + it('403', async () => { + const response = await agent + .delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`) + .expect(403); + + should(response.body).not.deepEqual({ + errors: [{ status: 403, detail: `not logged in as ${dit} creator` }] + }); + }); + }); + + }); + }); +} \ No newline at end of file diff --git a/test/dits-list.js b/test/dits-list.js new file mode 100644 index 0000000..0c0f1cb --- /dev/null +++ b/test/dits-list.js @@ -0,0 +1,1152 @@ +'use strict'; + +const path = require('path'), + should = require('should'), + sinon = require('sinon'); + +const models = require(path.resolve('./models')); + +const agentFactory = require('./agent'), + dbHandle = require('./handle-database'); + +testDitsList('idea'); +testDitsList('challenge'); + +function testDitsList(dit){ + describe(`read lists of ${dit}s`, () => { + + let agent, + dbData; + + // default supertest agent (not logged in) + beforeEach(() => { + agent = agentFactory(); + }); + + // clear database after each test + afterEach(async () => { + await dbHandle.clear(); + }); + + describe(`GET /${dit}s?filter[withMyTags]`, () => { + + let tag1, + dit0, + user0; + + // create and save testing data + beforeEach(async () => { + const data = { + users: 3, + verifiedUsers: [0, 1, 2], + tags: 6, + [`${dit}s`]: Array(7).fill([]), + userTag: [ + [0,0,'',5],[0,1,'',4],[0,2,'',3],[0,4,'',1], + [1,1,'',4],[1,3,'',2], + [2,5,'',2] + ], + [`${dit}Tags`]: [ + [0,0],[0,1],[0,2], + [1,1],[1,2], + [2,1],[2,2],[2,4], + [4,0],[4,1],[4,2],[4,3],[4,4], + [5,2],[5,3], + [6,3] + ] + }; + + dbData = await dbHandle.fill(data); + + [user0] = dbData.users; + [dit0] = dbData[`${dit}s`]; + tag1 = dbData.tags[1]; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[withMyTags]`) + .expect(200); + + // we should find 5 dits... + should(response.body).have.property('data').Array().length(5); + + // ...sorted by sum of my tag relevances + should(response.body.data.map(dit => dit.attributes.title)) + .eql([4, 0, 2, 1, 5].map(no => `${dit} title ${no}`)); + + // ditTags should be present as relationships + should(response.body.data[1]).have.propertyByPath('relationships', `${dit}Tags`, 'data').Array().length(3); + should(response.body.data[1].relationships[`${dit}Tags`].data[1]).deepEqual({ + type: `${dit}-tags`, + id: `${dit0.id}--${tag1.tagname}` + }); + + // and dit-tags should be included, too + const includedDitTags = response.body.included.filter(included => included.type === `${dit}-tags`); + should(includedDitTags).Array().length(13); + }); + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[withMyTags]&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by sum of my tag relevances and started from the 2nd one + should(response.body.data.map(dit => dit.attributes.title)) + .eql([0, 2, 1].map(no => `${dit} title ${no}`)); + }); + }); + + context('invalid data', () => { + + it('[invalid query.filter.withMyTags] 400', async () => { + await agent + .get(`/${dit}s?filter[withMyTags]=1`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[withMyTags]&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[withMyTags]&filter[Foo]=bar`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[withMyTags]`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[withTags]=tag0,tag1,tag2`, () => { + + let tag0, + tag1, + tag3, + tag4, + dit0, + user0; + + // create and save testing data + beforeEach(async () => { + const data = { + users: 3, + verifiedUsers: [0, 1, 2], + tags: 6, + [`${dit}s`]: Array(7).fill([]), + [`${dit}Tags`]: [ + [0,0],[0,1],[0,2], + [1,1],[1,2], + [2,1],[2,2],[2,4], + [4,0],[4,1],[4,2],[4,3],[4,4], + [5,2],[5,3], + [6,3] + ] + }; + + dbData = await dbHandle.fill(data); + + [user0] = dbData.users; + [dit0] = dbData[`${dit}s`]; + [tag0, tag1,, tag3, tag4] = dbData.tags; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[withTags]=${tag0.tagname},${tag1.tagname},${tag3.tagname},${tag4.tagname}`) + .expect(200); + + // we should find 6 dits... + should(response.body).have.property('data').Array().length(6); + + // ...sorted by sum of my tag relevances + should(response.body.data.map(dit => dit.attributes.title)) + .eql([4, 0, 2, 1, 5, 6].map(no => `${dit} title ${no}`)); + + // ditTags should be present as relationships + should(response.body.data[1]).have.propertyByPath('relationships', `${dit}Tags`, 'data').Array().length(2); + should(response.body.data[1].relationships[`${dit}Tags`].data[1]).deepEqual({ + type: `${dit}-tags`, + id: `${dit0.id}--${tag1.tagname}` + }); + + // and dit-tags should be included, too + const includedDitTags = response.body.included.filter(included => included.type === `${dit}-tags`); + should(includedDitTags).Array().length(11); + }); + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[withTags]=${tag0.tagname},${tag1.tagname},${tag3.tagname},${tag4.tagname}&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by sum of my tag relevances and started from the 2nd one + should(response.body.data.map(dit => dit.attributes.title)) + .eql([0, 2, 1].map(no => `${dit} title ${no}`)); + }); + }); + + context('invalid data', () => { + + it('[invalid tagnames in a list] error 400', async () => { + await agent + .get(`/${dit}s?filter[withTags]=invalid--tagname,other-invalid*tagname`) + .expect(400); + }); + + it('[too many tags provided] error 400', async () => { + await agent + .get(`/${dit}s?filter[withTags]=t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10`) + .expect(400); + }); + + it('[no tags provided] error 400', async () => { + await agent + .get(`/${dit}s?filter[withTags]=`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[withTags]=tag1,tag2,tag3&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[withTags]=tag&filter[Foo]=bar`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[withTags]=tag0`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?sort=-created (new ${dit}s)`, () => { + + let loggedUser; + + // create and save testing data + beforeEach(async () => { + const data = { + users: 3, + verifiedUsers: [0, 1, 2], + tags: 6, + [`${dit}s`]: Array(11).fill([]) + }; + + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid', () => { + it(`200 and array of new ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?sort=-created`) + .expect(200); + + // we should find 5 dits... + should(response.body).have.property('data').Array().length(5); + + // ...sorted from newest to oldest + should(response.body.data.map(dit => dit.attributes.title)) + .eql([10, 9, 8, 7, 6].map(no => `${dit} title ${no}`)); + }); + + it(`[pagination] 200 and array of new ${dit}s, offseted and limited`, async () => { + + // request + const response = await agent + .get(`/${dit}s?sort=-created&page[offset]=3&page[limit]=4`) + .expect(200); + + // we should find 4 dits... + should(response.body).have.property('data').Array().length(4); + + // ...sorted from newest to oldest + should(response.body.data.map(dit => dit.attributes.title)) + .eql([7, 6, 5, 4].map(no => `${dit} title ${no}`)); + }); + }); + + context('invalid', () => { + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?sort=-created&page[offset]=3&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?sort=-created&foo=bar`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?sort=-created`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[random]`, () => { + + let loggedUser; + + // create and save testing data + beforeEach(async () => { + const data = { + users: 3, + verifiedUsers: [0, 1, 2], + [`${dit}s`]: Array(11).fill([]) + }; + + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid', () => { + it(`200 and array of random ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[random]`) + .expect(200); + + // we should find 1 dit by default + should(response.body).have.property('data').Array().length(1); + }); + + it(`[pagination] 200 and array of random ${dit}s, limited`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[random]&page[offset]=0&page[limit]=4`) + .expect(200); + + // we should find 4 dits... + should(response.body).have.property('data').Array().length(4); + }); + }); + + context('invalid', () => { + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[random]&page[offset]=3&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[random]&foo=bar`) + .expect(400); + }); + + it('[random with value] 400', async () => { + await agent + .get(`/${dit}s?filter[random]=bar`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[random]`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[creators]=user0,user1,user2`, () => { + let user0, + user2, + user3, + user4; + // create and save testing data + beforeEach(async () => { + const data = { + users: 6, + verifiedUsers: [0, 1, 2, 3, 4], + [`${dit}s`]: [[{}, 0], [{}, 0],[{}, 1],[{}, 2],[{}, 2],[{}, 2],[{}, 3]] + }; + + dbData = await dbHandle.fill(data); + + [user0, , user2, user3, user4, ] = dbData.users; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`[one creator] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[creators]=${user2.username}`) + .expect(200); + + // we should find 2 dits... + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([5, 4, 3].map(no => `${dit} title ${no}`)); + + }); + + + it(`[two creators] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[creators]=${user2.username},${user3.username}`) + .expect(200); + + // we should find 5 dits... + should(response.body).have.property('data').Array().length(4); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([6, 5, 4, 3].map(no => `${dit} title ${no}`)); + }); + + it(`[creator without ${dit}s] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[creators]=${user4.username}`) + .expect(200); + + // we should find 0 dits... + should(response.body).have.property('data').Array().length(0); + + }); + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[creators]=${user2.username},${user3.username}&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([5, 4, 3].map(no => `${dit} title ${no}`)); + }); + + it(`[nonexistent creator] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[creators]=nonexistentcreator`) + .expect(200); + + // we should find 0 dits... + should(response.body).have.property('data').Array().length(0); + + }); + }); + + context('invalid data', () => { + + it('[invalid query.filter.creators] 400', async () => { + await agent + .get(`/${dit}s?filter[creators]=1`) + .expect(400); + }); + + it('[too many users] 400', async () => { + await agent + .get(`/${dit}s?filter[creators]=user1,user2,user3,user4,user5,user6,user7,user8,user9,user190,user11`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[creators]=${user2.username},${user3.username}&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[creators]=${user2.username},${user3.username}&additional[param]=3&page[offset]=1&page[limit]=3`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[creators]=${user2.username}`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[commentedBy]=user0,user1,user2`, () => { + let user0, + user2, + user3, + user4; + // create and save testing data + beforeEach(async () => { + const data = { + users: 6, + verifiedUsers: [0, 1, 2, 3, 4], + [`${dit}s`]: Array(7).fill([]), + [`${dit}Comments`]: [[0, 0],[0, 1], [0,2],[0,2], [0,4], [1,1], [1,2], [2,1], [2,2], [3,4] ] + }; + + dbData = await dbHandle.fill(data); + + [user0, , user2, user3, user4, ] = dbData.users; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`[${dit}s commented by one user] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username}`) + .expect(200); + + // we should find 3 dit... + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([2, 1, 0].map(no => `${dit} title ${no}`)); + + }); + + + it(`[${dit}s commented by two users] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username},${user4.username}`) + .expect(200); + + // we should find 4 dits... + should(response.body).have.property('data').Array().length(4); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([3, 2, 1, 0].map(no => `${dit} title ${no}`)); + }); + + it(`[${dit}s commented by user who didn't commented anyting] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[commentedBy]=${user3.username}`) + .expect(200); + + // we should find 0 dits... + should(response.body).have.property('data').Array().length(0); + + }); + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username},${user4.username}&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([2, 1, 0].map(no => `${dit} title ${no}`)); + }); + + it(`[nonexistent user who commented] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[commentedBy]=nonexistentuser`) + .expect(200); + + // we should find 0 dits... + should(response.body).have.property('data').Array().length(0); + + }); + }); + + context('invalid data', () => { + + it('[invalid query.filter.commentedBy] 400', async () => { + await agent + .get(`/${dit}s?filter[commentedBy]=1`) + .expect(400); + }); + + it('[too many users] 400', async () => { + await agent + .get(`/${dit}s?filter[commentedBy]=user1,user2,user3,user4,user5,user6,user7,user8,user9,user190,user11`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username},${user3.username}&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username},${user3.username}&additional[param]=3&page[offset]=1&page[limit]=3`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[commentedBy]=${user2.username}`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[highlyVoted]=voteSumBottomLimit`, () => { + let user0; + // create and save testing data + beforeEach(async () => { + const primarys = `${dit}s`; + const data = { + users: 6, + verifiedUsers: [0, 1, 2, 3, 4], + [`${dit}s`]: Array(7).fill([]), + // dits with votes: 3:3, 1:3, 5:1, 2:1, 0:0, 6: -1, 4:-2 + votes: [ + [0, [primarys, 0], -1], + [1, [primarys, 0], 1], + [0, [primarys, 1], 1], + [1, [primarys, 1], 1], + [2, [primarys, 1], 1], + [0, [primarys, 2], -1], + [1, [primarys, 2], 1], + [2, [primarys, 2], 1], + [0, [primarys, 3], 1], + [1, [primarys, 3], 1], + [2, [primarys, 3], 1], + [3, [primarys, 3], 1], + [4, [primarys, 3], -1], + [0, [primarys, 4], -1], + [1, [primarys, 4], -1], + [3, [primarys, 5], 1], + [3, [primarys, 6], -1] + ] + }; + + dbData = await dbHandle.fill(data); + + [user0, , , , , ] = dbData.users; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`[highly voted ${dit}s] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[highlyVoted]=0`) + .expect(200); + + // without pagination, limit for ideas 5 we should find 5 dits... + should(response.body).have.property('data').Array().length(5); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([3, 1, 5, 2, 0].map(no => `${dit} title ${no}`)); + + }); + + it(`[highly voted ${dit}s with at least 2 votes in plus] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[highlyVoted]=2`) + .expect(200); + + // without pagination, limit for ideas 5 we should find 5 dits... + should(response.body).have.property('data').Array().length(2); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([3, 1].map(no => `${dit} title ${no}`)); + + // shoud value be at least 2 + should(Math.min(...response.body.data.map(dit => dit.meta.voteSum))) + .aboveOrEqual(2); + }); + + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[highlyVoted]=0&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([1, 5, 2].map(no => `${dit} title ${no}`)); + }); + + }); + + context('invalid data', () => { + + it('[invalid query.filter.highlyVoted] 400', async () => { + await agent + .get(`/${dit}s?filter[highlyVoted]=string`) + .expect(400); + }); + + it('[invalid query.filter.highlyVoted] 400', async () => { + await agent + .get(`/${dit}s?filter[highlyVoted]`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[highlyVoted]=0&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[highlyVoted]=0&additional[param]=3&page[offset]=1&page[limit]=3`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[highlyVoted]=0`) + .expect(403); + }); + }); + }); + // ///////////////////////////////// TUTAJ + describe(`GET /${dit}s?filter[trending]`, () => { + let user0, + user1, + user2, + user3, + user4, + user5, + user6, + user7, + user8, + dit1, + dit2, + dit3, + dit4, + dit5, + dit6; + const now = Date.now(); + let sandbox; + const threeMonths = 7776000000; + const threeWeeks = 1814400000; + const oneWeek = 604800000; + const twoDays = 172800000; + // create and save testing data + beforeEach(async () => { + sandbox = sinon.sandbox.create(); + const primarys = `${dit}s`; + const data = { + users: 10, + verifiedUsers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [`${dit}s`]: Array(11).fill([]), + // dits with votes: 3:3, 1:3, 5:1, 2:1, 0:0, 6: -1, 4:-2 + votes: [ + [0, [primarys, 1], 1], + [0, [primarys, 2], 1], + [0, [primarys, 3], 1], + [1, [primarys, 3], 1], + [2, [primarys, 3], 1], + [0, [primarys, 4], 1], + [1, [primarys, 4], 1], + [2, [primarys, 4], 1], + [3, [primarys, 4], 1], + [4, [primarys, 4], 1], + [0, [primarys, 5], 1], + [1, [primarys, 5], 1], + [2, [primarys, 5], 1], + [3, [primarys, 5], 1], + [4, [primarys, 5], 1], + [5, [primarys, 5], 1], + [6, [primarys, 5], 1], + [0, [primarys, 6], 1], + [1, [primarys, 6], 1] + ] + }; + // post initial data and oldest votes with date three monts ago without two days + sandbox.useFakeTimers(now - threeMonths + twoDays); + dbData = await dbHandle.fill(data); + + [user0, user1, user2, user3, user4, user5, user6, user7, user8 ] = dbData.users; + [ , dit1, dit2, dit3, dit4, dit5, dit6] = dbData[`${dit}s`]; + + // create data to post with time: three weeks ago + const dataThreeWeeksAgo = { + votes: [ + {from: user1.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user1.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user2.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user3.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user3.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user5.username, to: {type: primarys, id: dit4.id}, value: 1}, + {from: user6.username, to: {type: primarys, id: dit4.id}, value: 1}, + {from: user7.username, to: {type: primarys, id: dit4.id}, value: 1}, + {from: user7.username, to: {type: primarys, id: dit5.id}, value: 1}, + {from: user2.username, to: {type: primarys, id: dit6.id}, value: 1}, + {from: user3.username, to: {type: primarys, id: dit6.id}, value: 1} + ] + }; + // stub time to three weeks ago without two days + sandbox.clock.restore(); + sandbox.useFakeTimers(now - threeWeeks + twoDays); + // add data to database hree weeks ago without two days + for(const i in dataThreeWeeksAgo.votes){ + await models.vote.create(dataThreeWeeksAgo.votes[i]); + } + + const dataOneWeekAgo = { + votes: [ + {from: user2.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user3.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user4.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user5.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user6.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user7.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user8.username, to: {type: primarys, id: dit1.id}, value: 1}, + {from: user4.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user5.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user6.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user7.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user8.username, to: {type: primarys, id: dit2.id}, value: 1}, + {from: user4.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user5.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user6.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user7.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user8.username, to: {type: primarys, id: dit3.id}, value: 1}, + {from: user8.username, to: {type: primarys, id: dit4.id}, value: 1}, + {from: user8.username, to: {type: primarys, id: dit5.id}, value: 1}, + {from: user4.username, to: {type: primarys, id: dit6.id}, value: 1}, + {from: user5.username, to: {type: primarys, id: dit6.id}, value: 1} + ] + }; + // stub time to one week ago without two days + sandbox.clock.restore(); + sandbox.useFakeTimers( now - oneWeek + twoDays); + for(const i in dataOneWeekAgo.votes){ + await models.vote.create(dataOneWeekAgo.votes[i]); + } + sandbox.clock.restore(); + }); + afterEach(async () => { + sandbox.restore(); + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`[trending] 200 and return array of matched ${dit}s`, async () => { + // request + const response = await agent + .get(`/${dit}s?filter[trending]`) + .expect(200); + // without pagination, limit for dits 5 we should find 5 dits... + should(response.body).have.property('data').Array().length(5); + + // sorted by trending rate + should(response.body.data.map(dit => dit.attributes.title)) + .eql([1, 2, 3, 6, 4].map(no => `${dit} title ${no}`)); + + }); + + it('[trending with pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[trending]&page[offset]=1&page[limit]=3`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(3); + + // sorted by trending rate + should(response.body.data.map(dit => dit.attributes.title)) + .eql([2, 3, 6].map(no => `${dit} title ${no}`)); + }); + + }); + + context('invalid data', () => { + + it('[trending invalid query.filter.highlyRated] 400', async () => { + await agent + .get(`/${dit}s?filter[trending]=string&page[offset]=1&page[limit]=3`) + .expect(400); + }); + + it('[trending invalid query.filter.highlyRated] 400', async () => { + await agent + .get(`/${dit}s?filter[trending]=1&page[offset]=1&page[limit]=3`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[trending]&additional[param]=3&page[offset]=1&page[limit]=3`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[trending]`) + .expect(403); + }); + }); + }); + + describe(`GET /${dit}s?filter[title][like]=string1,string2,string3`, () => { + let user0; + // create and save testing data + beforeEach(async () => { + const data = { + users: 2, + verifiedUsers: [0], + [`${dit}s`]: [ [{title:`${dit}-title1`}, 0], [{title:`${dit}-title2-keyword1`}, 0], [{title:`${dit}-title3-keyword2`}, 0], [{title:`${dit}-title4-keyword3`}, 0], [{title:`${dit}-title5-keyword2-keyword3`}, 0], [{title:`${dit}-title6-keyword1`}, 0], [{title:`${dit}-title7-keyword1-keyword4`}, 0] ] + }; + + dbData = await dbHandle.fill(data); + + [user0, ] = dbData.users; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(user0); + }); + + context('valid data', () => { + + it(`[find ${dit}s with one word] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[title][like]=keyword1`) + .expect(200); + + // we should find 2 dits... + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([`${dit}-title2-keyword1`,`${dit}-title6-keyword1`, `${dit}-title7-keyword1-keyword4`]); + + }); + + + it(`[find ${dit}s with two words] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[title][like]=keyword2,keyword3`) + .expect(200); + + // we should find 4 dits... + should(response.body).have.property('data').Array().length(3); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([`${dit}-title5-keyword2-keyword3`, `${dit}-title3-keyword2`, `${dit}-title4-keyword3`]); + }); + + it(`[find ${dit}s with word not present in any] 200 and return array of matched ${dit}s`, async () => { + + // request + const response = await agent + .get(`/${dit}s?filter[title][like]=keyword10`) + .expect(200); + + // we should find 0 dits... + should(response.body).have.property('data').Array().length(0); + + }); + + it('[pagination] offset and limit the results', async () => { + const response = await agent + .get(`/${dit}s?filter[title][like]=keyword1&page[offset]=1&page[limit]=2`) + .expect(200); + + // we should find 3 dits + should(response.body).have.property('data').Array().length(2); + + // sorted by creation date desc + should(response.body.data.map(dit => dit.attributes.title)) + .eql([`${dit}-title6-keyword1`, `${dit}-title7-keyword1-keyword4`]); + }); + + it('should be fine to provide a keyword which includes empty spaces and/or special characters', async () => { + // request + await agent + .get(`/${dit}s?filter[title][like]=keyword , aa,1-i`) + .expect(200); + }); + + }); + + context('invalid data', () => { + + it('[too many keywords] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1,keyword2,keyword3,keyword4,keyword5,keyword6,keyword7,keyword8,keyword9,keyword10,keyword11`) + .expect(400); + }); + + it('[empty keywords] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1,`) + .expect(400); + }); + + it('[too long keywords] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1,${'a'.repeat(257)}`) + .expect(400); + }); + + it('[keywords spaces only] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]= ,keyword2`) + .expect(400); + }); + + it('[invalid pagination] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1&page[offset]=1&page[limit]=21`) + .expect(400); + }); + + it('[unexpected query params] 400', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1&additional[param]=3&page[offset]=1&page[limit]=3`) + .expect(400); + }); + }); + }); + + context('not logged in', () => { + it('403', async () => { + await agent + .get(`/${dit}s?filter[title][like]=keyword1`) + .expect(403); + }); + }); + }); + }); +} \ No newline at end of file diff --git a/test/dits.js b/test/dits.js new file mode 100644 index 0000000..589cb25 --- /dev/null +++ b/test/dits.js @@ -0,0 +1,507 @@ +'use strict'; + +const path = require('path'), + should = require('should'); + +const agentFactory = require('./agent'), + dbHandle = require('./handle-database'), + models = require(path.resolve('./models')); + +/* + Tests for functionalities common for sll of the dits. + Those are: ideas, challenges + */ + +testDits('idea'); +testDits('challenge'); + +/* + Function takes type of a dit as an argument + and runs all of the test +*/ +function testDits(dit){ + describe('dits', () => { + let agent, + dbData, + loggedUser; + + afterEach(async () => { + await dbHandle.clear(); + }); + + beforeEach(() => { + agent = agentFactory(); + }); + + describe(`POST /${dit}s`, () => { + let newDitBody; + + beforeEach(() => { + newDitBody = { data: { + type: `${dit}s`, + attributes: { + title: `A testing ${dit} 1`, + detail: `This is a testing ${dit} detail.`, + ditType: `${dit}` + } + } }; + }); + + // put pre-data into database + beforeEach(async () => { + const data = { + users: 3, // how many users to make + verifiedUsers: [0, 1] // which users to make verified + }; + // create data in database + dbData = await dbHandle.fill(data); + + loggedUser = dbData.users[0]; + }); + + context('logged in', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid data', () => { + + it(`should create ${dit} and respond with 201`, async () => { + const response = await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(201) + .expect('Content-Type', /^application\/vnd\.api\+json/); + + // respond with the new dit + const newDitResponseBody = response.body; + // is response body correct? + should(newDitResponseBody).match({ + data: { + type: `${dit}s`, + attributes: { + title: `A testing ${dit} 1`, + detail: `This is a testing ${dit} detail.` + } + } + }); + + should(newDitResponseBody).have.propertyByPath('data', 'id'); + should(newDitResponseBody).have.propertyByPath('data', 'attributes', 'created'); + + // is the new dit saved in database? + const newDitDb = await models.dit.read(`${dit}`, response.body.data.id); + // does the dit id equal the dit key in database? + should(newDitDb.id).eql(response.body.data.id); + + // data should contain creator as relationship + should(newDitResponseBody) + .have.propertyByPath('data', 'relationships', 'creator') + .match({ + data: { + type: 'users', id: loggedUser.username + } + }); + }); + + }); + + context('invalid data', () => { + + it(`[empty ${dit} title] 400`, async () => { + // invalid body + newDitBody.data.attributes.title = ' '; + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it(`[too long ${dit} title] 400`, async () => { + // invalid body + newDitBody.data.attributes.title = 'a'.repeat(257); + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it(`[missing ${dit} title] 400`, async () => { + // invalid body + delete newDitBody.data.attributes.title; + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it(`[too long ${dit} detail] 400`, async () => { + // invalid body + newDitBody.data.attributes.detail = 'a'.repeat(2049); + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it(`[missing ${dit} detail] 400`, async () => { + // invalid body + delete newDitBody.data.attributes.detail; + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it('[unexpected property] 400', async () => { + // invalid body + newDitBody.data.attributes.unexpected = 'asdf'; + + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it('[XSS in body] sanitize', async () => { + // body with XSS + newDitBody.data.attributes.detail = ` + foo + italic + bar + `; + const response = await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(201) + .expect('Content-Type', /^application\/vnd\.api\+json/); + + // respond with the new dit + const newDitResponseBody = response.body; + + should(newDitResponseBody).have.propertyByPath('data', 'attributes', 'detail').eql(`italic + bar`); + + }); + }); + }); + + context('not logged in', () => { + it('should say 403 Forbidden', async () => { + await agent + .post(`/${dit}s`) + .send(newDitBody) + .expect(403) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + }); + }); + + describe(`GET /${dit}s/:id`, () => { + + let dit0; + + beforeEach(async () => { + const data = { + users: 3, // how many users to make + verifiedUsers: [0, 1], // which users to make verified + [`${dit}s`]: [[{}, 0]] + }; + // create data in database + dbData = await dbHandle.fill(data); + loggedUser = dbData.users[0]; + dit0 = dbData[`${dit}s`][0]; + }); + + context('logged', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid', () => { + it(`[exists] read ${dit} by id`, async () => { + const response = await agent + .get(`/${dit}s/${dit0.id}`) + .expect(200) + .expect('Content-Type', /^application\/vnd\.api\+json/); + + should(response.body).match({ + data: { + type: `${dit}s`, + id: dit0.id, + attributes: { + title: dit0.title, + detail: dit0.detail + }, + relationships: { + creator: { + data: { type: 'users', id: dit0.creator.username } + } + } + } + }); + }); + + it('[not exist] 404', async () => { + await agent + .get(`/${dit}s/0013310`) + .expect(404) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + }); + + context('invalid', () => { + it('[invalid id] 400', async () => { + await agent + .get(`/${dit}s/invalid-id`) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + }); + }); + + context('not logged', () => { + it('403', async () => { + await agent + .get(`/${dit}s/${dit0.id}`) + .expect(403) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + }); + }); + + describe(`PATCH /${dit}s/:id`, () => { + + let dit0, + dit1, + patchBody; + + beforeEach(async () => { + const data = { + users: 3, // how many users to make + verifiedUsers: [0, 1], // which users to make verified + [`${dit}s`]: [[{ }, 0], [{ }, 1]] + }; + // create data in database + dbData = await dbHandle.fill(data); + + loggedUser = dbData[`${dit}s`][0]; + [dit0, dit1] = dbData[`${dit}s`]; + }); + + beforeEach(() => { + patchBody = { + data: { + type: `${dit}s`, + id: dit0.id, + attributes: { + title: 'this is a new title', + detail: 'this is a new detail' + } + } + }; + }); + + context('logged', () => { + + beforeEach(() => { + agent = agentFactory.logged(loggedUser); + }); + + context('valid', () => { + it(`[${dit} exists, creator, title] 200 and update in db`, async () => { + delete patchBody.data.attributes.detail; + + const { title } = patchBody.data.attributes; + const { id, detail } = dit0; + const response = await agent + .patch(`/${dit}s/${id}`) + .send(patchBody) + .expect(200); + + should(response.body).match({ + data: { + type: `${dit}s`, + id, + attributes: { title, detail } + } + }); + + const ditDb = await models.dit.read(`${dit}`, dit0.id); + + should(ditDb).match({ id, title, detail }); + }); + + it(`[${dit} exists, creator, detail] 200 and update in db`, async () => { + delete patchBody.data.attributes.title; + + const { detail } = patchBody.data.attributes; + const { id, title } = dit0; + + const response = await agent + .patch(`/${dit}s/${id}`) + .send(patchBody) + .expect(200); + + should(response.body).match({ + data: { + type: `${dit}s`, + id, + attributes: { title, detail } + } + }); + + const ditDb = await models.dit.read(`${dit}`, dit0.id); + + should(ditDb).match({ id, title, detail }); + }); + + it(`[${dit} exists, creator, title, detail] 200 and update in db`, async () => { + const { title, detail } = patchBody.data.attributes; + const { id } = dit0; + + const response = await agent + .patch(`/${dit}s/${id}`) + .send(patchBody) + .expect(200); + + should(response.body).match({ + data: { + type: `${dit}s`, + id, + attributes: { title, detail } + } + }); + + const ditDb = await models.dit.read(`${dit}`, dit0.id); + should(ditDb).match({ id, title, detail }); + }); + + it(`[${dit} exists, not creator] 403`, async () => { + patchBody.data.id = dit1.id; + + const response = await agent + .patch(`/${dit}s/${dit1.id}`) + .send(patchBody) + .expect(403); + + should(response.body).match({ + errors: [{ + status: 403, + detail: 'only creator can update' + }] + }); + }); + + it(`[${dit} not exist] 404`, async () => { + patchBody.data.id = '00011122'; + + const response = await agent + .patch(`/${dit}s/00011122`) + .send(patchBody) + .expect(404); + + should(response.body).match({ + errors: [{ + status: 404, + detail: `${dit} not found` + }] + }); + }); + }); + + context('invalid', () => { + it(`[invalid ${dit} id] 400`, async () => { + patchBody.data.id = 'invalid-id'; + + await agent + .patch(`/${dit}s/invalid-id`) + .send(patchBody) + .expect(400); + }); + + it('[id in body doesn\'t equal id in params] 400', async () => { + patchBody.data.id = '00011122'; + + await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(400); + }); + + it('[invalid title] 400', async () => { + patchBody.data.attributes.title = ' '; + + await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(400); + }); + + it('[invalid detail] 400', async () => { + patchBody.data.attributes.detail = '.'.repeat(2049); + + await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(400); + }); + + it('[not title nor detail (nothing to update)] 400', async () => { + delete patchBody.data.attributes.title; + delete patchBody.data.attributes.detail; + + await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(400); + }); + + it('[unexpected attribute] 400', async () => { + patchBody.data.attributes.foo = 'bar'; + + await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(400); + }); + }); + }); + + context('not logged', () => { + it('403', async () => { + const response = await agent + .patch(`/${dit}s/${dit0.id}`) + .send(patchBody) + .expect(403); + + // this should fail in authorization controller and not in dit controller + should(response.body).not.match({ + errors: [{ + status: 403, + detail: 'only creator can update' + }] + }); + }); + }); + }); + + describe(`DELETE /${dit}s/:id`, () => { + it('todo'); + }); + }); +} \ No newline at end of file diff --git a/test/handle-database.js b/test/handle-database.js index b0fb873..dde1a96 100644 --- a/test/handle-database.js +++ b/test/handle-database.js @@ -24,6 +24,9 @@ exports.fill = async function (data) { ideas: [], ideaTags: [], ideaComments: [], + challenges: [], + challengeTags: [], + challengeComments: [], reactions: [], votes: [] }; @@ -115,6 +118,40 @@ exports.fill = async function (data) { ideaComment.id = newComment.id; } + for(const challenge of processed.challenges) { + const creator = challenge.creator.username; + const title = challenge.title; + const detail = challenge.detail; + const created = challenge.created; + const outChallenge = await models.dit.create( 'challenge', { title, detail, created, creator }); + if (!outChallenge) { + const e = new Error('challenge could not be saved'); + e.data = challenge; + throw e; + } + challenge.id = outChallenge.id; + } + + for(const challengeTag of processed.challengeTags) { + const creator = challengeTag.creator.username; + const challengeId = challengeTag.challenge.id; + const tagname = challengeTag.tag.tagname; + + await models.ditTag.create('challenge', challengeId, tagname, { }, creator); + } + + for(const challengeComment of processed.challengeComments) { + const creator = challengeComment.creator.username; + const challengeId = challengeComment.challenge.id; + const { content, created } = challengeComment; + const primary = { type: 'challenges', id: challengeId }; + + const newComment = await models.comment.create({ primary, creator, content, created }); + + // save the comment's id + challengeComment.id = newComment.id; + } + for(const reaction of processed.reactions) { const creator = reaction.creator.username; const commentId = reaction.comment.id; @@ -310,8 +347,51 @@ function processData(data) { return resp; }); + output.challenges = data.challenges.map(function ([attrs = { }, _creator = 0], i) { + const { title = `challenge title ${i}`, detail = `challenge detail ${i}`, created = Date.now() + 1000 * i } = attrs; + const resp = { + _creator, + get creator() { + return output.users[_creator]; + }, + title, + detail, + created + }; + + resp.creator._ideas.push(i); + + return resp; + }); + + output.challengeTags = data.challengeTags.map(function ([_challenge, _tag]) { + const resp = { + _challenge, + _tag, + get challenge() { return output.challenges[this._challenge]; }, + get tag() { return output.tags[this._tag]; }, + get creator() { return this.challenge.creator; } + }; + + return resp; + }); + + output.challengeComments = data.challengeComments.map(([_challenge, _creator, attrs = { }], i) => { + const { content = `challenge comment ${i}`, created = Date.now() + 1000 * i } = attrs; + const resp = { + _creator, + _challenge, + get creator() { return output.users[this._creator]; }, + get challenge() { return output.challenges[this._challenge]; }, + content, + created + }; + + return resp; + }); + // put comments together - Object.defineProperty(output, 'comments', { get: function () { return this.ideaComments; } }); + Object.defineProperty(output, 'comments', { get: function () { return this.ideaComments.length === 0 ? this.challengeComments : this.ideaComments; } }); output.reactions = data.reactions.map(([_comment, _creator, attrs = { }], i) => { const { content = `reaction content ${i}`, created = Date.now() + 1000 * i } = attrs; diff --git a/test/ideas.js b/test/ideas.js index b5620b3..c7f68e4 100644 --- a/test/ideas.js +++ b/test/ideas.js @@ -28,7 +28,8 @@ describe('ideas', () => { type: 'ideas', attributes: { title: 'A testing idea 1', - detail: 'This is a testing idea detail.' + detail: 'This is a testing idea detail.', + ditType: 'idea' } } }; }); diff --git a/test/votes.js b/test/votes.js index 83c7d7d..87cd916 100644 --- a/test/votes.js +++ b/test/votes.js @@ -9,6 +9,7 @@ const agentFactory = require('./agent'), voteTestFactory('idea'); voteTestFactory('comment'); +voteTestFactory('challenge'); /** * We can test votes to different objects.