Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/modules/webhooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as webhookRouter } from './webhook.router';
export * from './webhook.types';
export { dispatchWebhookEvent } from './webhook.service';
export { verifyWebhookSignature } from './webhook-signature.utils';
128 changes: 74 additions & 54 deletions src/modules/webhooks/webhook-signature.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,88 @@ import { verifyWebhookSignature } from './webhook-signature.utils';
// ── Helpers ───────────────────────────────────────────────────────────────────

const secret = 'test-webhook-signing-secret';
const payload = Buffer.from(JSON.stringify({ event_type: 'buy', amount: '10' }));
const payload = Buffer.from(
JSON.stringify({ event_type: 'buy', amount: '10' })
);

function computeValidHeader(): string {
const hex = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return `sha256=${hex}`;
const hex = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${hex}`;
}

function flipHexChar(char: string): string {
return char === '0' ? '1' : '0';
return char === '0' ? '1' : '0';
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('verifyWebhookSignature', () => {
it('returns true for a valid signature', () => {
expect(verifyWebhookSignature(payload, computeValidHeader(), secret)).toBe(true);
});

it('returns false when the signature differs in the last character', () => {
const header = computeValidHeader();
const tampered =
header.slice(0, -1) + flipHexChar(header[header.length - 1]);

expect(verifyWebhookSignature(payload, tampered, secret)).toBe(false);
});

it('returns false when the signature differs in the first character', () => {
const header = computeValidHeader();
const prefix = 'sha256=';
const firstHexChar = header[prefix.length];
const tampered =
prefix + flipHexChar(firstHexChar) + header.slice(prefix.length + 1);

expect(verifyWebhookSignature(payload, tampered, secret)).toBe(false);
});

it('returns false without throwing when the signature is one character shorter than expected', () => {
const header = computeValidHeader();
const shortened = header.slice(0, -1);

expect(() => verifyWebhookSignature(payload, shortened, secret)).not.toThrow();
expect(verifyWebhookSignature(payload, shortened, secret)).toBe(false);
});

it('returns false without throwing when the signature is one character longer than expected', () => {
const header = computeValidHeader();
const lengthened = `${header}a`;

expect(() => verifyWebhookSignature(payload, lengthened, secret)).not.toThrow();
expect(verifyWebhookSignature(payload, lengthened, secret)).toBe(false);
});

it('returns false without throwing for a malformed header', () => {
expect(() =>
verifyWebhookSignature(payload, 'not-a-valid-header', secret)
).not.toThrow();
expect(verifyWebhookSignature(payload, 'not-a-valid-header', secret)).toBe(
false
);
});

it('returns false for an empty header', () => {
expect(verifyWebhookSignature(payload, '', secret)).toBe(false);
});
it('returns true for a valid signature', () => {
expect(
verifyWebhookSignature(payload, computeValidHeader(), secret)
).toBe(true);
});

it('returns false for a tampered payload', () => {
const tamperedPayload = Buffer.from(
JSON.stringify({ event_type: 'buy', amount: '999' })
);
expect(
verifyWebhookSignature(tamperedPayload, computeValidHeader(), secret)
).toBe(false);
});

it('returns false when the signature differs in the last character', () => {
const header = computeValidHeader();
const tampered =
header.slice(0, -1) + flipHexChar(header[header.length - 1]);

expect(verifyWebhookSignature(payload, tampered, secret)).toBe(false);
});

it('returns false when the signature differs in the first character', () => {
const header = computeValidHeader();
const prefix = 'sha256=';
const firstHexChar = header[prefix.length];
const tampered =
prefix + flipHexChar(firstHexChar) + header.slice(prefix.length + 1);

expect(verifyWebhookSignature(payload, tampered, secret)).toBe(false);
});

it('returns false without throwing when the signature is one character shorter than expected', () => {
const header = computeValidHeader();
const shortened = header.slice(0, -1);

expect(() =>
verifyWebhookSignature(payload, shortened, secret)
).not.toThrow();
expect(verifyWebhookSignature(payload, shortened, secret)).toBe(false);
});

it('returns false without throwing when the signature is one character longer than expected', () => {
const header = computeValidHeader();
const lengthened = `${header}a`;

expect(() =>
verifyWebhookSignature(payload, lengthened, secret)
).not.toThrow();
expect(verifyWebhookSignature(payload, lengthened, secret)).toBe(false);
});

it('returns false without throwing for a malformed header', () => {
expect(() =>
verifyWebhookSignature(payload, 'not-a-valid-header', secret)
).not.toThrow();
expect(
verifyWebhookSignature(payload, 'not-a-valid-header', secret)
).toBe(false);
});

it('returns false for an empty header', () => {
expect(verifyWebhookSignature(payload, '', secret)).toBe(false);
});
});
Loading