Skip to content

Commit

Permalink
fix(HTTP Request Tool Node): Fix the undefined response issue when au…
Browse files Browse the repository at this point in the history
…thentication is enabled (#11343)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <[email protected]>
  • Loading branch information
burivuhster and netroy authored Oct 22, 2024
1 parent cade9b2 commit 094ec68
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export class ToolHttpRequest implements INodeType {
'User-Agent': undefined,
},
body: {},
// We will need a full response object later to extract the headers and check the response's content type.
returnFullResponse: true,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,165 +1,240 @@
import get from 'lodash/get';
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';

import type { N8nTool } from '../../../../utils/N8nTool';
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', () => {
let httpTool: ToolHttpRequest;
let mockRequest: jest.Mock;
const httpTool = new ToolHttpRequest();
const helpers = mock<IExecuteFunctions['helpers']>();
const executeFunctions = mock<IExecuteFunctions>({ helpers });

describe('Binary response', () => {
beforeEach(() => {
httpTool = new ToolHttpRequest();
mockRequest = jest.fn();
jest.resetAllMocks();
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 () => {
mockRequest.mockResolvedValue({
helpers.httpRequest.mockResolvedValue({
body: Buffer.from(''),
headers: {
'content-type': 'image/jpeg',
},
});

const { response } = await httpTool.supplyData.call(
createExecuteFunctionsMock(
{
method: 'GET',
url: 'https://httpbin.org/image/jpeg',
options: {},
placeholderDefinitions: {
values: [],
},
},
mockRequest,
),
0,
);
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/image/jpeg';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});

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('Binary data is not supported');
});

it('should return the response text when receiving a text response', async () => {
mockRequest.mockResolvedValue({
helpers.httpRequest.mockResolvedValue({
body: 'Hello World',
headers: {
'content-type': 'text/plain',
},
});

const { response } = await httpTool.supplyData.call(
createExecuteFunctionsMock(
{
method: 'GET',
url: 'https://httpbin.org/text/plain',
options: {},
placeholderDefinitions: {
values: [],
},
},
mockRequest,
),
0,
);
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/text/plain';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});

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');
});

it('should return the response text when receiving a text response with a charset', async () => {
mockRequest.mockResolvedValue({
helpers.httpRequest.mockResolvedValue({
body: 'こんにちは世界',
headers: {
'content-type': 'text/plain; charset=iso-2022-jp',
},
});

const { response } = await httpTool.supplyData.call(
createExecuteFunctionsMock(
{
method: 'GET',
url: 'https://httpbin.org/text/plain',
options: {},
placeholderDefinitions: {
values: [],
},
},
mockRequest,
),
0,
);
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/text/plain';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});

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('こんにちは世界');
});

it('should return the response object when receiving a JSON response', async () => {
const mockJson = { hello: 'world' };

mockRequest.mockResolvedValue({
body: mockJson,
helpers.httpRequest.mockResolvedValue({
body: JSON.stringify(mockJson),
headers: {
'content-type': 'application/json',
},
});

const { response } = await httpTool.supplyData.call(
createExecuteFunctionsMock(
{
method: 'GET',
url: 'https://httpbin.org/json',
options: {},
placeholderDefinitions: {
values: [],
},
},
mockRequest,
),
0,
);
executeFunctions.getNodeParameter.mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return 'https://httpbin.org/json';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
default:
return undefined;
}
});

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);
});

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',
}),
}),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,11 @@ const predefinedCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: nu
const additionalOptions = getOAuth2AdditionalParameters(predefinedType);

return async (options: IHttpRequestOptions) => {
return await ctx.helpers.requestWithAuthentication.call(
return await ctx.helpers.httpRequestWithAuthentication.call(
ctx,
predefinedType,
options,
additionalOptions && { oauth2: additionalOptions },
itemIndex,
);
};
};
Expand Down

0 comments on commit 094ec68

Please sign in to comment.