From bac8f4b8db35ca51563b9c6996cd31a452e647cd Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Thu, 4 Aug 2022 15:51:23 +0200 Subject: [PATCH] Added bio to members api (#15168) refs https://github.com/TryGhost/Team/issues/1716 - Adds the bio field to the API output - Allow setting bio when updating the member - Includes new E2E tests for the members API that were missing --- .../server/services/members/middleware.js | 2 +- .../core/server/services/members/utils.js | 1 + .../__snapshots__/middleware.test.js.snap | 291 ++++++++++++++++++ .../test/e2e-api/members/middleware.test.js | 181 +++++++++++ .../server/services/members/utils.test.js | 4 + ghost/members-api/lib/repositories/member.js | 3 +- 6 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap create mode 100644 ghost/core/test/e2e-api/members/middleware.test.js diff --git a/ghost/core/core/server/services/members/middleware.js b/ghost/core/core/server/services/members/middleware.js index 4a21a32598..e25fb83ad0 100644 --- a/ghost/core/core/server/services/members/middleware.js +++ b/ghost/core/core/server/services/members/middleware.js @@ -116,7 +116,7 @@ const updateMemberNewsletters = async function (req, res) { const updateMemberData = async function (req, res) { try { - const data = _.pick(req.body, 'name', 'subscribed', 'newsletters', 'enable_comment_notifications'); + const data = _.pick(req.body, 'name', 'bio', 'subscribed', 'newsletters', 'enable_comment_notifications'); const member = await membersService.ssr.getMemberDataFromSession(req, res); if (member) { const options = { diff --git a/ghost/core/core/server/services/members/utils.js b/ghost/core/core/server/services/members/utils.js index f93b353562..04ccfd7734 100644 --- a/ghost/core/core/server/services/members/utils.js +++ b/ghost/core/core/server/services/members/utils.js @@ -13,6 +13,7 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member email: member.email, name: member.name, firstname: member.name && member.name.split(' ')[0], + bio: member.bio, avatar_image: member.avatar_image, subscribed: !!member.subscribed, subscriptions: member.subscriptions || [], diff --git a/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap new file mode 100644 index 0000000000..b661ff79d1 --- /dev/null +++ b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap @@ -0,0 +1,291 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Comments API when authenticated can get member data 1: [body] 1`] = ` +Object { + "avatar_image": null, + "bio": null, + "email": "member@example.com", + "enable_comment_notifications": true, + "firstname": null, + "name": null, + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sort_order": 0, + }, + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "sort_order": 2, + }, + ], + "paid": false, + "subscribed": false, + "subscriptions": Array [], + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when authenticated can get member data 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": "430", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when authenticated can update comment notifications 1: [body] 1`] = ` +Object { + "avatar_image": null, + "bio": "Head of Testing", + "email": "member@example.com", + "enable_comment_notifications": false, + "firstname": "Test", + "name": "Test User", + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sort_order": 0, + }, + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "sort_order": 2, + }, + ], + "paid": false, + "subscribed": false, + "subscriptions": Array [], + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when authenticated can update comment notifications 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": "453", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when authenticated can update comment notifications 3: [body] 1`] = ` +Object { + "email": "member@example.com", + "enable_comment_notifications": true, + "name": "Test User", + "newsletters": Array [ + Object { + "body_font_category": "sans_serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sender_email": null, + "sender_name": null, + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "default-newsletter", + "sort_order": 0, + "status": "active", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "sans_serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + Object { + "body_font_category": "serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg", + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "sender_email": "jamie@example.com", + "sender_name": "Jamie", + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "weekly-newsletter", + "sort_order": 2, + "status": "active", + "subscribe_on_signup": true, + "title_alignment": "center", + "title_font_category": "serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], + "status": "free", + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when authenticated can update comment notifications 4: [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": "1506", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when authenticated can update member bio 1: [body] 1`] = ` +Object { + "avatar_image": null, + "bio": "Head of Testing", + "email": "member@example.com", + "enable_comment_notifications": true, + "firstname": null, + "name": null, + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sort_order": 0, + }, + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "sort_order": 2, + }, + ], + "paid": false, + "subscribed": false, + "subscriptions": Array [], + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when authenticated can update member bio 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": "443", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when authenticated can update name 1: [body] 1`] = ` +Object { + "avatar_image": null, + "bio": "Head of Testing", + "email": "member@example.com", + "enable_comment_notifications": true, + "firstname": "Test", + "name": "Test User", + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "sort_order": 0, + }, + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "sort_order": 2, + }, + ], + "paid": false, + "subscribed": false, + "subscriptions": Array [], + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when authenticated can update name 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": "452", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when not authenticated but enabled can update comment notifications 1: [body] 1`] = ` +Object { + "email": "member1@test.com", + "enable_comment_notifications": false, + "name": "Mr Egg", + "newsletters": Array [ + Object { + "body_font_category": "serif", + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "footer_content": null, + "header_image": "http://127.0.0.1:2369/content/images/2022/05/test.jpg", + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Daily newsletter", + "sender_email": "jamie@example.com", + "sender_name": "Jamie", + "sender_reply_to": "newsletter", + "show_badge": true, + "show_feature_image": true, + "show_header_icon": true, + "show_header_name": true, + "show_header_title": true, + "slug": "daily-newsletter", + "sort_order": 1, + "status": "active", + "subscribe_on_signup": false, + "title_alignment": "center", + "title_font_category": "serif", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "visibility": "members", + }, + ], + "status": "free", + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, +} +`; + +exports[`Comments API when not authenticated but enabled can update comment notifications 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": "858", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; diff --git a/ghost/core/test/e2e-api/members/middleware.test.js b/ghost/core/test/e2e-api/members/middleware.test.js new file mode 100644 index 0000000000..85f306fb15 --- /dev/null +++ b/ghost/core/test/e2e-api/members/middleware.test.js @@ -0,0 +1,181 @@ +const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework'); +const {anyEtag, anyObjectId, anyUuid, anyISODateTime} = matchers; +const models = require('../../../core/server/models'); +require('should'); + +let membersAgent; + +const memberMatcher = (newslettersCount) => { + return { + uuid: anyUuid, + newsletters: new Array(newslettersCount).fill( + { + id: anyObjectId + } + ) + }; +}; + +// @todo: we currently don't serialise the output of /api/member/newsletters/, we should fix this +const memberMatcherUnserialised = (newslettersCount) => { + return { + uuid: anyUuid, + newsletters: new Array(newslettersCount).fill( + { + id: anyObjectId, + uuid: anyUuid, + created_at: anyISODateTime, + updated_at: anyISODateTime + } + ) + }; +}; + +describe('Comments API', function () { + before(async function () { + membersAgent = await agentProvider.getMembersAPIAgent(); + + await fixtureManager.init('newsletters', 'members:newsletters'); + }); + + beforeEach(function () { + mockManager.mockMail(); + }); + + afterEach(function () { + mockManager.restore(); + }); + + describe('when not authenticated but enabled', function () { + it('can not get member data', async function () { + await membersAgent + .get(`/api/member/`) + .expectStatus(204) + .expectEmptyBody(); + }); + + it('can update comment notifications', async function () { + // Only via updateMemberNewsletters + let member = await models.Member.findOne({id: fixtureManager.get('members', 0).id}, {require: true}); + member.get('enable_comment_notifications').should.eql(true, 'This test requires the initial value to be true'); + + await membersAgent + .put(`/api/member/newsletters/?uuid=${member.get('uuid')}`) + .body({ + enable_comment_notifications: false + }) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcherUnserialised(1)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + body.enable_comment_notifications.should.eql(false); + }); + member = await models.Member.findOne({id: member.id}, {require: true}); + member.get('enable_comment_notifications').should.eql(false); + }); + }); + + describe('when authenticated', function () { + let member; + + before(async function () { + await membersAgent.loginAs('member@example.com'); + member = await models.Member.findOne({email: 'member@example.com'}, {require: true}); + }); + + it('can get member data', async function () { + await membersAgent + .get(`/api/member/`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcher(2)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + }); + }); + + it('can update member bio', async function () { + await membersAgent + .put(`/api/member/`) + .body({ + bio: 'Head of Testing' + }) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcher(2)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + body.bio.should.eql('Head of Testing'); + }); + member = await models.Member.findOne({id: member.id}, {require: true}); + member.get('bio').should.eql('Head of Testing'); + }); + + it('can update name', async function () { + await membersAgent + .put(`/api/member/`) + .body({ + name: 'Test User' + }) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcher(2)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + body.name.should.eql('Test User'); + body.firstname.should.eql('Test'); + }); + member = await models.Member.findOne({id: member.id}, {require: true}); + member.get('name').should.eql('Test User'); + }); + + it('can update comment notifications', async function () { + member.get('enable_comment_notifications').should.eql(true, 'This test requires the initial value to be true'); + + // Via general way + await membersAgent + .put(`/api/member/`) + .body({ + enable_comment_notifications: false + }) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcher(2)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + body.enable_comment_notifications.should.eql(false); + }); + member = await models.Member.findOne({id: member.id}, {require: true}); + member.get('enable_comment_notifications').should.eql(false); + + // Via updateMemberNewsletters + await membersAgent + .put(`/api/member/newsletters/?uuid=${member.get('uuid')}`) + .body({ + enable_comment_notifications: true + }) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(memberMatcherUnserialised(2)) + .expect(({body}) => { + body.email.should.eql(member.get('email')); + body.enable_comment_notifications.should.eql(true); + }); + member = await models.Member.findOne({id: member.id}, {require: true}); + member.get('enable_comment_notifications').should.eql(true); + }); + }); +}); diff --git a/ghost/core/test/unit/server/services/members/utils.test.js b/ghost/core/test/unit/server/services/members/utils.test.js index 3322b29015..763ef9e972 100644 --- a/ghost/core/test/unit/server/services/members/utils.test.js +++ b/ghost/core/test/unit/server/services/members/utils.test.js @@ -19,6 +19,7 @@ describe('Members Service - utils', function () { uuid: 'uuid-1', email: 'jamie+1@example.com', name: 'Jamie Larson', + bio: null, avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank', subscribed: true, status: 'free', @@ -29,6 +30,7 @@ describe('Members Service - utils', function () { uuid: 'uuid-1', email: 'jamie+1@example.com', name: 'Jamie Larson', + bio: null, firstname: 'Jamie', avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank', subscribed: true, @@ -43,6 +45,7 @@ describe('Members Service - utils', function () { uuid: 'uuid-1', email: 'jamie+1@example.com', name: 'Jamie Larson', + bio: 'Hello world', avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank', subscribed: true, status: 'comped', @@ -61,6 +64,7 @@ describe('Members Service - utils', function () { uuid: 'uuid-1', email: 'jamie+1@example.com', name: 'Jamie Larson', + bio: 'Hello world', firstname: 'Jamie', avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank', subscribed: true, diff --git a/ghost/members-api/lib/repositories/member.js b/ghost/members-api/lib/repositories/member.js index 0cb91402a1..f8370ae181 100644 --- a/ghost/members-api/lib/repositories/member.js +++ b/ghost/members-api/lib/repositories/member.js @@ -319,7 +319,8 @@ module.exports = class MemberRepository { 'newsletters', 'enable_comment_notifications', 'last_seen_at', - 'last_commented_at' + 'last_commented_at', + 'bio' ]); // Determine if we need to fetch the initial member with relations