mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-13 16:14:07 -08:00
test(HTTP Request Node): Improve http request node testing (no-changelog) (#11044)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
67c3453885
commit
2596ddbe8d
1177
packages/nodes-base/nodes/HttpRequest/V3/Description.ts
Normal file
1177
packages/nodes-base/nodes/HttpRequest/V3/Description.ts
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,229 @@
|
|||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import type { IExecuteFunctions, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
|
||||
import { HttpRequestV3 } from '../../V3/HttpRequestV3.node';
|
||||
|
||||
describe('HttpRequestV3', () => {
|
||||
let node: HttpRequestV3;
|
||||
let executeFunctions: IExecuteFunctions;
|
||||
|
||||
const baseUrl = 'http://example.com';
|
||||
const options = {
|
||||
redirect: '',
|
||||
batching: { batch: { batchSize: 1, batchInterval: 1 } },
|
||||
proxy: '',
|
||||
timeout: '',
|
||||
allowUnauthoridCerts: '',
|
||||
queryParameterArrays: '',
|
||||
response: '',
|
||||
lowercaseHeaders: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'HTTP Request',
|
||||
name: 'httpRequest',
|
||||
description: 'Makes an HTTP request and returns the response data',
|
||||
group: [],
|
||||
};
|
||||
node = new HttpRequestV3(baseDescription);
|
||||
executeFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => {
|
||||
return {
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
};
|
||||
}),
|
||||
getCredentials: jest.fn(),
|
||||
helpers: {
|
||||
request: jest.fn(),
|
||||
requestOAuth1: jest.fn(
|
||||
async () =>
|
||||
await Promise.resolve({
|
||||
statusCode: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(JSON.stringify({ success: true })),
|
||||
}),
|
||||
),
|
||||
requestOAuth2: jest.fn(
|
||||
async () =>
|
||||
await Promise.resolve({
|
||||
statusCode: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(JSON.stringify({ success: true })),
|
||||
}),
|
||||
),
|
||||
requestWithAuthentication: jest.fn(),
|
||||
requestWithAuthenticationPaginated: jest.fn(),
|
||||
assertBinaryData: jest.fn(),
|
||||
getBinaryStream: jest.fn(),
|
||||
getBinaryMetadata: jest.fn(),
|
||||
binaryToString: jest.fn((buffer: Buffer) => {
|
||||
return buffer.toString();
|
||||
}),
|
||||
prepareBinaryData: jest.fn(),
|
||||
},
|
||||
getContext: jest.fn(),
|
||||
sendMessageToUI: jest.fn(),
|
||||
continueOnFail: jest.fn(),
|
||||
getMode: jest.fn(),
|
||||
} as unknown as IExecuteFunctions;
|
||||
});
|
||||
|
||||
it('should make a GET request', async () => {
|
||||
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
|
||||
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'method':
|
||||
return 'GET';
|
||||
case 'url':
|
||||
return baseUrl;
|
||||
case 'authentication':
|
||||
return 'none';
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
const response = {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(JSON.stringify({ success: true })),
|
||||
};
|
||||
|
||||
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
|
||||
|
||||
const result = await node.execute.call(executeFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
|
||||
});
|
||||
|
||||
it('should handle authentication', async () => {
|
||||
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
|
||||
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'method':
|
||||
return 'GET';
|
||||
case 'url':
|
||||
return baseUrl;
|
||||
case 'authentication':
|
||||
return 'genericCredentialType';
|
||||
case 'genericAuthType':
|
||||
return 'httpBasicAuth';
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
(executeFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
user: 'username',
|
||||
password: 'password',
|
||||
});
|
||||
const response = {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(JSON.stringify({ success: true })),
|
||||
};
|
||||
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
|
||||
|
||||
const result = await node.execute.call(executeFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
|
||||
expect(executeFunctions.helpers.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
auth: {
|
||||
user: 'username',
|
||||
pass: 'password',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('Authentication Handling', () => {
|
||||
const authenticationTypes = [
|
||||
{
|
||||
genericCredentialType: 'httpBasicAuth',
|
||||
credentials: { user: 'username', password: 'password' },
|
||||
authField: 'auth',
|
||||
authValue: { user: 'username', pass: 'password' },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'httpDigestAuth',
|
||||
credentials: { user: 'username', password: 'password' },
|
||||
authField: 'auth',
|
||||
authValue: { user: 'username', pass: 'password', sendImmediately: false },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'httpHeaderAuth',
|
||||
credentials: { name: 'Authorization', value: 'Bearer token' },
|
||||
authField: 'headers',
|
||||
authValue: { Authorization: 'Bearer token' },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'httpQueryAuth',
|
||||
credentials: { name: 'Token', value: 'secretToken' },
|
||||
authField: 'qs',
|
||||
authValue: { Token: 'secretToken' },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'oAuth1Api',
|
||||
credentials: { oauth_token: 'token', oauth_token_secret: 'secret' },
|
||||
authField: 'oauth',
|
||||
authValue: { oauth_token: 'token', oauth_token_secret: 'secret' },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'oAuth2Api',
|
||||
credentials: { access_token: 'accessToken' },
|
||||
authField: 'auth',
|
||||
authValue: { bearer: 'accessToken' },
|
||||
},
|
||||
];
|
||||
|
||||
it.each(authenticationTypes)(
|
||||
'should handle %s authentication',
|
||||
async ({ genericCredentialType, credentials, authField, authValue }) => {
|
||||
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
|
||||
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'method':
|
||||
return 'GET';
|
||||
case 'url':
|
||||
return baseUrl;
|
||||
case 'authentication':
|
||||
return 'genericCredentialType';
|
||||
case 'genericAuthType':
|
||||
return genericCredentialType;
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
(executeFunctions.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
const response = {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: Buffer.from(JSON.stringify({ success: true })),
|
||||
};
|
||||
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
|
||||
|
||||
const result = await node.execute.call(executeFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
|
||||
if (genericCredentialType === 'oAuth1Api') {
|
||||
expect(executeFunctions.helpers.requestOAuth1).toHaveBeenCalled();
|
||||
} else if (genericCredentialType === 'oAuth2Api') {
|
||||
expect(executeFunctions.helpers.requestOAuth2).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(executeFunctions.helpers.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[authField]: expect.objectContaining(authValue),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,9 +1,17 @@
|
|||
import type { IRequestOptions } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
REDACTED,
|
||||
prepareRequestBody,
|
||||
sanitizeUiMessage,
|
||||
setAgentOptions,
|
||||
replaceNullValues,
|
||||
getSecrets,
|
||||
} from '../../GenericFunctions';
|
||||
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';
|
||||
|
||||
|
@ -210,4 +218,113 @@ describe('HTTP Node Utils', () => {
|
|||
expect(sanitizedRequest.headers).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceNullValues', () => {
|
||||
it('should replace null json with an empty object', () => {
|
||||
const item: INodeExecutionData = {
|
||||
json: {},
|
||||
};
|
||||
const result = replaceNullValues(item);
|
||||
expect(result.json).toEqual({});
|
||||
});
|
||||
|
||||
it('should not modify json if it is already an object', () => {
|
||||
const jsonObject = { key: 'value' };
|
||||
const item: INodeExecutionData = { json: jsonObject };
|
||||
const result = replaceNullValues(item);
|
||||
expect(result.json).toBe(jsonObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSecrets', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return secrets for sensitive properties', () => {
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Api Key',
|
||||
name: 'apiKey',
|
||||
typeOptions: { password: true },
|
||||
type: 'string',
|
||||
default: undefined,
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
default: undefined,
|
||||
},
|
||||
];
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
apiKey: 'sensitive-api-key',
|
||||
username: 'user123',
|
||||
};
|
||||
|
||||
const secrets = getSecrets(properties, credentials);
|
||||
expect(secrets).toEqual(['sensitive-api-key']);
|
||||
});
|
||||
|
||||
it('should not return non-sensitive properties', () => {
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
default: undefined,
|
||||
},
|
||||
];
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
username: 'user123',
|
||||
};
|
||||
|
||||
const secrets = getSecrets(properties, credentials);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not include non-string values in sensitive properties', () => {
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'ApiKey',
|
||||
name: 'apiKey',
|
||||
typeOptions: { password: true },
|
||||
type: 'string',
|
||||
default: undefined,
|
||||
},
|
||||
];
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
apiKey: 12345,
|
||||
};
|
||||
|
||||
const secrets = getSecrets(properties, credentials);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if properties and credentials are empty', () => {
|
||||
const properties: INodeProperties[] = [];
|
||||
const credentials: ICredentialDataDecryptedObject = {};
|
||||
|
||||
const secrets = getSecrets(properties, credentials);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not include null or undefined values in sensitive properties', () => {
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'ApiKey',
|
||||
name: 'apiKey',
|
||||
typeOptions: { password: true },
|
||||
type: 'string',
|
||||
default: undefined,
|
||||
},
|
||||
];
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
apiKey: {},
|
||||
};
|
||||
|
||||
const secrets = getSecrets(properties, credentials);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue