mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-22 04:10:01 +00:00
test(Telegram Trigger Node): Add tests for Telegram Trigger (no-changelog) (#14537)
This commit is contained in:
@@ -9,8 +9,9 @@ import type {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionTypes } 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 type { IEvent } from './IEvent';
|
||||||
|
import { downloadFile } from './util/triggerUtils';
|
||||||
|
|
||||||
export class TelegramTrigger implements INodeType {
|
export class TelegramTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
@@ -277,86 +278,10 @@ export class TelegramTrigger implements INodeType {
|
|||||||
|
|
||||||
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
|
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
|
||||||
|
|
||||||
if (additionalFields.download === true) {
|
if (additionalFields.download) {
|
||||||
let imageSize = 'large';
|
const downloadFilesResult = await downloadFile(this, credentials, bodyData, additionalFields);
|
||||||
|
|
||||||
let key: 'message' | 'channel_post' = 'message';
|
if (Object.entries(downloadFilesResult).length !== 0) return downloadFilesResult;
|
||||||
|
|
||||||
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 (nodeVersion >= 1.2) {
|
if (nodeVersion >= 1.2) {
|
||||||
|
|||||||
@@ -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 }]] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
95
packages/nodes-base/nodes/Telegram/util/triggerUtils.ts
Normal file
95
packages/nodes-base/nodes/Telegram/util/triggerUtils.ts
Normal 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 {};
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type * as express from 'express';
|
import type * as express from 'express';
|
||||||
|
import { type IncomingHttpHeaders } from 'http';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
@@ -31,6 +32,7 @@ type TestTriggerNodeOptions = {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
workflowStaticData?: IDataObject;
|
workflowStaticData?: IDataObject;
|
||||||
credential?: ICredentialDataDecryptedObject;
|
credential?: ICredentialDataDecryptedObject;
|
||||||
|
helpers?: Partial<ITriggerFunctions['helpers']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
|
type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
|
||||||
@@ -38,6 +40,8 @@ type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
|
|||||||
request?: MockDeepPartial<express.Request>;
|
request?: MockDeepPartial<express.Request>;
|
||||||
bodyData?: IDataObject;
|
bodyData?: IDataObject;
|
||||||
childNodes?: NodeTypeAndVersion[];
|
childNodes?: NodeTypeAndVersion[];
|
||||||
|
workflow?: Workflow;
|
||||||
|
headerData?: IncomingHttpHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {};
|
type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {};
|
||||||
@@ -117,6 +121,7 @@ export async function testWebhookTriggerNode(
|
|||||||
const version = trigger.description.version;
|
const version = trigger.description.version;
|
||||||
const node = merge(
|
const node = merge(
|
||||||
{
|
{
|
||||||
|
id: options.node?.id ?? '1',
|
||||||
type: trigger.description.name,
|
type: trigger.description.name,
|
||||||
name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`,
|
name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`,
|
||||||
typeVersion: typeof version === 'number' ? version : version.at(-1),
|
typeVersion: typeof version === 'number' ? version : version.at(-1),
|
||||||
@@ -130,6 +135,7 @@ export async function testWebhookTriggerNode(
|
|||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
registerCron: (cronExpression, onTick) =>
|
registerCron: (cronExpression, onTick) =>
|
||||||
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
|
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
|
||||||
|
prepareBinaryData: options.helpers?.prepareBinaryData ?? jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = mock<express.Request>({
|
const request = mock<express.Request>({
|
||||||
@@ -147,13 +153,14 @@ export async function testWebhookTriggerNode(
|
|||||||
getMode: () => options.mode ?? 'trigger',
|
getMode: () => options.mode ?? 'trigger',
|
||||||
getInstanceId: () => 'instanceId',
|
getInstanceId: () => 'instanceId',
|
||||||
getBodyData: () => options.bodyData ?? {},
|
getBodyData: () => options.bodyData ?? {},
|
||||||
getHeaderData: () => ({}),
|
getHeaderData: () => options.headerData ?? {},
|
||||||
getInputConnectionData: async () => ({}),
|
getInputConnectionData: async () => ({}),
|
||||||
getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`,
|
getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`,
|
||||||
getParamsData: () => ({}),
|
getParamsData: () => ({}),
|
||||||
getQueryData: () => ({}),
|
getQueryData: () => ({}),
|
||||||
getRequestObject: () => request,
|
getRequestObject: () => request,
|
||||||
getResponseObject: () => response,
|
getResponseObject: () => response,
|
||||||
|
getWorkflow: () => options.workflow ?? mock<Workflow>(),
|
||||||
getWebhookName: () => options.webhookName ?? 'default',
|
getWebhookName: () => options.webhookName ?? 'default',
|
||||||
getWorkflowStaticData: () => options.workflowStaticData ?? {},
|
getWorkflowStaticData: () => options.workflowStaticData ?? {},
|
||||||
getNodeParameter: (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback,
|
getNodeParameter: (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback,
|
||||||
|
|||||||
Reference in New Issue
Block a user