mirror of
				https://github.com/HabitRPG/habitica.git
				synced 2025-10-31 13:13:10 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			338 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			338 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import stripeModule from 'stripe';
 | |
| import nconf from 'nconf';
 | |
| import { v4 as uuid } from 'uuid';
 | |
| import moment from 'moment';
 | |
| import {
 | |
|   generateGroup,
 | |
| } from '../../../../../helpers/api-unit.helper';
 | |
| import { model as User } from '../../../../../../website/server/models/user';
 | |
| import stripePayments from '../../../../../../website/server/libs/payments/stripe';
 | |
| import payments from '../../../../../../website/server/libs/payments/payments';
 | |
| import common from '../../../../../../website/common';
 | |
| import logger from '../../../../../../website/server/libs/logger';
 | |
| import * as oneTimePayments from '../../../../../../website/server/libs/payments/stripe/oneTimePayments';
 | |
| import * as subscriptions from '../../../../../../website/server/libs/payments/stripe/subscriptions';
 | |
| 
 | |
| const { i18n } = common;
 | |
| 
 | |
| describe('Stripe - Webhooks', () => {
 | |
|   const stripe = stripeModule('test');
 | |
|   const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
 | |
|   const headers = {};
 | |
|   const body = {};
 | |
| 
 | |
|   describe('all events', () => {
 | |
|     let event;
 | |
|     let constructEventStub;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       event = { type: 'payment_intent.created' };
 | |
|       constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
 | |
|       constructEventStub.returns(event);
 | |
|       sandbox.stub(logger, 'error');
 | |
|     });
 | |
| 
 | |
|     it('throws if the event can\'t be validated', async () => {
 | |
|       const err = new Error('fail');
 | |
|       constructEventStub.throws(err);
 | |
|       await expect(stripePayments.handleWebhooks({ body: event, headers }, stripe))
 | |
|         .to.eventually.be.rejected.and.to.eql({
 | |
|           httpCode: 400,
 | |
|           name: 'BadRequest',
 | |
|           message: `Webhook Error: ${err.message}`,
 | |
|         });
 | |
| 
 | |
|       expect(logger.error).to.have.been.calledOnce;
 | |
|       const calledWith = logger.error.getCall(0).args;
 | |
|       expect(calledWith[0].message).to.equal('Error verifying Stripe webhook');
 | |
|       expect(calledWith[1]).to.eql({ err });
 | |
|     });
 | |
| 
 | |
|     it('logs an error if an unsupported webhook event is passed', async () => {
 | |
|       event.type = 'account.updated';
 | |
|       await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
 | |
|         .to.eventually.be.rejected.and.to.eql({
 | |
|           httpCode: 400,
 | |
|           name: 'BadRequest',
 | |
|           message: `Missing handler for Stripe webhook ${event.type}`,
 | |
|         });
 | |
| 
 | |
|       expect(logger.error).to.have.been.calledOnce;
 | |
|       const calledWith = logger.error.getCall(0).args;
 | |
|       expect(calledWith[0].message).to.equal('Error handling Stripe webhook');
 | |
|       expect(calledWith[1].event).to.eql(event);
 | |
|       expect(calledWith[1].err.message).to.eql(`Missing handler for Stripe webhook ${event.type}`);
 | |
|     });
 | |
| 
 | |
|     it('retrieves and validates the event from Stripe', async () => {
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(stripe.webhooks.constructEvent)
 | |
|         .to.have.been.calledWith(body, undefined, endpointSecret);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('customer.subscription.deleted', () => {
 | |
|     const eventType = 'customer.subscription.deleted';
 | |
|     let event;
 | |
|     let constructEventStub;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       event = { type: eventType };
 | |
|       constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
 | |
|       constructEventStub.returns(event);
 | |
|     });
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       sandbox.stub(stripe.customers, 'del').resolves({});
 | |
|       sandbox.stub(payments, 'cancelSubscription').resolves({});
 | |
|     });
 | |
| 
 | |
|     it('does not do anything if event.request is not null (subscription cancelled manually)', async () => {
 | |
|       constructEventStub.returns({
 | |
|         id: 123,
 | |
|         type: eventType,
 | |
|         request: { id: 123 },
 | |
|       });
 | |
| 
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(stripe.customers.del).to.not.have.been.called;
 | |
|       expect(payments.cancelSubscription).to.not.have.been.called;
 | |
|     });
 | |
| 
 | |
|     describe('user subscription', () => {
 | |
|       it('throws an error if the user is not found', async () => {
 | |
|         const customerId = 456;
 | |
|         constructEventStub.returns({
 | |
|           id: 123,
 | |
|           type: eventType,
 | |
|           data: {
 | |
|             object: {
 | |
|               plan: {
 | |
|                 id: 'basic_earned',
 | |
|               },
 | |
|               customer: customerId,
 | |
|             },
 | |
|           },
 | |
|           request: { id: null },
 | |
|         });
 | |
| 
 | |
|         await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
 | |
|           .to.eventually.be.rejectedWith({
 | |
|             message: i18n.t('userNotFound'),
 | |
|             httpCode: 404,
 | |
|             name: 'NotFound',
 | |
|           });
 | |
| 
 | |
|         expect(stripe.customers.del).to.not.have.been.called;
 | |
|         expect(payments.cancelSubscription).to.not.have.been.called;
 | |
|       });
 | |
| 
 | |
|       it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
 | |
|         const customerId = '456';
 | |
| 
 | |
|         const subscriber = new User();
 | |
|         subscriber.purchased.plan.customerId = customerId;
 | |
|         subscriber.purchased.plan.paymentMethod = 'Stripe';
 | |
|         await subscriber.save();
 | |
| 
 | |
|         constructEventStub.returns({
 | |
|           id: 123,
 | |
|           type: eventType,
 | |
|           data: {
 | |
|             object: {
 | |
|               plan: {
 | |
|                 id: 'basic_earned',
 | |
|               },
 | |
|               customer: customerId,
 | |
|             },
 | |
|           },
 | |
|           request: { id: null },
 | |
|         });
 | |
| 
 | |
|         await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|         expect(stripe.customers.del).to.have.been.calledOnce;
 | |
|         expect(stripe.customers.del).to.have.been.calledWith(customerId);
 | |
|         expect(payments.cancelSubscription).to.have.been.calledOnce;
 | |
| 
 | |
|         const cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
 | |
|         expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
 | |
|         expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
 | |
|         expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
 | |
|         expect(cancelSubscriptionOpts.groupId).to.be.undefined;
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('group plan subscription', () => {
 | |
|       it('throws an error if the group is not found', async () => {
 | |
|         const customerId = 456;
 | |
|         constructEventStub.returns({
 | |
|           id: 123,
 | |
|           type: eventType,
 | |
|           data: {
 | |
|             object: {
 | |
|               plan: {
 | |
|                 id: 'group_monthly',
 | |
|               },
 | |
|               customer: customerId,
 | |
|             },
 | |
|           },
 | |
|           request: { id: null },
 | |
|         });
 | |
| 
 | |
|         await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
 | |
|           .to.eventually.be.rejectedWith({
 | |
|             message: i18n.t('groupNotFound'),
 | |
|             httpCode: 404,
 | |
|             name: 'NotFound',
 | |
|           });
 | |
| 
 | |
|         expect(stripe.customers.del).to.not.have.been.called;
 | |
|         expect(payments.cancelSubscription).to.not.have.been.called;
 | |
|       });
 | |
| 
 | |
|       it('throws an error if the group leader is not found', async () => {
 | |
|         const customerId = 456;
 | |
| 
 | |
|         const subscriber = generateGroup({
 | |
|           name: 'test group',
 | |
|           type: 'guild',
 | |
|           privacy: 'public',
 | |
|           leader: uuid(),
 | |
|         });
 | |
|         subscriber.purchased.plan.customerId = customerId;
 | |
|         subscriber.purchased.plan.paymentMethod = 'Stripe';
 | |
|         await subscriber.save();
 | |
| 
 | |
|         constructEventStub.returns({
 | |
|           id: 123,
 | |
|           type: eventType,
 | |
|           data: {
 | |
|             object: {
 | |
|               plan: {
 | |
|                 id: 'group_monthly',
 | |
|               },
 | |
|               customer: customerId,
 | |
|             },
 | |
|           },
 | |
|           request: { id: null },
 | |
|         });
 | |
| 
 | |
|         await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
 | |
|           .to.eventually.be.rejectedWith({
 | |
|             message: i18n.t('userNotFound'),
 | |
|             httpCode: 404,
 | |
|             name: 'NotFound',
 | |
|           });
 | |
| 
 | |
|         expect(stripe.customers.del).to.not.have.been.called;
 | |
|         expect(payments.cancelSubscription).to.not.have.been.called;
 | |
|       });
 | |
| 
 | |
|       it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
 | |
|         const customerId = '456';
 | |
| 
 | |
|         const leader = new User();
 | |
|         await leader.save();
 | |
| 
 | |
|         const subscriber = generateGroup({
 | |
|           name: 'test group',
 | |
|           type: 'guild',
 | |
|           privacy: 'public',
 | |
|           leader: leader._id,
 | |
|         });
 | |
|         subscriber.purchased.plan.customerId = customerId;
 | |
|         subscriber.purchased.plan.paymentMethod = 'Stripe';
 | |
|         await subscriber.save();
 | |
| 
 | |
|         constructEventStub.returns({
 | |
|           id: 123,
 | |
|           type: eventType,
 | |
|           data: {
 | |
|             object: {
 | |
|               plan: {
 | |
|                 id: 'group_monthly',
 | |
|               },
 | |
|               customer: customerId,
 | |
|             },
 | |
|           },
 | |
|           request: { id: null },
 | |
|         });
 | |
| 
 | |
|         await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|         expect(stripe.customers.del).to.have.been.calledOnce;
 | |
|         expect(stripe.customers.del).to.have.been.calledWith(customerId);
 | |
|         expect(payments.cancelSubscription).to.have.been.calledOnce;
 | |
| 
 | |
|         const cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
 | |
|         expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
 | |
|         expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
 | |
|         expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
 | |
|         expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('checkout.session.completed', () => {
 | |
|     const eventType = 'checkout.session.completed';
 | |
|     let event;
 | |
|     let constructEventStub;
 | |
|     const session = {};
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       session.metadata = {};
 | |
|       event = { type: eventType, data: { object: session } };
 | |
|       constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
 | |
|       constructEventStub.returns(event);
 | |
| 
 | |
|       sandbox.stub(oneTimePayments, 'applyGemPayment').resolves({});
 | |
|       sandbox.stub(subscriptions, 'applySubscription').resolves({});
 | |
|       sandbox.stub(subscriptions, 'handlePaymentMethodChange').resolves({});
 | |
|     });
 | |
| 
 | |
|     it('handles changing an user sub', async () => {
 | |
|       session.metadata.type = 'edit-card-user';
 | |
| 
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(subscriptions.handlePaymentMethodChange).to.have.been.calledOnce;
 | |
|       expect(subscriptions.handlePaymentMethodChange).to.have.been.calledWith(session);
 | |
|     });
 | |
| 
 | |
|     it('handles changing a group sub', async () => {
 | |
|       session.metadata.type = 'edit-card-group';
 | |
| 
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(subscriptions.handlePaymentMethodChange).to.have.been.calledOnce;
 | |
|       expect(subscriptions.handlePaymentMethodChange).to.have.been.calledWith(session);
 | |
|     });
 | |
| 
 | |
|     it('applies a subscription', async () => {
 | |
|       session.metadata.type = 'subscription';
 | |
| 
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(subscriptions.applySubscription).to.have.been.calledOnce;
 | |
|       expect(subscriptions.applySubscription).to.have.been.calledWith(session);
 | |
|     });
 | |
| 
 | |
|     it('handles a one time payment', async () => {
 | |
|       session.metadata.type = 'something else';
 | |
| 
 | |
|       await stripePayments.handleWebhooks({ body, headers }, stripe);
 | |
| 
 | |
|       expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
 | |
|       expect(oneTimePayments.applyGemPayment).to.have.been.calledOnce;
 | |
|       expect(oneTimePayments.applyGemPayment).to.have.been.calledWith(session);
 | |
|     });
 | |
|   });
 | |
| });
 |