0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Fixed comment likes being incorrect when logged in as an Admin (#21833)

ref https://linear.app/ghost/issue/PLG-296/

When logged in as an Admin, comments-ui switches comment reads from the Members API over to the Admin API so that hidden comments can be displayed to allow moderation activities. However, the Admin API not using member authentication and CORS preventing the front-end members auth cookie being passed over to the Admin API domain meant that the logged-in member's likes were missing when fetching via the Admin API as there is no available reference to the logged in member.

This change works around the problem by introducing an `impersonate_member_uuid` param to the comments read/browse endpoints of the Admin API. When passed, the provided uuid is used to simulate that member being logged in so that likes are correctly shown.

- Introduced `impersonation_member_id` parameter to resolve issues with admin API not returning correct "liked" status for comments when an admin is logged in.
- Updated API endpoints in `comment-replies.js` and `comments.js` to handle `impersonation_member_id`.
- Adjusted `CommentsController` to validate and process the `impersonation_member_id` parameter before passing it to database queries.
- Enhanced test coverage to ensure proper handling of the new parameter and accurate "liked" status behavior.
This commit is contained in:
Ronald Langeveld 2024-12-10 22:44:15 +08:00 committed by GitHub
parent ece7c93759
commit 04f0b9fc3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 359 additions and 24 deletions

View file

@ -116,7 +116,7 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
admin = await adminApi.getUser();
if (admin && state.labs.commentImprovements) {
// this is a bit of a hack, but we need to fetch the comments fully populated if the user is an admin
const adminComments = await adminApi.browse({page: 1, postId: options.postId, order: state.order});
const adminComments = await adminApi.browse({page: 1, postId: options.postId, order: state.order, memberUuid: state.member?.uuid});
setState({
...state,
adminApi: adminApi,

View file

@ -10,7 +10,7 @@ async function loadMoreComments({state, api, options, order}: {state: EditableAp
}
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.browse({page, postId: options.postId, order: order || state.order});
data = await state.adminApi.browse({page, postId: options.postId, order: order || state.order, memberUuid: state.member?.uuid});
} else {
data = await api.comments.browse({page, postId: options.postId, order: order || state.order});
}
@ -34,7 +34,7 @@ async function setOrder({state, data: {order}, options, api, dispatchAction}: {s
try {
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.browse({page: 1, postId: options.postId, order});
data = await state.adminApi.browse({page: 1, postId: options.postId, order, memberUuid: state.member?.uuid});
} else {
data = await api.comments.browse({page: 1, postId: options.postId, order});
}
@ -55,7 +55,7 @@ async function setOrder({state, data: {order}, options, api, dispatchAction}: {s
async function loadMoreReplies({state, api, data: {comment, limit}, isReply}: {state: EditableAppContext, api: GhostApi, data: {comment: any, limit?: number | 'all'}, isReply: boolean}): Promise<Partial<EditableAppContext>> {
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements && !isReply) { // we don't want the admin api to load reply data for replying to a reply, so we pass isReply: true
data = await state.adminApi.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit});
data = await state.adminApi.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit, memberUuid: state.member?.uuid});
} else {
data = await api.comments.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit});
}
@ -150,13 +150,13 @@ async function hideComment({state, data: comment}: {state: EditableAppContext, a
async function showComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, adminApi: any, data: {id: string}}) {
if (state.adminApi) {
await state.adminApi.showComment(comment.id);
await state.adminApi.showComment({id: comment.id});
}
// We need to refetch the comment, to make sure we have an up to date HTML content
// + all relations are loaded as the current member (not the admin)
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.read({commentId: comment.id});
data = await state.adminApi.read({commentId: comment.id, memberUuid: state.member?.uuid});
} else {
data = await api.comments.read(comment.id);
}

View file

@ -97,7 +97,7 @@ describe('setupAdminAPI', () => {
const adminUrl = 'https://example.com';
const api = setupAdminAPI({adminUrl});
const apiPromise = api.showComment('123');
const apiPromise = api.showComment({id: '123'});
const eventHandler = addEventListenerSpy.mock.calls.find(
([eventType]) => eventType === 'message'

View file

@ -59,11 +59,11 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
async hideComment(id: string) {
return await callApi('hideComment', {id});
},
async showComment(id: string) {
async showComment({id} : {id: string}) {
return await callApi('showComment', {id});
},
async browse({page, postId, order}: {page: number, postId: string, order?: string}) {
async browse({page, postId, order, memberUuid}: {page: number, postId: string, order?: string, memberUuid?: string}) {
let filter = null;
if (firstCommentCreatedAt && !order) {
filter = `created_at:<=${firstCommentCreatedAt}`;
@ -80,6 +80,10 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
params.set('order', order);
}
if (memberUuid) {
params.set('impersonate_member_uuid', memberUuid);
}
const response = await callApi('browseComments', {postId, params: params.toString()});
if (!firstCommentCreatedAt) {
const firstComment = response.comments[0];
@ -90,7 +94,7 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
return response;
},
async replies({commentId, afterReplyId, limit}: {commentId: string; afterReplyId: string; limit?: number | 'all'}) {
async replies({commentId, afterReplyId, limit, memberUuid}: {commentId: string; afterReplyId: string; limit?: number | 'all', memberUuid?: string}) {
const filter = `id:>'${afterReplyId}'`;
const params = new URLSearchParams();
@ -103,14 +107,26 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
params.set('filter', filter);
}
if (memberUuid) {
params.set('impersonate_member_uuid', memberUuid);
}
const response = await callApi('getReplies', {commentId, params: params.toString()});
return response;
},
async read({commentId}: {commentId: string}) {
const response = await callApi('readComment', {commentId});
return response;
async read({commentId, memberUuid}: {commentId: string, memberUuid?: string}) {
const params = new URLSearchParams();
if (memberUuid) {
params.set('impersonate_member_uuid', memberUuid);
}
return await callApi('readComment', {
commentId,
...(params.toString() && {params: params.toString()})
});
}
};

View file

@ -18,8 +18,7 @@ test.describe('Admin moderation', async () => {
member?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
};
async function initializeTest(page, options: InitializeTestOptions = {}) {
options = {isAdmin: true, labs: false, member: {id: '1'}, ...options};
options = {isAdmin: true, labs: false, member: {id: '1', uuid: '12345'}, ...options};
if (options.isAdmin) {
await mockAdminAuthFrame({page, admin});
} else {
@ -124,6 +123,106 @@ test.describe('Admin moderation', async () => {
});
test.describe('commentImprovements', function () {
test('memeber uuid are passed to admin browse api params', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'browseComments');
const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');
await expect(comments).toHaveCount(1);
expect(adminBrowseSpy.called).toBe(true);
const lastCall = adminBrowseSpy.lastCall.args[0];
const url = new URL(lastCall.request().url());
expect(url.searchParams.get('impersonate_member_uuid')).toBe('12345');
});
test('member uuid gets set when loading more comments', async ({page}) => {
// create 25 comments
for (let i = 0; i < 25; i++) {
mockedApi.addComment({html: `<p>This is comment ${i}</p>`});
}
const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'browseComments');
const {frame} = await initializeTest(page, {labs: true});
await frame.getByTestId('pagination-component').click();
const lastCall = adminBrowseSpy.lastCall.args[0];
const url = new URL(lastCall.request().url());
expect(url.searchParams.get('impersonate_member_uuid')).toBe('12345');
});
test('member uuid gets set when changing order', async ({page}) => {
mockedApi.addComment({
html: '<p>This is the oldest</p>',
created_at: new Date('2024-02-01T00:00:00Z')
});
mockedApi.addComment({
html: '<p>This is comment 2</p>',
created_at: new Date('2024-03-02T00:00:00Z')
});
mockedApi.addComment({
html: '<p>This is the newest comment</p>',
created_at: new Date('2024-04-03T00:00:00Z')
});
const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'browseComments');
const {frame} = await initializeTest(page, {labs: true});
const sortingForm = await frame.getByTestId('comments-sorting-form');
await sortingForm.click();
const sortingDropdown = await frame.getByTestId(
'comments-sorting-form-dropdown'
);
const optionSelect = await sortingDropdown.getByText('Newest');
mockedApi.setDelay(100);
await optionSelect.click();
const lastCall = adminBrowseSpy.lastCall.args[0];
const url = new URL(lastCall.request().url());
expect(url.searchParams.get('impersonate_member_uuid')).toBe('12345');
});
test('member uuid gets set when loading more replies', async ({page}) => {
mockedApi.addComment({
html: '<p>This is comment 1</p>',
replies: [
buildReply({html: '<p>This is reply 1</p>'}),
buildReply({html: '<p>This is reply 2</p>'}),
buildReply({html: '<p>This is reply 3</p>'}),
buildReply({html: '<p>This is reply 4</p>'}),
buildReply({html: '<p>This is reply 5</p>'}),
buildReply({html: '<p>This is reply 6</p>'})
]
});
const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'getReplies');
const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');
const comment = comments.nth(0);
await comment.getByTestId('reply-pagination-button').click();
const lastCall = adminBrowseSpy.lastCall.args[0];
const url = new URL(lastCall.request().url());
expect(url.searchParams.get('impersonate_member_uuid')).toBe('12345');
});
test('member uuid gets set when reading a comment (after unhiding)', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
mockedApi.addComment({html: '<p>This is comment 2</p>', status: 'hidden'});
const adminReadSpy = sinon.spy(mockedApi.adminRequestHandlers, 'getOrUpdateComment');
const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');
await expect(comments).toHaveCount(2);
await expect(comments.nth(1)).toContainText('Hidden for members');
const moreButtons = comments.nth(1).getByTestId('more-button');
await moreButtons.click();
await moreButtons.getByTestId('show-button').click();
await expect(comments.nth(1)).not.toContainText('Hidden for members');
const lastCall = adminReadSpy.lastCall.args[0];
const url = new URL(lastCall.request().url());
expect(url.searchParams.get('impersonate_member_uuid')).toBe('12345');
});
test('hidden comments are not displayed for non-admins', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
mockedApi.addComment({html: '<p>This is comment 2</p>', status: 'hidden'});

View file

@ -435,6 +435,7 @@ export class MockedApi {
const limit = parseInt(url.searchParams.get('limit') ?? '5');
const filter = url.searchParams.get('filter') ?? '';
const order = url.searchParams.get('order') ?? '';
const memberUuid = url.searchParams.get('impersonate_member_uuid') ?? '';
await route.fulfill({
status: 200,
@ -443,7 +444,28 @@ export class MockedApi {
limit,
filter,
order,
admin: true
admin: true,
memberUuid
}))
});
},
async getReplies(route) {
await this.#delayResponse();
const url = new URL(route.request().url());
const limit = parseInt(url.searchParams.get('limit') ?? '5');
const commentId = url.pathname.split('/').reverse()[2];
const filter = url.searchParams.get('filter') ?? '';
const memberUuid = url.searchParams.get('impersonate_member_uuid') ?? '';
await route.fulfill({
status: 200,
body: JSON.stringify(this.browseReplies({
limit,
filter,
commentId,
memberUuid
}))
});
},
@ -454,6 +476,7 @@ export class MockedApi {
if (route.request().method() === 'GET') {
const commentId = url.pathname.split('/').reverse()[1];
const memberUuid = url.searchParams.get('impersonate_member_uuid') ?? '';
await route.fulfill({
status: 200,
body: JSON.stringify(this.browseComments({
@ -461,7 +484,8 @@ export class MockedApi {
filter: `id:'${commentId}'`,
page: 1,
order: '',
admin: true
admin: true,
memberUuid
}))
});
}
@ -470,7 +494,6 @@ export class MockedApi {
const commentId = url.pathname.split('/').reverse()[1];
const payload = JSON.parse(route.request().postData());
const comment = findCommentById(this.comments, commentId);
if (!comment) {
await route.fulfill({status: 404});
return;
@ -506,6 +529,7 @@ export class MockedApi {
// Admin API -----------------------------------------------------------
await page.route(`${path}/ghost/api/admin/users/me/`, this.adminRequestHandlers.getUser.bind(this));
await page.route(`${path}/ghost/api/admin/comments/post/*/*`, this.adminRequestHandlers.browseComments.bind(this));
await page.route(`${path}/ghost/api/admin/comments/*/`, this.adminRequestHandlers.getOrUpdateComment.bind(this));
await page.route(`${path}/ghost/api/admin/comments/*/*`, this.adminRequestHandlers.getOrUpdateComment.bind(this));
await page.route(`${path}/ghost/api/admin/comments/*/replies/*`, this.adminRequestHandlers.getReplies.bind(this));
}
}

View file

@ -70,6 +70,7 @@ function authFrameMain() {
respond(null, json);
} catch (err) {
console.log('e2e Admin endpoint error:', err); // eslint-disable-line no-console
console.log('error with', data); // eslint-disable-line no-console
respond(err, null);
}
}

View file

@ -51,8 +51,8 @@ window.addEventListener('message', async function (event) {
if (data.action === 'readComment') {
try {
const {commentId} = data;
const res = await fetch(adminUrl + '/comments/' + commentId + '/');
const {commentId, params} = data;
const res = await fetch(adminUrl + '/comments/' + commentId + '/' + '?' + new URLSearchParams(params).toString());
const json = await res.json();
respond(null, json);
} catch (err) {

View file

@ -18,7 +18,8 @@ const controller = {
'filter',
'order',
'debug',
'id'
'id',
'impersonate_member_uuid'
],
validation: {
options: {
@ -37,7 +38,8 @@ const controller = {
cacheInvalidate: false
},
options: [
'include'
'include',
'impersonate_member_uuid'
],
data: [
'id',

View file

@ -54,7 +54,8 @@ const controller = {
'fields',
'filter',
'order',
'debug'
'debug',
'impersonate_member_uuid'
],
validation: {
options: {

View file

@ -23,6 +23,14 @@ module.exports = class CommentsController {
this.stats = stats;
}
async #setImpersonationContext(options) {
if (options.impersonate_member_uuid) {
options.context = options.context || {};
options.context.member = options.context.member || {};
options.context.member.id = await this.service.getMemberIdByUUID(options.impersonate_member_uuid);
}
}
#checkMember(frame) {
if (!frame.options?.context?.member?.id) {
throw new errors.UnauthorizedError({
@ -51,6 +59,7 @@ module.exports = class CommentsController {
frame.options.filter = `post_id:${frame.options.post_id}`;
}
}
return await this.service.getComments(frame.options);
}
@ -71,7 +80,15 @@ module.exports = class CommentsController {
frame.options.filter = `post_id:${frame.options.post_id}`;
}
}
frame.options.isAdmin = true;
// Admin routes in Comments-UI lack member context due to cross-domain constraints (CORS), which prevents
// credentials from being passed. This causes issues like the inability to determine if a
// logged-in admin (acting on behalf of a member) has already liked a comment.
// To resolve this, we retrieve the `impersonate_member_uuid` from the request params and
// explicitly set it in the context options as the acting member's ID.
// Note: This approach is applied to several admin routes where member context is required.
await this.#setImpersonationContext(frame.options);
return await this.service.getAdminComments(frame.options);
}
@ -88,6 +105,8 @@ module.exports = class CommentsController {
async adminReplies(frame) {
frame.options.isAdmin = true;
frame.options.order = 'created_at asc'; // we always want to load replies from oldest to newest
await this.#setImpersonationContext(frame.options);
return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
}
@ -95,6 +114,7 @@ module.exports = class CommentsController {
* @param {Frame} frame
*/
async read(frame) {
await this.#setImpersonationContext(frame.options);
return await this.service.getCommentByID(frame.data.id, frame.options);
}

View file

@ -400,6 +400,18 @@ class CommentsService {
return model;
}
async getMemberIdByUUID(uuid, options) {
const member = await this.models.Member.findOne({uuid}, options);
if (!member) {
throw new errors.NotFoundError({
message: tpl(messages.memberNotFound)
});
}
return member.id;
}
}
module.exports = CommentsService;

View file

@ -156,6 +156,16 @@ Object {
}
`;
exports[`Admin Comments API - commentImprovements off likes 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Hide Can hide comments 1: [body] 1`] = `
Object {
"comments": Array [
@ -319,3 +329,103 @@ Object {
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin browse route 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin get by comment id read route 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin get by comment read route 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin get by comment replies route 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin get by comment replies route 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",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member gets own likes via admin api can get comment liked status by impersonating member via admin get by comment replies route 3: [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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/, /api/members/comments/67568ca99db975825f9772a5/replies/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on Logged in member likes via admin api can get comment liked status by impersonating member 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on can get logged in member likes via admin api 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;
exports[`Admin Comments API - commentImprovements on likes 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-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6e1/",
"x-powered-by": "Express",
}
`;

View file

@ -718,5 +718,55 @@ async function getMemberComments(url, commentsMatcher = [membersCommentMatcher])
});
}
});
if (enableCommentImprovements) {
describe('Logged in member gets own likes via admin api', function () {
let comment;
let post;
this.beforeEach(async function () {
post = fixtureManager.get('posts', 1);
comment = await dbFns.addComment({
post_id: post.id,
member_id: fixtureManager.get('members', 1).id
});
await membersApi.loginAs(fixtureManager.get('members', 1).email);
await membersApi
.post(`/api/comments/${comment.get('id')}/like/`)
.expectStatus(204)
.matchHeaderSnapshot({
etag: anyEtag
})
.expectEmptyBody();
});
it('can get comment liked status by impersonating member via admin browse route', async function () {
// Like the comment
const res = await adminApi.get(`/comments/post/${post.id}/?impersonate_member_uuid=${fixtureManager.get('members', 1).uuid}`);
res.body.comments[0].liked.should.eql(true);
});
it('can get comment liked status by impersonating member via admin get by comment id read route', async function () {
const res = await adminApi.get(`/comments/${comment.get('id')}/?impersonate_member_uuid=${fixtureManager.get('members', 1).uuid}`);
res.body.comments[0].liked.should.eql(true);
});
it('can get comment liked status by impersonating member via admin get by comment replies route', async function () {
const {parent, replies} = await dbFns.addCommentWithReplies({
member_id: fixtureManager.get('members', 1).id,
replies: [{
member_id: fixtureManager.get('members', 1).id
}]
});
await membersApi
.post(`/api/comments/${replies[0].id}/like/`)
.expectStatus(204)
.expectEmptyBody();
const res = await adminApi.get(`/comments/${parent.get('id')}/replies/?impersonate_member_uuid=${fixtureManager.get('members', 1).uuid}`);
res.body.comments[0].liked.should.eql(true);
});
});
}
});
});