diff --git a/core/frontend/services/theme-engine/middleware/update-local-template-options.js b/core/frontend/services/theme-engine/middleware/update-local-template-options.js index 4d06331f24..3b31d249c6 100644 --- a/core/frontend/services/theme-engine/middleware/update-local-template-options.js +++ b/core/frontend/services/theme-engine/middleware/update-local-template-options.js @@ -33,7 +33,9 @@ function updateLocalTemplateOptions(req, res, next) { default_payment_card_last4: sub.default_payment_card_last4 || '****' }); }), - paid: req.member.status !== 'free' + paid: req.member.status !== 'free', + status: req.member.status, + products: req.member.products } : null; hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, { diff --git a/test/e2e-frontend/helpers/get.test.js b/test/e2e-frontend/helpers/get.test.js new file mode 100644 index 0000000000..878fa673e1 --- /dev/null +++ b/test/e2e-frontend/helpers/get.test.js @@ -0,0 +1,292 @@ +const should = require('should'); +const sinon = require('sinon'); +const testUtils = require('../../utils'); +const models = require('../../../core/server/models/index'); + +const API_VERSION = 'canary'; +const DEFAULT_POST_FIXTURE_COUNT = 7; + +const get = require('../../../core/frontend/helpers/get'); + +async function createPost(data) { + const post = testUtils.DataGenerator.forKnex.createPost(data); + await models.Post.add(post, {context: {internal: true}}); + return post; +} + +function buildMember(status, products = []) { + return { + uuid: '1234', + email: 'test@example.com', + name: 'John Doe', + firstname: 'John', + avatar_image: null, + subscriptions: [], + paid: status !== 'free', + status: status, + products + }; +} + +function testPosts(posts, map) { + posts.should.be.an.Array(); + posts.length.should.eql(DEFAULT_POST_FIXTURE_COUNT + Object.keys(map).length); + + // Free post + for (const postID in map) { + const expectData = map[postID]; + + const post = posts.find(p => p.id === postID); + should.exist(post); + + post.should.match(expectData); + } +} + +describe('e2e {{#get}} helper', function () { + let fn; + let inverse; + let locals = {}; + let publicPost, membersPost, paidPost, basicTierPost; + + before(async function () { + await testUtils.startGhost({ + backend: true, + frontend: false + }); + + publicPost = await createPost({ + slug: 'free-to-see', + visibility: 'public', + published_at: new Date() // here to ensure sorting is not modified + }); + + membersPost = await createPost({ + slug: 'members-post', + visibility: 'members', + published_at: new Date() // here to ensure sorting is not modified + }); + + paidPost = await createPost({ + slug: 'paid-to-see', + visibility: 'paid', + published_at: new Date() // here to ensure sorting is not modified + }); + + basicTierPost = await createPost({ + slug: 'tiers-post', + visibility: 'tiers', + tiers: [{ + slug: 'default-product' + }], + published_at: new Date() // here to ensure sorting is not modified + }); + }); + + // Assert fixtures are correct + it('has valid fixtures', function () { + publicPost.visibility.should.eql('public'); + membersPost.visibility.should.eql('members'); + paidPost.visibility.should.eql('paid'); + basicTierPost.visibility.should.eql('tiers'); + }); + + beforeEach(function () { + fn = sinon.spy(); + inverse = sinon.spy(); + locals = {root: {_locals: {apiVersion: API_VERSION}}}; + }); + + describe('{{access}} property', function () { + let member; + + it('not authenticated member', async function () { + member = null; + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: false + }, + [paidPost.id]: { + access: false + }, + [basicTierPost.id]: { + access: false + } + }); + }); + + it('free member', async function () { + member = buildMember('free'); + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: false + }, + [basicTierPost.id]: { + access: false + } + }); + }); + + it('paid member', async function () { + member = buildMember('paid'); + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: true + }, + [basicTierPost.id]: { + access: false + } + }); + }); + + it('comped member', async function () { + member = buildMember('comped'); + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: true + }, + [basicTierPost.id]: { + access: false + } + }); + }); + + /** + * When using the get helper, you need to include tiers to properly determine {{access}} for posts with specific tiers + */ + it('tiers member not including tiers', async function () { + member = buildMember('paid', [{ + name: 'Default Product', + slug: 'default-product', + type: 'paid', + active: true + }]); + + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: true + }, + [basicTierPost.id]: { + access: false + } + }); + }); + + it('tiers member including tiers', async function () { + member = buildMember('paid', [{ + name: 'Default Product', + slug: 'default-product', + type: 'paid', + active: true + }]); + + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {include: 'tiers'}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: true + }, + [basicTierPost.id]: { + access: true + } + }); + }); + + it('tiers member with different product', async function () { + member = buildMember('paid', [{ + name: 'Default Product', + slug: 'pro-product', + type: 'paid', + active: true + }]); + + locals = {root: {_locals: {apiVersion: API_VERSION}}, member}; + await get.call( + {}, + 'posts', + {hash: {include: 'tiers'}, data: locals, fn: fn, inverse: inverse} + ); + testPosts(fn.firstCall.args[0].posts, { + [publicPost.id]: { + access: true + }, + [membersPost.id]: { + access: true + }, + [paidPost.id]: { + access: true + }, + [basicTierPost.id]: { + access: false + } + }); + }); + }); +}); \ No newline at end of file diff --git a/test/e2e-frontend/helpers/next_post.test.js b/test/e2e-frontend/helpers/next_post.test.js new file mode 100644 index 0000000000..43c07f3c7d --- /dev/null +++ b/test/e2e-frontend/helpers/next_post.test.js @@ -0,0 +1,332 @@ +const should = require('should'); +const sinon = require('sinon'); +const testUtils = require('../../utils'); +const models = require('../../../core/server/models/index'); + +const API_VERSION = 'canary'; + +const next_post = require('../../../core/frontend/helpers/prev_post'); + +async function createPost(data) { + const post = testUtils.DataGenerator.forKnex.createPost(data); + await models.Post.add(post, {context: {internal: true}}); + return post; +} + +function buildMember(status, products = []) { + return { + uuid: '1234', + email: 'test@example.com', + name: 'John Doe', + firstname: 'John', + avatar_image: null, + subscriptions: [], + paid: status !== 'free', + status: status, + products + }; +} + +describe('e2e {{#next_post}} helper', function () { + let fn; + let inverse; + let publicPost, membersPost, paidPost, basicTierPost, publicPost2; + + before(async function () { + await testUtils.startGhost({ + backend: true, + frontend: false + }); + + publicPost = await createPost({ + slug: 'free-to-see', + visibility: 'public', + published_at: new Date(2020, 0, 1) // here to ensure sorting is not modified + }); + + membersPost = await createPost({ + slug: 'members-post', + visibility: 'members', + published_at: new Date(2020, 0, 2) // here to ensure sorting is not modified + }); + + paidPost = await createPost({ + slug: 'paid-to-see', + visibility: 'paid', + published_at: new Date(2020, 0, 3) // here to ensure sorting is not modified + }); + + basicTierPost = await createPost({ + slug: 'tiers-post', + visibility: 'tiers', + tiers: [{ + slug: 'default-product' + }], + published_at: new Date(2020, 0, 4) // here to ensure sorting is not modified + }); + + publicPost2 = await createPost({ + slug: 'free-to-see', + visibility: 'public', + published_at: new Date(2020, 0, 5) // here to ensure sorting is not modified + }); + }); + + // Assert fixtures are correct + it('has valid fixtures', function () { + publicPost.visibility.should.eql('public'); + membersPost.visibility.should.eql('members'); + paidPost.visibility.should.eql('paid'); + basicTierPost.visibility.should.eql('tiers'); + publicPost2.visibility.should.eql('public'); + }); + + beforeEach(function () { + fn = sinon.spy(); + inverse = sinon.spy(); + }); + + describe('{{access}} property', function () { + describe('not authenticated member', function () { + const member = null; + const locals = { + root: { + _locals: { + apiVersion: API_VERSION + }, + context: ['post'] + }, + member + }; + let optionsData; + + beforeEach(function () { + optionsData = {name: 'next_post', data: locals, fn, inverse}; + }); + + it('next members post', async function () { + await next_post + .call(publicPost, optionsData); + + fn.firstCall.args[0].should.match({id: membersPost.id, access: false}); + }); + + it('next paid post', async function () { + await next_post + .call(membersPost, optionsData); + + fn.firstCall.args[0].should.match({id: paidPost.id, access: false}); + }); + + it('next tiers post', async function () { + await next_post + .call(paidPost, optionsData); + + fn.firstCall.args[0].should.match({id: basicTierPost.id, access: false}); + }); + + it('next public post', async function () { + await next_post + .call(basicTierPost, optionsData); + + fn.firstCall.args[0].should.match({id: publicPost2.id, access: true}); + }); + }); + + describe('free member', function () { + const member = buildMember('free'); + const locals = { + root: { + _locals: { + apiVersion: API_VERSION + }, + context: ['post'] + }, + member + }; + let optionsData; + + beforeEach(function () { + optionsData = {name: 'next_post', data: locals, fn, inverse}; + }); + + it('next members post', async function () { + await next_post + .call(publicPost, optionsData); + + fn.firstCall.args[0].should.match({id: membersPost.id, access: true}); + }); + + it('next paid post', async function () { + await next_post + .call(membersPost, optionsData); + + fn.firstCall.args[0].should.match({id: paidPost.id, access: false}); + }); + + it('next tiers post', async function () { + await next_post + .call(paidPost, optionsData); + + fn.firstCall.args[0].should.match({id: basicTierPost.id, access: false}); + }); + + it('next public post', async function () { + await next_post + .call(basicTierPost, optionsData); + + fn.firstCall.args[0].should.match({id: publicPost2.id, access: true}); + }); + }); + + describe('paid member', function () { + const member = buildMember('paid'); + const locals = { + root: { + _locals: { + apiVersion: API_VERSION + }, + context: ['post'] + }, + member + }; + let optionsData; + + beforeEach(function () { + optionsData = {name: 'next_post', data: locals, fn, inverse}; + }); + + it('next members post', async function () { + await next_post + .call(publicPost, optionsData); + + fn.firstCall.args[0].should.match({id: membersPost.id, access: true}); + }); + + it('next paid post', async function () { + await next_post + .call(membersPost, optionsData); + + fn.firstCall.args[0].should.match({id: paidPost.id, access: true}); + }); + + it('next tiers post', async function () { + await next_post + .call(paidPost, optionsData); + + fn.firstCall.args[0].should.match({id: basicTierPost.id, access: false}); + }); + + it('next public post', async function () { + await next_post + .call(basicTierPost, optionsData); + + fn.firstCall.args[0].should.match({id: publicPost2.id, access: true}); + }); + }); + + describe('tiers member', function () { + const member = buildMember('tiers', [{ + name: 'Default Product', + slug: 'default-product', + type: 'paid', + active: true + }]); + + const locals = { + root: { + _locals: { + apiVersion: API_VERSION + }, + context: ['post'] + }, + member + }; + let optionsData; + + beforeEach(function () { + optionsData = {name: 'next_post', data: locals, fn, inverse}; + }); + + it('next members post', async function () { + await next_post + .call(publicPost, optionsData); + + fn.firstCall.args[0].should.match({id: membersPost.id, access: true}); + }); + + it('next paid post', async function () { + await next_post + .call(membersPost, optionsData); + + fn.firstCall.args[0].should.match({id: paidPost.id, access: true}); + }); + + it('next tiers post', async function () { + await next_post + .call(paidPost, optionsData); + + fn.firstCall.args[0].should.match({id: basicTierPost.id, access: true}); + }); + + it('next public post', async function () { + await next_post + .call(basicTierPost, optionsData); + + fn.firstCall.args[0].should.match({id: publicPost2.id, access: true}); + }); + }); + + describe('tiers member with different product', function () { + const member = buildMember('tiers', [{ + name: 'Default Product', + slug: 'pro-product', + type: 'paid', + active: true + }]); + + const locals = { + root: { + _locals: { + apiVersion: API_VERSION + }, + context: ['post'] + }, + member + }; + let optionsData; + + beforeEach(function () { + optionsData = {name: 'next_post', data: locals, fn, inverse}; + }); + + it('next members post', async function () { + await next_post + .call(publicPost, optionsData); + + fn.firstCall.args[0].should.match({id: membersPost.id, access: true}); + }); + + it('next paid post', async function () { + await next_post + .call(membersPost, optionsData); + + fn.firstCall.args[0].should.match({id: paidPost.id, access: true}); + }); + + it('next tiers post', async function () { + await next_post + .call(paidPost, optionsData); + + fn.firstCall.args[0].should.match({id: basicTierPost.id, access: false}); + }); + + it('next public post', async function () { + await next_post + .call(basicTierPost, optionsData); + + fn.firstCall.args[0].should.match({id: publicPost2.id, access: true}); + }); + }); + }); +}); \ No newline at end of file