diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts index aa294edadd612..2fbac434746d4 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts @@ -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, }; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts index 161aa140f54de..f18a2437e336a 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts @@ -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(); + const executeFunctions = mock({ helpers }); describe('Binary response', () => { beforeEach(() => { - httpTool = new ToolHttpRequest(); - mockRequest = jest.fn(); + jest.resetAllMocks(); + executeFunctions.getNode.mockReturnValue( + mock({ + 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', + }), + }), + ); + }); }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts index e637251a74c7f..cd12ac00eb5d8 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts @@ -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, ); }; };