Browse Source

Vote for ideas (#51)

* vote for ideas: POST /ideas/:id/votes

* Remove vote from idea DELETE /ideas/:id/votes/vote

* show amount of up and down votes and my vote when GET /ideas/:id

* generalize votes for other objects

use it to vote for comments

* add error messages, remove TODO comment
mrkvon 2 years ago
parent
commit
7c1d8541ef
No account linked to committer's email

+ 8
- 0
apidoc.raml View File

@@ -421,6 +421,14 @@ types:
421 421
           403:
422 422
           404:
423 423
       get:
424
+    /votes:
425
+      post:
426
+        description: Vote for an idea.
427
+        responses:
428
+          201:
429
+          400:
430
+          403:
431
+          404:
424 432
 /comments:
425 433
   /{id}:
426 434
     patch:

+ 3
- 0
app.js View File

@@ -71,6 +71,9 @@ app.use('/messages', require('./routes/messages'));
71 71
 app.use('/account', require('./routes/account'));
72 72
 app.use('/contacts', require('./routes/contacts'));
73 73
 app.use('/ideas', require('./routes/ideas'));
74
+// vote for ideas, ...
75
+app.use('/ideas', require('./routes/votes'));
76
+app.use('/comments', require('./routes/votes'));
74 77
 
75 78
 // following are route factories
76 79
 // they need to know what is the primary object (i.e. idea, comment, etc.)

+ 13
- 0
collections.js View File

@@ -117,6 +117,19 @@ module.exports = {
117 117
         fields: ['creator']
118 118
       }
119 119
     ]
120
+  },
121
+
122
+  votes: {
123
+    type: 'edge',
124
+    from: ['users'],
125
+    to: ['ideas', 'comments'],
126
+    indexes: [
127
+      {
128
+        type: 'hash',
129
+        fields: ['_from', '_to'],
130
+        unique: true
131
+      }
132
+    ]
120 133
   }
121 134
 
122 135
 };

+ 17
- 0
controllers/comments.js View File

@@ -57,6 +57,9 @@ module.exports = function controllersFactory(primary) {
57 57
    * Middleware to read comments of a primary object (i.e. idea)
58 58
    */
59 59
   async function get(req, res, next) {
60
+
61
+    const { username } = req.auth;
62
+
60 63
     try {
61 64
       // gather data
62 65
       const { id } = req.params;
@@ -67,6 +70,20 @@ module.exports = function controllersFactory(primary) {
67 70
       // read the comments from database
68 71
       const comments = await models.comment.readCommentsOf(primary, { offset, limit, sort });
69 72
 
73
+      // read votes of comments
74
+      if (comment === 'comment') {
75
+        const ids = comments.map(comment => comment.id);
76
+        // read votes of the comments from database
77
+        const votes = await models.vote.readVotesToMany({ type: 'comments', ids });
78
+        const myVotes = await models.vote.readMany({ from: username, to: { type: 'comments', ids } });
79
+
80
+        // add the votes to their comments
81
+        comments.forEach((comment, i) => {
82
+          comment.votes = votes[i];
83
+          comment.myVote = myVotes[i];
84
+        });
85
+      }
86
+
70 87
       // serialize the comments
71 88
       const serializedComments = serialize.comment(comments);
72 89
 

+ 5
- 0
controllers/ideas.js View File

@@ -35,12 +35,17 @@ async function get(req, res, next) {
35 35
   try {
36 36
     // gather data
37 37
     const { id } = req.params;
38
+    const { username } = req.auth;
38 39
 
39 40
     // read the idea from database
40 41
     const idea = await models.idea.read(id);
41 42
 
42 43
     if (!idea) return res.status(404).json({ });
43 44
 
45
+    // see how many votes were given to idea and if/how logged user voted (0, -1, 1)
46
+    idea.votes = await models.vote.readVotesTo({ type: 'ideas', id });
47
+    idea.myVote = await models.vote.read({ from: username, to: { type: 'ideas', id } });
48
+
44 49
     // serialize the idea (JSON API)
45 50
     const serializedIdea = serialize.idea(idea);
46 51
 

+ 1
- 0
controllers/validators/index.js View File

@@ -9,4 +9,5 @@ exports.account = require('./account');
9 9
 exports.users = require('./users');
10 10
 exports.tags = require('./tags');
11 11
 exports.userTags = require('./user-tags');
12
+exports.votes = require('./votes');
12 13
 exports.params = require('./params');

+ 5
- 0
controllers/validators/schema/definitions.js View File

@@ -126,6 +126,11 @@ module.exports = {
126 126
       pattern: '\\S' // at least one non-space character
127 127
     }
128 128
   },
129
+  vote: {
130
+    value: {
131
+      enum: [-1, 1]
132
+    }
133
+  },
129 134
   query: {
130 135
     page: {
131 136
       properties: {

+ 3
- 2
controllers/validators/schema/index.js View File

@@ -12,8 +12,9 @@ const account = require('./account'),
12 12
       params = require('./params'),
13 13
       tags = require('./tags'),
14 14
       userTags = require('./user-tags'),
15
-      users = require('./users');
15
+      users = require('./users'),
16
+      votes = require('./votes');
16 17
 
17 18
 
18 19
 module.exports = Object.assign({ definitions }, account, authenticate, avatar,
19
-  comments, contacts, ideas, ideaTags, messages, params, tags, users, userTags);
20
+  comments, contacts, ideas, ideaTags, messages, params, tags, users, userTags, votes);

+ 2
- 1
controllers/validators/schema/paths.js View File

@@ -23,5 +23,6 @@ module.exports = {
23 23
   title: { $ref: 'sch#/definitions/idea/titl' },
24 24
   detail: { $ref: 'sch#/definitions/idea/detail' },
25 25
   content: { $ref: 'sch#/definitions/comment/content' },
26
-  id: { $ref: 'sch#/definitions/shared/objectId' }
26
+  id: { $ref: 'sch#/definitions/shared/objectId' },
27
+  voteValue: { $ref: 'sch#/definitions/vote/value' },
27 28
 };

+ 32
- 0
controllers/validators/schema/votes.js View File

@@ -0,0 +1,32 @@
1
+'use strict';
2
+
3
+const { id, voteValue: value } = require('./paths');
4
+
5
+const postVotes = {
6
+  properties: {
7
+    params: {
8
+      properties: { id },
9
+      required: ['id'],
10
+      additionalProperties: false
11
+    },
12
+    body: {
13
+      properties: { value },
14
+      required: ['value'],
15
+      additionalProperties: false
16
+    },
17
+    required: ['body', 'params']
18
+  }
19
+};
20
+
21
+const deleteVote = {
22
+  properties: {
23
+    params: {
24
+      properties: { id },
25
+      required: ['id'],
26
+      additionalProperties: false
27
+    },
28
+    required: ['params']
29
+  }
30
+};
31
+
32
+module.exports = { deleteVote, postVotes };

+ 8
- 0
controllers/validators/votes.js View File

@@ -0,0 +1,8 @@
1
+'use strict';
2
+
3
+const validate = require('./validate-by-schema');
4
+
5
+const del = validate('deleteVote');
6
+const post = validate('postVotes');
7
+
8
+module.exports = { del, post };

+ 91
- 0
controllers/votes.js View File

@@ -0,0 +1,91 @@
1
+const path = require('path'),
2
+      models = require(path.resolve('./models')),
3
+      serializers = require(path.resolve('./serializers'));
4
+
5
+/**
6
+ * Middleware to POST a vote to idea (and other objects in the future)
7
+ */
8
+async function post(req, res, next) {
9
+
10
+  // read data from request
11
+  const { id } = req.params;
12
+  const { value } = req.body;
13
+  const { username } = req.auth;
14
+
15
+  // what is the type of the object we vote for (i.e. ideas, comments, ...)
16
+  const primarys = req.baseUrl.substring(1);
17
+  const primary = primarys.slice(0, -1);
18
+
19
+  try {
20
+    // save the vote to database
21
+    const vote = await models.vote.create({ from: username, to: { type: primarys, id }, value });
22
+    // respond
23
+    const serializedVote = serializers.serialize.vote(vote);
24
+    return res.status(201).json(serializedVote);
25
+  } catch (e) {
26
+    // handle errors
27
+    switch (e.code) {
28
+      // duplicate vote
29
+      case 409: {
30
+        return res.status(409).json({
31
+          errors: [{
32
+            status: 409,
33
+            detail: 'duplicate vote'
34
+          }]
35
+        });
36
+      }
37
+      // missing idea
38
+      case 404: {
39
+        return res.status(404).json({
40
+          errors: [{
41
+            status: 404,
42
+            detail: `${primary} doesn't exist`
43
+          }]
44
+        });
45
+
46
+      }
47
+      default: {
48
+        return next(e);
49
+      }
50
+    }
51
+  }
52
+}
53
+
54
+/**
55
+ * Middleware to DELETE a vote from an idea (and other objects in the future).
56
+ */
57
+async function del(req, res, next) {
58
+
59
+  // read data from request
60
+  const { id } = req.params;
61
+  const { username } = req.auth;
62
+
63
+  // what is the type of the object we vote for (i.e. ideas, comments, ...)
64
+  const primarys = req.baseUrl.substring(1);
65
+  const primary = primarys.slice(0, -1);
66
+
67
+  try {
68
+    // remove the vote from database
69
+    await models.vote.remove({ from: username, to: { type: primarys, id } });
70
+    // respond
71
+    return res.status(204).end();
72
+  } catch (e) {
73
+    // handle errors
74
+    switch (e.code) {
75
+      // primary object or vote doesn't exist
76
+      case 404: {
77
+        return res.status(404).json({
78
+          errors: [{
79
+            status: 404,
80
+            detail: `vote or ${primary} doesn't exist`
81
+          }]
82
+        });
83
+      }
84
+      default: {
85
+        return next(e);
86
+      }
87
+    }
88
+  }
89
+}
90
+
91
+module.exports = { del, post };

+ 3
- 2
models/index.js View File

@@ -8,7 +8,8 @@ const comment = require('./comment'),
8 8
       model = require('./model'),
9 9
       tag = require('./tag'),
10 10
       user = require('./user'),
11
-      userTag = require('./user-tag');
11
+      userTag = require('./user-tag'),
12
+      vote = require('./vote');
12 13
 
13 14
 
14 15
 const models = {
@@ -21,4 +22,4 @@ const models = {
21 22
   }
22 23
 };
23 24
 
24
-module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag });
25
+module.exports = Object.assign(models, { comment, contact, idea, ideaTag, message, model, tag, user, userTag, vote });

+ 153
- 0
models/vote/index.js View File

@@ -0,0 +1,153 @@
1
+'use strict';
2
+
3
+const path = require('path');
4
+
5
+const Model = require(path.resolve('./models/model')),
6
+      schema = require('./schema');
7
+
8
+class Vote extends Model {
9
+
10
+  /**
11
+   * Create a vote.
12
+   * @param {string} from - username of the vote giver
13
+   * @param {object} to - receiver object of the vote
14
+   * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas')
15
+   * @param {string} to.id - id of the receiver
16
+   * @param {number} value - 1 or -1 - value of the vote
17
+   * @returns Promise - the saved vote
18
+   */
19
+  static async create({ from: username, to: { type, id }, value }) {
20
+    // generate the vote
21
+    const vote = schema({ value });
22
+
23
+    const query = `
24
+      FOR u IN users FILTER u.username == @username
25
+        FOR i IN @@type FILTER i._key == @id
26
+          LET vote = MERGE(@vote, { _from: u._id, _to: i._id })
27
+          INSERT vote INTO votes
28
+
29
+          LET from = MERGE(KEEP(u, 'username'), u.profile)
30
+          LET to = MERGE(KEEP(i, 'title', 'detail', 'created'), { id: i._key })
31
+          LET savedVote = MERGE(KEEP(NEW, 'created', 'value'), { id: NEW._key }, { from }, { to })
32
+          RETURN savedVote`;
33
+    const params = { username, '@type': type, id, vote };
34
+    const cursor = await this.db.query(query, params);
35
+    const out = await cursor.all();
36
+
37
+    // when nothing was created, throw error
38
+    if (out.length === 0) {
39
+      const e = new Error('not found');
40
+      e.code = 404;
41
+      throw e;
42
+    }
43
+
44
+    // what is the type of the object the vote was given to (important for serialization)
45
+    out[0].to.type = type;
46
+
47
+    return out[0];
48
+  }
49
+
50
+  /**
51
+   * Read a vote from user to idea or something.
52
+   * @param {string} from - username of the vote giver
53
+   * @param {object} to - receiver object of the vote
54
+   * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas')
55
+   * @param {string} to.id - id of the receiver
56
+   * @returns Promise - the found vote or undefined
57
+   */
58
+  static async read({ from: username, to: { type, id } }) {
59
+    const query = `
60
+      FOR u IN users FILTER u.username == @username
61
+        FOR i IN @@type FILTER i._key == @id
62
+          FOR v IN votes FILTER v._from == u._id && v._to == i._id
63
+            RETURN v`;
64
+    const params = { username, '@type': type, id };
65
+    const cursor = await this.db.query(query, params);
66
+
67
+    return (await cursor.all())[0];
68
+  }
69
+
70
+  /**
71
+   * Read a vote from user to multiple ideas or comments or something
72
+   * @param {string} from - username of the vote giver
73
+   * @param {object} to - receiver object of the vote
74
+   * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas')
75
+   * @param {string[]} to.ids - array of ids of the receivers
76
+   * @returns Promise - the array of found votes or nulls
77
+   */
78
+  static async readMany({ from: username, to: { type, ids } }) {
79
+    const query = `
80
+      FOR id IN @ids
81
+        LET vote = (FOR u IN users FILTER u.username == @username
82
+          FOR i IN @@type FILTER i._key == id
83
+            FOR v IN votes FILTER v._from == u._id && v._to == i._id
84
+              RETURN v)
85
+        RETURN vote[0]`;
86
+    const params = { username, '@type': type, ids };
87
+    const cursor = await this.db.query(query, params);
88
+
89
+    return await cursor.all();
90
+  }
91
+
92
+  /**
93
+   * Remove a vote.
94
+   * @param {string} from - username of the vote giver
95
+   * @param {object} to - receiver object of the vote
96
+   * @param {string} to.type - type of the receiver (collection name, i.e. 'ideas')
97
+   * @param {string} to.id - id of the receiver
98
+   * @returns Promise
99
+   */
100
+  static async remove({ from: username, to: { type, id } }) {
101
+    const query = `
102
+      FOR u IN users FILTER u.username == @username
103
+        FOR i IN @@type FILTER i._key == @id
104
+          FOR v IN votes FILTER v._from == u._id AND v._to == i._id
105
+          REMOVE v IN votes`;
106
+    const params = { username, '@type': type, id };
107
+    const cursor = await this.db.query(query, params);
108
+
109
+    if (cursor.extra.stats.writesExecuted === 0) {
110
+      const e = new Error('primary or vote not found');
111
+      e.code = 404;
112
+      throw e;
113
+    }
114
+  }
115
+
116
+  /**
117
+   * Read votes of a primary object.
118
+   * @param {string} type - type of the receiver (collection name, i.e. 'ideas')
119
+   * @param {string} id - id of the receiver
120
+   * @returns Promise - array of votes
121
+   */
122
+  static async readVotesTo({ type, id }) {
123
+    const query = `
124
+      FOR i IN @@type FILTER i._key == @id
125
+        FOR v, e IN 1..1 INBOUND i INBOUND votes
126
+          RETURN e`;
127
+    const params = { '@type': type, id };
128
+    const cursor = await this.db.query(query, params);
129
+
130
+    return await cursor.all();
131
+  }
132
+
133
+  /**
134
+   * Read votes of array of primary objects.
135
+   * @param {string} type - type of the receiver (collection name, i.e. 'ideas')
136
+   * @param {string[]} ids - array of ids of the receivers
137
+   * @returns Promise - array of arrays of votes
138
+   */
139
+  static async readVotesToMany({ type, ids }) {
140
+    const query = `
141
+      FOR id IN @ids
142
+        LET votes = (FOR i IN @@type FILTER i._key == id
143
+          FOR v, e IN 1..1 INBOUND i INBOUND votes
144
+            RETURN e)
145
+        RETURN votes`;
146
+    const params = { '@type': type, ids };
147
+    const cursor = await this.db.query(query, params);
148
+
149
+    return await cursor.all();
150
+  }
151
+}
152
+
153
+module.exports = Vote;

+ 5
- 0
models/vote/schema.js View File

@@ -0,0 +1,5 @@
1
+'use strict';
2
+
3
+module.exports = function ({ value, created = Date.now() }) {
4
+  return { value, created };
5
+};

+ 19
- 0
routes/votes.js View File

@@ -0,0 +1,19 @@
1
+'use strict';
2
+
3
+const express = require('express'),
4
+      path = require('path');
5
+
6
+const authorize = require(path.resolve('./controllers/authorize')),
7
+      voteControllers = require(path.resolve('./controllers/votes')),
8
+      voteValidators = require(path.resolve('./controllers/validators/votes'));
9
+
10
+// create router and controllers
11
+const router = express.Router();
12
+
13
+router.route('/:id/votes')
14
+  .post(authorize.onlyLogged, voteValidators.post, voteControllers.post);
15
+
16
+router.route('/:id/votes/vote')
17
+  .delete(authorize.onlyLogged, voteValidators.del, voteControllers.del);
18
+
19
+module.exports = router;

+ 14
- 0
serializers/comments.js View File

@@ -55,6 +55,20 @@ function serializerFactory(type='comments') {
55 55
       relationshipLinks: {
56 56
         related: (data, { id }) => `${config.url.all}/reactions/${id}`
57 57
       }
58
+    },
59
+    dataMeta: {
60
+      votesUp(record, current) {
61
+        if (!current.votes) return;
62
+        return current.votes.filter(vote => vote.value === 1).length;
63
+      },
64
+      votesDown(record, current) {
65
+        if (!current.votes) return;
66
+        return current.votes.filter(vote => vote.value === -1).length;
67
+      },
68
+      myVote(record, current) {
69
+        if (!current.hasOwnProperty('myVote')) return;
70
+        return (current.myVote) ? current.myVote.value : 0;
71
+      }
58 72
     }
59 73
   });
60 74
 }

+ 14
- 0
serializers/ideas.js View File

@@ -44,6 +44,20 @@ const ideaSerializer = new Serializer('ideas', {
44 44
     tag: {
45 45
       ref: 'tagname'
46 46
     }
47
+  },
48
+  dataMeta: {
49
+    votesUp(record, current) {
50
+      if (!current.votes) return;
51
+      return current.votes.filter(vote => vote.value === 1).length;
52
+    },
53
+    votesDown(record, current) {
54
+      if (!current.votes) return;
55
+      return current.votes.filter(vote => vote.value === -1).length;
56
+    },
57
+    myVote(record, current) {
58
+      if (!current.hasOwnProperty('myVote')) return;
59
+      return (current.myVote) ? current.myVote.value : 0;
60
+    }
47 61
   }
48 62
 
49 63
 });

+ 3
- 2
serializers/index.js View File

@@ -8,7 +8,8 @@ const comments = require('./comments'),
8 8
       ideaTags = require('./idea-tags'),
9 9
       messages = require('./messages'),
10 10
       tags = require('./tags'),
11
-      users = require('./users');
11
+      users = require('./users'),
12
+      votes = require('./votes');
12 13
 
13 14
 // deserializing
14 15
 const deserializer = new Deserializer({
@@ -43,6 +44,6 @@ function deserialize(req, res, next) {
43 44
 }
44 45
 
45 46
 module.exports = {
46
-  serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users),
47
+  serialize: Object.assign({ }, comments, contacts, ideas, ideaTags, messages, tags, users, votes),
47 48
   deserialize
48 49
 };

+ 27
- 0
serializers/votes.js View File

@@ -0,0 +1,27 @@
1
+'use strict';
2
+
3
+const Serializer = require('jsonapi-serializer').Serializer;
4
+
5
+const voteSerializer = new Serializer('votes', {
6
+  id: 'id',
7
+  attributes: ['title', 'detail', 'created', 'value', 'from', 'to'],
8
+  keyForAttribute: 'camelCase',
9
+  typeForAttribute(attribute, doc) {
10
+    if (attribute === 'from') return 'users';
11
+    if (attribute === 'to') return doc.type;
12
+  },
13
+  from: {
14
+    ref: 'username',
15
+    type: 'users'
16
+  },
17
+  to: {
18
+    ref: 'id',
19
+    type: 'ideas'
20
+  }
21
+});
22
+
23
+function vote(data) {
24
+  return voteSerializer.serialize(data);
25
+}
26
+
27
+module.exports = { vote };

+ 24
- 1
test/handle-database.js View File

@@ -24,7 +24,8 @@ exports.fill = async function (data) {
24 24
     ideas: [],
25 25
     ideaTags: [],
26 26
     ideaComments: [],
27
-    reactions: []
27
+    reactions: [],
28
+    votes: []
28 29
   };
29 30
 
30 31
   data = _.defaults(data, def);
@@ -126,6 +127,17 @@ exports.fill = async function (data) {
126 127
     reaction.id = newReaction.id;
127 128
   }
128 129
 
130
+  for (const vote of processed.votes) {
131
+    const from = vote.from.username;
132
+    const to = { type: vote._to.type, id: vote.to.id };
133
+    const value = vote.value;
134
+
135
+    const newVote = await models.vote.create({ from, to, value });
136
+
137
+    // save the vote's id
138
+    vote.id = newVote.id;
139
+  }
140
+
129 141
   return processed;
130 142
 };
131 143
 
@@ -315,5 +327,16 @@ function processData(data) {
315 327
     return resp;
316 328
   });
317 329
 
330
+  output.votes = data.votes.map(([from, to, value]) => {
331
+    const [type, id] = to;
332
+    return {
333
+      _from: from,
334
+      _to: { type, id },
335
+      get from() { return output.users[this._from]; },
336
+      get to() { return output[this._to.type][this._to.id]; },
337
+      value
338
+    };
339
+  });
340
+
318 341
   return output;
319 342
 }

+ 362
- 0
test/votes.js View File

@@ -0,0 +1,362 @@
1
+'use strict';
2
+
3
+const path = require('path'),
4
+      should = require('should');
5
+
6
+const agentFactory = require('./agent'),
7
+      dbHandle = require('./handle-database'),
8
+      models = require(path.resolve('./models'));
9
+
10
+voteTestFactory('idea');
11
+voteTestFactory('comment');
12
+
13
+/**
14
+ * We can test votes to different objects.
15
+ * @param {string} primary - what is the object we vote for? i.e. comment, idea
16
+ * @param {boolean} [only=false] - should we run only these tests or not? (describe vs. describe.only in mocha)
17
+ */
18
+function voteTestFactory(primary, only=false) {
19
+  const primarys = primary + 's';
20
+
21
+  const ds = (only) ? describe.only : describe;
22
+
23
+  function fillPrimary(data, count) {
24
+    switch (primary) {
25
+      case 'comment':
26
+        data.ideas = Array(1).fill([]);
27
+        data.ideaComments = Array(count).fill([0, 0]);
28
+        break;
29
+
30
+      default:
31
+        data[primarys] = Array(count).fill([{}, 0]);
32
+    }
33
+  }
34
+
35
+  ds(`votes for ${primarys}, ...`, () => {
36
+    let agent,
37
+        dbData,
38
+        existentPrimary,
39
+        primary0,
40
+        primary1,
41
+        loggedUser,
42
+        otherUser;
43
+
44
+    afterEach(async () => {
45
+      await dbHandle.clear();
46
+    });
47
+
48
+    beforeEach(() => {
49
+      agent = agentFactory();
50
+    });
51
+
52
+    describe(`POST /${primarys}/:id/votes (create a vote)`, () => {
53
+      let voteBody;
54
+
55
+      beforeEach(() => {
56
+        voteBody = { data: {
57
+          type: 'votes',
58
+          attributes: {
59
+            value: -1
60
+          }
61
+        } };
62
+      });
63
+
64
+      // put pre-data into database
65
+      beforeEach(async () => {
66
+        const data = {
67
+          users: 3, // how many users to make
68
+          verifiedUsers: [0, 1], // which  users to make verified
69
+        };
70
+
71
+        fillPrimary(data, 1);
72
+
73
+        // create data in database
74
+        dbData = await dbHandle.fill(data);
75
+
76
+        loggedUser = dbData.users[0];
77
+        existentPrimary = dbData[primarys][0];
78
+      });
79
+
80
+      context('logged', () => {
81
+
82
+        beforeEach(() => {
83
+          agent = agentFactory.logged(loggedUser);
84
+        });
85
+
86
+        context('valid data', () => {
87
+          it('[all is fine] save the vote', async () => {
88
+            const response = await agent
89
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
90
+              .send(voteBody)
91
+              .expect(201);
92
+
93
+            // saved into database?
94
+            const dbVote = await models.vote.read({ from: loggedUser.username, to: { type: primarys, id: existentPrimary.id } });
95
+            should(dbVote).ok();
96
+
97
+            // correct response?
98
+            should(response.body).match({
99
+              data: {
100
+                type: 'votes',
101
+                attributes: {
102
+                  value: -1
103
+                },
104
+                relationships: {
105
+                  from: { data: { type: 'users', id: loggedUser.username } },
106
+                  to: { data: { type: primarys, id: existentPrimary.id } }
107
+                }
108
+              }
109
+            });
110
+          });
111
+
112
+          it('[a vote already exists] 409', async () => {
113
+            await agent
114
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
115
+              .send(voteBody)
116
+              .expect(201);
117
+
118
+            voteBody.data.attributes.value = 1;
119
+
120
+            const response = await agent
121
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
122
+              .send(voteBody)
123
+              .expect(409);
124
+
125
+            should(response.body).deepEqual({
126
+              errors: [{
127
+                status: 409,
128
+                detail: 'duplicate vote'
129
+              }]
130
+            });
131
+          });
132
+
133
+          it(`[${primary} doesn't exist] 404`, async () => {
134
+            const response = await agent
135
+              .post(`/${primarys}/1111111/votes`)
136
+              .send(voteBody)
137
+              .expect(404);
138
+
139
+            should(response.body).deepEqual({
140
+              errors: [{
141
+                status: 404,
142
+                detail: `${primary} doesn't exist`
143
+              }]
144
+            });
145
+          });
146
+        });
147
+
148
+        context('invalid data', () => {
149
+          it('[not +1 or -1] 400', async () => {
150
+            voteBody.data.attributes.value = 0;
151
+
152
+            await agent
153
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
154
+              .send(voteBody)
155
+              .expect(400);
156
+          });
157
+
158
+          it('[missing value in body] 400', async () => {
159
+            delete voteBody.data.attributes.value;
160
+
161
+            await agent
162
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
163
+              .send(voteBody)
164
+              .expect(400);
165
+          });
166
+
167
+          it('[additional body parameters] 400', async () => {
168
+            voteBody.data.attributes.foo = 'bar';
169
+
170
+            await agent
171
+              .post(`/${primarys}/${existentPrimary.id}/votes`)
172
+              .send(voteBody)
173
+              .expect(400);
174
+          });
175
+
176
+          it(`[invalid ${primary} id] 400`, async () => {
177
+            await agent
178
+              .post(`/${primarys}/invalid--id/votes`)
179
+              .send(voteBody)
180
+              .expect(400);
181
+          });
182
+        });
183
+      });
184
+
185
+      context('not logged', () => {
186
+        it('403', async () => {
187
+          await agent
188
+            .post(`/${primarys}/${existentPrimary.id}/votes`)
189
+            .send(voteBody)
190
+            .expect(403);
191
+        });
192
+      });
193
+    });
194
+
195
+    describe(`DELETE /${primarys}/:id/votes/vote (remove a vote)`, () => {
196
+
197
+      beforeEach(async () => {
198
+        const data = {
199
+          users: 3, // how many users to make
200
+          verifiedUsers: [0, 1, 2], // which  users to make verified
201
+          votes: [
202
+            [0, [primarys, 0], -1],
203
+            [1, [primarys, 0], 1]
204
+          ] // user, primary, value
205
+        };
206
+
207
+        fillPrimary(data, 2);
208
+
209
+        // create data in database
210
+        dbData = await dbHandle.fill(data);
211
+
212
+        [loggedUser, otherUser] = dbData.users;
213
+        [primary0, primary1] = dbData[primarys];
214
+      });
215
+
216
+      context('logged', () => {
217
+
218
+        beforeEach(() => {
219
+          agent = agentFactory.logged(loggedUser);
220
+        });
221
+
222
+        context('valid data', () => {
223
+          it('[vote exists] remove the vote and 204', async () => {
224
+            // first the vote should exist
225
+            const dbVote = await models.vote.read({ from: loggedUser.username, to: { type: primarys, id: primary0.id } });
226
+            should(dbVote).ok();
227
+
228
+            // then we remove the vote
229
+            await agent
230
+              .delete(`/${primarys}/${primary0.id}/votes/vote`)
231
+              .expect(204);
232
+
233
+            // then the vote should not exist
234
+            const dbVoteAfter = await models.vote.read({ from: loggedUser.username, to: { type: primarys, id: primary0.id } });
235
+            should(dbVoteAfter).not.ok();
236
+            // and other votes are still there
237
+            const dbOtherVote = await models.vote.read({ from: otherUser.username, to: { type: primarys, id: primary0.id } });
238
+            should(dbOtherVote).ok();
239
+          });
240
+
241
+          it('[vote doesn\'t exist] 404', async () => {
242
+            const response = await agent
243
+              .delete(`/${primarys}/${primary1.id}/votes/vote`)
244
+              .expect(404);
245
+
246
+            should(response.body).deepEqual({
247
+              errors: [{
248
+                status: 404,
249
+                detail: `vote or ${primary} doesn't exist`
250
+              }]
251
+            });
252
+
253
+          });
254
+
255
+          it(`[${primary} doesn't exist] 404`, async () => {
256
+            const response = await agent
257
+              .delete(`/${primarys}/111111/votes/vote`)
258
+              .expect(404);
259
+
260
+            should(response.body).deepEqual({
261
+              errors: [{
262
+                status: 404,
263
+                detail: `vote or ${primary} doesn't exist`
264
+              }]
265
+            });
266
+          });
267
+        });
268
+
269
+        context('invalid data', () => {
270
+          it(`[invalid ${primary} id] 400`, async () => {
271
+            await agent
272
+              .delete(`/${primarys}/invalid--id/votes/vote`)
273
+              .expect(400);
274
+          });
275
+        });
276
+      });
277
+
278
+      context('not logged', () => {
279
+        it('403', async () => {
280
+          await agent
281
+            .delete(`/${primarys}/${primary0.id}/votes/vote`)
282
+            .expect(403);
283
+        });
284
+      });
285
+    });
286
+
287
+    describe(`PATCH /${primarys}/:id/votes/vote (change vote value)`, () => {
288
+      it('maybe todo');
289
+    });
290
+
291
+    describe(`show amount of votes up and down and current user\`s vote when GET /${primarys}${(primary === 'comment') ? '' : '/:id'}`, () => {
292
+
293
+      let idea0,
294
+          url;
295
+
296
+      beforeEach(async () => {
297
+        const data = {
298
+          users: 5, // how many users to make
299
+          verifiedUsers: [0, 1, 2, 3, 4], // which  users to make verified
300
+          votes: [
301
+            [0, [primarys, 0], -1],
302
+            [1, [primarys, 0], 1],
303
+            [2, [primarys, 0], 1],
304
+            [2, [primarys, 1], -1],
305
+            [3, [primarys, 0], -1],
306
+            [4, [primarys, 0], 1],
307
+          ] // user, primary, value
308
+        };
309
+
310
+        fillPrimary(data, 2);
311
+
312
+        // create data in database
313
+        dbData = await dbHandle.fill(data);
314
+
315
+        [loggedUser, otherUser] = dbData.users;
316
+        [primary0, primary1] = dbData[primarys];
317
+        [idea0] = dbData.ideas;
318
+      });
319
+
320
+      beforeEach(() => {
321
+        agent = agentFactory.logged(loggedUser);
322
+      });
323
+
324
+      beforeEach(() => {
325
+        url = (primary === 'comment') ? `/ideas/${idea0.id}/comments` : `/${primarys}/${primary0.id}`;
326
+      });
327
+
328
+      it('show amount of votes up and down', async () => {
329
+        const response = await agent
330
+          .get(url)
331
+          .expect(200);
332
+
333
+        const objectToTest = (primary === 'comment') ? response.body.data[0] : response.body.data;
334
+
335
+        should(objectToTest).match({ meta: { votesUp: 3, votesDown: 2 } });
336
+      });
337
+
338
+      it('show my vote', async () => {
339
+        const response = await agent
340
+          .get(url)
341
+          .expect(200);
342
+
343
+        const objectToTest = (primary === 'comment') ? response.body.data[0] : response.body.data;
344
+
345
+        should(objectToTest).match({ meta: { myVote: -1 } });
346
+      });
347
+
348
+      it('show 0 as my vote if I didn\'t vote', async () => {
349
+        const url2 = (primary === 'comment') ? `/ideas/${idea0.id}/comments` : `/${primarys}/${primary1.id}`;
350
+
351
+        const response = await agent
352
+          .get(url2)
353
+          .expect(200);
354
+
355
+        const objectToTest = (primary === 'comment') ? response.body.data[1] : response.body.data;
356
+
357
+        should(objectToTest).match({ meta: { myVote: 0 } });
358
+      });
359
+    });
360
+
361
+  });
362
+}