Unit test three main areas in a Paystack integration: (1) webhook signature validation — does your HMAC check correctly accept valid signatures and reject invalid ones? (2) business logic — does your order fulfillment function correctly update the DB given a successful charge event? (3) idempotency — does your handler skip processing if the reference was already processed? Mock the Paystack API and the database in these tests.
Three areas worth unit testing:
What you do not need to unit test: Paystack's own API behavior, the correctness of the transaction data Paystack returns, or network-level reliability.
// webhook-handler.test.js
const crypto = require('crypto');
const { handleWebhook } = require('./webhook-handler');
const SECRET = 'test_secret_key';
function makeSignature(body) {
return crypto.createHmac('sha512', SECRET).update(body).digest('hex');
}
describe('Webhook signature validation', () => {
it('accepts a request with valid signature', async () => {
var body = JSON.stringify({ event: 'charge.success', data: { reference: 'ref_001', amount: 100000, status: 'success', metadata: { order_id: '123' } } });
var sig = makeSignature(body);
var result = await handleWebhook(body, sig, SECRET);
expect(result.status).toBe(200);
});
it('rejects a request with wrong signature', async () => {
var body = JSON.stringify({ event: 'charge.success', data: {} });
var result = await handleWebhook(body, 'wrong_sig', SECRET);
expect(result.status).toBe(401);
});
});
describe('Charge fulfillment', () => {
it('fulfills order on charge.success', async () => {
var mockDb = { orders: { update: jest.fn() } };
var event = { event: 'charge.success', data: { reference: 'ref_002', amount: 100000, status: 'success', metadata: { order_id: '456' } } };
await fulfillOrder(event.data, mockDb);
expect(mockDb.orders.update).toHaveBeenCalledWith({ id: '456', status: 'paid', paystack_ref: 'ref_002' });
});
it('does not fulfill on charge.failed', async () => {
var mockDb = { orders: { update: jest.fn() } };
var event = { event: 'charge.failed', data: { reference: 'ref_003', metadata: { order_id: '789' } } };
await handleFailedCharge(event.data, mockDb);
expect(mockDb.orders.update).toHaveBeenCalledWith({ id: '789', status: 'payment_failed' });
// Crucially: fulfillOrder should NOT have been called
});
});
describe('Idempotency', () => {
it('skips fulfillment if reference already processed', async () => {
var mockDb = {
webhookEvents: { findOne: jest.fn().mockResolvedValue({ id: 1 }), create: jest.fn() },
orders: { update: jest.fn() },
};
var event = { event: 'charge.success', data: { reference: 'ref_001', metadata: { order_id: '123' } } };
await handleWithIdempotency(event, mockDb);
expect(mockDb.orders.update).not.toHaveBeenCalled();
});
});
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
This guide is part of the Paystack testing guide.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course