[object Object]

← 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 →