Ghost/ghost/core/test/e2e-api/members/create-stripe-checkout-session.test.js
Simon Backx 80cec29144
Added Stripe Mocker to make testing easier (#16396)
no issue

The Stripe Mocker mocks the Stripe API in memory, to make it much easier
to test subscription flows. Currently it is more a POC to see if it
works well. It probably needs a bit more work to support more scenarios.
- Added new tests for the subscription stats endpoint for 3D secure +
free trial flows using the new Stripe Mocker
- Updated members admin api tests to use Stripe Mocker (+ added new test
for deleting members with Stripe cancellation)
- Some tests called mockStripe at the beginning, but that method did
nothing apart from disabling network (which is the default now), then
they mocked Stripe inside the tests file... so I've removed those
because those conflict with the new mocker that is enabled when calling
mockStripe. We'll need to port those over later.
2023-03-10 18:43:35 +01:00

528 lines
19 KiB
JavaScript

const querystring = require('querystring');
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const nock = require('nock');
const should = require('should');
const models = require('../../../core/server/models');
const urlService = require('../../../core/server/services/url');
let membersAgent, adminAgent;
async function getPost(id) {
// eslint-disable-next-line dot-notation
return await models['Post'].where('id', id).fetch({require: true});
}
describe('Create Stripe Checkout Session', function () {
before(async function () {
const agents = await agentProvider.getAgentsForMembers();
membersAgent = agents.membersAgent;
adminAgent = agents.adminAgent;
await fixtureManager.init('posts', 'members');
await adminAgent.loginAsOwner();
});
beforeEach(function () {
mockManager.mockMail();
});
afterEach(function () {
mockManager.restore();
});
it('Does not allow to create a checkout session if the customerEmail is associated with a paid member', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'paid@test.com',
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(403)
.matchBodySnapshot({
errors: [{
id: matchers.anyUuid,
code: 'CANNOT_CHECKOUT_WITH_EXISTING_SUBSCRIPTION'
}]
})
.matchHeaderSnapshot({
etag: matchers.anyEtag
});
});
it('Can create a checkout session when using offers', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
const {body: {offers: [offer]}} = await adminAgent.post('/offers/').body({
offers: [{
name: 'Test Offer',
code: 'test-offer',
cadence: 'month',
status: 'active',
currency: 'usd',
type: 'percent',
amount: 20,
duration: 'once',
duration_in_months: null,
display_title: 'Test Offer',
display_description: null,
tier: {
id: paidTier.id
}
}]
});
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri) => {
if (uri === '/v1/checkout/sessions') {
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/coupons') {
return [200, {id: 'coupon_123'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_1',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'free@test.com',
offerId: offer.id
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
it('Can create a checkout session without passing a customerEmail', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
const bodyJSON = querystring.parse(body);
// TODO: Actually work out what Stripe checks and when/how it errors
if (Reflect.has(bodyJSON, 'customerEmail')) {
return [400, {error: 'Invalid Email'}];
}
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_2',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
it('Does allow to create a checkout session if the customerEmail is not associated with a paid member', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri) => {
if (uri === '/v1/checkout/sessions') {
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_3',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'free@test.com',
tierId: paidTier.id,
cadence: 'month'
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
});
/**
* When a checkout session is created with an urlHistory, we should convert it to an
* attribution and check if that is set in the metadata of the stripe session
*/
describe('Member attribution', function () {
it('Does pass url attribution source to session metadata', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
const scope = nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
const parsed = new URLSearchParams(body);
should(parsed.get('metadata[attribution_url]')).eql('/test');
should(parsed.get('metadata[attribution_type]')).eql('url');
should(parsed.get('metadata[attribution_id]')).be.null();
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_4',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'attribution@test.com',
tierId: paidTier.id,
cadence: 'month',
metadata: {
urlHistory: [
{
path: '/test',
time: Date.now()
}
]
}
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
should(scope.isDone()).eql(true);
});
it('Does pass post attribution source to session metadata', async function () {
const post = await getPost(fixtureManager.get('posts', 0).id);
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 50,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
const scope = nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
const parsed = new URLSearchParams(body);
should(parsed.get('metadata[attribution_url]')).eql(url);
should(parsed.get('metadata[attribution_type]')).eql('post');
should(parsed.get('metadata[attribution_id]')).eql(post.id);
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_5',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'attribution-post@test.com',
tierId: paidTier.id,
cadence: 'month',
metadata: {
urlHistory: [
{
path: url,
time: Date.now()
}
]
}
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
should(scope.isDone()).eql(true);
});
it('Ignores attribution_* values in metadata', async function () {
const {body: {tiers}} = await adminAgent.get('/tiers/?include=monthly_price&yearly_price');
const paidTier = tiers.find(tier => tier.type === 'paid');
nock('https://api.stripe.com')
.persist()
.get(/v1\/.*/)
.reply((uri) => {
const [match, resource, id] = uri.match(/\/v1\/(\w+)\/(.+)\/?/) || [null];
if (match) {
if (resource === 'products') {
return [200, {
id: id,
active: true
}];
}
if (resource === 'prices') {
return [200, {
id: id,
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
}
return [500];
});
const scope = nock('https://api.stripe.com')
.persist()
.post(/v1\/.*/)
.reply((uri, body) => {
if (uri === '/v1/checkout/sessions') {
const parsed = new URLSearchParams(body);
should(parsed.get('metadata[attribution_url]')).be.null();
should(parsed.get('metadata[attribution_type]')).be.null();
should(parsed.get('metadata[attribution_id]')).be.null();
return [200, {id: 'cs_123', url: 'https://site.com'}];
}
if (uri === '/v1/prices') {
return [200, {
id: 'price_6',
active: true,
currency: 'usd',
unit_amount: 500,
recurring: {
interval: 'month'
}
}];
}
return [500];
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'attribution-2@test.com',
tierId: paidTier.id,
cadence: 'month',
metadata: {
attribution_type: 'url',
attribution_url: '/',
attribution_id: null
}
})
.expectStatus(200)
.matchBodySnapshot()
.matchHeaderSnapshot();
should(scope.isDone()).eql(true);
});
});
});