← 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(); }
});