100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { FakeGateway } from './helpers/fake-gateway';
|
|
import { captureDecision, refundAmount } from '../src/payments/payment.policy';
|
|
|
|
describe('provider-neutral payment blueprint', () => {
|
|
const gateway = new FakeGateway('test-only-secret');
|
|
it('replays payment creation and rejects changed retry payloads', async () => {
|
|
const input = {
|
|
orderId: randomUUID(),
|
|
amountMinor: '12500',
|
|
currency: 'INR',
|
|
idempotencyKey: randomUUID(),
|
|
};
|
|
const first = await gateway.createPayment(input);
|
|
expect(await gateway.createPayment(input)).toEqual(first);
|
|
await expect(
|
|
gateway.createPayment({ ...input, amountMinor: '1' }),
|
|
).rejects.toThrow('idempotency');
|
|
});
|
|
it('verifies exact raw bytes and rejects malformed, oversized and forged callbacks', () => {
|
|
const event = {
|
|
eventId: randomUUID(),
|
|
paymentId: randomUUID(),
|
|
orderId: randomUUID(),
|
|
amountMinor: '12500',
|
|
currency: 'INR',
|
|
};
|
|
const raw = Buffer.from(JSON.stringify(event));
|
|
expect(gateway.verifyCapture(raw, gateway.sign(raw))).toEqual(event);
|
|
for (const signature of ['', 'x'.repeat(64), '0'.repeat(64)])
|
|
expect(() => gateway.verifyCapture(raw, signature)).toThrow('signature');
|
|
expect(() =>
|
|
gateway.verifyCapture(
|
|
Buffer.concat([raw, Buffer.from(' ')]),
|
|
gateway.sign(raw),
|
|
),
|
|
).toThrow('signature');
|
|
const oversized = Buffer.alloc(32769);
|
|
expect(() =>
|
|
gateway.verifyCapture(oversized, gateway.sign(oversized)),
|
|
).toThrow('signature');
|
|
const malformed = Buffer.from('{}');
|
|
expect(() =>
|
|
gateway.verifyCapture(malformed, gateway.sign(malformed)),
|
|
).toThrow();
|
|
});
|
|
it('requires matching capture references, amount and currency before fulfillment', () => {
|
|
const now = new Date();
|
|
const event = {
|
|
eventId: randomUUID(),
|
|
paymentId: randomUUID(),
|
|
orderId: randomUUID(),
|
|
amountMinor: '12500',
|
|
currency: 'INR',
|
|
};
|
|
const expected = {
|
|
...event,
|
|
expiresAt: new Date(now.getTime() + 1000),
|
|
cancelled: false,
|
|
alreadyCaptured: false,
|
|
};
|
|
expect(captureDecision(expected, event, now)).toBe('COMMIT_RESERVED_STOCK');
|
|
expect(
|
|
captureDecision({ ...expected, alreadyCaptured: true }, event, now),
|
|
).toBe('DUPLICATE');
|
|
expect(captureDecision({ ...expected, cancelled: true }, event, now)).toBe(
|
|
'REVIEW_AND_REFUND',
|
|
);
|
|
expect(captureDecision({ ...expected, expiresAt: now }, event, now)).toBe(
|
|
'REVIEW_AND_REFUND',
|
|
);
|
|
for (const change of [
|
|
{ paymentId: randomUUID() },
|
|
{ orderId: randomUUID() },
|
|
{ amountMinor: '1' },
|
|
{ currency: 'USD' },
|
|
])
|
|
expect(() =>
|
|
captureDecision(expected, { ...event, ...change }, now),
|
|
).toThrow();
|
|
});
|
|
it('counts pending refunds against captured money and makes refund retries idempotent', async () => {
|
|
expect(refundAmount(1000n, 200n, 300n, 500n)).toBe('500');
|
|
for (const request of [0n, -1n, 501n])
|
|
expect(() => refundAmount(1000n, 200n, 300n, request)).toThrow();
|
|
expect(() => refundAmount(1000n, -1n, 0n, 1n)).toThrow();
|
|
const request = {
|
|
paymentId: randomUUID(),
|
|
amountMinor: '500',
|
|
idempotencyKey: randomUUID(),
|
|
};
|
|
expect(await gateway.requestRefund(request)).toEqual(
|
|
await gateway.requestRefund(request),
|
|
);
|
|
await expect(
|
|
gateway.requestRefund({ ...request, amountMinor: '501' }),
|
|
).rejects.toThrow('idempotency');
|
|
});
|
|
});
|