0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00

🐛 Fixed welcome pages not working for "subscribe" links (#14176)

- Fixed test fixtures so that members with subscriptions also have products/tiers
- Fixed test fixtures so that default&free tiers can be updated for tests
- Added tests for the signin functionality and welcome page redirects
- Extended `setupStripe` to setup other Members settings - this needs some more
  thought around how we proceed
This commit is contained in:
Fabien 'egg' O'Carroll 2022-02-20 16:02:42 +02:00 committed by GitHub
parent c09a81aabe
commit 9c5c41b927
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 148 additions and 27 deletions

View file

@ -198,7 +198,7 @@ const createSessionFromMagicLink = async function (req, res, next) {
const action = req.query.action; const action = req.query.action;
if (action === 'signup' || action === 'signup-paid') { if (action === 'signup' || action === 'signup-paid' || action === 'subscribe') {
let customRedirect = ''; let customRedirect = '';
const mostRecentActiveSubscription = subscriptions const mostRecentActiveSubscription = subscriptions
.sort((a, b) => { .sort((a, b) => {

View file

@ -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({ verificationTrigger = new VerificationTrigger({
configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'), configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
isVerified: () => config.get('hostSettings:emailVerification:verified') === true, isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
@ -181,13 +188,7 @@ module.exports = {
return membersSettings; return membersSettings;
}, },
ssr: MembersSSR({ ssr: null,
cookieSecure: urlUtils.isSSL(urlUtils.getSiteUrl()),
cookieKeys: [settingsCache.get('theme_session_secret')],
cookieName: 'ghost-members-ssr',
cookieCacheName: 'ghost-members-ssr-cache',
getMembersApi: () => module.exports.api
}),
stripeConnect: require('./stripe-connect'), stripeConnect: require('./stripe-connect'),

View file

@ -548,7 +548,7 @@ exports[`Members API Can browse 2: [headers] 1`] = `
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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", "content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding", "vary": "Origin, Accept-Encoding",
@ -1009,7 +1009,7 @@ exports[`Members API Can filter by paid status 2: [headers] 1`] = `
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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", "content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding", "vary": "Origin, Accept-Encoding",
@ -2166,7 +2166,7 @@ exports[`Members API Search for paid members retrieves member with email paid@te
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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", "content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding", "vary": "Origin, Accept-Encoding",

View file

@ -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.*/);
});
});

View file

@ -338,11 +338,11 @@ describe('Front-end members behaviour', function () {
.expect(assertContentIsAbsent); .expect(assertContentIsAbsent);
}); });
it('cannot read product-only post content', async function () { it('can read product-only post content', async function () {
await request await request
.get('/thou-must-have-default-product/') .get('/thou-must-have-default-product/')
.expect(200) .expect(200)
.expect(assertContentIsAbsent); .expect(assertContentIsPresent);
}); });
}); });
@ -392,11 +392,11 @@ describe('Front-end members behaviour', function () {
.expect(assertContentIsAbsent); .expect(assertContentIsAbsent);
}); });
it('cannot read product-only post content', async function () { it('can read product-only post content', async function () {
await request await request
.get('/thou-must-have-default-product/') .get('/thou-must-have-default-product/')
.expect(200) .expect(200)
.expect(assertContentIsAbsent); .expect(assertContentIsPresent);
}); });
}); });

View file

@ -8,6 +8,7 @@ const settingsCache = require('../../../../../core/shared/settings-cache');
describe('Members Service Middleware', function () { describe('Members Service Middleware', function () {
describe('createSessionFromMagicLink', function () { describe('createSessionFromMagicLink', function () {
let oldSSR;
let req; let req;
let res; let res;
let next; let next;
@ -20,13 +21,17 @@ describe('Members Service Middleware', function () {
res.redirect = sinon.stub().returns(''); res.redirect = sinon.stub().returns('');
// Stub the members Service, handle this in separate tests // 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, 'getSubdir').returns('/blah');
sinon.stub(urlUtils, 'getSiteUrl').returns('https://site.com/blah'); sinon.stub(urlUtils, 'getSiteUrl').returns('https://site.com/blah');
}); });
afterEach(function () { afterEach(function () {
membersService.ssr = oldSSR;
sinon.restore(); sinon.restore();
}); });

View file

@ -37,6 +37,9 @@ const setupStripe = () => {
if (key === 'stripe_connect_account_name') { if (key === 'stripe_connect_account_name') {
return 'Test Account'; return 'Test Account';
} }
if (key === 'theme_session_secret') {
return '1337_h4xx0r_53cR37';
}
return settingsCache.get.wrappedMethod.call(settingsCache, key, ...args); return settingsCache.get.wrappedMethod.call(settingsCache, key, ...args);
}); });
}; };

View file

@ -461,15 +461,26 @@ const fixtures = {
return Promise.map(DataGenerator.forKnex.labels, function (label) { return Promise.map(DataGenerator.forKnex.labels, function (label) {
return models.Label.add(label, context.internal); return models.Label.add(label, context.internal);
}).then(function () { }).then(function () {
let productsToInsert = fixtureManager.findModelFixtures('Product').entries; let coreProductFixtures = fixtureManager.findModelFixtures('Product').entries;
return Promise.map(productsToInsert, async (product) => { return Promise.map(coreProductFixtures, async (product) => {
const found = await models.Product.findOne(product, context.internal); const found = await models.Product.findOne(product, context.internal);
if (!found) { if (!found) {
await models.Product.add(product, context.internal); 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 () { }).then(function () {
return models.Product.findOne({}, context.internal); return models.Product.findOne({type: 'paid'}, context.internal);
}).then(function (product) { }).then(function (product) {
return Promise.props({ return Promise.props({
stripeProducts: Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) { 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 Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_customer_subscriptions), function (subscription) {
return models.StripeCustomerSubscription.add(subscription, context.internal); 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});
}
}
}); });
}, },

View file

@ -367,16 +367,16 @@ DataGenerator.Content = {
products: [ products: [
{ {
id: ObjectId().toHexString(), // No ID because these are in the core fixtures.json
name: 'Free',
slug: 'free', 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(), // No ID because these are in the core fixtures.json
name: 'Ghost Product', slug: 'default-product',
slug: 'ghost-product', // slug is to match the product, the below are updated for the product
type: 'paid' welcome_page_url: '/welcome-paid'
} }
], ],
@ -1271,7 +1271,8 @@ DataGenerator.forKnex = (function () {
]; ];
const products = [ const products = [
createBasic(DataGenerator.Content.products[0]) DataGenerator.Content.products[0],
DataGenerator.Content.products[1]
]; ];
const members_stripe_customers = [ const members_stripe_customers = [