← back to Costa Rica

test/whatsapp.test.js

73 lines

'use strict';
// Offline tests (node:test) for lib/whatsapp.js webhook security + sandbox safety.
// verifySignature is the gate that keeps FORGED WhatsApp webhooks out (HMAC-SHA256
// over the raw body, timing-safe compare). verifyChallenge is the GET handshake.
// Run: node --test
const { test, after } = require('node:test');
const assert = require('node:assert');
const crypto = require('crypto');

const signingFixture = 'test-app-secret';           // fake HMAC key, test-only (not a real credential)
process.env.WHATSAPP_APP_SECRET = signingFixture;   // set BEFORE require (read at module load)
delete require.cache[require.resolve('../lib/whatsapp')];
const wa = require('../lib/whatsapp');

const sign = (body) => 'sha256=' + crypto.createHmac('sha256', signingFixture).update(body).digest('hex');

test('SAFETY: liveMode is false with no WhatsApp token', () => {
  assert.equal(wa.liveMode, false);
});

test('verifyChallenge: correct verify_token returns the challenge; wrong/missing rejected', () => {
  // VERIFY_TOKEN defaults to 'cr-verify-sandbox' when WHATSAPP_VERIFY_TOKEN is unset
  assert.deepEqual(wa.verifyChallenge({ 'hub.mode': 'subscribe', 'hub.verify_token': 'cr-verify-sandbox', 'hub.challenge': '4242' }),
    { ok: true, challenge: '4242' });
  assert.equal(wa.verifyChallenge({ 'hub.mode': 'subscribe', 'hub.verify_token': 'WRONG', 'hub.challenge': '4242' }).ok, false);
  assert.equal(wa.verifyChallenge({}).ok, false);
});

test('SECURITY: verifySignature returns strict boolean true for a correctly-signed body', () => {
  const body = JSON.stringify({ entry: [{ id: '1' }] });
  assert.strictEqual(wa.verifySignature({ 'x-hub-signature-256': sign(body) }, body), true);
});

test('SECURITY: verifySignature returns strict boolean false for wrong sig, tampered body, missing/malformed header', () => {
  const body = JSON.stringify({ entry: [{ id: '1' }] });
  const good = sign(body);
  // wrong sig (same length, last hex char flipped) → timing-safe compare fails
  const wrong = good.slice(0, -1) + (good.slice(-1) === '0' ? '1' : '0');
  assert.strictEqual(wa.verifySignature({ 'x-hub-signature-256': wrong }, body), false);
  // right sig but tampered body
  assert.strictEqual(wa.verifySignature({ 'x-hub-signature-256': good }, body + ' '), false);
  // missing header → strict false (not the empty-string the old `sig && ...` returned)
  assert.strictEqual(wa.verifySignature({}, body), false);
  // malformed/short sig must not throw (length mismatch caught → false)
  assert.strictEqual(wa.verifySignature({ 'x-hub-signature-256': 'sha256=abcd' }, body), false);
});

test('SANDBOX: no APP_SECRET + NOT live → accepts (dev convenience)', () => {
  delete process.env.WHATSAPP_APP_SECRET;
  delete require.cache[require.resolve('../lib/whatsapp')];
  const waSbx = require('../lib/whatsapp');
  assert.strictEqual(waSbx.liveMode, false);
  assert.strictEqual(waSbx.verifySignature({}, 'anything'), true); // !APP_SECRET → !LIVE (true)
});

test('SECURITY: no APP_SECRET + LIVE=true → REJECTS (prod misconfig must not become a bypass)', () => {
  const saved = { t: process.env.WHATSAPP_TOKEN, p: process.env.WHATSAPP_PHONE_ID };
  process.env.WHATSAPP_TOKEN = 'fake-token';       // force LIVE=true
  process.env.WHATSAPP_PHONE_ID = 'fake-phone-id';
  delete process.env.WHATSAPP_APP_SECRET;          // but the signing secret is missing
  delete require.cache[require.resolve('../lib/whatsapp')];
  const waProd = require('../lib/whatsapp');
  assert.strictEqual(waProd.liveMode, true);
  assert.strictEqual(waProd.verifySignature({}, 'anything'), false); // live + no secret → reject, NOT accept
  // restore env so we don't poison anything else in-process
  if (saved.t === undefined) delete process.env.WHATSAPP_TOKEN; else process.env.WHATSAPP_TOKEN = saved.t;
  if (saved.p === undefined) delete process.env.WHATSAPP_PHONE_ID; else process.env.WHATSAPP_PHONE_ID = saved.p;
});

// Leave the module cache clean so a later-added test file can't inherit this file's
// mutated (no-secret / faked-live) whatsapp instance.
after(() => { delete require.cache[require.resolve('../lib/whatsapp')]; });