fix(HTTP Request Tool Node): Fix the undefined response issue when authentication is enabled (#11343)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Eugene 2024-10-22 16:28:42 +02:00 committed by GitHub
parent cade9b2d91
commit 094ec68d4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 108 deletions

View file

@ -281,6 +281,7 @@ export class ToolHttpRequest implements INodeType {
'User-Agent': undefined, 'User-Agent': undefined,
}, },
body: {}, body: {},
// We will need a full response object later to extract the headers and check the response's content type.
returnFullResponse: true, returnFullResponse: true,
}; };

View file

@ -1,165 +1,240 @@
import get from 'lodash/get'; import { mock } from 'jest-mock-extended';
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import type { N8nTool } from '../../../../utils/N8nTool'; import type { N8nTool } from '../../../../utils/N8nTool';
import { ToolHttpRequest } from '../ToolHttpRequest.node'; import { ToolHttpRequest } from '../ToolHttpRequest.node';
const createExecuteFunctionsMock = (parameters: IDataObject, requestMock: any) => {
const nodeParameters = parameters;
return {
getNodeParameter(parameter: string) {
return get(nodeParameters, parameter);
},
getNode() {
return {
name: 'HTTP Request',
};
},
getInputData() {
return [{ json: {} }];
},
getWorkflow() {
return {
name: 'Test Workflow',
};
},
continueOnFail() {
return false;
},
addInputData() {
return { index: 0 };
},
addOutputData() {
return;
},
helpers: {
httpRequest: requestMock,
},
} as unknown as IExecuteFunctions;
};
describe('ToolHttpRequest', () => { describe('ToolHttpRequest', () => {
let httpTool: ToolHttpRequest; const httpTool = new ToolHttpRequest();
let mockRequest: jest.Mock; const helpers = mock<IExecuteFunctions['helpers']>();
const executeFunctions = mock<IExecuteFunctions>({ helpers });
describe('Binary response', () => { describe('Binary response', () => {
beforeEach(() => { beforeEach(() => {
httpTool = new ToolHttpRequest(); jest.resetAllMocks();
mockRequest = jest.fn(); executeFunctions.getNode.mockReturnValue(
mock<INode>({
type: 'n8n-nodes-base.httpRequest',
name: 'HTTP Request',
typeVersion: 1.1,
}),
);
executeFunctions.addInputData.mockReturnValue({ index: 0 });
}); });
it('should return the error when receiving a binary response', async () => { it('should return the error when receiving a binary response', async () => {
mockRequest.mockResolvedValue({ helpers.httpRequest.mockResolvedValue({
body: Buffer.from(''), body: Buffer.from(''),
headers: { headers: {
'content-type': 'image/jpeg', 'content-type': 'image/jpeg',
}, },
}); });
const { response } = await httpTool.supplyData.call( executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
createExecuteFunctionsMock( switch (paramName) {
{ case 'method':
method: 'GET', return 'GET';
url: 'https://httpbin.org/image/jpeg', case 'url':
options: {}, return 'https://httpbin.org/image/jpeg';
placeholderDefinitions: { case 'options':
values: [], return {};
}, case 'placeholderDefinitions.values':
}, return [];
mockRequest, default:
), return undefined;
0, }
); });
const res = await (response as N8nTool).invoke(''); const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(helpers.httpRequest).toHaveBeenCalled();
expect(res).toContain('error'); expect(res).toContain('error');
expect(res).toContain('Binary data is not supported'); expect(res).toContain('Binary data is not supported');
}); });
it('should return the response text when receiving a text response', async () => { it('should return the response text when receiving a text response', async () => {
mockRequest.mockResolvedValue({ helpers.httpRequest.mockResolvedValue({
body: 'Hello World', body: 'Hello World',
headers: { headers: {
'content-type': 'text/plain', 'content-type': 'text/plain',
}, },
}); });
const { response } = await httpTool.supplyData.call( executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
createExecuteFunctionsMock( switch (paramName) {
{ case 'method':
method: 'GET', return 'GET';
url: 'https://httpbin.org/text/plain', case 'url':
options: {}, return 'https://httpbin.org/text/plain';
placeholderDefinitions: { case 'options':
values: [], return {};
}, case 'placeholderDefinitions.values':
}, return [];
mockRequest, default:
), return undefined;
0, }
); });
const res = await (response as N8nTool).invoke(''); const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(helpers.httpRequest).toHaveBeenCalled();
expect(res).toEqual('Hello World'); expect(res).toEqual('Hello World');
}); });
it('should return the response text when receiving a text response with a charset', async () => { it('should return the response text when receiving a text response with a charset', async () => {
mockRequest.mockResolvedValue({ helpers.httpRequest.mockResolvedValue({
body: 'こんにちは世界', body: 'こんにちは世界',
headers: { headers: {
'content-type': 'text/plain; charset=iso-2022-jp', 'content-type': 'text/plain; charset=iso-2022-jp',
}, },
}); });
const { response } = await httpTool.supplyData.call( executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
createExecuteFunctionsMock( switch (paramName) {
{ case 'method':
method: 'GET', return 'GET';
url: 'https://httpbin.org/text/plain', case 'url':
options: {}, return 'https://httpbin.org/text/plain';
placeholderDefinitions: { case 'options':
values: [], return {};
}, case 'placeholderDefinitions.values':
}, return [];
mockRequest, default:
), return undefined;
0, }
); });
const res = await (response as N8nTool).invoke(''); const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(helpers.httpRequest).toHaveBeenCalled();
expect(res).toEqual('こんにちは世界'); expect(res).toEqual('こんにちは世界');
}); });
it('should return the response object when receiving a JSON response', async () => { it('should return the response object when receiving a JSON response', async () => {
const mockJson = { hello: 'world' }; const mockJson = { hello: 'world' };
mockRequest.mockResolvedValue({ helpers.httpRequest.mockResolvedValue({
body: mockJson, body: JSON.stringify(mockJson),
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',
}, },
}); });
const { response } = await httpTool.supplyData.call( executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
createExecuteFunctionsMock( switch (paramName) {
{ case 'method':
method: 'GET', return 'GET';
url: 'https://httpbin.org/json', case 'url':
options: {}, return 'https://httpbin.org/json';
placeholderDefinitions: { case 'options':
values: [], return {};
}, case 'placeholderDefinitions.values':
}, return [];
mockRequest, default:
), return undefined;
0, }
); });
const res = await (response as N8nTool).invoke(''); const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(helpers.httpRequest).toHaveBeenCalled();
expect(jsonParse(res)).toEqual(mockJson); expect(jsonParse(res)).toEqual(mockJson);
}); });
it('should handle authentication with predefined credentials', async () => {
helpers.httpRequestWithAuthentication.mockResolvedValue({
body: 'Hello World',
headers: {
'content-type': 'text/plain',
},
});
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/text/plain';
case 'authentication':
return 'predefinedCredentialType';
case 'nodeCredentialType':
return 'linearApi';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});
const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(res).toEqual('Hello World');
expect(helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'linearApi',
expect.objectContaining({
returnFullResponse: true,
}),
undefined,
);
});
it('should handle authentication with generic credentials', async () => {
helpers.httpRequest.mockResolvedValue({
body: 'Hello World',
headers: {
'content-type': 'text/plain',
},
});
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/text/plain';
case 'authentication':
return 'genericCredentialType';
case 'genericAuthType':
return 'httpBasicAuth';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});
executeFunctions.getCredentials.mockResolvedValue({
user: 'username',
password: 'password',
});
const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({});
expect(res).toEqual('Hello World');
expect(helpers.httpRequest).toHaveBeenCalledWith(
expect.objectContaining({
returnFullResponse: true,
auth: expect.objectContaining({
username: 'username',
password: 'password',
}),
}),
);
});
}); });
}); });

View file

@ -109,12 +109,11 @@ const predefinedCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: nu
const additionalOptions = getOAuth2AdditionalParameters(predefinedType); const additionalOptions = getOAuth2AdditionalParameters(predefinedType);
return async (options: IHttpRequestOptions) => { return async (options: IHttpRequestOptions) => {
return await ctx.helpers.requestWithAuthentication.call( return await ctx.helpers.httpRequestWithAuthentication.call(
ctx, ctx,
predefinedType, predefinedType,
options, options,
additionalOptions && { oauth2: additionalOptions }, additionalOptions && { oauth2: additionalOptions },
itemIndex,
); );
}; };
}; };