1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Server: Handle Stripe webhook receiving multiple times the same event

This commit is contained in:
Laurent Cozic 2021-07-22 21:54:21 +01:00
parent 27c3cbdf8f
commit 252d0695a4
2 changed files with 93 additions and 6 deletions

View File

@ -0,0 +1,68 @@
import { AccountType } from '../../models/UserModel';
import { initStripe, stripeConfig } from '../../utils/stripe';
import { beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, expectNotThrow } from '../../utils/testing/testUtils';
import uuidgen from '../../utils/uuidgen';
import { postHandlers } from './stripe';
async function createUserViaSubscription(userEmail: string, eventId: string = '') {
eventId = eventId || uuidgen();
const stripeSessionId = 'sess_123';
const stripePriceId = stripeConfig().proPriceId;
await models().keyValue().setValue(`stripeSessionToPriceId::${stripeSessionId}`, stripePriceId);
const ctx = await koaAppContext();
const stripe = initStripe();
await postHandlers.webhook(stripe, {}, ctx, {
id: eventId,
type: 'checkout.session.completed',
data: {
object: {
id: stripeSessionId,
customer: 'cus_123',
subscription: 'sub_123',
customer_details: {
email: userEmail,
},
},
},
}, false);
}
describe('index/stripe', function() {
beforeAll(async () => {
await beforeAllDb('index/stripe');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should handle the checkout.session.completed event', async function() {
const startTime = Date.now();
await createUserViaSubscription('toto@example.com');
const user = await models().user().loadByEmail('toto@example.com');
expect(user.account_type).toBe(AccountType.Pro);
const sub = await models().subscription().byUserId(user.id);
expect(sub.stripe_subscription_id).toBe('sub_123');
expect(sub.is_deleted).toBe(0);
expect(sub.last_payment_time).toBeGreaterThanOrEqual(startTime);
expect(sub.last_payment_failed_time).toBe(0);
});
test('should not process the same event twice', async function() {
await createUserViaSubscription('toto@example.com', 'evt_1');
const v = await models().keyValue().value('stripeEventDone::evt_1');
expect(v).toBe(1);
// This event should simply be skipped
await expectNotThrow(async () => createUserViaSubscription('toto@example.com', 'evt_1'));
});
});

View File

@ -41,7 +41,12 @@ function priceIdToAccountType(priceId: string): AccountType {
type StripeRouteHandler = (stripe: Stripe, path: SubPath, ctx: AppContext)=> Promise<any>;
const postHandlers: Record<string, StripeRouteHandler> = {
interface PostHandlers {
createCheckoutSession: Function;
webhook: Function;
}
export const postHandlers: PostHandlers = {
createCheckoutSession: async (stripe: Stripe, __path: SubPath, ctx: AppContext) => {
const fields = await bodyFields<CreateCheckoutSessionFields>(ctx.req);
@ -99,8 +104,18 @@ const postHandlers: Record<string, StripeRouteHandler> = {
// - The public config is under packages/server/stripeConfig.json
// - The private config is in the server .env file
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => {
const event = await stripeEvent(stripe, ctx.req);
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext, event: Stripe.Event = null, logErrors: boolean = true) => {
event = event ? event : await stripeEvent(stripe, ctx.req);
// Webhook endpoints might occasionally receive the same event more than
// once.
// https://stripe.com/docs/webhooks/best-practices#duplicate-events
const eventDoneKey = `stripeEventDone::${event.id}`;
if (await ctx.joplin.models.keyValue().value<number>(eventDoneKey)) {
logger.info(`Skipping event that has already been done: ${event.id}`);
return;
}
await ctx.joplin.models.keyValue().setValue(eventDoneKey, 1);
const hooks: any = {
@ -235,7 +250,11 @@ const postHandlers: Record<string, StripeRouteHandler> = {
try {
await hooks[event.type]();
} catch (error) {
logger.error(`Error processing event ${event.type}:`, event, error);
if (logErrors) {
logger.error(`Error processing event ${event.type}:`, event, error);
} else {
throw error;
}
}
} else {
logger.info(`Got Stripe event: ${event.type} [Unhandled]`);
@ -336,8 +355,8 @@ const getHandlers: Record<string, StripeRouteHandler> = {
};
router.post('stripe/:id', async (path: SubPath, ctx: AppContext) => {
if (!postHandlers[path.id]) throw new ErrorNotFound(`No such action: ${path.id}`);
return postHandlers[path.id](initStripe(), path, ctx);
if (!(postHandlers as any)[path.id]) throw new ErrorNotFound(`No such action: ${path.id}`);
return (postHandlers as any)[path.id](initStripe(), path, ctx);
});
router.get('stripe/:id', async (path: SubPath, ctx: AppContext) => {