0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added comments API like and unlike actions

refs https://github.com/TryGhost/Team/issues/1664

- Added comment-like model
- Added like endpoint
- Added unlike endpoint
- Added basic tests for liking and unliking comments
- Added permissions for liking and unliking
- Added migration for permissions
This commit is contained in:
Simon Backx 2022-07-06 15:32:42 +02:00 committed by Simon Backx
parent 1b4f8f0c95
commit e96ff3fa81
5 changed files with 202 additions and 3 deletions

View file

@ -2,13 +2,14 @@ const Promise = require('bluebird');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const models = require('../../models');
const {identity} = require('lodash');
const ALLOWED_INCLUDES = ['post', 'member'];
const UNSAFE_ATTRS = ['status'];
const messages = {
commentNotFound: 'Comment could not be found',
memberNotFound: 'Unable to find member'
memberNotFound: 'Unable to find member',
likeNotFound: 'Unable to find like',
alreadyLiked: 'This comment was liked already'
};
module.exports = {
@ -151,5 +152,70 @@ module.exports = {
}));
});
}
},
like: {
statusCode: 204,
options: [
'id'
],
validation: {
},
permissions: true,
async query(frame) {
// TODO: move to likes service
if (frame.options?.context?.member?.id) {
const data = {
member_id: frame.options.context.member.id,
comment_id: frame.options.id
};
const existing = await models.CommentLike.findOne(data, frame.options);
if (existing) {
throw new errors.BadRequestError({
message: tpl(messages.alreadyLiked)
});
}
return await models.CommentLike.add(data, frame.options);
} else {
throw new errors.NotFoundError({
message: tpl(messages.memberNotFound)
});
}
}
},
unlike: {
statusCode: 204,
options: [
'id'
],
validation: {},
permissions: true,
query(frame) {
frame.options.require = true;
// TODO: move to likes service
if (frame.options?.context?.member?.id) {
return models.CommentLike.destroy({
...frame.options,
destroyBy: {
member_id: frame.options.context.member.id,
comment_id: frame.options.id
}
}).then(() => null)
.catch(models.CommentLike.NotFoundError, () => {
return Promise.reject(new errors.NotFoundError({
message: tpl(messages.likeNotFound)
}));
});
} else {
return Promise.reject(new errors.NotFoundError({
message: tpl(messages.memberNotFound)
}));
}
}
}
};

View file

@ -0,0 +1,34 @@
const ghostBookshelf = require('./base');
const CommentLike = ghostBookshelf.Model.extend({
tableName: 'comment_likes',
defaults: function defaults() {
return {};
},
comment() {
return this.belongsTo('Comment', 'comment_id');
},
member() {
return this.belongsTo('Member', 'member_id');
},
emitChange: function emitChange(event, options) {
const eventToTrigger = 'comment_like' + '.' + event;
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
},
onCreated: function onCreated(model, options) {
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
model.emitChange('added', options);
}
}, {
});
module.exports = {
CommentLike: ghostBookshelf.model('CommentLike', CommentLike)
};

View file

@ -19,5 +19,8 @@ module.exports = function apiRoutes() {
router.put('/:id', http(api.commentsComments.edit));
router.delete('/:id', http(api.commentsComments.destroy));
router.post('/:id/like', http(api.commentsComments.like));
router.delete('/:id/like', http(api.commentsComments.unlike));
return router;
};

View file

@ -89,3 +89,60 @@ Object {
"x-powered-by": "Express",
}
`;
exports[`Comments API when authenticated Can like a comment 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"x-powered-by": "Express",
}
`;
exports[`Comments API when authenticated Can remove a like 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"x-powered-by": "Express",
}
`;
exports[`Comments API when authenticated Cannot like a comment multiple times 1: [body] 1`] = `
Object {
"errors": Array [
Object {
"code": null,
"context": null,
"details": null,
"ghostErrorCode": null,
"help": null,
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"message": "This comment was liked already",
"property": null,
"type": "BadRequestError",
},
],
}
`;
exports[`Comments API when authenticated Cannot like a comment multiple times 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"x-powered-by": "Express",
}
`;
exports[`Comments API when authenticated Cannot like a comment multiple times 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "218",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Encoding",
"x-powered-by": "Express",
}
`;

View file

@ -1,7 +1,7 @@
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyObjectId, anyLocationFor, anyISODateTime, anyUuid} = matchers;
let membersAgent, membersService, postId;
let membersAgent, membersService, postId, commentId;
describe('Comments API', function () {
before(async function () {
@ -47,6 +47,8 @@ describe('Comments API', function () {
updated_at: anyISODateTime
}]
});
// Save for other tests
commentId = body.comments[0].id;
});
it('Can browse all comments of a post', async function () {
@ -72,5 +74,42 @@ describe('Comments API', function () {
}]
});
});
it('Can like a comment', async function () {
// Create a temporary comment
await membersAgent
.post(`/api/comments/${commentId}/like/`)
.expectStatus(204)
.matchHeaderSnapshot({
etag: anyEtag
})
.expectEmptyBody();
});
it('Cannot like a comment multiple times', async function () {
// Create a temporary comment
await membersAgent
.post(`/api/comments/${commentId}/like/`)
.expectStatus(400)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
errors: [{
id: anyUuid
}]
});
});
it('Can remove a like', async function () {
// Create a temporary comment
const {body} = await membersAgent
.delete(`/api/comments/${commentId}/like/`)
.expectStatus(204)
.matchHeaderSnapshot({
etag: anyEtag
})
.expectEmptyBody();
});
});
});