← back to Costa Rica

test/whatsapp-timeout.test.js

91 lines

'use strict';
// lib/whatsapp.js now routes its live Graph API sends through fetchT (PRE-FLIGHT
// #4/#5 class). WHY it matters: sends are best-effort and callers wrap them in
// try/catch, but that catches an ERROR, not a HANG — a stalled Graph connection on
// raw fetch() would hang confirmBooking forever (and its money-path callers). This
// proves the wiring: a header-fast/body-stalled send rejects with PROVIDER_TIMEOUT
// (so the caller's try/catch fires) instead of hanging, and a Graph HTTP error
// still fails closed (throws). Forces LIVE via env + require-cache reset; faked
// fetch, so no network and no DB (the throw happens in _send before any persistence).

const { test } = require('node:test');
const assert = require('node:assert');

const WA = require.resolve('../lib/whatsapp');
const realFetch = global.fetch;

function loadLiveWa() {
  process.env.WHATSAPP_TOKEN = 'tok';
  process.env.WHATSAPP_PHONE_ID = '123456';
  delete require.cache[WA];
  return require(WA);
}
function unloadLiveWa() {
  delete process.env.WHATSAPP_TOKEN;
  delete process.env.WHATSAPP_PHONE_ID;
  delete require.cache[WA];
}

test('wa sendText: a body-stalled Graph response rejects PROVIDER_TIMEOUT (not an infinite hang that would stall confirmBooking)', async () => {
  process.env.PROVIDER_HTTP_TIMEOUT_MS = '40';
  const wa = loadLiveWa();
  global.fetch = (url, opts) => Promise.resolve({
    ok: true, status: 200,
    json: () => new Promise((_resolve, reject) => {
      opts.signal.addEventListener('abort', () => { const e = new Error('aborted'); e.name = 'AbortError'; reject(e); });
    }),
  });
  try {
    assert.equal(wa.liveMode, true, 'live mode for this test');
    await assert.rejects(
      () => wa.sendText('50688887777', 'hola'),
      (err) => { assert.equal(err.code, 'PROVIDER_TIMEOUT'); return true; },
    );
  } finally {
    global.fetch = realFetch;
    unloadLiveWa();
    delete process.env.PROVIDER_HTTP_TIMEOUT_MS;
  }
});

test('wa sendText: a Graph HTTP error fails closed (throws) before any DB persistence', async () => {
  const wa = loadLiveWa();
  let dbTouched = false;
  // If _send throws on !res.ok, contactByWaId/logMessage (pool.query) are never reached.
  const db = require('../lib/db');
  const origQuery = db.pool.query;
  db.pool.query = async () => { dbTouched = true; return { rows: [{ id: 1 }] }; };
  global.fetch = () => Promise.resolve({ ok: false, status: 401, json: async () => ({ error: { message: 'invalid token' } }) });
  try {
    await assert.rejects(
      () => wa.sendText('50688887777', 'hola'),
      (err) => { assert.match(err.message, /wa send HTTP 401/); return true; },
    );
    assert.equal(dbTouched, false, 'a failed send never reaches persistence');
  } finally {
    db.pool.query = origQuery;
    global.fetch = realFetch;
    unloadLiveWa();
  }
});

test('wa sendText: sandbox (no creds) simulates the send, no fetch', async () => {
  delete require.cache[WA];
  const wa = require(WA); // no token -> liveMode false
  const db = require('../lib/db');
  const origQuery = db.pool.query;
  db.pool.query = async () => ({ rows: [{ id: 1 }] }); // contactByWaId + logMessage
  let fetched = false;
  global.fetch = () => { fetched = true; return Promise.reject(new Error('should not fetch in sandbox')); };
  try {
    const r = await wa.sendText('50688887777', 'hola');
    assert.equal(wa.liveMode, false);
    assert.match(r.messages[0].id, /^wamid\.SBX_/);
    assert.equal(fetched, false, 'sandbox never hits the Graph API');
  } finally {
    db.pool.query = origQuery;
    global.fetch = realFetch;
    delete require.cache[WA];
  }
});