mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Added new hidden comments API implementation (#21444)
ref PLG-227 - Behind flags - Changed Comments API for members and guests to not return hidden or removed comments - with the only exception being if a hidden or removed comment have published replies, in which case it will be greyed out as per the previous version on the UI. - Wired up a new admin API endpoint for comment to receive all comments. It's on par with the members / guests endpoint, with the difference being that it it shows hidden comment's content, where previously the html property was nullified.
This commit is contained in:
parent
c336d46352
commit
c349b9bf26
13 changed files with 443 additions and 27 deletions
|
@ -107,23 +107,25 @@ type UnpublishedCommentProps = {
|
||||||
openEditMode: () => void;
|
openEditMode: () => void;
|
||||||
}
|
}
|
||||||
const UnpublishedComment: React.FC<UnpublishedCommentProps> = ({comment, openEditMode}) => {
|
const UnpublishedComment: React.FC<UnpublishedCommentProps> = ({comment, openEditMode}) => {
|
||||||
const {admin, t} = useAppContext();
|
const {t} = useAppContext();
|
||||||
|
let notPublishedMessage:string = '';
|
||||||
let notPublishedMessage;
|
|
||||||
if (admin && comment.status === 'hidden') {
|
|
||||||
notPublishedMessage = t('This comment has been hidden.');
|
|
||||||
} else {
|
|
||||||
notPublishedMessage = t('This comment has been removed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatar = (<BlankAvatar />);
|
const avatar = (<BlankAvatar />);
|
||||||
const hasReplies = comment.replies && comment.replies.length > 0;
|
const hasReplies = comment.replies && comment.replies.length > 0;
|
||||||
|
|
||||||
|
if (comment.status === 'hidden') {
|
||||||
|
notPublishedMessage = t('This comment has been hidden.');
|
||||||
|
} else if (comment.status === 'deleted') {
|
||||||
|
notPublishedMessage = t('This comment has been removed.');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentLayout avatar={avatar} hasReplies={hasReplies}>
|
<CommentLayout avatar={avatar} hasReplies={hasReplies}>
|
||||||
<div className="mt-[-3px] flex items-start">
|
<div className="mt-[-3px] flex items-start">
|
||||||
<div className="flex h-10 flex-row items-center gap-4 pb-[8px] pr-4">
|
<div className="flex h-10 flex-row items-center gap-4 pb-[8px] pr-4">
|
||||||
<p className="text-md mt-[4px] font-sans italic leading-normal text-black/20 sm:text-lg dark:text-white/35">{notPublishedMessage}</p>
|
<p className="text-md mt-[4px] font-sans italic leading-normal text-black/20 sm:text-lg dark:text-white/35">
|
||||||
|
{notPublishedMessage}
|
||||||
|
</p>
|
||||||
<div className="mt-[4px]">
|
<div className="mt-[4px]">
|
||||||
<MoreButton comment={comment} toggleEdit={openEditMode} />
|
<MoreButton comment={comment} toggleEdit={openEditMode} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,10 +14,12 @@ const Content = () => {
|
||||||
|
|
||||||
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, secundaryFormCount} = useAppContext();
|
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, secundaryFormCount} = useAppContext();
|
||||||
let commentsElements;
|
let commentsElements;
|
||||||
|
const commentsDataset = comments;
|
||||||
|
|
||||||
if (labs && labs.commentImprovements) {
|
if (labs && labs.commentImprovements) {
|
||||||
commentsElements = comments.slice().map(comment => <Comment key={comment.id} comment={comment} />);
|
commentsElements = commentsDataset.slice().map(comment => <Comment key={comment.id} comment={comment} />);
|
||||||
} else {
|
} else {
|
||||||
commentsElements = comments.slice().reverse().map(comment => <Comment key={comment.id} comment={comment} />);
|
commentsElements = commentsDataset.slice().reverse().map(comment => <Comment key={comment.id} comment={comment} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -107,7 +107,9 @@ test.describe('Actions', async () => {
|
||||||
await expect(frame.getByText('This is a reply 123')).toHaveCount(1);
|
await expect(frame.getByText('This is a reply 123')).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Reply-to-reply action not shown without labs flag', async ({page}) => {
|
test('Reply-to-reply action not shown without labs flag', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
mockedApi.addComment({
|
mockedApi.addComment({
|
||||||
html: '<p>This is comment 1</p>',
|
html: '<p>This is comment 1</p>',
|
||||||
replies: [
|
replies: [
|
||||||
|
@ -193,11 +195,15 @@ test.describe('Actions', async () => {
|
||||||
const profileModal = detailsFrame.getByTestId('profile-modal');
|
const profileModal = detailsFrame.getByTestId('profile-modal');
|
||||||
await expect(profileModal).toBeVisible();
|
await expect(profileModal).toBeVisible();
|
||||||
|
|
||||||
await expect(detailsFrame.getByTestId('name-input')).toHaveValue('John Doe');
|
await expect(detailsFrame.getByTestId('name-input')).toHaveValue(
|
||||||
|
'John Doe'
|
||||||
|
);
|
||||||
await expect(detailsFrame.getByTestId('expertise-input')).toHaveValue('');
|
await expect(detailsFrame.getByTestId('expertise-input')).toHaveValue('');
|
||||||
|
|
||||||
await detailsFrame.getByTestId('name-input').fill('Testy McTest');
|
await detailsFrame.getByTestId('name-input').fill('Testy McTest');
|
||||||
await detailsFrame.getByTestId('expertise-input').fill('Software development');
|
await detailsFrame
|
||||||
|
.getByTestId('expertise-input')
|
||||||
|
.fill('Software development');
|
||||||
|
|
||||||
await detailsFrame.getByTestId('save-button').click();
|
await detailsFrame.getByTestId('save-button').click();
|
||||||
|
|
||||||
|
@ -209,7 +215,9 @@ test.describe('Actions', async () => {
|
||||||
await waitEditorFocused(editor);
|
await waitEditorFocused(editor);
|
||||||
|
|
||||||
await expect(frame.getByTestId('member-name')).toHaveText('Testy McTest');
|
await expect(frame.getByTestId('member-name')).toHaveText('Testy McTest');
|
||||||
await expect(frame.getByTestId('expertise-button')).toHaveText('·Software development');
|
await expect(frame.getByTestId('expertise-button')).toHaveText(
|
||||||
|
'·Software development'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Sorting - flag needs to be enabled', () => {
|
test.describe('Sorting - flag needs to be enabled', () => {
|
||||||
|
@ -292,7 +300,9 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
await expect(comments.nth(0)).toContainText('This is comment 3');
|
await expect(comments.nth(0)).toContainText('This is comment 3');
|
||||||
});
|
});
|
||||||
test('Renders Sorting Form dropdown, with Best, Newest Oldest', async ({page}) => {
|
test('Renders Sorting Form dropdown, with Best, Newest Oldest', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
mockedApi.addComment({
|
mockedApi.addComment({
|
||||||
html: '<p>This is comment 1</p>'
|
html: '<p>This is comment 1</p>'
|
||||||
});
|
});
|
||||||
|
@ -330,7 +340,9 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
await sortingForm.click();
|
await sortingForm.click();
|
||||||
|
|
||||||
const sortingDropdown = frame.getByTestId('comments-sorting-form-dropdown');
|
const sortingDropdown = frame.getByTestId(
|
||||||
|
'comments-sorting-form-dropdown'
|
||||||
|
);
|
||||||
await expect(sortingDropdown).toBeVisible();
|
await expect(sortingDropdown).toBeVisible();
|
||||||
|
|
||||||
// check if inner options are visible
|
// check if inner options are visible
|
||||||
|
@ -370,7 +382,9 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
await sortingForm.click();
|
await sortingForm.click();
|
||||||
|
|
||||||
const sortingDropdown = await frame.getByTestId('comments-sorting-form-dropdown');
|
const sortingDropdown = await frame.getByTestId(
|
||||||
|
'comments-sorting-form-dropdown'
|
||||||
|
);
|
||||||
|
|
||||||
const newestOption = await sortingDropdown.getByText('Newest');
|
const newestOption = await sortingDropdown.getByText('Newest');
|
||||||
await newestOption.click();
|
await newestOption.click();
|
||||||
|
@ -407,7 +421,9 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
await sortingForm.click();
|
await sortingForm.click();
|
||||||
|
|
||||||
const sortingDropdown = await frame.getByTestId('comments-sorting-form-dropdown');
|
const sortingDropdown = await frame.getByTestId(
|
||||||
|
'comments-sorting-form-dropdown'
|
||||||
|
);
|
||||||
|
|
||||||
const newestOption = await sortingDropdown.getByText('Oldest');
|
const newestOption = await sortingDropdown.getByText('Oldest');
|
||||||
await newestOption.click();
|
await newestOption.click();
|
||||||
|
|
|
@ -6,14 +6,16 @@ export class MockedApi {
|
||||||
postId: string;
|
postId: string;
|
||||||
member: any;
|
member: any;
|
||||||
settings: any;
|
settings: any;
|
||||||
|
members: any[];
|
||||||
|
|
||||||
#lastCommentDate = new Date('2021-01-01T00:00:00.000Z');
|
#lastCommentDate = new Date('2021-01-01T00:00:00.000Z');
|
||||||
|
|
||||||
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}}: {postId?: string, comments?: any[], member?: any, settings?: any}) {
|
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, members = []}: {postId?: string, comments?: any[], member?: any, settings?: any, members?: any[]}) {
|
||||||
this.postId = postId;
|
this.postId = postId;
|
||||||
this.comments = comments;
|
this.comments = comments;
|
||||||
this.member = member;
|
this.member = member;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
this.members = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addComment(overrides: any = {}) {
|
addComment(overrides: any = {}) {
|
||||||
|
@ -47,10 +49,20 @@ export class MockedApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createMember(overrides) {
|
||||||
|
const newMember = buildMember(overrides);
|
||||||
|
this.members.push(newMember);
|
||||||
|
return newMember;
|
||||||
|
}
|
||||||
|
|
||||||
setMember(overrides) {
|
setMember(overrides) {
|
||||||
this.member = buildMember(overrides);
|
this.member = buildMember(overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logoutMember() {
|
||||||
|
this.member = null;
|
||||||
|
}
|
||||||
|
|
||||||
setSettings(overrides) {
|
setSettings(overrides) {
|
||||||
this.settings = buildSettings(overrides);
|
this.settings = buildSettings(overrides);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,18 @@ window.addEventListener('message', async function (event) {
|
||||||
}), siteOrigin);
|
}), siteOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.action === 'browseComments') {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
adminUrl + '/comments/?limit=50&order=created_at%20desc'
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
respond(null, json);
|
||||||
|
} catch (err) {
|
||||||
|
respond(err, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.action === 'getUser') {
|
if (data.action === 'getUser') {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const models = require('../../models');
|
const models = require('../../models');
|
||||||
|
const commentsService = require('../../services/comments');
|
||||||
|
|
||||||
function handleCacheHeaders(model, frame) {
|
function handleCacheHeaders(model, frame) {
|
||||||
if (model) {
|
if (model) {
|
||||||
|
@ -39,6 +40,33 @@ const controller = {
|
||||||
|
|
||||||
handleCacheHeaders(result, frame);
|
handleCacheHeaders(result, frame);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
browse: {
|
||||||
|
headers: {
|
||||||
|
cacheInvalidate: false
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
'post_id',
|
||||||
|
'include',
|
||||||
|
'page',
|
||||||
|
'limit',
|
||||||
|
'fields',
|
||||||
|
'filter',
|
||||||
|
'order',
|
||||||
|
'debug'
|
||||||
|
],
|
||||||
|
validation: {
|
||||||
|
options: {
|
||||||
|
post_id: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permissions: true,
|
||||||
|
async query(frame) {
|
||||||
|
const result = await commentsService.controller.adminBrowse(frame);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const _ = require('lodash');
|
||||||
const errors = require('@tryghost/errors');
|
const errors = require('@tryghost/errors');
|
||||||
const tpl = require('@tryghost/tpl');
|
const tpl = require('@tryghost/tpl');
|
||||||
const {ValidationError} = require('@tryghost/errors');
|
const {ValidationError} = require('@tryghost/errors');
|
||||||
|
const labs = require('../../shared/labs');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
emptyComment: 'The body of a comment cannot be empty',
|
emptyComment: 'The body of a comment cannot be empty',
|
||||||
|
@ -60,6 +61,44 @@ const Comment = ghostBookshelf.Model.extend({
|
||||||
// Note: this limit is not working
|
// Note: this limit is not working
|
||||||
.query('limit', 3);
|
.query('limit', 3);
|
||||||
},
|
},
|
||||||
|
customQuery(qb) {
|
||||||
|
qb.where(function () {
|
||||||
|
this.whereNotIn('comments.status', ['hidden', 'deleted'])
|
||||||
|
.orWhereExists(function () {
|
||||||
|
this.select(1)
|
||||||
|
.from('comments as replies')
|
||||||
|
.whereRaw('replies.parent_id = comments.id')
|
||||||
|
.whereNotIn('replies.status', ['hidden', 'deleted']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
adminCustomQuery(qb) {
|
||||||
|
qb.where(function () {
|
||||||
|
this.whereNotIn('comments.status', ['deleted'])
|
||||||
|
.orWhereExists(function () {
|
||||||
|
this.select(1)
|
||||||
|
.from('comments as replies')
|
||||||
|
.whereRaw('replies.parent_id = comments.id')
|
||||||
|
.whereNotIn('replies.status', ['deleted']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyCustomQuery(options) {
|
||||||
|
if (labs.isSet('commentImprovements')) {
|
||||||
|
if (!options.isAdmin) { // if it's an admin request, we don't need to apply the custom query
|
||||||
|
this.query((qb) => {
|
||||||
|
this.customQuery(qb, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.isAdmin) {
|
||||||
|
this.query((qb) => {
|
||||||
|
this.adminCustomQuery(qb, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
emitChange: function emitChange(event, options) {
|
emitChange: function emitChange(event, options) {
|
||||||
const eventToTrigger = 'comment' + '.' + event;
|
const eventToTrigger = 'comment' + '.' + event;
|
||||||
|
@ -122,6 +161,7 @@ const Comment = ghostBookshelf.Model.extend({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|
||||||
destroy: function destroy(unfilteredOptions) {
|
destroy: function destroy(unfilteredOptions) {
|
||||||
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
||||||
|
|
||||||
|
@ -222,11 +262,9 @@ const Comment = ghostBookshelf.Model.extend({
|
||||||
'replies.count.liked'
|
'replies.count.liked'
|
||||||
].filter(relation => withRelated.includes(relation));
|
].filter(relation => withRelated.includes(relation));
|
||||||
const result = await ghostBookshelf.Model.findPage.call(this, options);
|
const result = await ghostBookshelf.Model.findPage.call(this, options);
|
||||||
|
|
||||||
for (const model of result.data) {
|
for (const model of result.data) {
|
||||||
await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
|
await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -276,6 +314,7 @@ const Comment = ghostBookshelf.Model.extend({
|
||||||
|
|
||||||
// The comment model additionally supports having a parentId option
|
// The comment model additionally supports having a parentId option
|
||||||
options.push('parentId');
|
options.push('parentId');
|
||||||
|
options.push('isAdmin');
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,10 +51,30 @@ module.exports = class CommentsController {
|
||||||
frame.options.filter = `post_id:${frame.options.post_id}`;
|
frame.options.filter = `post_id:${frame.options.post_id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.service.getComments(frame.options);
|
return await this.service.getComments(frame.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminBrowse(frame) {
|
||||||
|
if (frame.options.post_id) {
|
||||||
|
if (frame.options.filter) {
|
||||||
|
frame.options.mongoTransformer = function (query) {
|
||||||
|
return {
|
||||||
|
$and: [
|
||||||
|
{
|
||||||
|
post_id: frame.options.post_id
|
||||||
|
},
|
||||||
|
query
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
frame.options.filter = `post_id:${frame.options.post_id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame.options.isAdmin = true;
|
||||||
|
return await this.service.getAdminComments(frame.options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Frame} frame
|
* @param {Frame} frame
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -176,6 +176,13 @@ class CommentsService {
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAdminComments(options) {
|
||||||
|
this.checkEnabled();
|
||||||
|
const page = await this.models.Comment.findPage({...options, parentId: null});
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} id - The ID of the Comment to get replies from
|
* @param {string} id - The ID of the Comment to get replies from
|
||||||
* @param {any} options
|
* @param {any} options
|
||||||
|
|
|
@ -51,6 +51,7 @@ module.exports = function apiRoutes() {
|
||||||
|
|
||||||
router.get('/mentions', mw.authAdminApi, http(api.mentions.browse));
|
router.get('/mentions', mw.authAdminApi, http(api.mentions.browse));
|
||||||
|
|
||||||
|
router.get('/comments/post/:post_id', mw.authAdminApi, http(api.comments.browse));
|
||||||
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
|
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
|
||||||
|
|
||||||
// ## Pages
|
// ## Pages
|
||||||
|
|
|
@ -4,8 +4,67 @@ const {
|
||||||
fixtureManager,
|
fixtureManager,
|
||||||
mockManager
|
mockManager
|
||||||
} = require('../../utils/e2e-framework');
|
} = require('../../utils/e2e-framework');
|
||||||
|
const models = require('../../../core/server/models');
|
||||||
|
let postId;
|
||||||
|
const dbFns = {
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AddCommentData
|
||||||
|
* @property {string} [post_id=post_id]
|
||||||
|
* @property {string} member_id
|
||||||
|
* @property {string} [parent_id]
|
||||||
|
* @property {string} [html='This is a comment']
|
||||||
|
* @property {string} [status='published']
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AddCommentReplyData
|
||||||
|
* @property {string} member_id
|
||||||
|
* @property {string} [html='This is a reply']
|
||||||
|
* @property {date} [created_at]
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @typedef {AddCommentData & {replies: AddCommentReplyData[]}} AddCommentWithRepliesData
|
||||||
|
*/
|
||||||
|
|
||||||
describe('Comments API', function () {
|
/**
|
||||||
|
* @param {AddCommentData} data
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
addComment: async (data) => {
|
||||||
|
return await models.Comment.add({
|
||||||
|
post_id: data.post_id || postId,
|
||||||
|
member_id: data.member_id,
|
||||||
|
parent_id: data.parent_id,
|
||||||
|
html: data.html || '<p>This is a comment</p>',
|
||||||
|
created_at: data.created_at,
|
||||||
|
status: data.status || 'published'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {AddCommentWithRepliesData} data
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
addCommentWithReplies: async (data) => {
|
||||||
|
const {replies, ...commentData} = data;
|
||||||
|
|
||||||
|
const parent = await dbFns.addComment(commentData);
|
||||||
|
const createdReplies = [];
|
||||||
|
|
||||||
|
for (const reply of replies) {
|
||||||
|
const createdReply = await dbFns.addComment({
|
||||||
|
post_id: parent.get('post_id'),
|
||||||
|
member_id: reply.member_id,
|
||||||
|
parent_id: parent.get('id'),
|
||||||
|
html: reply.html || '<p>This is a reply</p>',
|
||||||
|
status: reply.status || 'published'
|
||||||
|
});
|
||||||
|
createdReplies.push(createdReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {parent, replies: createdReplies};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Admin Comments API', function () {
|
||||||
let adminApi;
|
let adminApi;
|
||||||
let membersApi;
|
let membersApi;
|
||||||
|
|
||||||
|
@ -84,4 +143,103 @@ describe('Comments API', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('browse', function () {
|
||||||
|
it('Can browse comments as an admin', async function () {
|
||||||
|
const post = fixtureManager.get('posts', 1);
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 1',
|
||||||
|
status: 'published'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 2',
|
||||||
|
status: 'published'
|
||||||
|
});
|
||||||
|
const res = await adminApi.get('/comments/post/' + post.id + '/');
|
||||||
|
assert.equal(res.body.comments.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not return deleted comments, but returns hidden comments', async function () {
|
||||||
|
const post = fixtureManager.get('posts', 1);
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 1',
|
||||||
|
status: 'deleted'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 2',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
const res = await adminApi.get('/comments/post/' + post.id + '/');
|
||||||
|
// check that there is no deleted comments by looping through the returned comments
|
||||||
|
for (const comment of res.body.comments) {
|
||||||
|
assert.notEqual(comment.status, 'deleted');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Returns deleted comments if is has hidden or published replies', async function () {
|
||||||
|
const post = fixtureManager.get('posts', 1);
|
||||||
|
await dbFns.addCommentWithReplies({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 1',
|
||||||
|
status: 'deleted',
|
||||||
|
replies: [
|
||||||
|
{
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
html: 'Reply 1',
|
||||||
|
status: 'published'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await adminApi.get('/comments/post/' + post.id + '/');
|
||||||
|
|
||||||
|
// find deleted comment
|
||||||
|
const deletedComment = res.body.comments.find(comment => comment.status === 'deleted');
|
||||||
|
|
||||||
|
assert.equal(deletedComment.html, 'Comment 1');
|
||||||
|
|
||||||
|
const publishedReply = res.body.comments.find(comment => comment.id === deletedComment.id).replies?.find(reply => reply.status === 'published');
|
||||||
|
|
||||||
|
assert.equal(publishedReply.html, 'Reply 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does show HTML of deleted and hidden comments since we are admin', async function () {
|
||||||
|
const post = fixtureManager.get('posts', 1);
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 1',
|
||||||
|
status: 'deleted'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
post_id: post.id,
|
||||||
|
html: 'Comment 2',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
const res = await adminApi.get('/comments/post/' + post.id + '/');
|
||||||
|
|
||||||
|
const deletedComment = res.body.comments.find(comment => comment.status === 'deleted');
|
||||||
|
assert.equal(deletedComment.html, 'Comment 1');
|
||||||
|
|
||||||
|
const hiddenComment = res.body.comments.find(comment => comment.status === 'hidden');
|
||||||
|
assert.equal(hiddenComment.html, 'Comment 2');
|
||||||
|
// console.log(res.body);
|
||||||
|
// assert.equal(res.body.comments[0].html, 'Comment 2');
|
||||||
|
|
||||||
|
// assert.equal(res.body.comments[1].html, 'Comment 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -398,6 +398,125 @@ describe('Comments API', function () {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes hidden comments', async function () {
|
||||||
|
const hiddenComment = await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
html: 'This is a hidden comment',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data2 = await membersAgent
|
||||||
|
.get(`/api/comments/post/${postId}/`)
|
||||||
|
.expectStatus(200);
|
||||||
|
|
||||||
|
// check that hiddenComment.id is not in the response
|
||||||
|
should(data2.body.comments.map(c => c.id)).not.containEql(hiddenComment.id);
|
||||||
|
should(data2.body.comments.length).eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes deleted comments', async function () {
|
||||||
|
// await mockManager.mockLabsEnabled('commentImprovements');
|
||||||
|
await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
html: 'This is a deleted comment',
|
||||||
|
status: 'deleted'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data2 = await membersAgent
|
||||||
|
.get(`/api/comments/post/${postId}/`)
|
||||||
|
.expectStatus(200);
|
||||||
|
|
||||||
|
// go through all comments and check if the deleted comment is not there
|
||||||
|
data2.body.comments.forEach((comment) => {
|
||||||
|
should(comment.html).not.eql('This is a deleted comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
data2.body.comments.length.should.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows hidden and deleted comment where there is a reply', async function () {
|
||||||
|
await mockManager.mockLabsEnabled('commentImprovements');
|
||||||
|
await setupBrowseCommentsData();
|
||||||
|
const hiddenComment = await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
html: 'This is a hidden comment',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedComment = await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
html: 'This is a deleted comment',
|
||||||
|
status: 'deleted'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
parent_id: hiddenComment.get('id'),
|
||||||
|
html: 'This is a reply to a hidden comment'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 2).id,
|
||||||
|
parent_id: deletedComment.get('id'),
|
||||||
|
html: 'This is a reply to a deleted comment'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data2 = await membersAgent
|
||||||
|
.get(`/api/comments/post/${postId}`)
|
||||||
|
.expectStatus(200);
|
||||||
|
|
||||||
|
// check if hidden and deleted comments have their html removed
|
||||||
|
data2.body.comments.forEach((comment) => {
|
||||||
|
should.notEqual(comment.html, 'This is a hidden comment');
|
||||||
|
should.notEqual(comment.html, 'This is a deleted comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if hiddenComment.id and deletedComment.id are in the response
|
||||||
|
should(data2.body.comments.map(c => c.id)).containEql(hiddenComment.id);
|
||||||
|
should(data2.body.comments.map(c => c.id)).containEql(deletedComment.id);
|
||||||
|
|
||||||
|
// check if the replies to hidden and deleted comments are in the response
|
||||||
|
data2.body.comments.forEach((comment) => {
|
||||||
|
if (comment.id === hiddenComment.id) {
|
||||||
|
should(comment.replies.length).eql(1);
|
||||||
|
should(comment.replies[0].html).eql('This is a reply to a hidden comment');
|
||||||
|
} else if (comment.id === deletedComment.id) {
|
||||||
|
should(comment.replies.length).eql(1);
|
||||||
|
should(comment.replies[0].html).eql('This is a reply to a deleted comment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Returns nothing if both parent and reply are hidden', async function () {
|
||||||
|
await mockManager.mockLabsEnabled('commentImprovements');
|
||||||
|
const hiddenComment = await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 0).id,
|
||||||
|
html: 'This is a hidden comment',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFns.addComment({
|
||||||
|
post_id: postId,
|
||||||
|
member_id: fixtureManager.get('members', 1).id,
|
||||||
|
parent_id: hiddenComment.get('id'),
|
||||||
|
html: 'This is a reply to a hidden comment',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data2 = await membersAgent
|
||||||
|
.get(`/api/comments/post/${postId}`)
|
||||||
|
.expectStatus(200);
|
||||||
|
|
||||||
|
should(data2.body.comments.length).eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('cannot comment on a post', async function () {
|
it('cannot comment on a post', async function () {
|
||||||
await testCannotCommentOnPost(401);
|
await testCannotCommentOnPost(401);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue