diff --git a/apps/comments-ui/src/App.tsx b/apps/comments-ui/src/App.tsx index efe413e295..f990f1f1ec 100644 --- a/apps/comments-ui/src/App.tsx +++ b/apps/comments-ui/src/App.tsx @@ -116,7 +116,7 @@ const App: React.FC = ({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, diff --git a/apps/comments-ui/src/actions.ts b/apps/comments-ui/src/actions.ts index cd883aff0f..3ce3953876 100644 --- a/apps/comments-ui/src/actions.ts +++ b/apps/comments-ui/src/actions.ts @@ -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> { 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); } diff --git a/apps/comments-ui/src/utils/adminAPI.test.ts b/apps/comments-ui/src/utils/adminAPI.test.ts index 61404fff18..350324b057 100644 --- a/apps/comments-ui/src/utils/adminAPI.test.ts +++ b/apps/comments-ui/src/utils/adminAPI.test.ts @@ -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' diff --git a/apps/comments-ui/src/utils/adminApi.ts b/apps/comments-ui/src/utils/adminApi.ts index 3ed39e174a..78bbfb2df6 100644 --- a/apps/comments-ui/src/utils/adminApi.ts +++ b/apps/comments-ui/src/utils/adminApi.ts @@ -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()}) + }); } }; diff --git a/apps/comments-ui/test/e2e/admin-moderation.test.ts b/apps/comments-ui/test/e2e/admin-moderation.test.ts index db33fcd430..fb69fca1f6 100644 --- a/apps/comments-ui/test/e2e/admin-moderation.test.ts +++ b/apps/comments-ui/test/e2e/admin-moderation.test.ts @@ -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: '

This is comment 1

'}); + 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: `

This is comment ${i}

`}); + } + 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: '

This is the oldest

', + created_at: new Date('2024-02-01T00:00:00Z') + }); + mockedApi.addComment({ + html: '

This is comment 2

', + created_at: new Date('2024-03-02T00:00:00Z') + }); + mockedApi.addComment({ + html: '

This is the newest comment

', + 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: '

This is comment 1

', + replies: [ + buildReply({html: '

This is reply 1

'}), + buildReply({html: '

This is reply 2

'}), + buildReply({html: '

This is reply 3

'}), + buildReply({html: '

This is reply 4

'}), + buildReply({html: '

This is reply 5

'}), + buildReply({html: '

This is reply 6

'}) + ] + }); + + 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: '

This is comment 1

'}); + mockedApi.addComment({html: '

This is comment 2

', 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: '

This is comment 1

'}); mockedApi.addComment({html: '

This is comment 2

', status: 'hidden'}); diff --git a/apps/comments-ui/test/utils/MockedApi.ts b/apps/comments-ui/test/utils/MockedApi.ts index 6f5ccc0dd7..398ea52dd8 100644 --- a/apps/comments-ui/test/utils/MockedApi.ts +++ b/apps/comments-ui/test/utils/MockedApi.ts @@ -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)); } } diff --git a/apps/comments-ui/test/utils/e2e.ts b/apps/comments-ui/test/utils/e2e.ts index 6f9c6debd6..3b0235c291 100644 --- a/apps/comments-ui/test/utils/e2e.ts +++ b/apps/comments-ui/test/utils/e2e.ts @@ -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); } } diff --git a/ghost/core/core/frontend/src/admin-auth/message-handler.js b/ghost/core/core/frontend/src/admin-auth/message-handler.js index b108959f4a..3022662d24 100644 --- a/ghost/core/core/frontend/src/admin-auth/message-handler.js +++ b/ghost/core/core/frontend/src/admin-auth/message-handler.js @@ -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) { diff --git a/ghost/core/core/server/api/endpoints/comment-replies.js b/ghost/core/core/server/api/endpoints/comment-replies.js index 662d6d5a48..d33fd33ded 100644 --- a/ghost/core/core/server/api/endpoints/comment-replies.js +++ b/ghost/core/core/server/api/endpoints/comment-replies.js @@ -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', diff --git a/ghost/core/core/server/api/endpoints/comments.js b/ghost/core/core/server/api/endpoints/comments.js index ee7980ed30..3a065f16a7 100644 --- a/ghost/core/core/server/api/endpoints/comments.js +++ b/ghost/core/core/server/api/endpoints/comments.js @@ -54,7 +54,8 @@ const controller = { 'fields', 'filter', 'order', - 'debug' + 'debug', + 'impersonate_member_uuid' ], validation: { options: { diff --git a/ghost/core/core/server/services/comments/CommentsController.js b/ghost/core/core/server/services/comments/CommentsController.js index 40baef493c..a4a6c40e5f 100644 --- a/ghost/core/core/server/services/comments/CommentsController.js +++ b/ghost/core/core/server/services/comments/CommentsController.js @@ -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); } diff --git a/ghost/core/core/server/services/comments/CommentsService.js b/ghost/core/core/server/services/comments/CommentsService.js index 9aa160deb0..c76598a0ea 100644 --- a/ghost/core/core/server/services/comments/CommentsService.js +++ b/ghost/core/core/server/services/comments/CommentsService.js @@ -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; diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap index ca47d8dc92..eaac91b8cd 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap @@ -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", +} +`; diff --git a/ghost/core/test/e2e-api/admin/comments.test.js b/ghost/core/test/e2e-api/admin/comments.test.js index 0d1ce662c4..70a2efab8f 100644 --- a/ghost/core/test/e2e-api/admin/comments.test.js +++ b/ghost/core/test/e2e-api/admin/comments.test.js @@ -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); + }); + }); + } }); });