manicanldes-backend/test/payment-blueprint.spec.ts

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');
});
});