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