UNPKG

node-red-contrib-telegrambot

Version:
429 lines (381 loc) 16.6 kB
const helper = require('node-red-node-test-helper'); const { expect } = require('chai'); const telegrambotModule = require('../../telegrambot/99-telegrambot.js'); helper.init(require.resolve('node-red')); // Wait for a predicate to become true, polling every 10 ms up to maxMs. function waitFor(predicate, maxMs) { return new Promise(function (resolve, reject) { const deadline = Date.now() + (maxMs || 1000); (function tick() { if (predicate()) return resolve(); if (Date.now() > deadline) return reject(new Error('waitFor timed out')); setTimeout(tick, 10); })(); }); } describe('bot-node — auto-restart on fatal error (issue #442 / #440)', function () { this.timeout(5000); before(function (done) { helper.startServer(done); }); after(function (done) { helper.stopServer(done); }); afterEach(function () { helper.unload(); }); function flow() { return [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; } it('scheduleRestart is a function on the config node', function (done) { helper.load(telegrambotModule, flow(), { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); expect(n.scheduleRestart).to.be.a('function'); expect(n.restartCount).to.equal(0); expect(n.restartTimer).to.equal(null); done(); } catch (err) { done(err); } }); }); it('scheduleRestart sets restartTimer and increments restartCount (single-flight)', function (done) { helper.load(telegrambotModule, flow(), { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); n.scheduleRestart('first'); expect(n.restartTimer).to.not.equal(null); expect(n.restartCount).to.equal(1); // Second call must be dropped while a restart is queued. n.scheduleRestart('second'); expect(n.restartCount).to.equal(1); // Cancel so the test cleanup doesn't trip the actual restart. clearTimeout(n.restartTimer); n.restartTimer = null; done(); } catch (err) { done(err); } }); }); it('surrenders after 8 consecutive failed restarts and logs node.error', function (done) { helper.load(telegrambotModule, flow(), { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); let errorMsg = null; n.error = function (m) { errorMsg = m; }; n.restartCount = 8; // already at the cap n.scheduleRestart('boom'); expect(errorMsg).to.match(/gave up restarting after fatal: boom/); expect(n.restartTimer).to.equal(null); done(); } catch (err) { done(err); } }); }); it('uses exponential back-off capped at 60 s', function (done) { helper.load(telegrambotModule, flow(), { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); // count 0 -> 3000 ms, 1 -> 6000, 2 -> 12000, 3 -> 24000, 4 -> 48000, 5 -> 60000 cap const delays = []; const origSetTimeout = setTimeout; global.setTimeout = function (fn, ms) { delays.push(ms); // Return a fake timer handle so cleanup doesn't break. return { fake: true }; }; try { for (let i = 0; i < 6; i++) { n.restartTimer = null; // unblock single-flight for the next call n.scheduleRestart('test'); } } finally { global.setTimeout = origSetTimeout; } expect(delays).to.deep.equal([3000, 6000, 12000, 24000, 48000, 60000]); done(); } catch (err) { global.setTimeout = require('timers').setTimeout; done(err); } }); }); it('close handler clears the pending restart timer', async function () { await new Promise(function (resolve) { helper.load(telegrambotModule, flow(), { b1: { token: 'fake' } }, resolve); }); const n = helper.getNode('b1'); n.scheduleRestart('queued'); expect(n.restartTimer).to.not.equal(null); await helper.unload(); // After unload, the close handler must have cleared the timer; otherwise // it'd fire later against a deleted node. expect(n.restartTimer).to.equal(null); }); }); describe('bot-node — stable-window restartCount reset (issue #442 retest, V17.4.2)', function () { this.timeout(5000); before(function (done) { helper.startServer(done); }); after(function (done) { helper.stopServer(done); }); afterEach(function () { helper.unload(); }); it('a fresh error inside the stable window keeps the backoff escalating', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); // Simulate the post-restart "looks stable" timer being set (which is what the // success path of the restartTimer callback does) without actually firing // the abortBot+create chain. n.restartCount = 3; // pretend we've already had 3 escalating restarts n.restartStableTimer = setTimeout(function () { n.restartStableTimer = null; n.restartCount = 0; }, 60000); // A new error before the 60 s elapses must: // 1. cancel the stableTimer (so the previous "success" doesn't reset count) // 2. NOT reset restartCount — the next backoff continues from where we were n.scheduleRestart('another error'); expect(n.restartStableTimer).to.equal(null); expect(n.restartCount).to.equal(4); // 3 -> 4, not 0 -> 1 // delay for count=3 going to 4 is 3000 * 2^3 = 24000 ms. // (We don't assert delay directly here — covered separately below.) // Cleanup so the actual scheduled restart doesn't fire during teardown. clearTimeout(n.restartTimer); n.restartTimer = null; done(); } catch (err) { done(err); } }); }); it('an error AFTER the stable window has fired resets to the minimum backoff', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); n.restartCount = 5; // Stable window fired and successfully reset: n.restartStableTimer = null; n.restartCount = 0; // New error after the stable window: clean slate. n.scheduleRestart('much later'); expect(n.restartCount).to.equal(1); clearTimeout(n.restartTimer); n.restartTimer = null; done(); } catch (err) { done(err); } }); }); it('close handler clears the pending stable-window timer too', async function () { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; await new Promise(function (resolve) { helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, resolve); }); const n = helper.getNode('b1'); n.restartStableTimer = setTimeout(function () {}, 60000); expect(n.restartStableTimer).to.not.equal(null); await helper.unload(); expect(n.restartStableTimer).to.equal(null); }); }); describe('bot-node — fatal-error log suppression while restart is queued (issue #411 retest)', function () { this.timeout(5000); before(function (done) { helper.startServer(done); }); after(function (done) { helper.stopServer(done); }); afterEach(function () { helper.unload(); }); it('emits one warn for the first error of a burst, then suppresses while the restart is queued', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); const warnLines = []; n.warn = function (m) { warnLines.push(m); }; // Simulate three rapid 'error' events as the bot library would emit during // a network outage. The auto-restart handler is set up by createTelegramBot, // so we drive it directly via scheduleRestart + the warn-suppression check // that the new handler does. function simulateFatalErrorEvent(msg) { if (!n.restartTimer) { n.warn('Bot error: ' + msg); } n.scheduleRestart('fatal: ' + msg); } simulateFatalErrorEvent('ETIMEDOUT 1'); simulateFatalErrorEvent('ETIMEDOUT 2'); simulateFatalErrorEvent('ETIMEDOUT 3'); // Only the first error of the burst logs; the next two see restartTimer // already set and stay silent (scheduleRestart also dedupes via single-flight). const botErrorLines = warnLines.filter(function (line) { return line.indexOf('Bot error:') === 0; }); expect(botErrorLines).to.have.length(1); expect(botErrorLines[0]).to.include('ETIMEDOUT 1'); clearTimeout(n.restartTimer); n.restartTimer = null; done(); } catch (err) { done(err); } }); }); }); describe('bot-node — request pool rebuild on scheduleRestart (issue #442, V17.4.5)', function () { this.timeout(5000); before(function (done) { helper.startServer(done); }); after(function (done) { helper.stopServer(done); }); afterEach(function () { helper.unload(); }); it('constructor wires up a per-bot requestPool object', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); expect(n.requestPool).to.be.an('object'); expect(n.request.pool).to.equal(n.requestPool); expect(n.buildRequestOptions).to.be.a('function'); expect(n.destroyRequestPool).to.be.a('function'); done(); } catch (err) { done(err); } }); }); it('buildRequestOptions returns a fresh pool object each call', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); const first = n.buildRequestOptions(); const second = n.buildRequestOptions(); expect(first.pool).to.not.equal(second.pool); expect(n.requestPool).to.equal(second.pool); // tracked is the latest one done(); } catch (err) { done(err); } }); }); it('destroyRequestPool calls destroy() on every agent and nulls the pool', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); const destroyed = []; n.requestPool['https:'] = { destroy: function () { destroyed.push('https'); }, }; n.requestPool['http:'] = { destroy: function () { destroyed.push('http'); }, }; // An entry without .destroy must be tolerated, not crash. n.requestPool['weird'] = { foo: 'bar' }; n.destroyRequestPool(); expect(destroyed.sort()).to.deep.equal(['http', 'https']); expect(n.requestPool).to.equal(null); done(); } catch (err) { done(err); } }); }); it('destroyRequestPool with no pool set is a no-op', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); n.requestPool = null; n.destroyRequestPool(); // must not throw expect(n.requestPool).to.equal(null); done(); } catch (err) { done(err); } }); }); it('non-SOCKS bot uses keepAlive agentOptions and a pool field', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); expect(n.request.agentOptions).to.be.an('object'); expect(n.request.agentOptions.keepAlive).to.equal(true); expect(n.request.pool).to.be.an('object'); expect(n.request.agentClass).to.equal(undefined); done(); } catch (err) { done(err); } }); }); it('close handler destroys the request pool', async function () { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; await new Promise(function (resolve) { helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, resolve); }); const n = helper.getNode('b1'); let destroyed = false; n.requestPool['https:'] = { destroy: function () { destroyed = true; }, }; await helper.unload(); expect(destroyed).to.equal(true); expect(n.requestPool).to.equal(null); }); }); describe('bot-node — polling-restart single-flight guard (issue #442)', function () { this.timeout(5000); before(function (done) { helper.startServer(done); }); after(function (done) { helper.stopServer(done); }); afterEach(function () { helper.unload(); }); it('exposes pollingRestartTimer as a tracked timer slot', function (done) { const flow = [{ id: 'b1', type: 'telegram bot', botname: 'b', updatemode: 'sendonly' }]; helper.load(telegrambotModule, flow, { b1: { token: 'fake' } }, function () { try { const n = helper.getNode('b1'); // Initial state: no pending polling restart. expect(n.pollingRestartTimer === null || n.pollingRestartTimer === undefined).to.equal(true); done(); } catch (err) { done(err); } }); }); });