node-red-contrib-telegrambot
Version:
Telegram bot nodes for Node-RED
207 lines (180 loc) • 8.11 kB
JavaScript
const helper = require('node-red-node-test-helper');
const { expect } = require('chai');
const telegrambotModule = require('../../telegrambot/99-telegrambot.js');
const { startMock } = require('../fixtures/telegram-mock.js');
helper.init(require.resolve('node-red'));
describe('integration: outbound send transport against a mocked Telegram API', function () {
this.timeout(10000);
let mock;
before(async function () {
mock = await startMock();
await new Promise(function (r) {
helper.startServer(r);
});
});
after(async function () {
await new Promise(function (r) {
helper.stopServer(r);
});
await mock.stop();
});
afterEach(async function () {
await helper.unload();
mock.clearCalls();
});
function senderOnlyFlow() {
// Send-only mode — the bot never polls. Sending still routes through baseApiUrl.
return [
{
id: 'b1',
type: 'telegram bot',
botname: 'b',
updatemode: 'sendonly',
baseapiurl: mock.url,
},
{ id: 's1', type: 'telegram sender', bot: 'b1', wires: [['out']] },
{ id: 'out', type: 'helper' },
];
}
it('POSTs to /bot<TOKEN>/sendMessage when a text "message" is enqueued', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
out.on('input', function (msg) {
try {
const calls = mock.callsTo('sendMessage');
expect(calls).to.have.length(1);
expect(calls[0].body.chat_id).to.equal('999');
expect(calls[0].body.text).to.equal('hello over HTTP');
// processResult writes the api result onto msg.payload
expect(msg.payload.sentMessageId).to.equal(1000); // mock's first id
done();
} catch (err) {
done(err);
}
});
s.receive({ payload: { chatId: 999, type: 'message', content: 'hello over HTTP' } });
});
});
it('chunks a >4000-char message into N sequential POSTs and emits exactly once', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
let emits = 0;
out.on('input', function () {
emits++;
});
s.receive({ payload: { chatId: 999, type: 'message', content: 'a'.repeat(9001) } });
// Wait for the queue to drain — three sequential sends over the loopback
// mock should complete well under 300 ms.
setTimeout(function () {
try {
const calls = mock.callsTo('sendMessage');
expect(calls).to.have.length(3);
calls.forEach(function (c) {
expect(c.body.text.length).to.be.lessThanOrEqual(4000);
});
// The chunk-sequencing fix in V17.3.0 guarantees exactly one emit per
// original message regardless of chunk count.
expect(emits).to.equal(1);
done();
} catch (err) {
done(err);
}
}, 300);
});
});
it('retries on HTTP 429 with the API-supplied retry_after delay', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
// First sendMessage fails with 429, retry_after = 1 second.
// (retry_after MUST be > 0 — bot-node's processError uses `|| default`, so 0 falls
// back to the 3-second default; and a missing `parameters` field would crash the
// current handler. That defensive gap is tracked separately.)
mock.failNext('sendMessage', { code: 429, description: 'Too Many Requests: retry after 1', retry_after: 1 });
out.on('input', function (msg) {
try {
const calls = mock.callsTo('sendMessage');
// Exactly one retry → two total POSTs.
expect(calls).to.have.length(2);
expect(msg.payload.sentMessageId).to.exist;
done();
} catch (err) {
done(err);
}
});
s.receive({ payload: { chatId: 999, type: 'message', content: 'will-retry' } });
});
});
it('falls back to plain mode when Markdown parse fails on the first chunk', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
// First call fails the Markdown way; the chunk re-sends without parse_mode.
mock.failNext('sendMessage', {
code: 400,
description: "Bad Request: can't parse entities in message text: unexpected character",
});
out.on('input', function () {
try {
const calls = mock.callsTo('sendMessage');
expect(calls).to.have.length(2);
// First attempt carried parse_mode; the fallback attempt did not.
expect(calls[0].body.parse_mode).to.equal('Markdown');
expect(calls[1].body.parse_mode).to.be.undefined;
done();
} catch (err) {
done(err);
}
});
s.receive({
payload: { chatId: 999, type: 'message', content: 'hello *_world_*', options: { parse_mode: 'Markdown' } },
});
});
});
it('routes "forward" payloads through forwardMessage', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
out.on('input', function () {
try {
expect(mock.callsTo('forwardMessage')).to.have.length(1);
done();
} catch (err) {
done(err);
}
});
s.receive({
payload: {
chatId: 100,
messageId: 555,
forward: { chatId: 200 },
},
});
});
});
it('serialises sends to the same chat through the per-chat queue', function (done) {
helper.load(telegrambotModule, senderOnlyFlow(), { b1: { token: 'fake' } }, function () {
const s = helper.getNode('s1');
const out = helper.getNode('out');
let arrivals = 0;
out.on('input', function () {
arrivals++;
if (arrivals === 3) {
try {
const calls = mock.callsTo('sendMessage');
expect(calls).to.have.length(3);
expect(calls.map(function (c) { return c.body.text; })).to.deep.equal(['one', 'two', 'three']);
done();
} catch (err) {
done(err);
}
}
});
s.receive({ payload: { chatId: 7, type: 'message', content: 'one' } });
s.receive({ payload: { chatId: 7, type: 'message', content: 'two' } });
s.receive({ payload: { chatId: 7, type: 'message', content: 'three' } });
});
});
});