← back to Costa Rica
test/preflight.test.js
112 lines
'use strict';
// TK-10346 — boot-time fail-closed guard for live-integration-without-webhook-secret.
const { test } = require('node:test');
const assert = require('node:assert');
const { checkWebhookSecrets, runPreflight } = require('../lib/preflight');
const liveNoSecretPay = { name: 'tilopay', liveMode: true, webhookSecretSet: false };
const liveWithSecretPay = { name: 'tilopay', liveMode: true, webhookSecretSet: true };
const sandboxPay = { name: 'tilopay', liveMode: false, webhookSecretSet: false };
const wa = (live, secret, vtoken = true) => ({ liveMode: live, webhookSecretSet: secret, verifyTokenSet: vtoken });
test('checkWebhookSecrets: LIVE payment provider with NO webhook secret is flagged', () => {
const p = checkWebhookSecrets({ getProvider: () => liveNoSecretPay });
assert.equal(p.length, 1);
assert.match(p[0], /LIVE but its webhook secret is missing/);
assert.match(p[0], /TILOPAY_WEBHOOK_SECRET/);
});
test('checkWebhookSecrets: LIVE payment provider WITH a webhook secret is clean', () => {
assert.deepEqual(checkWebhookSecrets({ getProvider: () => liveWithSecretPay }), []);
});
test('checkWebhookSecrets: SANDBOX (not live) is clean even with no secret', () => {
assert.deepEqual(checkWebhookSecrets({ getProvider: () => sandboxPay, whatsapp: wa(false, false) }), []);
});
test('checkWebhookSecrets: onvo names its own secret env var', () => {
const p = checkWebhookSecrets({ getProvider: () => ({ name: 'onvo', liveMode: true, webhookSecretSet: false }) });
assert.match(p[0], /ONVO_WEBHOOK_SECRET/);
});
test('checkWebhookSecrets: LIVE WhatsApp with no APP_SECRET is flagged', () => {
const p = checkWebhookSecrets({ whatsapp: wa(true, false) });
assert.equal(p.length, 1);
assert.match(p[0], /WHATSAPP_APP_SECRET/);
});
test('runPreflight: THROWS in production when a live provider lacks its secret', () => {
assert.throws(
() => runPreflight({ getProvider: () => liveNoSecretPay, env: { NODE_ENV: 'production' } }),
/preflight failed/
);
});
test('runPreflight: dev/sandbox only WARNS (returns problems, does not throw)', () => {
let out;
assert.doesNotThrow(() => { out = runPreflight({ getProvider: () => liveNoSecretPay, env: { NODE_ENV: 'development' } }); });
assert.equal(out.length, 1); // problem surfaced (logged) but non-fatal off-prod
});
test('checkWebhookSecrets: LIVE WhatsApp with the DEFAULT verify token is flagged', () => {
const p = checkWebhookSecrets({ whatsapp: { liveMode: true, webhookSecretSet: true, verifyTokenSet: false } });
assert.equal(p.length, 1);
assert.match(p[0], /WHATSAPP_VERIFY_TOKEN/);
});
test('runPreflight: production with everything sandbox does NOT throw (guard is inert)', () => {
assert.doesNotThrow(() => runPreflight({ getProvider: () => sandboxPay, whatsapp: { liveMode: false, webhookSecretSet: false, verifyTokenSet: false }, env: { NODE_ENV: 'production' } }));
});
// INTEGRATION: exercise the REAL env -> module property -> guard chain (not fakes), so a
// getter rename (webhookSecretSet/liveMode) or an env-wiring regression is actually caught.
function freshRequire(relPath, env) {
const p = require.resolve(relPath);
const saved = {}; for (const k of Object.keys(env)) { saved[k] = process.env[k]; process.env[k] = env[k]; }
delete require.cache[p];
// tilopay/onvo are pulled in by lib/payments/index — bust those too
for (const dep of ['../lib/payments/index', '../lib/payments/tilopay', '../lib/payments/onvo', '../lib/whatsapp']) {
try { delete require.cache[require.resolve(dep)]; } catch {}
}
const mod = require(relPath);
const restore = () => { for (const k of Object.keys(env)) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } };
return { mod, restore };
}
test('INTEGRATION: real tilopay module — LIVE creds + webhook secret => guard clean', () => {
const { mod, restore } = freshRequire('../lib/payments/tilopay',
{ TILOPAY_API_USER: 'u', TILOPAY_API_PASSWORD: 'p', TILOPAY_API_KEY: 'k', TILOPAY_WEBHOOK_SECRET: 's' });
try {
assert.equal(mod.liveMode, true, 'real module should read LIVE from env');
assert.equal(mod.webhookSecretSet, true, 'real module should report secret set');
assert.deepEqual(checkWebhookSecrets({ getProvider: () => mod }), []);
} finally { restore(); }
});
test('INTEGRATION: real tilopay module — LIVE creds + NO webhook secret => guard flags it (the exact original bug)', () => {
const { mod, restore } = freshRequire('../lib/payments/tilopay',
{ TILOPAY_API_USER: 'u', TILOPAY_API_PASSWORD: 'p', TILOPAY_API_KEY: 'k', TILOPAY_WEBHOOK_SECRET: '' });
try {
assert.equal(mod.liveMode, true);
assert.equal(mod.webhookSecretSet, false);
// and the runtime symptom the guard exists to prevent: verifyWebhook rejects when live+no-secret
assert.equal(mod.verifyWebhook({}, '{}').ok, false, 'live + no secret must reject the webhook');
const p = checkWebhookSecrets({ getProvider: () => mod });
assert.equal(p.length, 1);
assert.match(p[0], /TILOPAY_WEBHOOK_SECRET/);
} finally { restore(); }
});
test('INTEGRATION: real whatsapp module — LIVE + default verify token => guard flags VERIFY_TOKEN', () => {
const { mod, restore } = freshRequire('../lib/whatsapp',
{ WHATSAPP_TOKEN: 't', WHATSAPP_PHONE_ID: 'pid', WHATSAPP_APP_SECRET: 'sec' }); // no WHATSAPP_VERIFY_TOKEN
try {
assert.equal(mod.liveMode, true);
assert.equal(mod.webhookSecretSet, true);
assert.equal(mod.verifyTokenSet, false, 'default verify token must not count as set');
const p = checkWebhookSecrets({ whatsapp: mod });
assert.equal(p.length, 1);
assert.match(p[0], /WHATSAPP_VERIFY_TOKEN/);
} finally { restore(); }
});