← back to Costa Rica
costa-rica: payment-adapter tests — tilopay+onvo sandbox paths + webhook HMAC verify (7→23 tests) — TK-10346
633756df2d682bf9f20183ba6352ec806c8f4ee7 · 2026-08-07 11:25:06 -0700 · Steve
Files touched
Diff
commit 633756df2d682bf9f20183ba6352ec806c8f4ee7
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Aug 7 11:25:06 2026 -0700
costa-rica: payment-adapter tests — tilopay+onvo sandbox paths + webhook HMAC verify (7→23 tests) — TK-10346
---
test/payments.test.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 96 insertions(+)
diff --git a/test/payments.test.js b/test/payments.test.js
new file mode 100644
index 0000000..45705d7
--- /dev/null
+++ b/test/payments.test.js
@@ -0,0 +1,96 @@
+'use strict';
+// Payment-adapter sandbox + webhook-signature tests (node:test), zero deps.
+// Run: node --test (node runs each test file in its own process, so setting
+// *_WEBHOOK_SECRET here does not leak into the other test files).
+//
+// Env is set BEFORE requiring the adapters: they read WEBHOOK_SECRET at module
+// load. Setting ONLY the webhook secret keeps liveMode=false (live needs the
+// API user/pass/key), so these exercise the sandbox charge paths AND the real
+// HMAC verify branch in one process — no live creds, no real network, no money.
+process.env.TILOPAY_WEBHOOK_SECRET = 'whsec_test_tilopay';
+process.env.ONVO_WEBHOOK_SECRET = 'whsec_test_onvo';
+
+const { test } = require('node:test');
+const assert = require('node:assert');
+const crypto = require('crypto');
+
+const tilopay = require('../lib/payments/tilopay');
+const onvo = require('../lib/payments/onvo');
+
+const CHARGE = { amount: 12000, currency: 'USD', booking: { code: 'BK-1' },
+ customer: { email: 'a@b.co', name: 'A' }, returnUrl: 'https://x.test/return' };
+
+for (const p of [tilopay, onvo]) {
+ test(`${p.name}: sandbox even with a webhook secret (no live API creds)`, () => {
+ assert.equal(p.liveMode, false);
+ });
+
+ test(`${p.name}: createCharge card -> redirect clientAction with a ref`, async () => {
+ const r = await p.createCharge({ ...CHARGE, method: 'card' });
+ assert.equal(r.status, 'requires_action');
+ assert.equal(r.clientAction.type, 'redirect');
+ assert.ok(r.providerRef, 'has a providerRef');
+ assert.ok(r.clientAction.url.startsWith(CHARGE.returnUrl), 'redirect points at returnUrl');
+ assert.match(r.clientAction.url, /result=success/);
+ assert.equal(r.raw.sandbox, true);
+ });
+
+ test(`${p.name}: createCharge sinpe -> sinpe_instructions with a phone`, async () => {
+ const r = await p.createCharge({ ...CHARGE, method: 'sinpe' });
+ assert.equal(r.clientAction.type, 'sinpe_instructions');
+ assert.ok(r.clientAction.sinpe_phone, 'has a SINPE phone');
+ });
+
+ test(`${p.name}: getCharge on the sandbox ref reads succeeded`, async () => {
+ const c = await p.createCharge({ ...CHARGE, method: 'card' });
+ const g = await p.getCharge(c.providerRef);
+ assert.equal(g.status, 'succeeded'); // sandbox refs contain _sbx_
+ });
+
+ test(`${p.name}: getCharge on an unknown ref stays processing (not succeeded)`, async () => {
+ const g = await p.getCharge('unknown-ref-000');
+ assert.equal(g.status, 'processing');
+ });
+
+ test(`${p.name}: refund (sandbox) -> refunded`, async () => {
+ const r = await p.refund('some-ref', 100);
+ assert.equal(r.status, 'refunded');
+ });
+
+ test(`${p.name}: verifyWebhook accepts a correctly-signed body, rejects tamper`, () => {
+ const secret = p.name === 'tilopay' ? process.env.TILOPAY_WEBHOOK_SECRET : process.env.ONVO_WEBHOOK_SECRET;
+ const sigHeader = p.name === 'tilopay' ? 'x-tilopay-signature' : 'onvo-signature';
+ const body = JSON.stringify({ event: 'payment.succeeded', id: 'pay_1' });
+ const good = crypto.createHmac('sha256', secret).update(body).digest('hex');
+
+ const ok = p.verifyWebhook({ [sigHeader]: good }, body);
+ assert.equal(ok.ok, true, 'valid signature accepted');
+ assert.equal(ok.event.id, 'pay_1', 'event parsed on accept');
+
+ // Tampered body, same signature -> reject, no event leaked.
+ const bad = p.verifyWebhook({ [sigHeader]: good }, body + ' ');
+ assert.equal(bad.ok, false);
+ assert.equal(bad.event, null);
+
+ // Wrong-length signature must NOT throw (timingSafeEqual guard) -> reject.
+ const shortSig = p.verifyWebhook({ [sigHeader]: 'abc' }, body);
+ assert.equal(shortSig.ok, false);
+
+ // Missing signature header -> reject.
+ const noSig = p.verifyWebhook({}, body);
+ assert.equal(noSig.ok, false);
+ });
+}
+
+test('tilopay: payout (sandbox SINPE) returns a ref and processes', async () => {
+ const r = await tilopay.payout({ method: { sinpe_phone: '8888-0000' }, amount: 5000, currency: 'CRC', reference: 'PO-1' });
+ assert.ok(r.providerRef);
+ assert.equal(r.status, 'processing');
+});
+
+test('payments registry: unknown provider throws, known ones resolve', () => {
+ const { getProvider } = require('../lib/payments');
+ assert.equal(getProvider('tilopay').name, 'tilopay');
+ assert.equal(getProvider('onvo').name, 'onvo');
+ assert.throws(() => getProvider('stripe'), /unknown payment provider/);
+});
← a167acd auto-data-snapshot: 2026-08-07T11:14:36 (2 data files) — .pl
·
back to Costa Rica
·
costa-rica: places map viewer (/map) — Leaflet + marker clus a273d21 →