Skip to content

Commit 7ad4bad

Browse files
authored
test(Telegram Node): Add some tests for Telegram (no-changelog) (#11043)
1 parent 0a8a57e commit 7ad4bad

File tree

7 files changed

+2156
-1
lines changed

7 files changed

+2156
-1
lines changed

packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export class TelegramTrigger implements INodeType {
276276
) as IDataObject;
277277

278278
// When the image is sent from the desktop app telegram does not resize the image
279-
// So return the only image avaiable
279+
// So return the only image available
280280
// Basically the Image Size parameter would work just when the images comes from the mobile app
281281
if (image === undefined) {
282282
image = bodyData[key]!.photo![0];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import {
2+
NodeApiError,
3+
type IDataObject,
4+
type IExecuteFunctions,
5+
type IHookFunctions,
6+
type IHttpRequestMethods,
7+
type ILoadOptionsFunctions,
8+
type IWebhookFunctions,
9+
} from 'n8n-workflow';
10+
11+
import {
12+
addAdditionalFields,
13+
apiRequest,
14+
getPropertyName,
15+
getSecretToken,
16+
} from '../GenericFunctions';
17+
18+
describe('Telegram > GenericFunctions', () => {
19+
describe('apiRequest', () => {
20+
let mockThis: IHookFunctions & IExecuteFunctions & ILoadOptionsFunctions & IWebhookFunctions;
21+
const credentials = { baseUrl: 'https://api.telegram.org', accessToken: 'testToken' };
22+
beforeEach(() => {
23+
mockThis = {
24+
getCredentials: jest.fn(),
25+
helpers: {
26+
request: jest.fn(),
27+
},
28+
getNode: jest.fn(),
29+
} as unknown as IHookFunctions &
30+
IExecuteFunctions &
31+
ILoadOptionsFunctions &
32+
IWebhookFunctions;
33+
34+
jest.clearAllMocks();
35+
});
36+
37+
it('should make a successful API request', async () => {
38+
const method: IHttpRequestMethods = 'POST';
39+
const endpoint = 'sendMessage';
40+
const body: IDataObject = { text: 'Hello, world!' };
41+
const query: IDataObject = { chat_id: '12345' };
42+
const option: IDataObject = { headers: { 'Custom-Header': 'value' } };
43+
44+
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
45+
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
46+
47+
const result = await apiRequest.call(mockThis, method, endpoint, body, query, option);
48+
49+
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
50+
expect(mockThis.helpers.request).toHaveBeenCalledWith({
51+
headers: { 'Custom-Header': 'value' },
52+
method: 'POST',
53+
uri: 'https://api.telegram.org/bottestToken/sendMessage',
54+
body: { text: 'Hello, world!' },
55+
qs: { chat_id: '12345' },
56+
json: true,
57+
});
58+
expect(result).toEqual({ success: true });
59+
});
60+
61+
it('should handle an API request with no body and query', async () => {
62+
const method: IHttpRequestMethods = 'GET';
63+
const endpoint = 'getMe';
64+
const body: IDataObject = {};
65+
const query: IDataObject = {};
66+
67+
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
68+
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
69+
70+
const result = await apiRequest.call(mockThis, method, endpoint, body, query);
71+
72+
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
73+
expect(mockThis.helpers.request).toHaveBeenCalledWith({
74+
headers: {},
75+
method: 'GET',
76+
uri: 'https://api.telegram.org/bottestToken/getMe',
77+
json: true,
78+
});
79+
expect(result).toEqual({ success: true });
80+
});
81+
82+
it('should handle an API request with no additional options', async () => {
83+
const method: IHttpRequestMethods = 'POST';
84+
const endpoint = 'sendMessage';
85+
const body: IDataObject = { text: 'Hello, world!' };
86+
87+
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
88+
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
89+
90+
const result = await apiRequest.call(mockThis, method, endpoint, body);
91+
92+
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
93+
expect(mockThis.helpers.request).toHaveBeenCalledWith({
94+
headers: {},
95+
method: 'POST',
96+
uri: 'https://api.telegram.org/bottestToken/sendMessage',
97+
body: { text: 'Hello, world!' },
98+
json: true,
99+
});
100+
expect(result).toEqual({ success: true });
101+
});
102+
103+
it('should throw a NodeApiError on request failure', async () => {
104+
const method: IHttpRequestMethods = 'POST';
105+
const endpoint = 'sendMessage';
106+
const body: IDataObject = { text: 'Hello, world!' };
107+
108+
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
109+
(mockThis.helpers.request as jest.Mock).mockRejectedValue(new Error('Request failed'));
110+
111+
await expect(apiRequest.call(mockThis, method, endpoint, body)).rejects.toThrow(NodeApiError);
112+
113+
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
114+
expect(mockThis.helpers.request).toHaveBeenCalledWith({
115+
headers: {},
116+
method: 'POST',
117+
uri: 'https://api.telegram.org/bottestToken/sendMessage',
118+
body: { text: 'Hello, world!' },
119+
json: true,
120+
});
121+
});
122+
});
123+
describe('addAdditionalFields', () => {
124+
let mockThis: IExecuteFunctions;
125+
126+
beforeEach(() => {
127+
mockThis = {
128+
getNodeParameter: jest.fn(),
129+
} as unknown as IExecuteFunctions;
130+
131+
jest.clearAllMocks();
132+
});
133+
134+
it('should add additional fields and attribution for sendMessage operation', () => {
135+
const body: IDataObject = { text: 'Hello, world!' };
136+
const index = 0;
137+
const nodeVersion = 1.1;
138+
const instanceId = '45';
139+
140+
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
141+
switch (paramName) {
142+
case 'operation':
143+
return 'sendMessage';
144+
case 'additionalFields':
145+
return { appendAttribution: true };
146+
case 'replyMarkup':
147+
return 'none';
148+
default:
149+
return '';
150+
}
151+
});
152+
153+
addAdditionalFields.call(mockThis, body, index, nodeVersion, instanceId);
154+
155+
expect(body).toEqual({
156+
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)',
157+
parse_mode: 'Markdown',
158+
disable_web_page_preview: true,
159+
});
160+
});
161+
162+
it('should add reply markup for inlineKeyboard', () => {
163+
const body: IDataObject = { text: 'Hello, world!' };
164+
const index = 0;
165+
166+
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
167+
switch (paramName) {
168+
case 'operation':
169+
return 'sendMessage';
170+
case 'additionalFields':
171+
return {};
172+
case 'replyMarkup':
173+
return 'inlineKeyboard';
174+
case 'inlineKeyboard':
175+
return {
176+
rows: [
177+
{
178+
row: {
179+
buttons: [
180+
{ text: 'Button 1', additionalFields: { url: 'https://example.com' } },
181+
{ text: 'Button 2' },
182+
],
183+
},
184+
},
185+
],
186+
};
187+
default:
188+
return '';
189+
}
190+
});
191+
192+
addAdditionalFields.call(mockThis, body, index);
193+
194+
expect(body).toEqual({
195+
text: 'Hello, world!',
196+
disable_web_page_preview: true,
197+
parse_mode: 'Markdown',
198+
reply_markup: {
199+
inline_keyboard: [
200+
[{ text: 'Button 1', url: 'https://example.com' }, { text: 'Button 2' }],
201+
],
202+
},
203+
});
204+
});
205+
206+
it('should add reply markup for forceReply', () => {
207+
const body: IDataObject = { text: 'Hello, world!' };
208+
const index = 0;
209+
210+
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
211+
switch (paramName) {
212+
case 'operation':
213+
return 'sendMessage';
214+
case 'additionalFields':
215+
return {};
216+
case 'replyMarkup':
217+
return 'forceReply';
218+
case 'forceReply':
219+
return { force_reply: true };
220+
default:
221+
return '';
222+
}
223+
});
224+
225+
addAdditionalFields.call(mockThis, body, index);
226+
227+
expect(body).toEqual({
228+
text: 'Hello, world!',
229+
disable_web_page_preview: true,
230+
parse_mode: 'Markdown',
231+
reply_markup: { force_reply: true },
232+
});
233+
});
234+
235+
it('should add reply markup for replyKeyboardRemove', () => {
236+
const body: IDataObject = { text: 'Hello, world!' };
237+
const index = 0;
238+
239+
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
240+
switch (paramName) {
241+
case 'operation':
242+
return 'sendMessage';
243+
case 'additionalFields':
244+
return {};
245+
case 'replyMarkup':
246+
return 'replyKeyboardRemove';
247+
case 'replyKeyboardRemove':
248+
return { remove_keyboard: true };
249+
default:
250+
return '';
251+
}
252+
});
253+
254+
addAdditionalFields.call(mockThis, body, index);
255+
256+
expect(body).toEqual({
257+
text: 'Hello, world!',
258+
disable_web_page_preview: true,
259+
parse_mode: 'Markdown',
260+
reply_markup: { remove_keyboard: true },
261+
});
262+
});
263+
264+
it('should handle nodeVersion 1.2 and set disable_web_page_preview', () => {
265+
const body: IDataObject = { text: 'Hello, world!' };
266+
const index = 0;
267+
const nodeVersion = 1.2;
268+
269+
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
270+
switch (paramName) {
271+
case 'operation':
272+
return 'sendMessage';
273+
case 'additionalFields':
274+
return {};
275+
case 'replyMarkup':
276+
return 'none';
277+
default:
278+
return '';
279+
}
280+
});
281+
282+
addAdditionalFields.call(mockThis, body, index, nodeVersion);
283+
284+
expect(body).toEqual({
285+
disable_web_page_preview: true,
286+
parse_mode: 'Markdown',
287+
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)',
288+
});
289+
});
290+
});
291+
describe('getPropertyName', () => {
292+
it('should return the property name by removing "send" and converting to lowercase', () => {
293+
expect(getPropertyName('sendMessage')).toBe('message');
294+
expect(getPropertyName('sendEmail')).toBe('email');
295+
expect(getPropertyName('sendNotification')).toBe('notification');
296+
});
297+
298+
it('should return the original string in lowercase if it does not contain "send"', () => {
299+
expect(getPropertyName('receiveMessage')).toBe('receivemessage');
300+
expect(getPropertyName('fetchData')).toBe('fetchdata');
301+
});
302+
303+
it('should return an empty string if the input is "send"', () => {
304+
expect(getPropertyName('send')).toBe('');
305+
});
306+
307+
it('should handle empty strings', () => {
308+
expect(getPropertyName('')).toBe('');
309+
});
310+
});
311+
describe('getSecretToken', () => {
312+
const mockThis = {
313+
getWorkflow: jest.fn().mockReturnValue({ id: 'workflow123' }),
314+
getNode: jest.fn().mockReturnValue({ id: 'node123' }),
315+
} as unknown as IHookFunctions & IWebhookFunctions;
316+
317+
beforeEach(() => {
318+
jest.clearAllMocks();
319+
});
320+
321+
it('should return a valid secret token', () => {
322+
const secretToken = getSecretToken.call(mockThis);
323+
324+
expect(secretToken).toBe('workflow123_node123');
325+
});
326+
327+
it('should remove invalid characters from the secret token', () => {
328+
mockThis.getNode().id = 'node@123';
329+
mockThis.getWorkflow().id = 'workflow#123';
330+
331+
const secretToken = getSecretToken.call(mockThis);
332+
expect(secretToken).toBe('workflow123_node123');
333+
});
334+
});
335+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { get } from 'lodash';
2+
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
3+
4+
export const telegramNode: INode = {
5+
id: 'b3039263-29ad-4476-9894-51dfcc5a706d',
6+
name: 'Telegram node',
7+
typeVersion: 1.2,
8+
type: 'n8n-nodes-base.telegram',
9+
position: [0, 0],
10+
parameters: {
11+
resource: 'callback',
12+
operation: 'answerQuery',
13+
},
14+
};
15+
16+
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
17+
const fakeExecuteFunction = {
18+
getInputData() {
19+
return [{ json: {} }];
20+
},
21+
getNodeParameter(
22+
parameterName: string,
23+
_itemIndex: number,
24+
fallbackValue?: IDataObject | undefined,
25+
options?: IGetNodeParameterOptions | undefined,
26+
) {
27+
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
28+
return get(nodeParameters, parameter, fallbackValue);
29+
},
30+
getNode() {
31+
return telegramNode;
32+
},
33+
helpers: {},
34+
continueOnFail: () => false,
35+
} as unknown as IExecuteFunctions;
36+
return fakeExecuteFunction;
37+
};

0 commit comments

Comments
 (0)