Skip to content

Commit

Permalink
fix(payments-plugin): Fix Stripe controller crashing server instance (#…
Browse files Browse the repository at this point in the history
…2454)

Fixes #2450

Co-authored-by: Balázs Gallay <balazs.gallay@zooshgroup.com>
  • Loading branch information
carathorys and Balázs Gallay authored Oct 17, 2023
1 parent ec61b58 commit b0ece21
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
99 changes: 97 additions & 2 deletions packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS } from '@vendure/core/e2e/graph
import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
import gql from 'graphql-tag';
import nock from 'nock';
import fetch from 'node-fetch';
import path from 'path';
import { Stripe } from 'stripe';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { initialData } from '../../../e2e-common/e2e-initial-data';
Expand Down Expand Up @@ -293,6 +295,101 @@ describe('Stripe payments', () => {
});
});

// https://github.com/vendure-ecommerce/vendure/issues/2450
it('Should not crash on signature validation failure', async () => {
const MOCKED_WEBHOOK_PAYLOAD = {
id: 'evt_0',
object: 'event',
api_version: '2022-11-15',
data: {
object: {
id: 'pi_0',
currency: 'usd',
status: 'succeeded',
},
},
livemode: false,
pending_webhooks: 1,
request: {
id: 'req_0',
idempotency_key: '00000000-0000-0000-0000-000000000000',
},
type: 'payment_intent.succeeded',
};

const payloadString = JSON.stringify(MOCKED_WEBHOOK_PAYLOAD, null, 2);

const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
method: 'post',
body: payloadString,
headers: { 'Content-Type': 'application/json' },
});

// We didn't provided any signatures, it should result in a 400 - Bad request
expect(result.status).toEqual(400);
});

// TODO: Contribution welcome: test webhook handling and order settlement
// https://github.com/vendure-ecommerce/vendure/issues/2450
it("Should validate the webhook's signature properly", async () => {
await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');

const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
order = activeOrder!;

const MOCKED_WEBHOOK_PAYLOAD = {
id: 'evt_0',
object: 'event',
api_version: '2022-11-15',
data: {
object: {
id: 'pi_0',
currency: 'usd',
metadata: {
orderCode: order.code,
orderId: parseInt(order.id.replace('T_', ''), 10),
channelToken: E2E_DEFAULT_CHANNEL_TOKEN,
},
amount_received: order.totalWithTax,
status: 'succeeded',
},
},
livemode: false,
pending_webhooks: 1,
request: {
id: 'req_0',
idempotency_key: '00000000-0000-0000-0000-000000000000',
},
type: 'payment_intent.succeeded',
};

const payloadString = JSON.stringify(MOCKED_WEBHOOK_PAYLOAD, null, 2);
const stripeWebhooks = new Stripe('test-api-secret', { apiVersion: '2023-08-16' }).webhooks;
const header = stripeWebhooks.generateTestHeaderString({
payload: payloadString,
secret: 'test-signing-secret',
});

const event = stripeWebhooks.constructEvent(payloadString, header, 'test-signing-secret');
expect(event.id).to.equal(MOCKED_WEBHOOK_PAYLOAD.id);
await setShipping(shopClient);
// Due to the `this.orderService.transitionToState(...)` fails with the internal lookup by id,
// we need to put the order into `ArrangingPayment` state manually before calling the webhook handler.
// const transitionResult = await adminClient.query(TRANSITION_TO_ARRANGING_PAYMENT, { id: order.id });
// expect(transitionResult.transitionOrderToState.__typename).toBe('Order')

const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
method: 'post',
body: payloadString,
headers: { 'Content-Type': 'application/json', 'Stripe-Signature': header },
});

// I would expect to the status to be 200, but at the moment either the
// `orderService.transitionToState()` or the `orderService.addPaymentToOrder()`
// throws an error of 'error.entity-with-id-not-found'
expect(result.status).toEqual(200);
});

// https://github.com/vendure-ecommerce/vendure/issues/1630
describe('currencies with no fractional units', () => {
let japanProductId: string;
Expand Down Expand Up @@ -401,6 +498,4 @@ describe('Stripe payments', () => {
expect(createPaymentIntentPayload.currency).toBe('jpy');
});
});

// TODO: Contribution welcome: test webhook handling and order settlement
});
1 change: 1 addition & 0 deletions packages/payments-plugin/src/stripe/raw-body.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RequestWithRawBody } from './types';
* Stripe to properly verify webhook events.
*/
export const rawBodyMiddleware = raw({
type: '*/*',
verify(req: RequestWithRawBody, res: http.ServerResponse, buf: Buffer, encoding: string) {
if (Buffer.isBuffer(buf)) {
req.rawBody = Buffer.from(buf);
Expand Down
15 changes: 12 additions & 3 deletions packages/payments-plugin/src/stripe/stripe.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class StripeController {
return;
}

const event = request.body as Stripe.Event;
const event = JSON.parse(request.body.toString()) as Stripe.Event;
const paymentIntent = event.data.object as Stripe.PaymentIntent;

if (!paymentIntent) {
Expand Down Expand Up @@ -120,11 +120,20 @@ export class StripeController {
`Error adding payment to order ${orderCode}: ${addPaymentToOrderResult.message}`,
loggerCtx,
);
return;
}

// The payment intent ID is added to the order only if we can reach this point.
Logger.info(
`Stripe payment intent id ${paymentIntent.id} added to order ${orderCode}`,
loggerCtx,
);
});

Logger.info(`Stripe payment intent id ${paymentIntent.id} added to order ${orderCode}`, loggerCtx);
response.status(HttpStatus.OK).send('Ok');
// Send the response status only if we didn't sent anything yet.
if (!response.headersSent) {
response.status(HttpStatus.OK).send('Ok');
}
}

private async createContext(channelToken: string, req: RequestWithRawBody): Promise<RequestContext> {
Expand Down

0 comments on commit b0ece21

Please sign in to comment.