tududi/backend/tests/unit/services/telegramPoller.test.js
Chris 828b5ebc0d
fix: prevent Telegram polling errors from blocking container startup (#989) (#1019)
This fix addresses the issue where the container gets stuck in an endless
loop of Telegram connection errors when the bot is configured but Telegram
is unreachable during startup.

Changes:
- Add 10-second startup delay before initializing Telegram polling to allow
  the system to settle
- Implement exponential backoff (5s to 5min) when Telegram connection fails
- Add rate-limited error logging (max once per minute per user) to reduce
  log spam and prevent event loop blocking
- Track error state per user to manage backoff independently
- Auto-reset error state on successful connection
- Update tests to account for new error state tracking

Fixes #989
2026-04-13 20:44:27 +03:00

271 lines
8.6 KiB
JavaScript

const { User, InboxItem } = require('../../../models');
const telegramPoller = require('../../../modules/telegram/telegramPoller');
const https = require('https');
// Mock the database models
jest.mock('../../../models', () => ({
User: {
update: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
},
InboxItem: {
create: jest.fn(),
findOne: jest.fn(),
},
}));
// Mock https module
jest.mock('https', () => ({
get: jest.fn(),
request: jest.fn(),
}));
describe('TelegramPoller Duplicate Prevention', () => {
let mockUser;
beforeEach(() => {
jest.clearAllMocks();
mockUser = {
id: 1,
telegram_bot_token: 'test-token',
telegram_chat_id: '123456789',
};
// Reset poller state
telegramPoller.stopPolling();
});
describe('Update ID Tracking', () => {
test('should filter out already processed updates', () => {
const updates = [
{
update_id: 100,
message: {
text: 'Hello 1',
message_id: 1,
chat: { id: 123 },
},
},
{
update_id: 101,
message: {
text: 'Hello 2',
message_id: 2,
chat: { id: 123 },
},
},
{
update_id: 102,
message: {
text: 'Hello 3',
message_id: 3,
chat: { id: 123 },
},
},
];
// Test internal function for filtering
const processedUpdates = new Set(['1-100', '1-101']);
const newUpdates = updates.filter((update) => {
const updateKey = `1-${update.update_id}`;
return !processedUpdates.has(updateKey);
});
expect(newUpdates).toHaveLength(1);
expect(newUpdates[0].update_id).toBe(102);
});
test('should track highest update ID correctly', () => {
const updates = [
{ update_id: 98 },
{ update_id: 101 },
{ update_id: 99 },
];
const highestUpdateId = telegramPoller._getHighestUpdateId(updates);
expect(highestUpdateId).toBe(101);
});
test('should handle empty updates array', () => {
const highestUpdateId = telegramPoller._getHighestUpdateId([]);
expect(highestUpdateId).toBe(0);
});
});
describe('User List Management', () => {
test('should not add duplicate users', () => {
const users = [{ id: 1, name: 'User 1' }];
const newUser = { id: 1, name: 'User 1 Updated' };
const userExists = telegramPoller._userExistsInList(users, 1);
expect(userExists).toBe(true);
const updatedUsers = telegramPoller._addUserToList(users, newUser);
expect(updatedUsers).toHaveLength(1);
expect(updatedUsers).toEqual(users); // Should return original array unchanged
});
test('should add new users correctly', () => {
const users = [{ id: 1, name: 'User 1' }];
const newUser = { id: 2, name: 'User 2' };
const userExists = telegramPoller._userExistsInList(users, 2);
expect(userExists).toBe(false);
const updatedUsers = telegramPoller._addUserToList(users, newUser);
expect(updatedUsers).toHaveLength(2);
expect(updatedUsers).toContain(newUser);
});
test('should remove users correctly', () => {
const users = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
];
const updatedUsers = telegramPoller._removeUserFromList(users, 2);
expect(updatedUsers).toHaveLength(2);
expect(updatedUsers.find((u) => u.id === 2)).toBeUndefined();
expect(updatedUsers.find((u) => u.id === 1)).toBeDefined();
expect(updatedUsers.find((u) => u.id === 3)).toBeDefined();
});
});
describe('Message Parameters', () => {
test('should create message parameters without reply', () => {
const params = telegramPoller._createMessageParams(
'123',
'Hello World'
);
expect(params).toEqual({
chat_id: '123',
text: 'Hello World',
});
});
test('should create message parameters with reply', () => {
const params = telegramPoller._createMessageParams(
'123',
'Hello World',
456
);
expect(params).toEqual({
chat_id: '123',
text: 'Hello World',
reply_to_message_id: 456,
});
});
});
describe('Telegram URL Creation', () => {
test('should create URL without parameters', () => {
const url = telegramPoller._createTelegramUrl('token123', 'getMe');
expect(url).toBe('https://api.telegram.org/bottoken123/getMe');
});
test('should create URL with parameters', () => {
const url = telegramPoller._createTelegramUrl(
'token123',
'getUpdates',
{
offset: '100',
timeout: '30',
}
);
expect(url).toBe(
'https://api.telegram.org/bottoken123/getUpdates?offset=100&timeout=30'
);
});
});
describe('State Management', () => {
test('should return correct initial state', () => {
const state = telegramPoller._createPollerState();
expect(state).toEqual({
running: false,
interval: null,
pollInterval: 5000,
usersToPool: [],
userStatus: {},
processedUpdates: expect.any(Set),
userErrorState: {},
});
});
test('should track poller status correctly', () => {
const status = telegramPoller.getStatus();
expect(status).toEqual({
running: false,
usersCount: 0,
pollInterval: 5000,
userStatus: {},
});
});
});
});
describe('Multiple Telegram Allowed Users', () => {
test('should allow multiple authorized Telegram users when telegram_allowed_users is configured', async () => {
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
https.request.mockImplementation((url, options, callback) => {
// Simulate successful response
const mockResponse = {
on: jest.fn((event, handler) => {
if (event === 'data') handler(JSON.stringify({}));
if (event === 'end') handler();
return mockResponse;
}),
};
callback(mockResponse);
return {
on: jest.fn(),
write: jest.fn(),
end: jest.fn(),
};
});
const user = {
id: 1,
telegram_bot_token: 'test-token',
telegram_chat_id: '111111111',
telegram_allowed_users: '@bob, @alice',
};
const bobUpdate = {
message: {
from: { id: 100, username: 'bob' },
chat: { id: 111111111 }, // telegram_chat_id
text: 'Test message from Bob',
message_id: 1,
},
};
const aliceUpdate = {
message: {
from: { id: 200, username: 'alice' },
chat: { id: 222222222 }, // Different chat ID
text: 'Test message from Alice',
message_id: 2,
},
};
await telegramPoller._processMessage(user, aliceUpdate);
await telegramPoller._processMessage(user, bobUpdate);
// Both should log successful processing
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Test message from Alice')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Test message from Bob')
);
consoleLogSpy.mockRestore();
});
});