n8n/packages/nodes-base/nodes/Telegram/tests/TelegramTrigger.node.test.ts
n8n-assistant[bot] 46761a60a9
chore: Bundle/2.x (#41035)
Co-authored-by: Matsu <matias.huhta@n8n.io>
Co-authored-by: Bernhard Wittmann <bernhard.wittmann@n8n.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: n8n-assistant[bot] <n8n-assistant[bot]@users.noreply.github.com>
Co-authored-by: João Almeida <jg.almeida56@gmail.com>
Co-authored-by: Savelii <savelii.sychov@n8n.io>
Co-authored-by: Rick Gale <richardgale90@gmail.com>
Co-authored-by: Emilia <100027345+sovietspaceship@users.noreply.github.com>
Co-authored-by: oleg <me@olegivaniv.com>
Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io>
Co-authored-by: Stephen Wright <sjw948@gmail.com>
Co-authored-by: Yuliia Pominchuk <31064937+yuliia-pominchuk@users.noreply.github.com>
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
Co-authored-by: Miguel Ángel Moreno <1411774+miguelsaddress@users.noreply.github.com>
Co-authored-by: Anne Aguirre <anne.aguirre@n8n.io>
Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com>
Co-authored-by: Kai <kai.hartlage@n8n.io>
Co-authored-by: Luigi Gubello <27034386+luigigubello@users.noreply.github.com>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: MHMD <45968454+dou-mhmd@users.noreply.github.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Charlie Kolb <charlie@n8n.io>
2026-10-14 06:33:56 +00:00

704 lines
20 KiB
TypeScript

import { GlobalConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { HITL_CALLBACK_PREFIX, InstanceSettings } from 'n8n-core';
import type { IHookFunctions, IWebhookFunctions, INode, Workflow } from 'n8n-workflow';
import { mock } from 'vitest-mock-extended';
import { testWebhookTriggerNode } from '@test/nodes/TriggerHelpers';
import { apiRequest } from '../GenericFunctions';
import type * as _importType0 from '../GenericFunctions';
import { deriveHitlSecretToken } from '../hitl/tokens';
import { TelegramTrigger } from '../TelegramTrigger.node';
Container.set(InstanceSettings, { hmacSignatureSecret: 'test-hmac-secret' } as InstanceSettings);
vi.mock('../GenericFunctions', async () => {
const originalModule = await vi.importActual<typeof _importType0>('../GenericFunctions');
return {
...originalModule,
apiRequest: vi.fn(async function (method: string, query: string) {
if (method === 'GET' && query.startsWith('getFile')) {
return { result: { file_path: 'path/to/file' } };
}
if (method === 'GET' && !query) {
return { body: 'test-file' };
}
return { result: { file_path: 'path/to/file' } };
}),
};
});
describe('TelegramTrigger', () => {
let mockResult: Record<string, object>;
const binaryData = {
fileName: 'mocked-file',
mimeType: 'image/png',
data: Buffer.from('mocked-data'),
};
const createOptions = ({
type,
attachment,
useChannelPost = false,
imageSize = 'small',
}: {
type: string;
attachment: any;
useChannelPost?: boolean;
imageSize?: string;
}) => {
const messageField = useChannelPost ? 'channel_post' : 'message';
mockResult[messageField] = {
chat: { id: 555 },
from: { id: 666 },
[type]: attachment,
};
return {
helpers: {
prepareBinaryData: vi.fn().mockResolvedValue(binaryData),
},
credential: {
accessToken: '999999',
baseUrl: 'https://api.telegram.org',
},
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: true,
chatIds: '555',
imageSize,
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
[messageField]: {
[type]: attachment,
chat: { id: 555 },
from: { id: 666 },
},
},
};
};
beforeEach(() => {
mockResult = {};
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Webhook', () => {
test('should return empty object in download files if attachment is not photo, video, or document', async () => {
const options = createOptions({ type: 'text', attachment: 'Hello world!' });
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should set the image if it is coming for desktop telegram', async () => {
const options = createOptions({
type: 'photo',
attachment: [{ file_id: 'photo0909' }],
imageSize: 'desktop',
});
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
expect(responseData).toEqual({
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
});
});
it.each([
{ type: 'photo', attachment: [{ file_id: 'photo0909' }] },
{ type: 'video', attachment: { file_id: 'vid666' } },
{ type: 'document', attachment: { file_id: '0909' } },
])(
'should return downloaded files for %s attachments with channel_post',
async ({ type, attachment }) => {
const options = createOptions({ type, attachment, useChannelPost: true });
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
expect(responseData).toEqual({
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
});
},
);
it.each([
{ type: 'photo', attachment: [{ file_id: 'photo0909' }] },
{ type: 'video', attachment: { file_id: 'vid666' } },
{ type: 'document', attachment: { file_id: '0909' } },
])(
'should return downloaded files for %s attachments with message',
async ({ type, attachment }) => {
const options = createOptions({ type, attachment });
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
expect(responseData).toEqual({
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
});
},
);
test('should receive a webhook event without downloading files', async () => {
mockResult.message = {
chat: { id: 555 },
from: { id: 666 },
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '555',
userIds: '666',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
message: {
chat: { id: 555 },
from: { id: 666 },
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should pass chatIds filter for callback_query events', async () => {
mockResult.callback_query = {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '555',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
callback_query: {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should pass userIds filter for callback_query events', async () => {
mockResult.callback_query = {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
userIds: '777',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
callback_query: {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should reject callback_query when chatId does not match', async () => {
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '999',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
callback_query: {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
},
},
});
expect(responseData).toEqual({});
});
test('should pass chatIds filter for edited_message events', async () => {
mockResult.edited_message = {
chat: { id: 555 },
from: { id: 666 },
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '555',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
edited_message: {
chat: { id: 555 },
from: { id: 666 },
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should pass chatIds filter for channel_post events', async () => {
mockResult.channel_post = {
chat: { id: 555 },
from: { id: 666 },
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '555',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
channel_post: {
chat: { id: 555 },
from: { id: 666 },
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
test('should reject events without a resolvable chat ID when chatIds filter is active', async () => {
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
chatIds: '555',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
inline_query: {
from: { id: 777 },
},
},
});
expect(responseData).toEqual({});
});
test('should reject events without a resolvable user ID when userIds filter is active', async () => {
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
parameters: {
additionalFields: {
download: false,
userIds: '777',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
poll: {
id: 'poll1',
},
},
});
expect(responseData).toEqual({});
});
describe('version 1.3 (legacy filter resolution)', () => {
test('should drop callback_query when chatIds filter is set, matching only message updates', async () => {
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
typeVersion: 1.3,
parameters: {
additionalFields: {
download: false,
chatIds: '555',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
callback_query: {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
},
},
});
expect(responseData).toEqual({});
});
test('should drop callback_query when userIds filter is set, matching only message updates', async () => {
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
typeVersion: 1.3,
parameters: {
additionalFields: {
download: false,
userIds: '777',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
callback_query: {
from: { id: 777 },
message: {
chat: { id: 555 },
from: { id: 123 },
},
},
},
});
expect(responseData).toEqual({});
});
test('should still pass message updates matching the chatIds filter', async () => {
mockResult.message = {
chat: { id: 555 },
from: { id: 666 },
};
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
workflow: mock<Workflow>({ id: '1', active: true }),
node: mock<INode>({
id: '2',
typeVersion: 1.3,
parameters: {
additionalFields: {
download: false,
chatIds: '555',
userIds: '666',
},
},
}),
headerData: {
'x-telegram-bot-api-secret-token': '1_2',
},
bodyData: {
message: {
chat: { id: 555 },
from: { id: 666 },
},
},
});
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
});
});
});
describe('HITL callback forwarding', () => {
test('forwards to the configured waiting-webhook path instead of a hardcoded one', async () => {
Container.set(
GlobalConfig,
mock<GlobalConfig>({ endpoints: { webhookWaiting: 'custom-waiting' } as never }),
);
const httpRequest = vi.fn().mockResolvedValue({});
const telegramTrigger = new TelegramTrigger();
const webhookFunctions = mock<IWebhookFunctions>({
helpers: mock<IWebhookFunctions['helpers']>({ httpRequest }),
getNode: () => mock<INode>({ id: '2', typeVersion: 1.5 }),
getWorkflow: () => mock<Workflow>({ id: '1' }),
getNodeWebhookUrl: () => 'https://mybot.example.com/custom-waiting/webhook/abc/webhook',
getBodyData: () => ({
callback_query: { data: `${HITL_CALLBACK_PREFIX}42|a|deadbeef` },
}),
getHeaderData: () => ({ 'x-telegram-bot-api-secret-token': '1_2' }),
getCredentials: async <T extends object>() => ({ accessToken: '123456:test-token' }) as T,
getNodeParameter: () => [],
});
await telegramTrigger.webhook.call(webhookFunctions);
expect(httpRequest).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://mybot.example.com/custom-waiting-telegram',
headers: {
'x-telegram-bot-api-secret-token': deriveHitlSecretToken('123456:test-token'),
},
}),
);
});
test('preserves a reverse-proxy path prefix instead of forwarding to the origin root', async () => {
// e.g. n8n served under https://example.com/n8n-instance/ : the own webhook URL
// carries that prefix before the live-webhook segment, and it must survive.
Container.set(
GlobalConfig,
mock<GlobalConfig>({
endpoints: { webhook: 'webhook', webhookWaiting: 'webhook-waiting' } as never,
}),
);
const httpRequest = vi.fn().mockResolvedValue({});
const telegramTrigger = new TelegramTrigger();
const webhookFunctions = mock<IWebhookFunctions>({
helpers: mock<IWebhookFunctions['helpers']>({ httpRequest }),
getNode: () => mock<INode>({ id: '2', typeVersion: 1.5 }),
getWorkflow: () => mock<Workflow>({ id: '1' }),
getNodeWebhookUrl: () => 'https://example.com/n8n-instance/webhook/abc/webhook',
getBodyData: () => ({
callback_query: { data: `${HITL_CALLBACK_PREFIX}42|a|deadbeef` },
}),
getHeaderData: () => ({ 'x-telegram-bot-api-secret-token': '1_2' }),
getCredentials: async <T extends object>() => ({ accessToken: '123456:test-token' }) as T,
getNodeParameter: () => [],
});
await telegramTrigger.webhook.call(webhookFunctions);
expect(httpRequest).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://example.com/n8n-instance/webhook-waiting-telegram',
}),
);
});
const createForwardingWebhookFunctions = (
typeVersion: number,
headers: Record<string, string> = { 'x-telegram-bot-api-secret-token': '1_2' },
) => {
const httpRequest = vi.fn().mockResolvedValue({});
const status = vi.fn().mockReturnThis();
const webhookFunctions = mock<IWebhookFunctions>({
helpers: mock<IWebhookFunctions['helpers']>({ httpRequest }),
getNode: () => mock<INode>({ id: '2', typeVersion }),
getWorkflow: () => mock<Workflow>({ id: '1' }),
getNodeWebhookUrl: () => 'https://example.com/webhook/abc/webhook',
getBodyData: () => ({
callback_query: { data: `${HITL_CALLBACK_PREFIX}42|a|deadbeef` },
}),
getHeaderData: () => headers,
getResponseObject: () => ({ status, json: vi.fn() }) as never,
getCredentials: async <T extends object>() => ({ accessToken: '123456:test-token' }) as T,
getNodeParameter: () => [],
});
return { httpRequest, status, webhookFunctions };
};
test('does not forward HITL callbacks for node version 1', async () => {
const { httpRequest, webhookFunctions } = createForwardingWebhookFunctions(1);
await new TelegramTrigger().webhook.call(webhookFunctions);
expect(httpRequest).not.toHaveBeenCalled();
});
test('forwards HITL callbacks for node version 1.4 when the secret header matches', async () => {
const { httpRequest, webhookFunctions } = createForwardingWebhookFunctions(1.4);
await new TelegramTrigger().webhook.call(webhookFunctions);
expect(httpRequest).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
'x-telegram-bot-api-secret-token': deriveHitlSecretToken('123456:test-token'),
},
}),
);
});
test.each([
['an incorrect', { 'x-telegram-bot-api-secret-token': 'wrong' }],
['a missing', {}],
])(
'responds 403 and does not forward HITL callbacks with %s secret header',
async (_, headers) => {
const { httpRequest, status, webhookFunctions } = createForwardingWebhookFunctions(
1.4,
headers,
);
const result = await new TelegramTrigger().webhook.call(webhookFunctions);
expect(status).toHaveBeenCalledWith(403);
expect(httpRequest).not.toHaveBeenCalled();
expect(result).toEqual({ noWebhookResponse: true });
},
);
});
describe('checkExists', () => {
const webhookUrl = 'https://example.com/webhook/abc/webhook';
const createHookFunctions = () =>
mock<IHookFunctions>({
getNodeWebhookUrl: vi.fn().mockReturnValue(webhookUrl),
getNode: vi.fn().mockReturnValue({ id: '2', typeVersion: 1.5 }),
getWorkflow: vi.fn().mockReturnValue({ id: '1' }),
});
test('should return true when the registered URL matches', async () => {
vi.mocked(apiRequest).mockResolvedValueOnce({ ok: true, result: { url: webhookUrl } });
const telegramTrigger = new TelegramTrigger();
await expect(
telegramTrigger.webhookMethods.default.checkExists.call(createHookFunctions()),
).resolves.toBe(true);
});
// The credential lets the user point the Base URL at any host, so a 2xx response
// can carry a body that is not a getWebhookInfo payload.
test.each([
['a body that is not JSON', '<html>Access denied</html>'],
['a payload without a result', { ok: false, description: 'Unauthorized' }],
['an empty object', {}],
])('should return false for %s', async (_description, response) => {
vi.mocked(apiRequest).mockResolvedValueOnce(response);
const telegramTrigger = new TelegramTrigger();
await expect(
telegramTrigger.webhookMethods.default.checkExists.call(createHookFunctions()),
).resolves.toBe(false);
});
});
describe('create', () => {
test('should set drop_pending_updates for version 1.3', async () => {
const telegramTrigger = new TelegramTrigger();
const mockHookFunctions = mock<IHookFunctions>({
getNodeWebhookUrl: vi.fn().mockReturnValue('https://example.com/webhook'),
getNodeParameter: vi.fn().mockReturnValue(['message']),
getNode: vi.fn().mockReturnValue({ id: '2', typeVersion: 1.3 }),
getWorkflow: vi.fn().mockReturnValue({ id: '1' }),
});
await telegramTrigger.webhookMethods.default.create.call(mockHookFunctions);
expect(vi.mocked(apiRequest)).toHaveBeenCalledWith(
'POST',
'setWebhook',
expect.objectContaining({
drop_pending_updates: true,
}),
);
});
test('should not set drop_pending_updates for version 1.2', async () => {
const telegramTrigger = new TelegramTrigger();
const mockHookFunctions = mock<IHookFunctions>({
getNodeWebhookUrl: vi.fn().mockReturnValue('https://example.com/webhook'),
getNodeParameter: vi.fn().mockReturnValue(['*']),
getNode: vi.fn().mockReturnValue({ id: '2', typeVersion: 1.2 }),
getWorkflow: vi.fn().mockReturnValue({ id: '1' }),
});
await telegramTrigger.webhookMethods.default.create.call(mockHookFunctions);
expect(vi.mocked(apiRequest)).toHaveBeenCalledWith(
'POST',
'setWebhook',
expect.objectContaining({
drop_pending_updates: false,
}),
);
});
});
});