From 38360309500b64657b99c664c7d236478f9fb0de Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Wed, 11 May 2022 22:26:03 +0530 Subject: [PATCH] Allowed `tiers` include and data for member endpoints (#14790) refs https://github.com/TryGhost/Team/issues/1145 - allows members endpoint to accept `?include=tiers` - allows members endpoint to return `tiers` data --- core/server/api/canary/members.js | 2 +- .../canary/utils/serializers/input/members.js | 18 +++ .../utils/serializers/output/members.js | 1 + .../admin/__snapshots__/members.test.js.snap | 132 ++++++++++++++---- test/e2e-api/admin/members.test.js | 44 ++++-- .../utils/serializers/input/members.test.js | 19 +++ .../utils/serializers/output/members.test.js | 31 ++++ test/utils/fixtures/data-generator.js | 13 ++ 8 files changed, 226 insertions(+), 34 deletions(-) create mode 100644 test/unit/api/canary/utils/serializers/input/members.test.js diff --git a/core/server/api/canary/members.js b/core/server/api/canary/members.js index a8fc972425..ba6b34b14b 100644 --- a/core/server/api/canary/members.js +++ b/core/server/api/canary/members.js @@ -29,7 +29,7 @@ const messages = { resourceNotFound: '{resource} not found.' }; -const allowedIncludes = ['email_recipients', 'products']; +const allowedIncludes = ['email_recipients', 'products', 'tiers']; module.exports = { docName: 'members', diff --git a/core/server/api/canary/utils/serializers/input/members.js b/core/server/api/canary/utils/serializers/input/members.js index b90638fab4..5f2e6bb2ea 100644 --- a/core/server/api/canary/utils/serializers/input/members.js +++ b/core/server/api/canary/utils/serializers/input/members.js @@ -16,6 +16,19 @@ function defaultRelations(frame) { } module.exports = { + all(_apiConfig, frame) { + if (!frame.options.withRelated) { + return; + } + + frame.options.withRelated = frame.options.withRelated.map((relation) => { + if (relation === 'tiers') { + return 'products'; + } + return relation; + }); + }, + browse(apiConfig, frame) { debug('browse'); defaultRelations(frame); @@ -64,6 +77,11 @@ module.exports = { } }); } + + if (frame.data.members[0].tiers) { + frame.data.members[0].products = frame.data.members[0].tiers; + } + defaultRelations(frame); }, diff --git a/core/server/api/canary/utils/serializers/output/members.js b/core/server/api/canary/utils/serializers/output/members.js index cf3e13f539..e9b8e54ada 100644 --- a/core/server/api/canary/utils/serializers/output/members.js +++ b/core/server/api/canary/utils/serializers/output/members.js @@ -128,6 +128,7 @@ function serializeMember(member, options) { if (json.products) { serialized.products = json.products; + serialized.tiers = json.products; } if (labsService.isSet('multipleNewsletters')) { diff --git a/test/e2e-api/admin/__snapshots__/members.test.js.snap b/test/e2e-api/admin/__snapshots__/members.test.js.snap index 1a3b054d44..e4230a137f 100644 --- a/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -52,6 +52,7 @@ Object { "status": "free", "subscribed": false, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -63,7 +64,7 @@ exports[`Members API Can add 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "625", + "content-length": "636", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -94,6 +95,7 @@ Object { "status": "free", "subscribed": false, "subscriptions": Array [], + "tiers": Array [], "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\\}/, }, @@ -105,7 +107,7 @@ exports[`Members API Can add a member that is not subscribed (old) 2: [headers] Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "501", + "content-length": "512", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -173,6 +175,7 @@ Object { "status": "active", }, ], + "tiers": Array [], "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\\}/, }, @@ -184,7 +187,7 @@ exports[`Members API Can add a subscription 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2055", + "content-length": "2066", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -251,6 +254,7 @@ Object { "status": "active", }, ], + "tiers": Array [], "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\\}/, }, @@ -262,7 +266,7 @@ exports[`Members API Can add a subscription 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2055", + "content-length": "2066", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -319,6 +323,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -330,7 +335,7 @@ exports[`Members API Can add and edit with custom newsletters 2: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1294", + "content-length": "1305", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -388,6 +393,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -399,7 +405,7 @@ exports[`Members API Can add and edit with custom newsletters 4: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1293", + "content-length": "1304", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -482,6 +488,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Array [], + "tiers": Array [], "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\\}/, }, @@ -493,7 +500,7 @@ exports[`Members API Can add and send a signup confirmation email (old) 2: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1789", + "content-length": "1800", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -587,6 +594,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Array [], + "tiers": Array [], "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\\}/, }, @@ -598,7 +606,7 @@ exports[`Members API Can add and send a signup confirmation email 2: [headers] 1 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1784", + "content-length": "1795", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": Any, @@ -639,6 +647,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -650,7 +659,7 @@ exports[`Members API Can add complimentary subscription (out of date) 2: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1103", + "content-length": "1114", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -681,6 +690,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -692,7 +702,7 @@ exports[`Members API Can add complimentary subscription (out of date) 4: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1103", + "content-length": "1114", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -1046,6 +1056,7 @@ Object { "status": "comped", "subscribed": true, "subscriptions": Any, + "tiers": Any, "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\\}/, }, @@ -1057,7 +1068,7 @@ exports[`Members API Can create a member with an existing complimentary subscrip Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2433", + "content-length": "2802", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1115,6 +1126,7 @@ Object { "status": "paid", "subscribed": true, "subscriptions": Any, + "tiers": Any, "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\\}/, }, @@ -1126,7 +1138,7 @@ exports[`Members API Can create a member with an existing paid subscription 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2508", + "content-length": "2877", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1199,6 +1211,22 @@ Object { "status": "comped", "subscribed": true, "subscriptions": Any, + "tiers": Array [ + Object { + "active": true, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Product", + "slug": "default-product", + "type": "paid", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "visibility": "public", + "welcome_page_url": "/welcome-paid", + "yearly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + ], "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\\}/, }, @@ -1210,7 +1238,7 @@ exports[`Members API Can create a new member with a product (complementary) 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2389", + "content-length": "2758", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1251,6 +1279,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -1262,7 +1291,7 @@ exports[`Members API Can destroy 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1759", + "content-length": "1770", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1333,6 +1362,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -1344,7 +1374,7 @@ exports[`Members API Can edit by id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1121", + "content-length": "1132", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -1375,6 +1405,7 @@ Object { "status": "free", "subscribed": false, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -1386,7 +1417,7 @@ exports[`Members API Can edit by id 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "476", + "content-length": "487", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2294,6 +2325,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2305,7 +2337,7 @@ exports[`Members API Can read 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1254", + "content-length": "1265", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2336,6 +2368,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2347,7 +2380,49 @@ exports[`Members API Can read and include email_recipients 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1276", + "content-length": "1287", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Members API Can read and include tiers 1: [body] 1`] = ` +Object { + "members": Array [ + Object { + "avatar_image": null, + "comped": false, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "email": "member1@test.com", + "email_count": 0, + "email_open_rate": null, + "email_opened_count": 0, + "geolocation": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "labels": Any, + "last_seen_at": null, + "name": "Mr Egg", + "newsletters": Any, + "note": null, + "products": Array [], + "status": "free", + "subscribed": true, + "subscriptions": Any, + "tiers": Any, + "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\\}/, + }, + ], +} +`; + +exports[`Members API Can read and include tiers 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "1265", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2377,6 +2452,7 @@ Object { "status": "free", "subscribed": false, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2388,7 +2464,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "483", + "content-length": "494", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -2472,6 +2548,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2483,7 +2560,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1773", + "content-length": "1784", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2513,6 +2590,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2524,7 +2602,7 @@ exports[`Members API Can subscribe to a newsletter 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1111", + "content-length": "1122", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -2555,6 +2633,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2566,7 +2645,7 @@ exports[`Members API Can subscribe to a newsletter 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1116", + "content-length": "1127", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2765,6 +2844,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2776,7 +2856,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1129", + "content-length": "1140", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -2807,6 +2887,7 @@ Object { "status": "free", "subscribed": false, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -2818,7 +2899,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "488", + "content-length": "499", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -3116,6 +3197,7 @@ Object { "status": "free", "subscribed": true, "subscriptions": Any, + "tiers": Array [], "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\\}/, }, @@ -3127,7 +3209,7 @@ exports[`Members API Subscribes to default newsletters 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1760", + "content-length": "1771", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, diff --git a/test/e2e-api/admin/members.test.js b/test/e2e-api/admin/members.test.js index 526e8dbe6e..1c33e8b43c 100644 --- a/test/e2e-api/admin/members.test.js +++ b/test/e2e-api/admin/members.test.js @@ -14,7 +14,7 @@ const models = require('../../../core/server/models'); async function assertMemberEvents({eventType, memberId, asserts}) { const events = await models[eventType].where('member_id', memberId).fetchAll(); const eventsJSON = events.map(e => e.toJSON()); - + // Order shouldn't matter here for (const a of asserts) { eventsJSON.should.matchAny(a); @@ -77,6 +77,11 @@ const memberMatcherShallowIncludes = { newsletters: anyArray }; +const memberMatcherShallowIncludesWithTiers = { + ...memberMatcherShallowIncludes, + tiers: anyArray +}; + let agent; describe('Members API without Stripe', function () { @@ -319,6 +324,18 @@ describe('Members API', function () { }); }); + it('Can read and include tiers', async function () { + await agent + .get(`/members/${testUtils.DataGenerator.Content.members[0].id}/?include=tiers`) + .expectStatus(200) + .matchBodySnapshot({ + members: new Array(1).fill(memberMatcherShallowIncludesWithTiers) + }) + .matchHeaderSnapshot({ + etag: anyEtag + }); + }); + // Create a member it('Can add', async function () { @@ -577,7 +594,7 @@ describe('Members API', function () { const compedPayload = { id: newMember.id, email: newMember.email, - products: [ + tiers: [ { id: product.id } @@ -592,6 +609,7 @@ describe('Members API', function () { const updatedMember = body2.members[0]; assert.equal(updatedMember.status, 'comped', 'A comped member should have the comped status'); assert.equal(updatedMember.products.length, 1, 'The member should have one product'); + assert.equal(updatedMember.tiers.length, 1, 'The member should have one product'); await assertMemberEvents({ eventType: 'MemberStatusEvent', @@ -631,7 +649,7 @@ describe('Members API', function () { name: 'Name', email: 'compedtest3@test.com', newsletters: [newsletters[0]], - products: [ + tiers: [ { id: product.id } @@ -645,13 +663,13 @@ describe('Members API', function () { const newMember = body.members[0]; assert.equal(newMember.status, 'comped', 'The new member should have the comped status'); - assert.equal(newMember.products.length, 1, 'The member should have 1 product'); + assert.equal(newMember.tiers.length, 1, 'The member should have 1 product'); // Remove it const removePayload = { id: newMember.id, email: newMember.email, - products: [] + tiers: [] }; const {body: body2} = await agent @@ -662,6 +680,7 @@ describe('Members API', function () { const updatedMember = body2.members[0]; assert.equal(updatedMember.status, 'free', 'The member should have the free status'); assert.equal(updatedMember.products.length, 0, 'The member should have 0 products'); + assert.equal(updatedMember.tiers.length, 0, 'The member should have 0 products'); await assertMemberEvents({ eventType: 'MemberStatusEvent', @@ -704,7 +723,7 @@ describe('Members API', function () { email: 'compedtest4@test.com', subscribed: true, newsletters: [newsletters[0]], - products: [ + tiers: [ { id: product.id } @@ -730,6 +749,13 @@ describe('Members API', function () { created_at: anyISODateTime, updated_at: anyISODateTime }), + tiers: new Array(1).fill({ + id: anyObjectId, + monthly_price_id: anyObjectId, + yearly_price_id: anyObjectId, + created_at: anyISODateTime, + updated_at: anyISODateTime + }), newsletters: new Array(1).fill(newsletterSnapshot) }) }) @@ -845,6 +871,7 @@ describe('Members API', function () { labels: anyArray, subscriptions: anyArray, products: anyArray, + tiers: anyArray, newsletters: new Array(1).fill(newsletterSnapshot) }) }) @@ -973,6 +1000,7 @@ describe('Members API', function () { labels: anyArray, subscriptions: anyArray, products: anyArray, + tiers: anyArray, newsletters: new Array(1).fill(newsletterSnapshot) }) }) @@ -1046,7 +1074,7 @@ describe('Members API', function () { const readMember = readBody.members[0]; // Note that we explicitly need to ask to include products while browsing - const {body: browseBody} = await agent.get(`/members/?search=${memberWithPaidSubscription.email}&include=products`); + const {body: browseBody} = await agent.get(`/members/?search=${memberWithPaidSubscription.email}&include=tiers`); assert.equal(browseBody.members.length, 1, 'The member was not found in browse'); const browseMember = browseBody.members[0]; @@ -1229,7 +1257,7 @@ describe('Members API', function () { const after = new Date(); after.setMilliseconds(0); - + await agent .put(`/members/${newMember.id}/`) .body({members: [memberChanged]}) diff --git a/test/unit/api/canary/utils/serializers/input/members.test.js b/test/unit/api/canary/utils/serializers/input/members.test.js new file mode 100644 index 0000000000..dd1d3eb23c --- /dev/null +++ b/test/unit/api/canary/utils/serializers/input/members.test.js @@ -0,0 +1,19 @@ +const should = require('should'); +const serializers = require('../../../../../../../core/server/api/canary/utils/serializers'); + +describe('Unit: canary/utils/serializers/input/members', function () { + describe('all', function () { + it('converts tiers include', function () { + const apiConfig = {}; + const frame = { + options: { + context: {}, + withRelated: ['tiers'] + } + }; + + serializers.input.members.all(apiConfig, frame); + should(frame.options.withRelated).containEql('products'); + }); + }); +}); diff --git a/test/unit/api/canary/utils/serializers/output/members.test.js b/test/unit/api/canary/utils/serializers/output/members.test.js index e5435240ca..2d532c9aaa 100644 --- a/test/unit/api/canary/utils/serializers/output/members.test.js +++ b/test/unit/api/canary/utils/serializers/output/members.test.js @@ -34,6 +34,23 @@ describe('Unit: canary/utils/serializers/output/members', function () { should.exist(frame.response.members[0].newsletters); }); + it('browse: includes tiers data', function () { + const apiConfig = {docName: 'members'}; + const frame = { + options: { + context: {} + } + }; + + const ctrlResponse = memberModel(testUtils.DataGenerator.forKnex.createMemberWithProducts()); + memberSerializer.browse({ + data: [ctrlResponse], + meta: null + }, apiConfig, frame); + + should.exist(frame.response.members[0].tiers); + }); + it('browse: removes newsletter data when flag is disabled', function () { labsStub.returns(false); const apiConfig = {docName: 'members'}; @@ -77,4 +94,18 @@ describe('Unit: canary/utils/serializers/output/members', function () { memberSerializer.read(ctrlResponse, apiConfig, frame); should.not.exist(frame.response.members[0].newsletters); }); + + it('read: includes tiers data', function () { + const apiConfig = {docName: 'members'}; + const frame = { + options: { + context: {} + } + }; + + const ctrlResponse = memberModel(testUtils.DataGenerator.forKnex.createMemberWithProducts()); + memberSerializer.read(ctrlResponse, apiConfig, frame); + + should.exist(frame.response.members[0].tiers); + }); }); diff --git a/test/utils/fixtures/data-generator.js b/test/utils/fixtures/data-generator.js index e0caabaf96..3182f4101c 100644 --- a/test/utils/fixtures/data-generator.js +++ b/test/utils/fixtures/data-generator.js @@ -1015,6 +1015,18 @@ DataGenerator.forKnex = (function () { }); } + function createMemberWithProducts(overrides) { + const newObj = _.cloneDeep(overrides); + + return _.defaults(newObj, { + id: ObjectId().toHexString(), + email: 'member@ghost.org', + products: [{ + id: 'product-1' + }] + }); + } + function createLabel(overrides) { const newObj = _.cloneDeep(overrides); @@ -1530,6 +1542,7 @@ DataGenerator.forKnex = (function () { createToken, createMember, createMemberWithNewsletter, + createMemberWithProducts, createLabel, createMembersLabels, createMembersStripeCustomer: createBasic,