Skip to content

Commit

Permalink
test: added tests for routes PATCH /stripe/*
Browse files Browse the repository at this point in the history
  • Loading branch information
TeddyRoncin committed Sep 5, 2024
1 parent 7eb8cc3 commit 8235c66
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 32 deletions.
4 changes: 2 additions & 2 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import items from './items';
import teams from './teams';
import auth from './auth';
import tickets from './tickets';
import etupay from './stripe';
import stripe from './stripe';
import discord from './discord';
import admin from './admin';
import commissions from './commissions';
Expand Down Expand Up @@ -58,7 +58,7 @@ router.use('/items', items);
router.use('/tickets', tickets);

// Etupay routes
router.use('/etupay', etupay);
router.use('/stripe', stripe);

// Discord routes
router.use('/discord', discord);
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/stripe/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Router } from 'express';
import paymentAcceptedWebhook from "./paymentAcceptedWebhook";
import paymentExpiredWebhook from "./paymentExpiredWebhook";

const router = Router();

router.post('/accepted', paymentAcceptedWebhook);
router.post('/expired', paymentExpiredWebhook);

export default router;
19 changes: 4 additions & 15 deletions src/controllers/stripe/paymentAcceptedWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import stripe from 'stripe';
import Joi from 'joi';
import { fetchCartFromTransactionId, updateCart } from '../../operations/carts';
import { sendPaymentConfirmation } from '../../services/email';
import { Error, EtupayError, TransactionState } from '../../types';
import { badRequest, forbidden, notFound, success } from '../../utils/responses';
import { Error, TransactionState } from '../../types';
import { notFound, success } from '../../utils/responses';
import { validateBody } from '../../middlewares/validation';

// This route is a webhook called by stripe
Expand All @@ -27,12 +27,6 @@ export default [
}).unknown(true),
),

// Create a small middleware to be able to handle payload errors.
// The eslint disabling is important because the error argument can only be gotten in the 4 arguments function
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(error: EtupayError, request: Request, response: Response, next: NextFunction) =>
badRequest(response, Error.InvalidQueryParameters),

async (request: Request, response: Response, next: NextFunction) => {
try {
const session = request.body.data.object as stripe.Checkout.Session;
Expand All @@ -45,14 +39,9 @@ export default [
return notFound(response, Error.CartNotFound);
}

// If the transaction is already paid
if (cart.transactionState === TransactionState.paid) {
return forbidden(response, Error.CartAlreadyPaid);
}

// If the transaction is already errored
// If the transaction is already errored (force-payed, directly changed in database, ...)
if (cart.transactionState !== TransactionState.pending) {
return forbidden(response, Error.AlreadyErrored);
return success(response, { api: 'ok' });
}

// Update the cart with the callback data
Expand Down
16 changes: 2 additions & 14 deletions src/controllers/stripe/paymentExpiredWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { NextFunction, Request, Response } from 'express';
import stripe from 'stripe';
import Joi from 'joi';
import { fetchCartFromTransactionId, updateCart } from '../../operations/carts';
import { sendPaymentConfirmation } from '../../services/email';
import { Error, EtupayError, TransactionState } from '../../types';
import { badRequest, forbidden, notFound, success } from '../../utils/responses';
import { validateBody } from '../../middlewares/validation';

// This route is a webhook called by stripe
// To test it, first create an account on stripe, and install the stripe cli (https://docs.stripe.com/stripe-cli). Then, you can run the command :
// stripe listen --forward-to localhost:3000/stripe/accepted --events checkout.session.completed
// stripe listen --forward-to localhost:3000/stripe/accepted --events checkout.session.expired
// Small tip I discovered a bit later than I would have liked to : you can resend an event with
// stripe events resend <eventId>
// You can find the eventId at https://dashboard.stripe.com/test/workbench/webhooks
Expand All @@ -27,12 +26,6 @@ export default [
}).unknown(true),
),

// Create a small middleware to be able to handle payload errors.
// The eslint disabling is important because the error argument can only be gotten in the 4 arguments function
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(error: EtupayError, request: Request, response: Response, next: NextFunction) =>
badRequest(response, Error.InvalidQueryParameters),

async (request: Request, response: Response, next: NextFunction) => {
try {
const session = request.body.data.object as stripe.Checkout.Session;
Expand All @@ -45,12 +38,7 @@ export default [
return notFound(response, Error.CartNotFound);
}

// If the transaction is already paid
if (cart.transactionState === TransactionState.paid) {
return forbidden(response, Error.CartAlreadyPaid);
}

// If the transaction is already errored, simply return.
// If the transaction is already errored or paid (probably forced-pay, else that's strange), simply return.
if (cart.transactionState !== TransactionState.pending) {
return success(response, { api: 'ok' });
}
Expand Down
1 change: 0 additions & 1 deletion tests/admin/items/syncWithStripe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe('PATCH /admin/items/stripe-sync', () => {

after(async () => {
// Delete the user created
await database.orga.deleteMany();
await database.user.deleteMany();
});

Expand Down
62 changes: 62 additions & 0 deletions tests/stripe/paymentAcceptedWebhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import request from 'supertest';
import { TransactionState } from '@prisma/client';
import { expect } from 'chai';
import app from '../../src/app';
import { sandbox } from '../setup';
import * as cartOperations from '../../src/operations/carts';
import database from '../../src/services/database';
import { Error, User, Cart } from '../../src/types';
import { createFakeUser, createFakeCart } from '../utils';
import { updateCart } from '../../src/operations/carts';

describe('POST /stripe/accepted', () => {
let user: User;
let cart: Cart;

before(async () => {
user = await createFakeUser();
cart = await createFakeCart({ userId: user.id, items: [] });
});

after(async () => {
await database.user.deleteMany();
await database.cart.deleteMany();
});

const generateCart = (transactionId: string) => ({
object: 'event',
type: 'checkout.session.accepted',
data: {
object: {
object: 'checkout.session',
id: transactionId,
},
},
});

it('should fail with an internal server error', async () => {
sandbox.stub(cartOperations, 'fetchCartFromTransactionId').throws('Unexpected error');
await request(app).post('/stripe/accepted').send(generateCart('plz throw')).expect(500, { error: Error.InternalServerError });
});

it('should fail as the transaction id does not exist', () =>
request(app).post('/stripe/accepted').send(generateCart('I AM A H4X0R')).expect(404, { error: Error.CartNotFound }));

it('should change the transactionState of the cart from pending to paid', async () => {
await updateCart(cart.id, 'supersecret', TransactionState.pending);
await request(app).post('/stripe/accepted').send(generateCart('supersecret')).expect(200, { api: 'ok' });
const databaseCart = await database.cart.findUnique({ where: { id: cart.id } });
expect(databaseCart.transactionState).to.equal(TransactionState.paid);
});

describe('test for initial transactionState = `paid` | `expired` | `refunded`', () => {
for (const transactionState of [TransactionState.paid, TransactionState.expired, TransactionState.refunded]) {
it(`should not change the transactionState of the cart as it already equals ${transactionState}`, async () => {
await updateCart(cart.id, 'supersecret', transactionState);
await request(app).post('/stripe/expired').send(generateCart('supersecret')).expect(200, { api: 'ok' });
const databaseCart = await database.cart.findUnique({ where: { id: cart.id } });
expect(databaseCart.transactionState).to.equal(transactionState);
});
}
});
});
62 changes: 62 additions & 0 deletions tests/stripe/paymentExpiredWebhook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import request from 'supertest';
import { TransactionState } from '@prisma/client';
import { expect } from 'chai';
import app from '../../src/app';
import { sandbox } from '../setup';
import * as cartOperations from '../../src/operations/carts';
import database from '../../src/services/database';
import { Error, User, Cart } from '../../src/types';
import { createFakeUser, createFakeCart } from '../utils';
import { updateCart } from '../../src/operations/carts';

describe('POST /stripe/expired', () => {
let user: User;
let cart: Cart;

before(async () => {
user = await createFakeUser();
cart = await createFakeCart({ userId: user.id, items: [] });
});

after(async () => {
await database.cart.deleteMany();
await database.user.deleteMany();
});

const generateCart = (transactionId: string) => ({
object: 'event',
type: 'checkout.session.expired',
data: {
object: {
object: 'checkout.session',
id: transactionId,
},
},
});

it('should fail with an internal server error', async () => {
sandbox.stub(cartOperations, 'fetchCartFromTransactionId').throws('Unexpected error');
await request(app).post('/stripe/expired').send(generateCart('plz throw')).expect(500, { error: Error.InternalServerError });
});

it('should fail as the transaction id does not exist', () =>
request(app).post('/stripe/expired').send(generateCart('I AM A H4X0R')).expect(404, { error: Error.CartNotFound }));

it('should change the transactionState of the cart from `pending` to `expired`', async () => {
await updateCart(cart.id, 'supersecret', TransactionState.pending);
await request(app).post('/stripe/expired').send(generateCart('supersecret')).expect(200, { api: 'ok' });
const databaseCart = await database.cart.findUnique({ where: { id: cart.id } });
expect(databaseCart.transactionState).to.equal(TransactionState.expired);
});

describe('test for initial transactionState = `paid` | `expired` | `refunded`', () => {
for (const transactionState of [TransactionState.paid, TransactionState.expired, TransactionState.refunded]) {
it(`should not change the transactionState of the cart as it already equals ${transactionState}`, async () => {
await updateCart(cart.id, 'supersecret', transactionState);
await request(app).post('/stripe/expired').send(generateCart('supersecret')).expect(200, { api: 'ok' });
const databaseCart = await database.cart.findUnique({ where: { id: cart.id } });
expect(databaseCart.transactionState).to.equal(transactionState);
});
}
});
});

0 comments on commit 8235c66

Please sign in to comment.