0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Added E2E tests for donations API (#17694)

fixes https://github.com/TryGhost/Product/issues/3722
This commit is contained in:
Simon Backx 2023-08-11 14:25:53 +02:00 committed by GitHub
parent 38c1fbea40
commit e14df6479b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 230 additions and 4 deletions

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Create Stripe Checkout Session for Donations Can create a member checkout session for a donation 1: [body] 1`] = `
Object {
"url": "https://checkout.stripe.com/c/pay/fake-data",
}
`;
exports[`Create Stripe Checkout Session for Donations Can create an anonymous checkout session for a donation 1: [body] 1`] = `
Object {
"url": "https://checkout.stripe.com/c/pay/fake-data",
}
`;

View file

@ -0,0 +1,194 @@
const {agentProvider, mockManager, fixtureManager} = require('../../utils/e2e-framework');
const {stripeMocker} = require('../../utils/e2e-framework-mock-manager');
const models = require('../../../core/server/models');
const assert = require('assert/strict');
const urlService = require('../../../core/server/services/url');
const DomainEvents = require('@tryghost/domain-events');
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 for Donations', 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.mockStripe();
mockManager.mockMail();
});
afterEach(function () {
mockManager.restore();
});
it('Can create an anonymous checkout session for a donation', async function () {
// Fake a visit to a post
const post = await getPost(fixtureManager.get('posts', 0).id);
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'paid@test.com',
type: 'donation',
successUrl: 'https://example.com/?type=success',
cancelUrl: 'https://example.com/?type=cancel',
metadata: {
test: 'hello',
urlHistory: [
{
path: url,
time: Date.now(),
referrerMedium: null,
referrerSource: 'ghost-explore',
referrerUrl: 'https://example.com/blog/'
}
]
}
})
.expectStatus(200)
.matchBodySnapshot();
// Send a webhook of a paid invoice for this session
await stripeMocker.sendWebhook({
type: 'invoice.payment_succeeded',
data: {
object: {
type: 'invoice',
paid: true,
amount_paid: 1200,
currency: 'usd',
customer: (stripeMocker.checkoutSessions[0].customer),
customer_name: 'Paid Test',
customer_email: 'exampledonation@example.com',
metadata: {
...(stripeMocker.checkoutSessions[0].invoice_creation?.invoice_data?.metadata ?? {})
}
}
}
});
// Check email received
mockManager.assert.sentEmail({
subject: '💰 One-time payment received: $12.00 from Paid Test',
to: 'jbloggs@example.com'
});
// Check stored in database
const lastDonation = await models.DonationPaymentEvent.findOne({
email: 'exampledonation@example.com'
}, {require: true});
assert.equal(lastDonation.get('amount'), 1200);
assert.equal(lastDonation.get('currency'), 'usd');
assert.equal(lastDonation.get('email'), 'exampledonation@example.com');
assert.equal(lastDonation.get('name'), 'Paid Test');
assert.equal(lastDonation.get('member_id'), null);
// Check referrer
assert.equal(lastDonation.get('referrer_url'), 'example.com');
assert.equal(lastDonation.get('referrer_medium'), 'Ghost Network');
assert.equal(lastDonation.get('referrer_source'), 'Ghost Explore');
// Check attributed correctly
assert.equal(lastDonation.get('attribution_id'), post.id);
assert.equal(lastDonation.get('attribution_type'), 'post');
assert.equal(lastDonation.get('attribution_url'), url);
});
it('Can create a member checkout session for a donation', async function () {
// Fake a visit to a post
const post = await getPost(fixtureManager.get('posts', 0).id);
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
const email = 'test-member-create-donation-session@email.com';
const membersService = require('../../../core/server/services/members');
const member = await membersService.api.members.create({email, name: 'Member Test'});
const token = await membersService.api.getMemberIdentityToken(email);
await DomainEvents.allSettled();
// Check email received
mockManager.assert.sentEmail({
subject: '🥳 Free member signup: Member Test',
to: 'jbloggs@example.com'
});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: email,
identity: token,
type: 'donation',
successUrl: 'https://example.com/?type=success',
cancelUrl: 'https://example.com/?type=cancel',
metadata: {
test: 'hello',
urlHistory: [
{
path: url,
time: Date.now(),
referrerMedium: null,
referrerSource: 'ghost-explore',
referrerUrl: 'https://example.com/blog/'
}
]
}
})
.expectStatus(200)
.matchBodySnapshot();
// Send a webhook of a paid invoice for this session
await stripeMocker.sendWebhook({
type: 'invoice.payment_succeeded',
data: {
object: {
type: 'invoice',
paid: true,
amount_paid: 1220,
currency: 'eur',
customer: (stripeMocker.checkoutSessions[0].customer),
customer_name: 'Member Test',
customer_email: email,
metadata: {
...(stripeMocker.checkoutSessions[0].invoice_creation?.invoice_data?.metadata ?? {})
}
}
}
});
// Check email received
mockManager.assert.sentEmail({
subject: '💰 One-time payment received: €12.20 from Member Test',
to: 'jbloggs@example.com'
});
// Check stored in database
const lastDonation = await models.DonationPaymentEvent.findOne({
email
}, {require: true});
assert.equal(lastDonation.get('amount'), 1220);
assert.equal(lastDonation.get('currency'), 'eur');
assert.equal(lastDonation.get('email'), email);
assert.equal(lastDonation.get('name'), 'Member Test');
assert.equal(lastDonation.get('member_id'), member.id);
// Check referrer
assert.equal(lastDonation.get('referrer_url'), 'example.com');
assert.equal(lastDonation.get('referrer_medium'), 'Ghost Network');
assert.equal(lastDonation.get('referrer_source'), 'Ghost Explore');
// Check attributed correctly
assert.equal(lastDonation.get('attribution_id'), post.id);
assert.equal(lastDonation.get('attribution_type'), 'post');
assert.equal(lastDonation.get('attribution_url'), url);
});
});

View file

@ -80,6 +80,7 @@ const allowStripe = () => {
const mockStripe = () => {
disableNetwork();
stripeMocker.reset();
stripeMocker.stub();
};

View file

@ -18,6 +18,7 @@ class StripeMocker {
coupons = [];
prices = [];
products = [];
checkoutSessions = [];
nockInterceptors = [];
@ -39,6 +40,7 @@ class StripeMocker {
this.coupons = [];
this.prices = [];
this.products = [];
this.checkoutSessions = [];
// Fix for now, because of importing order breaking some things when they are not initialized
members = require('../../core/server/services/members');
@ -227,6 +229,17 @@ class StripeMocker {
}
}
if (resource === 'checkout') {
if (!id) {
// Add default fields
decoded = {
object: 'checkout.session',
...decoded,
url: 'https://checkout.stripe.com/c/pay/fake-data'
};
}
}
if (resource === 'subscriptions') {
// Convert price to price object
if (Array.isArray(decoded.items)) {
@ -381,6 +394,10 @@ class StripeMocker {
return this.#postData(this.products, id, body, resource);
}
if (resource === 'checkout' && id === 'sessions') {
return this.#postData(this.checkoutSessions, null, body, resource);
}
return [500];
});

View file

@ -117,13 +117,14 @@ class PaymentsService {
* @param {Object.<string, any>} [params.metadata]
* @param {string} params.successUrl
* @param {string} params.cancelUrl
* @param {boolean} [params.isAuthenticated]
* @param {string} [params.email]
*
* @returns {Promise<URL>}
*/
async getDonationPaymentLink({member, metadata, successUrl, cancelUrl, email}) {
async getDonationPaymentLink({member, metadata, successUrl, cancelUrl, email, isAuthenticated}) {
let customer = null;
if (member) {
if (member && isAuthenticated) {
customer = await this.getCustomerForMember(member);
}

View file

@ -115,9 +115,9 @@ module.exports = class WebhookController {
// Track a one time payment event
const amount = invoice.amount_paid;
const member = await this.deps.memberRepository.get({
const member = invoice.customer ? (await this.deps.memberRepository.get({
customer_id: invoice.customer
});
})) : null;
const data = DonationPaymentEvent.create({
name: member?.get('name') ?? invoice.customer_name,