diff --git a/core/server/services/members/middleware.js b/core/server/services/members/middleware.js index ec851918ac..f59973b661 100644 --- a/core/server/services/members/middleware.js +++ b/core/server/services/members/middleware.js @@ -198,7 +198,7 @@ const createSessionFromMagicLink = async function (req, res, next) { const action = req.query.action; - if (action === 'signup' || action === 'signup-paid') { + if (action === 'signup' || action === 'signup-paid' || action === 'subscribe') { let customRedirect = ''; const mostRecentActiveSubscription = subscriptions .sort((a, b) => { diff --git a/core/server/services/members/service.js b/core/server/services/members/service.js index 6a39603b78..f1c757ad7e 100644 --- a/core/server/services/members/service.js +++ b/core/server/services/members/service.js @@ -125,6 +125,13 @@ module.exports = { }); } + module.exports.ssr = MembersSSR({ + cookieSecure: urlUtils.isSSL(urlUtils.getSiteUrl()), + cookieKeys: [settingsCache.get('theme_session_secret')], + cookieName: 'ghost-members-ssr', + getMembersApi: () => module.exports.api + }); + verificationTrigger = new VerificationTrigger({ configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'), isVerified: () => config.get('hostSettings:emailVerification:verified') === true, @@ -181,13 +188,7 @@ module.exports = { return membersSettings; }, - ssr: MembersSSR({ - cookieSecure: urlUtils.isSSL(urlUtils.getSiteUrl()), - cookieKeys: [settingsCache.get('theme_session_secret')], - cookieName: 'ghost-members-ssr', - cookieCacheName: 'ghost-members-ssr-cache', - getMembersApi: () => module.exports.api - }), + ssr: null, stripeConnect: require('./stripe-connect'), diff --git a/test/e2e-api/admin/__snapshots__/members.test.js.snap b/test/e2e-api/admin/__snapshots__/members.test.js.snap index fcae5a9ac9..9ed4de9e14 100644 --- a/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -548,7 +548,7 @@ exports[`Members API Can browse 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": "7008", + "content-length": "8051", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -1009,7 +1009,7 @@ exports[`Members API Can filter by paid status 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": "5533", + "content-length": "6576", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -2166,7 +2166,7 @@ exports[`Members API Search for paid members retrieves member with email paid@te 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": "1296", + "content-length": "1640", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", diff --git a/test/e2e-api/members/signin.test.js b/test/e2e-api/members/signin.test.js new file mode 100644 index 0000000000..459b7bb07b --- /dev/null +++ b/test/e2e-api/members/signin.test.js @@ -0,0 +1,79 @@ +const membersService = require('../../../core/server/services/members'); +const {agentProvider, mockManager, fixtureManager} = require('../../utils/e2e-framework'); + +let membersAgent; + +describe('Members Signin', function () { + before(async function () { + // Weird - most of the mocks happen after getting the agent + // but to mock stripe we want to fake the stripe keys in the settings. + // And it's initialised at boot - so mocking it before + // Probably wanna replace this with a settinfs fixture mock or smth?? + mockManager.setupStripe(); + + const agents = await agentProvider.getAgentsForMembers(); + membersAgent = agents.membersAgent; + + await fixtureManager.init('members'); + }); + + beforeEach(function () { + mockManager.mockLabsEnabled('multipleProducts'); + mockManager.mockLabsEnabled('tierWelcomePages'); + mockManager.mockStripe(); + }); + + afterEach(function () { + mockManager.restore(); + }); + + it('Will not set a cookie if the token is invalid', async function () { + await membersAgent.get('/?token=blah') + .expectStatus(302) + .expectHeader('Location', /\?\w*success=false/); + }); + + it('Will set a cookie if the token is valid', async function () { + const magicLink = await membersService.api.getMagicLink('member1@test.com'); + const magicLinkUrl = new URL(magicLink); + const token = magicLinkUrl.searchParams.get('token'); + + await membersAgent.get(`/?token=${token}`) + .expectStatus(302) + .expectHeader('Location', /\?\w*success=true/) + .expectHeader('Set-Cookie', /members-ssr.*/); + }); + + it('Will redirect to the free welcome page for signup', async function () { + const magicLink = await membersService.api.getMagicLink('member1@test.com'); + const magicLinkUrl = new URL(magicLink); + const token = magicLinkUrl.searchParams.get('token'); + + await membersAgent.get(`/?token=${token}&action=signup`) + .expectStatus(302) + .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Set-Cookie', /members-ssr.*/); + }); + + it('Will redirect to the paid welcome page for signup-paid', async function () { + const magicLink = await membersService.api.getMagicLink('paid@test.com'); + const magicLinkUrl = new URL(magicLink); + const token = magicLinkUrl.searchParams.get('token'); + + await membersAgent.get(`/?token=${token}&action=signup-paid`) + .expectStatus(302) + .expectHeader('Location', /\/welcome-paid\/$/) + .expectHeader('Set-Cookie', /members-ssr.*/); + }); + + it('Will redirect to the free welcome page for subscribe', async function () { + const magicLink = await membersService.api.getMagicLink('member1@test.com'); + const magicLinkUrl = new URL(magicLink); + const token = magicLinkUrl.searchParams.get('token'); + + await membersAgent.get(`/?token=${token}&action=subscribe`) + .expectStatus(302) + .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Set-Cookie', /members-ssr.*/); + }); +}); diff --git a/test/e2e-frontend/members.test.js b/test/e2e-frontend/members.test.js index 83ced39485..52695cee55 100644 --- a/test/e2e-frontend/members.test.js +++ b/test/e2e-frontend/members.test.js @@ -338,11 +338,11 @@ describe('Front-end members behaviour', function () { .expect(assertContentIsAbsent); }); - it('cannot read product-only post content', async function () { + it('can read product-only post content', async function () { await request .get('/thou-must-have-default-product/') .expect(200) - .expect(assertContentIsAbsent); + .expect(assertContentIsPresent); }); }); @@ -392,11 +392,11 @@ describe('Front-end members behaviour', function () { .expect(assertContentIsAbsent); }); - it('cannot read product-only post content', async function () { + it('can read product-only post content', async function () { await request .get('/thou-must-have-default-product/') .expect(200) - .expect(assertContentIsAbsent); + .expect(assertContentIsPresent); }); }); diff --git a/test/unit/server/services/members/middleware.test.js b/test/unit/server/services/members/middleware.test.js index 330add577c..20c7e46aac 100644 --- a/test/unit/server/services/members/middleware.test.js +++ b/test/unit/server/services/members/middleware.test.js @@ -8,6 +8,7 @@ const settingsCache = require('../../../../../core/shared/settings-cache'); describe('Members Service Middleware', function () { describe('createSessionFromMagicLink', function () { + let oldSSR; let req; let res; let next; @@ -20,13 +21,17 @@ describe('Members Service Middleware', function () { res.redirect = sinon.stub().returns(''); // Stub the members Service, handle this in separate tests - membersService.ssr.exchangeTokenForSession = sinon.stub(); + oldSSR = membersService.ssr; + membersService.ssr = { + exchangeTokenForSession: sinon.stub() + }; sinon.stub(urlUtils, 'getSubdir').returns('/blah'); sinon.stub(urlUtils, 'getSiteUrl').returns('https://site.com/blah'); }); afterEach(function () { + membersService.ssr = oldSSR; sinon.restore(); }); diff --git a/test/utils/e2e-framework-mock-manager.js b/test/utils/e2e-framework-mock-manager.js index 98a0208ebe..5e8f2432bb 100644 --- a/test/utils/e2e-framework-mock-manager.js +++ b/test/utils/e2e-framework-mock-manager.js @@ -37,6 +37,9 @@ const setupStripe = () => { if (key === 'stripe_connect_account_name') { return 'Test Account'; } + if (key === 'theme_session_secret') { + return '1337_h4xx0r_53cR37'; + } return settingsCache.get.wrappedMethod.call(settingsCache, key, ...args); }); }; diff --git a/test/utils/fixture-utils.js b/test/utils/fixture-utils.js index 8822c7ba7a..66e38f70c3 100644 --- a/test/utils/fixture-utils.js +++ b/test/utils/fixture-utils.js @@ -461,15 +461,26 @@ const fixtures = { return Promise.map(DataGenerator.forKnex.labels, function (label) { return models.Label.add(label, context.internal); }).then(function () { - let productsToInsert = fixtureManager.findModelFixtures('Product').entries; - return Promise.map(productsToInsert, async (product) => { + let coreProductFixtures = fixtureManager.findModelFixtures('Product').entries; + return Promise.map(coreProductFixtures, async (product) => { const found = await models.Product.findOne(product, context.internal); if (!found) { await models.Product.add(product, context.internal); } }); + }).then(async function () { + let testProductFixtures = DataGenerator.forKnex.products; + for (const productFixture of testProductFixtures) { + if (productFixture.id) { // Not currently used - this is used to add new text fixtures, e.g. a Bronze/Silver/Gold Tier + await models.Product.add(productFixture, context.internal); + } else { // Used to update the core fixtures + // If it doesn't exist we have invalid fixtures, so require: true to ensure we throw + const existing = await models.Product.findOne({slug: productFixture.slug}, {...context.internal, require: true}); + await models.Product.edit(productFixture, {...context.internal, id: existing.id}); + } + } }).then(function () { - return models.Product.findOne({}, context.internal); + return models.Product.findOne({type: 'paid'}, context.internal); }).then(function (product) { return Promise.props({ stripeProducts: Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) { @@ -513,6 +524,27 @@ const fixtures = { return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_customer_subscriptions), function (subscription) { return models.StripeCustomerSubscription.add(subscription, context.internal); }); + }).then(async function () { + const members = (await models.Member.findAll({ + withRelated: [ + 'labels', + 'stripeSubscriptions', + 'stripeSubscriptions.customer', + 'stripeSubscriptions.stripePrice', + 'stripeSubscriptions.stripePrice.stripeProduct', + 'products', + 'offerRedemptions' + ] + })).toJSON(); + + for (const member of members) { + for (const subscription of member.subscriptions) { + const product = subscription.price.product.product_id; + await models.Member.edit({products: member.products.concat({ + id: product + })}, {id: member.id}); + } + } }); }, diff --git a/test/utils/fixtures/data-generator.js b/test/utils/fixtures/data-generator.js index 453c52113b..454f7c77d4 100644 --- a/test/utils/fixtures/data-generator.js +++ b/test/utils/fixtures/data-generator.js @@ -367,16 +367,16 @@ DataGenerator.Content = { products: [ { - id: ObjectId().toHexString(), - name: 'Free', + // No ID because these are in the core fixtures.json slug: 'free', - type: 'free' + // slug is to match the product, the below are updated for the product + welcome_page_url: '/welcome-free' }, { - id: ObjectId().toHexString(), - name: 'Ghost Product', - slug: 'ghost-product', - type: 'paid' + // No ID because these are in the core fixtures.json + slug: 'default-product', + // slug is to match the product, the below are updated for the product + welcome_page_url: '/welcome-paid' } ], @@ -1271,7 +1271,8 @@ DataGenerator.forKnex = (function () { ]; const products = [ - createBasic(DataGenerator.Content.products[0]) + DataGenerator.Content.products[0], + DataGenerator.Content.products[1] ]; const members_stripe_customers = [