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:
Shireen Missi 2024-10-03 11:48:40 +01:00 committed by GitHub
parent 67c3453885
commit 2596ddbe8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1531 additions and 1187 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,9 +1,17 @@
import type { IRequestOptions } from 'n8n-workflow'; import type {
ICredentialDataDecryptedObject,
INodeExecutionData,
INodeProperties,
IRequestOptions,
} from 'n8n-workflow';
import { import {
REDACTED, REDACTED,
prepareRequestBody, prepareRequestBody,
sanitizeUiMessage, sanitizeUiMessage,
setAgentOptions, setAgentOptions,
replaceNullValues,
getSecrets,
} from '../../GenericFunctions'; } from '../../GenericFunctions';
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions'; import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';
@ -210,4 +218,113 @@ describe('HTTP Node Utils', () => {
expect(sanitizedRequest.headers).toBeUndefined(); 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([]);
});
});
}); });