mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
🐛 Fixed complimentary members' content gating (#12761)
no issue Comped members were not able to view paid-member content because content gating was only looking for `member.status === 'paid'` which doesn't take into consideration members on a "complimentary" plan. - added front-end acceptance tests for member access to posts - updated content-gating check to take comped members into consideration
This commit is contained in:
parent
e87ded8d72
commit
19d5448101
6 changed files with 279 additions and 28 deletions
|
@ -23,7 +23,7 @@ function checkPostAccess(post, member) {
|
|||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
if (post.visibility === 'paid' && member.status === 'paid') {
|
||||
if (post.visibility === 'paid' && (member.status === 'paid' || member.status === 'comped' || member.comped)) {
|
||||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
|
@ -45,7 +45,7 @@ describe('Members API', function () {
|
|||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 4);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 5);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
|
@ -393,8 +393,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 4 from fixtures, 2 from above posts, 2 from above import
|
||||
jsonResponse.total.should.equal(8);
|
||||
// 5 from fixtures, 2 from above posts, 2 from above import
|
||||
jsonResponse.total.should.equal(9);
|
||||
|
||||
return jsonResponse;
|
||||
}
|
||||
|
@ -427,8 +427,8 @@ describe('Members API', function () {
|
|||
dateStr = currentRangeDate.format('YYYY-MM-DD');
|
||||
|
||||
// set the end date to match the number of members added from fixtures posts and imports
|
||||
// 4 from fixtures, 2 from above posts, 2 from above import
|
||||
output[dateStr] = 8;
|
||||
// 5 from fixtures, 2 from above posts, 2 from above import
|
||||
output[dateStr] = 9;
|
||||
|
||||
// deep equality check that the objects match...
|
||||
jsonResponse.total_on_date.should.eql(output);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const supertest = require('supertest');
|
||||
const moment = require('moment');
|
||||
const testUtils = require('../utils');
|
||||
const configUtils = require('../utils/configUtils');
|
||||
const settingsCache = require('../../core/server/services/settings/cache');
|
||||
|
@ -8,11 +9,12 @@ const settingsCache = require('../../core/server/services/settings/cache');
|
|||
// @TODO: if only this suite is run some of the tests will fail due to
|
||||
// wrong template loading issues which would need to be investigated
|
||||
// As a workaround run it with some of other tests e.g. "frontend_spec"
|
||||
describe('Basic Members Routes', function () {
|
||||
describe('Front-end members behaviour', function () {
|
||||
let request;
|
||||
|
||||
before(async function () {
|
||||
await testUtils.startGhost();
|
||||
await testUtils.initFixtures('members');
|
||||
request = supertest.agent(configUtils.config.get('url'));
|
||||
});
|
||||
|
||||
|
@ -32,7 +34,7 @@ describe('Basic Members Routes', function () {
|
|||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Routes', function () {
|
||||
describe('Member routes', function () {
|
||||
it('should error serving webhook endpoint without any parameters', async function () {
|
||||
await request.post('/members/webhooks/stripe')
|
||||
.expect(400);
|
||||
|
@ -90,4 +92,221 @@ describe('Basic Members Routes', function () {
|
|||
.expect('Location', '/?action=signup&success=false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content gating', function () {
|
||||
let publicPost;
|
||||
let membersPost;
|
||||
let paidPost;
|
||||
let membersPostWithPaywallCard;
|
||||
|
||||
before(function () {
|
||||
publicPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'free-to-see',
|
||||
visibility: 'public',
|
||||
published_at: moment().add(15, 'seconds').toDate() // here to ensure sorting is not modified
|
||||
});
|
||||
|
||||
membersPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-not-be-seen',
|
||||
visibility: 'members',
|
||||
published_at: moment().add(45, 'seconds').toDate() // here to ensure sorting is not modified
|
||||
});
|
||||
|
||||
paidPost = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-be-paid-for',
|
||||
visibility: 'paid',
|
||||
published_at: moment().add(30, 'seconds').toDate() // here to ensure sorting is not modified
|
||||
});
|
||||
|
||||
membersPostWithPaywallCard = testUtils.DataGenerator.forKnex.createPost({
|
||||
slug: 'thou-shalt-have-a-taste',
|
||||
visibility: 'members',
|
||||
mobiledoc: '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["paywall",{}]],"sections":[[1,"p",[[0,[],0,"Free content"]]],[10,0],[1,"p",[[0,[],0,"Members content"]]]]}',
|
||||
html: '<p>Free content</p><!--members-only--><p>Members content</p>',
|
||||
published_at: moment().add(5, 'seconds').toDate()
|
||||
});
|
||||
|
||||
return testUtils.fixtures.insertPosts([
|
||||
publicPost,
|
||||
membersPost,
|
||||
paidPost,
|
||||
membersPostWithPaywallCard
|
||||
]);
|
||||
});
|
||||
|
||||
describe('as non-member', function () {
|
||||
it('can read public post content', function () {
|
||||
return request
|
||||
.get('/free-to-see/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read members post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-not-be-seen/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
it('cannot read paid post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-be-paid-for/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as free member', function () {
|
||||
before(async function () {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink('member1@test.com');
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read public post content', function () {
|
||||
return request
|
||||
.get('/free-to-see/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read members post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-not-be-seen/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot read paid post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-be-paid-for/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.not.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as paid member', function () {
|
||||
before(async function () {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink('paid@test.com');
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read public post content', function () {
|
||||
return request
|
||||
.get('/free-to-see/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read members post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-not-be-seen/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read paid post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-be-paid-for/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as comped member', function () {
|
||||
before(async function () {
|
||||
// membersService needs to be required after Ghost start so that settings
|
||||
// are pre-populated with defaults
|
||||
const membersService = require('../../core/server/services/members');
|
||||
|
||||
const signinLink = await membersService.api.getMagicLink('comped@test.com');
|
||||
const signinURL = new URL(signinLink);
|
||||
// request needs a relative path rather than full url with host
|
||||
const signinPath = `${signinURL.pathname}${signinURL.search}`;
|
||||
|
||||
// perform a sign-in request to set members cookies on superagent
|
||||
await request.get(signinPath)
|
||||
.expect(302)
|
||||
.then((res) => {
|
||||
const redirectUrl = new URL(res.headers.location, testUtils.API.getURL());
|
||||
should.exist(redirectUrl.searchParams.get('success'));
|
||||
redirectUrl.searchParams.get('success').should.eql('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read public post content', function () {
|
||||
return request
|
||||
.get('/free-to-see/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read members post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-not-be-seen/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
|
||||
it('can read paid post content', function () {
|
||||
return request
|
||||
.get('/thou-shalt-be-paid-for/')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.members);
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('paid@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(80);
|
||||
|
@ -63,7 +63,7 @@ describe('Members API', function () {
|
|||
.then((res) => {
|
||||
const jsonResponse = res.body;
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('member2@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(50);
|
||||
|
@ -581,8 +581,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -605,8 +605,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -629,8 +629,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Members API', function () {
|
|||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.members);
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('paid@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(80);
|
||||
|
@ -63,7 +63,7 @@ describe('Members API', function () {
|
|||
.then((res) => {
|
||||
const jsonResponse = res.body;
|
||||
localUtils.API.checkResponse(jsonResponse, 'members');
|
||||
jsonResponse.members.should.have.length(4);
|
||||
jsonResponse.members.should.have.length(5);
|
||||
|
||||
jsonResponse.members[0].email.should.equal('member2@test.com');
|
||||
jsonResponse.members[0].email_open_rate.should.equal(50);
|
||||
|
@ -581,8 +581,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -605,8 +605,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -629,8 +629,8 @@ describe('Members API', function () {
|
|||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 3 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(10);
|
||||
// 5 from fixtures and 6 imported in previous tests
|
||||
jsonResponse.total.should.equal(11);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -334,6 +334,13 @@ DataGenerator.Content = {
|
|||
name: 'Ray Stantz',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343',
|
||||
status: 'paid'
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
email: 'comped@test.com',
|
||||
name: 'Vinz Clortho',
|
||||
uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b344',
|
||||
status: 'comped'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -364,6 +371,12 @@ DataGenerator.Content = {
|
|||
customer_id: 'cus_HR3tBmNhx4QsZZ',
|
||||
name: 'Ray Stantz',
|
||||
email: 'trialing@test.com'
|
||||
},
|
||||
{
|
||||
member_id: null, // relation added later
|
||||
customer_id: 'cus_HR3tBmNhx4QsZ0',
|
||||
name: 'Vinz Clortho',
|
||||
email: 'comped@test.com'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -397,6 +410,21 @@ DataGenerator.Content = {
|
|||
plan_interval: 'month',
|
||||
plan_amount: '1000',
|
||||
plan_currency: 'usd'
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
customer_id: 'cus_HR3tBmNhx4QsZ0',
|
||||
subscription_id: 'sub_HR3tLNgGAHsa7d',
|
||||
plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0',
|
||||
status: 'active',
|
||||
cancel_at_period_end: true,
|
||||
current_period_end: '2025-07-09 19:01:20',
|
||||
start_date: '2020-06-09 19:01:20',
|
||||
default_payment_card_last4: '4242',
|
||||
plan_nickname: 'Complimentary',
|
||||
plan_interval: 'year',
|
||||
plan_amount: '0',
|
||||
plan_currency: 'usd'
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -562,6 +590,7 @@ DataGenerator.Content.email_recipients[3].email_id = DataGenerator.Content.email
|
|||
DataGenerator.Content.email_recipients[3].member_id = DataGenerator.Content.members[3].id;
|
||||
DataGenerator.Content.members_stripe_customers[0].member_id = DataGenerator.Content.members[2].id;
|
||||
DataGenerator.Content.members_stripe_customers[1].member_id = DataGenerator.Content.members[3].id;
|
||||
DataGenerator.Content.members_stripe_customers[2].member_id = DataGenerator.Content.members[4].id;
|
||||
|
||||
DataGenerator.forKnex = (function () {
|
||||
function createBasic(overrides) {
|
||||
|
@ -1076,7 +1105,8 @@ DataGenerator.forKnex = (function () {
|
|||
createMember(DataGenerator.Content.members[0]),
|
||||
createMember(DataGenerator.Content.members[1]),
|
||||
createMember(DataGenerator.Content.members[2]),
|
||||
createMember(DataGenerator.Content.members[3])
|
||||
createMember(DataGenerator.Content.members[3]),
|
||||
createMember(DataGenerator.Content.members[4])
|
||||
];
|
||||
|
||||
const labels = [
|
||||
|
@ -1092,12 +1122,14 @@ DataGenerator.forKnex = (function () {
|
|||
|
||||
const members_stripe_customers = [
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[0]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[1])
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[1]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[2])
|
||||
];
|
||||
|
||||
const stripe_customer_subscriptions = [
|
||||
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[0]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[1])
|
||||
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[1]),
|
||||
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[2])
|
||||
];
|
||||
|
||||
const snippets = [
|
||||
|
|
Loading…
Add table
Reference in a new issue