test(Telegram Trigger Node): Add tests for Telegram Trigger (no-changelog) (#14537)

This commit is contained in:
Dana
2025-04-15 13:05:20 +02:00
committed by GitHub
parent 67ea795c82
commit dfc40397c1
4 changed files with 284 additions and 81 deletions

View File

@@ -9,8 +9,9 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { apiRequest, getImageBySize, getSecretToken } from './GenericFunctions';
import { apiRequest, getSecretToken } from './GenericFunctions';
import type { IEvent } from './IEvent';
import { downloadFile } from './util/triggerUtils';
export class TelegramTrigger implements INodeType {
description: INodeTypeDescription = {
@@ -277,86 +278,10 @@ export class TelegramTrigger implements INodeType {
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
if (additionalFields.download === true) {
let imageSize = 'large';
if (additionalFields.download) {
const downloadFilesResult = await downloadFile(this, credentials, bodyData, additionalFields);
let key: 'message' | 'channel_post' = 'message';
if (bodyData.channel_post) {
key = 'channel_post';
}
if (
(bodyData[key]?.photo && Array.isArray(bodyData[key]?.photo)) ||
bodyData[key]?.document ||
bodyData[key]?.video
) {
if (additionalFields.imageSize) {
imageSize = additionalFields.imageSize as string;
}
let fileId;
if (bodyData[key]?.photo) {
let image = getImageBySize(
bodyData[key]?.photo as IDataObject[],
imageSize,
) as IDataObject;
// When the image is sent from the desktop app telegram does not resize the image
// So return the only image available
// Basically the Image Size parameter would work just when the images comes from the mobile app
if (image === undefined) {
image = bodyData[key]!.photo![0];
}
fileId = image.file_id;
} else if (bodyData[key]?.video) {
fileId = bodyData[key]?.video?.file_id;
} else {
fileId = bodyData[key]?.document?.file_id;
}
const {
result: { file_path },
} = await apiRequest.call(this, 'GET', `getFile?file_id=${fileId}`, {});
const file = await apiRequest.call(
this,
'GET',
'',
{},
{},
{
json: false,
encoding: null,
uri: `${credentials.baseUrl}/file/bot${credentials.accessToken}/${file_path}`,
resolveWithFullResponse: true,
},
);
const data = Buffer.from(file.body as string);
const fileName = file_path.split('/').pop();
const binaryData = await this.helpers.prepareBinaryData(
data as unknown as Buffer,
fileName as string,
);
return {
workflowData: [
[
{
json: bodyData as unknown as IDataObject,
binary: {
data: binaryData,
},
},
],
],
};
}
if (Object.entries(downloadFilesResult).length !== 0) return downloadFilesResult;
}
if (nodeVersion >= 1.2) {

View File

@@ -0,0 +1,176 @@
import { mock } from 'jest-mock-extended';
import { type INode, type Workflow } from 'n8n-workflow';
import { testWebhookTriggerNode } from '@test/nodes/TriggerHelpers';
import { TelegramTrigger } from '../TelegramTrigger.node';
jest.mock('../GenericFunctions', () => {
const originalModule = jest.requireActual('../GenericFunctions');
return {
...originalModule,
apiRequest: jest.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: jest.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(() => {
jest.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 }]] });
});
});
});

View File

@@ -0,0 +1,95 @@
import {
type ICredentialDataDecryptedObject,
type IDataObject,
type IWebhookFunctions,
type IWebhookResponseData,
} from 'n8n-workflow';
import { apiRequest, getImageBySize } from '../GenericFunctions';
import { type IEvent } from '../IEvent';
export const downloadFile = async (
webhookFunctions: IWebhookFunctions,
credentials: ICredentialDataDecryptedObject,
bodyData: IEvent,
additionalFields: IDataObject,
): Promise<IWebhookResponseData> => {
let imageSize = 'large';
let key: 'message' | 'channel_post' = 'message';
if (bodyData.channel_post) {
key = 'channel_post';
}
if (
(bodyData[key]?.photo && Array.isArray(bodyData[key]?.photo)) ||
bodyData[key]?.document ||
bodyData[key]?.video
) {
if (additionalFields.imageSize) {
imageSize = additionalFields.imageSize as string;
}
let fileId;
if (bodyData[key]?.photo) {
let image = getImageBySize(bodyData[key]?.photo as IDataObject[], imageSize) as IDataObject;
// When the image is sent from the desktop app telegram does not resize the image
// So return the only image available
// Basically the Image Size parameter would work just when the images comes from the mobile app
if (image === undefined) {
image = bodyData[key]!.photo![0];
}
fileId = image.file_id;
} else if (bodyData[key]?.video) {
fileId = bodyData[key]?.video?.file_id;
} else {
fileId = bodyData[key]?.document?.file_id;
}
const {
result: { file_path },
} = await apiRequest.call(webhookFunctions, 'GET', `getFile?file_id=${fileId}`, {});
const file = await apiRequest.call(
webhookFunctions,
'GET',
'',
{},
{},
{
json: false,
encoding: null,
uri: `${credentials.baseUrl}/file/bot${credentials.accessToken}/${file_path}`,
resolveWithFullResponse: true,
},
);
const data = Buffer.from(file.body as string);
const fileName = file_path.split('/').pop();
const binaryData = await webhookFunctions.helpers.prepareBinaryData(
data as unknown as Buffer,
fileName as string,
);
return {
workflowData: [
[
{
json: bodyData as unknown as IDataObject,
binary: {
data: binaryData,
},
},
],
],
};
}
return {};
};

View File

@@ -1,4 +1,5 @@
import type * as express from 'express';
import { type IncomingHttpHeaders } from 'http';
import { mock } from 'jest-mock-extended';
import get from 'lodash/get';
import merge from 'lodash/merge';
@@ -31,6 +32,7 @@ type TestTriggerNodeOptions = {
timezone?: string;
workflowStaticData?: IDataObject;
credential?: ICredentialDataDecryptedObject;
helpers?: Partial<ITriggerFunctions['helpers']>;
};
type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
@@ -38,6 +40,8 @@ type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
request?: MockDeepPartial<express.Request>;
bodyData?: IDataObject;
childNodes?: NodeTypeAndVersion[];
workflow?: Workflow;
headerData?: IncomingHttpHeaders;
};
type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {};
@@ -117,6 +121,7 @@ export async function testWebhookTriggerNode(
const version = trigger.description.version;
const node = merge(
{
id: options.node?.id ?? '1',
type: trigger.description.name,
name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`,
typeVersion: typeof version === 'number' ? version : version.at(-1),
@@ -130,6 +135,7 @@ export async function testWebhookTriggerNode(
returnJsonArray,
registerCron: (cronExpression, onTick) =>
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
prepareBinaryData: options.helpers?.prepareBinaryData ?? jest.fn(),
});
const request = mock<express.Request>({
@@ -147,13 +153,14 @@ export async function testWebhookTriggerNode(
getMode: () => options.mode ?? 'trigger',
getInstanceId: () => 'instanceId',
getBodyData: () => options.bodyData ?? {},
getHeaderData: () => ({}),
getHeaderData: () => options.headerData ?? {},
getInputConnectionData: async () => ({}),
getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`,
getParamsData: () => ({}),
getQueryData: () => ({}),
getRequestObject: () => request,
getResponseObject: () => response,
getWorkflow: () => options.workflow ?? mock<Workflow>(),
getWebhookName: () => options.webhookName ?? 'default',
getWorkflowStaticData: () => options.workflowStaticData ?? {},
getNodeParameter: (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback,