Real World Example

Testing a Payment Webhook

Payment gateways (Razorpay, Cashfree, Stripe) call your API with a signed webhook when a payment succeeds. A bug here means paid orders stay "pending" and customers call support. Teams cover it with an e2e test that signs a fake event exactly like the gateway does.

test/razorpay-webhook.e2e-spec.ts

import { createHmac } from 'node:crypto';
import request from 'supertest';

const SECRET = 'whsec_test';
const sign = (body: string) => createHmac('sha256', SECRET).update(body).digest('hex');

describe('POST /webhooks/razorpay (e2e)', () => {
  // app setup as in the previous topic, with RAZORPAY_WEBHOOK_SECRET=whsec_test

  it('marks the order as paid for a valid signature', async () => {
    const body = JSON.stringify({ event: 'payment.captured', payload: { payment: { entity: { id: 'pay_N8kq2', notes: { orderId: 'ORD-7781' }, amount: 129900 } } } });
    await request(app.getHttpServer())
      .post('/webhooks/razorpay')
      .set('Content-Type', 'application/json')
      .set('X-Razorpay-Signature', sign(body))
      .send(body)
      .expect(200);
    const order = await prisma.order.findUniqueOrThrow({ where: { id: 'ORD-7781' } });
    expect(order.status).toBe('PAID');
  });

  it('rejects a forged signature', async () => {
    const body = JSON.stringify({ event: 'payment.captured' });
    await request(app.getHttpServer())
      .post('/webhooks/razorpay')
      .set('Content-Type', 'application/json')
      .set('X-Razorpay-Signature', 'deadbeef')
      .send(body)
      .expect(401);
  });
});

Note: To verify webhook signatures you need the raw request body: create the app with NestFactory.create(AppModule, { rawBody: true }) and read req.rawBody (type RawBodyRequest<Request>).