first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,335 @@
import {
NodeApiError,
type IDataObject,
type IExecuteFunctions,
type IHookFunctions,
type IHttpRequestMethods,
type ILoadOptionsFunctions,
type IWebhookFunctions,
} from 'n8n-workflow';
import {
addAdditionalFields,
apiRequest,
getPropertyName,
getSecretToken,
} from '../GenericFunctions';
describe('Telegram > GenericFunctions', () => {
describe('apiRequest', () => {
let mockThis: IHookFunctions & IExecuteFunctions & ILoadOptionsFunctions & IWebhookFunctions;
const credentials = { baseUrl: 'https://api.telegram.org', accessToken: 'testToken' };
beforeEach(() => {
mockThis = {
getCredentials: jest.fn(),
helpers: {
request: jest.fn(),
},
getNode: jest.fn(),
} as unknown as IHookFunctions &
IExecuteFunctions &
ILoadOptionsFunctions &
IWebhookFunctions;
jest.clearAllMocks();
});
it('should make a successful API request', async () => {
const method: IHttpRequestMethods = 'POST';
const endpoint = 'sendMessage';
const body: IDataObject = { text: 'Hello, world!' };
const query: IDataObject = { chat_id: '12345' };
const option: IDataObject = { headers: { 'Custom-Header': 'value' } };
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
const result = await apiRequest.call(mockThis, method, endpoint, body, query, option);
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
expect(mockThis.helpers.request).toHaveBeenCalledWith({
headers: { 'Custom-Header': 'value' },
method: 'POST',
uri: 'https://api.telegram.org/bottestToken/sendMessage',
body: { text: 'Hello, world!' },
qs: { chat_id: '12345' },
json: true,
});
expect(result).toEqual({ success: true });
});
it('should handle an API request with no body and query', async () => {
const method: IHttpRequestMethods = 'GET';
const endpoint = 'getMe';
const body: IDataObject = {};
const query: IDataObject = {};
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
const result = await apiRequest.call(mockThis, method, endpoint, body, query);
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
expect(mockThis.helpers.request).toHaveBeenCalledWith({
headers: {},
method: 'GET',
uri: 'https://api.telegram.org/bottestToken/getMe',
json: true,
});
expect(result).toEqual({ success: true });
});
it('should handle an API request with no additional options', async () => {
const method: IHttpRequestMethods = 'POST';
const endpoint = 'sendMessage';
const body: IDataObject = { text: 'Hello, world!' };
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
const result = await apiRequest.call(mockThis, method, endpoint, body);
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
expect(mockThis.helpers.request).toHaveBeenCalledWith({
headers: {},
method: 'POST',
uri: 'https://api.telegram.org/bottestToken/sendMessage',
body: { text: 'Hello, world!' },
json: true,
});
expect(result).toEqual({ success: true });
});
it('should throw a NodeApiError on request failure', async () => {
const method: IHttpRequestMethods = 'POST';
const endpoint = 'sendMessage';
const body: IDataObject = { text: 'Hello, world!' };
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
(mockThis.helpers.request as jest.Mock).mockRejectedValue(new Error('Request failed'));
await expect(apiRequest.call(mockThis, method, endpoint, body)).rejects.toThrow(NodeApiError);
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
expect(mockThis.helpers.request).toHaveBeenCalledWith({
headers: {},
method: 'POST',
uri: 'https://api.telegram.org/bottestToken/sendMessage',
body: { text: 'Hello, world!' },
json: true,
});
});
});
describe('addAdditionalFields', () => {
let mockThis: IExecuteFunctions;
beforeEach(() => {
mockThis = {
getNodeParameter: jest.fn(),
} as unknown as IExecuteFunctions;
jest.clearAllMocks();
});
it('should add additional fields and attribution for sendMessage operation', () => {
const body: IDataObject = { text: 'Hello, world!' };
const index = 0;
const nodeVersion = 1.1;
const instanceId = '45';
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'operation':
return 'sendMessage';
case 'additionalFields':
return { appendAttribution: true };
case 'replyMarkup':
return 'none';
default:
return '';
}
});
addAdditionalFields.call(mockThis, body, index, nodeVersion, instanceId);
expect(body).toEqual({
text: 'Hello, world!\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_45)',
parse_mode: 'Markdown',
disable_web_page_preview: true,
});
});
it('should add reply markup for inlineKeyboard', () => {
const body: IDataObject = { text: 'Hello, world!' };
const index = 0;
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'operation':
return 'sendMessage';
case 'additionalFields':
return {};
case 'replyMarkup':
return 'inlineKeyboard';
case 'inlineKeyboard':
return {
rows: [
{
row: {
buttons: [
{ text: 'Button 1', additionalFields: { url: 'https://example.com' } },
{ text: 'Button 2' },
],
},
},
],
};
default:
return '';
}
});
addAdditionalFields.call(mockThis, body, index);
expect(body).toEqual({
text: 'Hello, world!',
disable_web_page_preview: true,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[{ text: 'Button 1', url: 'https://example.com' }, { text: 'Button 2' }],
],
},
});
});
it('should add reply markup for forceReply', () => {
const body: IDataObject = { text: 'Hello, world!' };
const index = 0;
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'operation':
return 'sendMessage';
case 'additionalFields':
return {};
case 'replyMarkup':
return 'forceReply';
case 'forceReply':
return { force_reply: true };
default:
return '';
}
});
addAdditionalFields.call(mockThis, body, index);
expect(body).toEqual({
text: 'Hello, world!',
disable_web_page_preview: true,
parse_mode: 'Markdown',
reply_markup: { force_reply: true },
});
});
it('should add reply markup for replyKeyboardRemove', () => {
const body: IDataObject = { text: 'Hello, world!' };
const index = 0;
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'operation':
return 'sendMessage';
case 'additionalFields':
return {};
case 'replyMarkup':
return 'replyKeyboardRemove';
case 'replyKeyboardRemove':
return { remove_keyboard: true };
default:
return '';
}
});
addAdditionalFields.call(mockThis, body, index);
expect(body).toEqual({
text: 'Hello, world!',
disable_web_page_preview: true,
parse_mode: 'Markdown',
reply_markup: { remove_keyboard: true },
});
});
it('should handle nodeVersion 1.2 and set disable_web_page_preview', () => {
const body: IDataObject = { text: 'Hello, world!' };
const index = 0;
const nodeVersion = 1.2;
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'operation':
return 'sendMessage';
case 'additionalFields':
return {};
case 'replyMarkup':
return 'none';
default:
return '';
}
});
addAdditionalFields.call(mockThis, body, index, nodeVersion);
expect(body).toEqual({
disable_web_page_preview: true,
parse_mode: 'Markdown',
text: 'Hello, world!\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram)',
});
});
});
describe('getPropertyName', () => {
it('should return the property name by removing "send" and converting to lowercase', () => {
expect(getPropertyName('sendMessage')).toBe('message');
expect(getPropertyName('sendEmail')).toBe('email');
expect(getPropertyName('sendNotification')).toBe('notification');
});
it('should return the original string in lowercase if it does not contain "send"', () => {
expect(getPropertyName('receiveMessage')).toBe('receivemessage');
expect(getPropertyName('fetchData')).toBe('fetchdata');
});
it('should return an empty string if the input is "send"', () => {
expect(getPropertyName('send')).toBe('');
});
it('should handle empty strings', () => {
expect(getPropertyName('')).toBe('');
});
});
describe('getSecretToken', () => {
const mockThis = {
getWorkflow: jest.fn().mockReturnValue({ id: 'workflow123' }),
getNode: jest.fn().mockReturnValue({ id: 'node123' }),
} as unknown as IHookFunctions & IWebhookFunctions;
beforeEach(() => {
jest.clearAllMocks();
});
it('should return a valid secret token', () => {
const secretToken = getSecretToken.call(mockThis);
expect(secretToken).toBe('workflow123_node123');
});
it('should remove invalid characters from the secret token', () => {
mockThis.getNode().id = 'node@123';
mockThis.getWorkflow().id = 'workflow#123';
const secretToken = getSecretToken.call(mockThis);
expect(secretToken).toBe('workflow123_node123');
});
});
});
@@ -0,0 +1,37 @@
import get from 'lodash/get';
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
export const telegramNode: INode = {
id: 'b3039263-29ad-4476-9894-51dfcc5a706d',
name: 'Telegram node',
typeVersion: 1.2,
type: 'n8n-nodes-base.telegram',
position: [0, 0],
parameters: {
resource: 'callback',
operation: 'answerQuery',
},
};
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
const fakeExecuteFunction = {
getInputData() {
return [{ json: {} }];
},
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject,
options?: IGetNodeParameterOptions,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return telegramNode;
},
helpers: {},
continueOnFail: () => false,
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};
@@ -0,0 +1,753 @@
import { mockDeep } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INode,
INodeExecutionData,
NodeExecutionWithMetadata,
} from 'n8n-workflow';
import * as GenericFunctions from '../GenericFunctions';
import { Telegram } from '../Telegram.node';
describe('Telegram node', () => {
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
const apiRequestSpy = jest.spyOn(GenericFunctions, 'apiRequest');
const node = new Telegram();
const legacyBinaryAccessHelper = (index: number, propertyName: string | any) => {
const items = executeFunctionsMock.getInputData();
return items[index].binary![propertyName as string];
};
beforeEach(() => {
jest.resetAllMocks();
executeFunctionsMock.getCredentials.mockResolvedValue({
baseUrl: 'https://api.telegram.org',
accessToken: 'test-token',
});
executeFunctionsMock.getNode.mockReturnValue({
typeVersion: 1.2,
} as INode);
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
executeFunctionsMock.helpers.returnJsonArray.mockImplementation(
(input) => input as INodeExecutionData[],
);
executeFunctionsMock.helpers.constructExecutionMetaData.mockImplementation(
(input) => input as NodeExecutionWithMetadata[],
);
});
describe('file:get', () => {
beforeEach(() => {
executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
switch (p) {
case 'resource':
return 'file';
case 'operation':
return 'get';
case 'download':
return true;
case 'fileId':
return 'file-id';
default:
return undefined;
}
});
});
it('should determine the mime type of the file', async () => {
apiRequestSpy.mockResolvedValueOnce({
result: {
file_id: 'file-id',
file_path: 'documents/file_1.pdf',
},
});
apiRequestSpy.mockResolvedValueOnce({
body: Buffer.from('test-file'),
});
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
data: 'test-file',
mimeType: 'application/pdf',
});
const result = await node.execute.call(executeFunctionsMock);
expect(result).toEqual([
[
{
json: {
result: {
file_id: 'file-id',
file_path: 'documents/file_1.pdf',
},
},
binary: {
data: {
data: 'test-file',
mimeType: 'application/pdf',
},
},
pairedItem: { item: 0 },
},
],
]);
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
Buffer.from('test-file'),
'file_1.pdf',
'application/pdf',
);
});
it('should fallback to application/octet-stream if the mime type cannot be determined', async () => {
apiRequestSpy.mockResolvedValueOnce({
result: {
file_id: 'file-id',
file_path: 'documents/file_1.foo',
},
});
apiRequestSpy.mockResolvedValueOnce({
body: Buffer.from('test-file'),
});
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
data: 'test-file',
mimeType: 'application/octet-stream',
});
const result = await node.execute.call(executeFunctionsMock);
expect(result).toEqual([
[
{
json: {
result: {
file_id: 'file-id',
file_path: 'documents/file_1.foo',
},
},
binary: {
data: {
data: 'test-file',
mimeType: 'application/octet-stream',
},
},
pairedItem: { item: 0 },
},
],
]);
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
Buffer.from('test-file'),
'file_1.foo',
'application/octet-stream',
);
});
it('should use the provided mime type if it is specified', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
switch (p) {
case 'resource':
return 'file';
case 'operation':
return 'get';
case 'download':
return true;
case 'fileId':
return 'file-id';
case 'additionalFields':
return { mimeType: 'image/jpeg' };
default:
return undefined;
}
});
apiRequestSpy.mockResolvedValueOnce({
result: {
file_id: 'file-id',
file_path: 'documents/file_1.pdf',
},
});
apiRequestSpy.mockResolvedValueOnce({
body: Buffer.from('test-file'),
});
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
data: 'test-file',
mimeType: 'image/jpeg',
});
const result = await node.execute.call(executeFunctionsMock);
expect(result).toEqual([
[
{
json: {
result: {
file_id: 'file-id',
file_path: 'documents/file_1.pdf',
},
},
binary: {
data: {
data: 'test-file',
mimeType: 'image/jpeg',
},
},
pairedItem: { item: 0 },
},
],
]);
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
Buffer.from('test-file'),
'file_1.pdf',
'image/jpeg',
);
});
});
describe('assertBinaryData usage', () => {
beforeEach(() => {
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, _) => {
switch (paramName) {
case 'resource':
return 'message';
case 'operation':
return 'sendPhoto';
case 'binaryData':
return true;
case 'chatId':
return 'chat-id';
case 'binaryPropertyName':
return 'data';
case 'additionalFields.fileName':
return '';
case 'additionalFields':
return {};
default:
return undefined;
}
});
});
it('should call assertBinaryData with correct parameters', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data: {
data: 'binary-data',
mimeType: 'image/jpeg',
fileName: 'photo.jpg',
},
},
},
]);
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue({
data: 'binary-data',
mimeType: 'image/jpeg',
fileName: 'photo.jpg',
});
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
});
it('should call assertBinaryData for each item with correct index', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
switch (paramName) {
case 'resource':
return 'message';
case 'operation':
return 'sendPhoto';
case 'binaryData':
return true;
case 'chatId':
return `chat-id-${index}`;
case 'binaryPropertyName':
return `data${index}`;
case 'additionalFields.fileName':
return '';
case 'additionalFields':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
fileName: 'photo0.jpg',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
fileName: 'photo1.png',
},
},
},
{
json: {},
binary: {
data2: {
data: 'binary-data-2',
mimeType: 'image/gif',
fileName: 'photo2.gif',
},
},
},
]);
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledTimes(3);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(1, 0, 'data0');
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(2, 1, 'data1');
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(3, 2, 'data2');
});
it('should throw error when binary data is missing', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
// No binary data
},
]);
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
throw new Error('No binary data exists on item!');
});
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
'No binary data exists on item!',
);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
});
it('should throw error when specified binary property does not exist', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
switch (paramName) {
case 'resource':
return 'message';
case 'operation':
return 'sendPhoto';
case 'binaryData':
return true;
case 'chatId':
return 'chat-id';
case 'binaryPropertyName':
return 'nonExistentProperty';
case 'additionalFields.fileName':
return '';
case 'additionalFields':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data: {
data: 'binary-data',
mimeType: 'image/jpeg',
fileName: 'photo.jpg',
},
},
},
]);
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
throw new Error("There is no binary data property 'nonExistentProperty' on item!");
});
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
"There is no binary data property 'nonExistentProperty' on item!",
);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(
0,
'nonExistentProperty',
);
});
it('should use fileName from assertBinaryData result when additionalFields.fileName is not provided', async () => {
const mockBinaryData = {
data: 'binary-data',
mimeType: 'image/jpeg',
fileName: 'from-binary-data.jpg',
};
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data: mockBinaryData,
},
},
]);
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
expect(apiRequestSpy).toHaveBeenCalledWith(
'POST',
'sendPhoto',
{},
{},
expect.objectContaining({
formData: expect.objectContaining({
photo: expect.objectContaining({
options: expect.objectContaining({
filename: 'from-binary-data.jpg',
contentType: 'image/jpeg',
}),
}),
}),
}),
);
});
});
describe('message:sendPhoto with binary data', () => {
beforeEach(() => {
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
switch (paramName) {
case 'resource':
return 'message';
case 'operation':
return 'sendPhoto';
case 'binaryData':
return true;
case 'chatId':
return index === 0 ? 'chat-id-0' : index === 1 ? 'chat-id-1' : 'chat-id-2';
case 'binaryPropertyName':
return index === 0 ? 'data0' : index === 1 ? 'data1' : 'data2';
case 'additionalFields.fileName':
return index === 0 ? 'photo0.jpg' : index === 1 ? 'photo1.png' : 'photo2.gif';
case 'additionalFields':
return {};
default:
return undefined;
}
});
});
it('should use correct index for binaryPropertyName parameter', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
fileName: 'original0.jpg',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
fileName: 'original1.png',
},
},
},
]);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 0);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 1);
expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
'binaryPropertyName',
0,
expect.anything(),
);
expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
'binaryPropertyName',
1,
expect.anything(),
);
});
it('should use correct index for additionalFields.fileName parameter', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
},
},
},
]);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
'additionalFields.fileName',
0,
'',
);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
'additionalFields.fileName',
1,
'',
);
});
it('should use correct binary data for each item based on binaryPropertyName index', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
fileName: 'original0.jpg',
},
wrongData: {
data: 'wrong-binary-data',
mimeType: 'image/gif',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
fileName: 'original1.png',
},
wrongData: {
data: 'wrong-binary-data',
mimeType: 'image/gif',
},
},
},
]);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(apiRequestSpy).toHaveBeenCalledTimes(2);
expect(apiRequestSpy).toHaveBeenNthCalledWith(
1,
'POST',
'sendPhoto',
{},
{},
expect.objectContaining({
formData: expect.objectContaining({
chat_id: 'chat-id-0',
photo: expect.objectContaining({
value: expect.any(Buffer),
options: expect.objectContaining({
filename: 'photo0.jpg',
contentType: 'image/jpeg',
}),
}),
}),
}),
);
expect(apiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'sendPhoto',
{},
{},
expect.objectContaining({
formData: expect.objectContaining({
chat_id: 'chat-id-1',
photo: expect.objectContaining({
value: expect.any(Buffer),
options: expect.objectContaining({
filename: 'photo1.png',
contentType: 'image/png',
}),
}),
}),
}),
);
});
it('should fallback to binary fileName when additionalFields.fileName is empty', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
switch (paramName) {
case 'resource':
return 'message';
case 'operation':
return 'sendPhoto';
case 'binaryData':
return true;
case 'chatId':
return index === 0 ? 'chat-id-0' : 'chat-id-1';
case 'binaryPropertyName':
return index === 0 ? 'data0' : 'data1';
case 'additionalFields.fileName':
return index === 0 ? '' : 'custom-name.jpg';
case 'additionalFields':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
fileName: 'fallback-name.jpg',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
fileName: 'original-name.png',
},
},
},
]);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
const expectFileName = (index: number, filename: string) => {
expect(apiRequestSpy).toHaveBeenNthCalledWith(
index,
'POST',
'sendPhoto',
{},
{},
{
formData: expect.objectContaining({
photo: expect.objectContaining({
options: expect.objectContaining({
filename,
}),
}),
}),
},
);
};
expectFileName(1, 'fallback-name.jpg');
expectFileName(2, 'custom-name.jpg');
});
it('should process different chat IDs for multiple items correctly', async () => {
executeFunctionsMock.getInputData.mockReturnValue([
{
json: {},
binary: {
data0: {
data: 'binary-data-0',
mimeType: 'image/jpeg',
fileName: 'photo0.jpg',
},
},
},
{
json: {},
binary: {
data1: {
data: 'binary-data-1',
mimeType: 'image/png',
fileName: 'photo1.png',
},
},
},
{
json: {},
binary: {
data2: {
data: 'binary-data-2',
mimeType: 'image/gif',
fileName: 'photo2.gif',
},
},
},
]);
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
await node.execute.call(executeFunctionsMock);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 0);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 1);
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 2);
expect(apiRequestSpy).toHaveBeenCalledTimes(3);
const expectChatId = (n: number, chatId: string) => {
expect(apiRequestSpy).toHaveBeenNthCalledWith(
n,
'POST',
'sendPhoto',
{},
{},
{
formData: expect.objectContaining({
chat_id: chatId,
}),
},
);
};
expectChatId(1, 'chat-id-0');
expectChatId(2, 'chat-id-1');
expectChatId(3, 'chat-id-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 }]] });
});
});
});
@@ -0,0 +1,437 @@
export const getChatResponse = {
ok: true,
result: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
active_usernames: ['n8n'],
bio: 'Automation',
has_private_forwards: true,
max_reaction_count: 11,
accent_color_id: 3,
},
};
export const sendMessageResponse = {
ok: true,
result: {
message_id: 40,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732960606,
text: 'a\n\nThis message was sent automatically with n8n',
entities: [
{
offset: 3,
length: 41,
type: 'italic',
},
{
offset: 44,
length: 3,
type: 'text_link',
url: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd',
},
],
link_preview_options: {
is_disabled: true,
},
},
};
export const sendMediaGroupResponse = {
ok: true,
result: [
{
message_id: 41,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732963445,
photo: [
{
file_id:
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAANzAAM2BA',
file_unique_id: 'AQAD_bUxG9wzXVJ4',
file_size: 919,
width: 90,
height: 24,
},
{
file_id:
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAANtAAM2BA',
file_unique_id: 'AQAD_bUxG9wzXVJy',
file_size: 6571,
width: 320,
height: 87,
},
{
file_id:
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAAN4AAM2BA',
file_unique_id: 'AQAD_bUxG9wzXVJ9',
file_size: 9639,
width: 458,
height: 124,
},
],
},
],
};
export const sendLocationMessageResponse = {
ok: true,
result: {
message_id: 42,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732963630,
reply_to_message: {
message_id: 40,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732960606,
text: 'a\n\nThis message was sent automatically with n8n',
entities: [
{
offset: 3,
length: 41,
type: 'italic',
},
{
offset: 44,
length: 3,
type: 'text_link',
url: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd',
},
],
link_preview_options: {
is_disabled: true,
},
},
location: {
latitude: 0.00001,
longitude: 0.000003,
},
},
};
export const okTrueResponse = {
ok: true,
result: true,
};
export const sendStickerResponse = {
ok: true,
result: {
message_id: 44,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732965815,
document: {
file_name: '1_webp_ll.png',
mime_type: 'image/png',
thumbnail: {
file_id: 'AAMCBAADGQMAAyxnSvW31uMAAWa2AAFl0vD1zqc_3xXeAAIbBwACJ95cUvzVqVKE_cXTAQAHbQADNgQ',
file_unique_id: 'AQADGwcAAifeXFJy',
file_size: 12534,
width: 320,
height: 241,
},
thumb: {
file_id: 'AAMCBAADGQMAAyxnSvW31uMAAWa2AAFl0vD1zqc_3xXeAAIbBwACJ95cUvzVqVKE_cXTAQAHbQADNgQ',
file_unique_id: 'AQADGwcAAifeXFJy',
file_size: 12534,
width: 320,
height: 241,
},
file_id: 'BQACAgQAAxkDAAMsZ0r1t9bjAAFmtgABZdLw9c6nP98V3gACGwcAAifeXFL81alShP3F0zYE',
file_unique_id: 'AgADGwcAAifeXFI',
file_size: 122750,
},
},
};
export const editMessageTextResponse = {
ok: true,
result: {
message_id: 40,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732960606,
edit_date: 1732967008,
text: 'test',
reply_markup: {
inline_keyboard: [
[
{
text: 'foo',
callback_data: 'callback',
},
{
text: 'n8n',
url: 'https://n8n.io/',
},
],
],
},
},
};
export const chatAdministratorsResponse = {
ok: true,
result: [
{
user: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
status: 'administrator',
can_be_edited: false,
can_manage_chat: true,
can_change_info: true,
can_post_messages: true,
can_edit_messages: true,
can_delete_messages: true,
can_invite_users: true,
can_restrict_members: true,
can_promote_members: false,
can_manage_video_chats: true,
can_post_stories: true,
can_edit_stories: true,
can_delete_stories: true,
is_anonymous: false,
can_manage_voice_chats: true,
},
{
user: {
id: 123456789,
is_bot: false,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
language_code: 'en',
},
status: 'creator',
is_anonymous: false,
},
],
};
export const sendAnimationMessageResponse = {
ok: true,
result: {
message_id: 45,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732968868,
animation: {
file_name: 'Telegram---Opening-Image.gif.mp4',
mime_type: 'video/mp4',
duration: 6,
width: 320,
height: 320,
thumbnail: {
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
file_unique_id: 'AQADJgQAAvMJXFFy',
file_size: 36480,
width: 320,
height: 320,
},
thumb: {
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
file_unique_id: 'AQADJgQAAvMJXFFy',
file_size: 36480,
width: 320,
height: 320,
},
file_id: 'CgACAgQAAxkDAAMtZ0sBpKvfUg2GmktVUdBDAAFKzsNXAAImBAAC8wlcUe4yGEvHdx8nNgQ',
file_unique_id: 'AgADJgQAAvMJXFE',
file_size: 309245,
},
document: {
file_name: 'Telegram---Opening-Image.gif.mp4',
mime_type: 'video/mp4',
thumbnail: {
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
file_unique_id: 'AQADJgQAAvMJXFFy',
file_size: 36480,
width: 320,
height: 320,
},
thumb: {
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
file_unique_id: 'AQADJgQAAvMJXFFy',
file_size: 36480,
width: 320,
height: 320,
},
file_id: 'CgACAgQAAxkDAAMtZ0sBpKvfUg2GmktVUdBDAAFKzsNXAAImBAAC8wlcUe4yGEvHdx8nNgQ',
file_unique_id: 'AgADJgQAAvMJXFE',
file_size: 309245,
},
caption: 'Animation',
},
};
export const sendAudioResponse = {
ok: true,
result: {
message_id: 46,
from: {
id: 9876543210,
is_bot: true,
first_name: '@n8n',
username: 'n8n_test_bot',
},
chat: {
id: 123456789,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
type: 'private',
},
date: 1732969291,
audio: {
duration: 3,
file_name: 'sample-3s.mp3',
mime_type: 'audio/mpeg',
file_id: 'CQACAgQAAxkDAAMuZ0sDSxCh3hW89NQa-eTpxKioqGAAAjsEAAIBCU1SGtsPA4N9TSo2BA',
file_unique_id: 'AgADOwQAAgEJTVI',
file_size: 52079,
},
},
};
export const getMemberResponse = {
ok: true,
result: {
user: {
id: 123456789,
is_bot: false,
first_name: 'Nathan',
last_name: 'W',
username: 'n8n',
language_code: 'en',
},
status: 'creator',
is_anonymous: false,
},
};
export const sendMessageWithBinaryDataAndReplyMarkupResponse = {
ok: true,
result: {
message_id: 123,
from: {
id: 1234578901,
is_bot: true,
first_name: 'TestBot',
username: 'TestBot',
},
chat: {
id: 987654321,
first_name: 'Some',
last_name: 'Guy',
username: 'SomeGuy',
type: 'private',
},
date: 1750195377,
document: {
file_name: 'file.json',
mime_type: 'application/json',
file_id: 'BQACAgIAAxkDAANFaFHcsX7_6XEYxKTw3Y93hBKxdPEAAm1_AAJ3NpBKL3xbHXAyvIU2BA',
file_unique_id: 'AgADbX8AAnc2kEo',
file_size: 24,
},
reply_markup: {
inline_keyboard: [
[
{
text: 'Test Button',
callback_data: '123',
},
],
],
},
},
};
@@ -0,0 +1,170 @@
{
"name": "Telegram Binary Data and Reply Markup",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-700, 160],
"id": "6acb0d3b-6f5e-43dd-adeb-152ab4e9cc90",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "sendDocument",
"chatId": "123456789",
"binaryData": true,
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "Test Button",
"additionalFields": {
"callback_data": "123"
}
}
]
}
}
]
},
"additionalFields": {}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [-40, 160],
"id": "71580477-ff66-487d-9762-4bdf5cc0b5a9",
"name": "Send a document",
"webhookId": "bea3ccc9-bda6-4353-904e-bff92d608457",
"credentials": {
"telegramApi": {
"id": "HcIHBfGmAEOgtHgq",
"name": "Telegram account"
}
}
},
{
"parameters": {
"operation": "toJson",
"options": {}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [-260, 160],
"id": "deeef2c2-dd75-4719-9288-e8612d67c09f",
"name": "Convert to File"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "2c5d3b18-1876-49a8-bf71-02cfebf2e5f3",
"name": "data",
"value": "lorem ipsum",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-480, 160],
"id": "79bf86ea-d10d-4e00-b251-b4126554d993",
"name": "Edit Fields"
}
],
"pinData": {
"Send a document": [
{
"json": {
"ok": true,
"result": {
"message_id": 123,
"from": {
"id": 1234578901,
"is_bot": true,
"first_name": "TestBot",
"username": "TestBot"
},
"chat": {
"id": 987654321,
"first_name": "Some",
"last_name": "Guy",
"username": "SomeGuy",
"type": "private"
},
"date": 1750195377,
"document": {
"file_name": "file.json",
"mime_type": "application/json",
"file_id": "BQACAgIAAxkDAANFaFHcsX7_6XEYxKTw3Y93hBKxdPEAAm1_AAJ3NpBKL3xbHXAyvIU2BA",
"file_unique_id": "AgADbX8AAnc2kEo",
"file_size": 24
},
"reply_markup": {
"inline_keyboard": [
[
{
"text": "Test Button",
"callback_data": "123"
}
]
]
}
}
}
}
]
},
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "Send a document",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "007f0573-7089-4353-9d41-30d705b432ed",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "e115be144a6a5547dbfca93e774dfffa178aa94a181854c13e2ce5e14d195b2e"
},
"id": "6axpOZWb9wBsnrBS",
"tags": []
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import {
getChatResponse,
sendMediaGroupResponse,
sendMessageResponse,
sendLocationMessageResponse,
okTrueResponse,
sendStickerResponse,
editMessageTextResponse,
chatAdministratorsResponse,
sendAnimationMessageResponse,
sendAudioResponse,
getMemberResponse,
sendMessageWithBinaryDataAndReplyMarkupResponse,
} from './apiResponses';
describe('Telegram', () => {
const credentials = {
telegramApi: {
accessToken: 'testToken',
baseUrl: 'https://api.telegram.org',
},
};
describe('Run Telegram workflow', () => {
beforeAll(() => {
const mock = nock(credentials.telegramApi.baseUrl);
mock.post('/bottestToken/getChat').reply(200, getChatResponse);
mock.post('/bottestToken/getChat').reply(404, { error: 'Chat not found' });
mock.post('/bottestToken/sendMessage').reply(200, sendMessageResponse);
mock.post('/bottestToken/sendMediaGroup').reply(200, sendMediaGroupResponse);
mock.post('/bottestToken/sendLocation').reply(200, sendLocationMessageResponse);
mock.post('/bottestToken/deleteMessage').reply(200, okTrueResponse);
mock.post('/bottestToken/pinChatMessage').reply(200, okTrueResponse);
mock.post('/bottestToken/setChatDescription').reply(200, okTrueResponse);
mock.post('/bottestToken/setChatTitle').reply(200, okTrueResponse);
mock.post('/bottestToken/unpinChatMessage').reply(200, okTrueResponse);
mock.post('/bottestToken/sendChatAction').reply(200, okTrueResponse);
mock.post('/bottestToken/leaveChat').reply(200, okTrueResponse);
mock.post('/bottestToken/sendSticker').reply(200, sendStickerResponse);
mock.post('/bottestToken/editMessageText').reply(200, editMessageTextResponse);
mock.post('/bottestToken/getChatAdministrators').reply(200, chatAdministratorsResponse);
mock.post('/bottestToken/sendAnimation').reply(200, sendAnimationMessageResponse);
mock.post('/bottestToken/sendAudio').reply(200, sendAudioResponse);
mock.post('/bottestToken/getChatMember').reply(200, getMemberResponse);
});
new NodeTestHarness().setupTests({ credentials, workflowFiles: ['workflow.json'] });
});
describe('Binary Data and Reply Markup', () => {
beforeAll(() => {
const mock = nock(credentials.telegramApi.baseUrl);
mock
.post('/bottestToken/sendDocument')
.reply(200, sendMessageWithBinaryDataAndReplyMarkupResponse);
});
new NodeTestHarness().setupTests({ credentials, workflowFiles: ['binaryData.workflow.json'] });
});
});
@@ -0,0 +1,78 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import { type INode, SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow';
import * as genericFunctions from '../../GenericFunctions';
import { Telegram } from '../../Telegram.node';
jest.mock('../../GenericFunctions', () => {
const originalModule = jest.requireActual('../../GenericFunctions');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Telegram, message => sendAndWait', () => {
let telegram: Telegram;
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
beforeEach(() => {
telegram = new Telegram();
mockExecuteFunctions = mock<IExecuteFunctions>();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should send message and put execution to wait', async () => {
const items = [{ json: { data: 'test' } }];
//node
mockExecuteFunctions.getInputData.mockReturnValue(items);
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
//createSendAndWaitMessageBody
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('chatID');
//getSendAndWaitConfig
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
);
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // approvalOptions
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // options
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
// configureWaitTillDate
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values
const result = await telegram.execute.call(mockExecuteFunctions);
expect(result).toEqual([items]);
expect(genericFunctions.apiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
expect(genericFunctions.apiRequest).toHaveBeenCalledWith('POST', 'sendMessage', {
chat_id: 'chatID',
disable_web_page_preview: true,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{
text: 'Approve',
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
},
],
],
},
text: 'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)',
});
});
});