first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { updadeQueryParameterConfig } from '../GenericFunctions';
|
||||
|
||||
describe('updadeQueryParameterConfig', () => {
|
||||
describe('version < 4.3 (legacy behavior)', () => {
|
||||
const updateQueryParam = updadeQueryParameterConfig(4.2);
|
||||
|
||||
it('should set simple key-value pairs', () => {
|
||||
const qs = {};
|
||||
updateQueryParam(qs, 'key1', 'value1');
|
||||
expect(qs).toEqual({ key1: 'value1' });
|
||||
});
|
||||
|
||||
it('should overwrite existing values', () => {
|
||||
const qs = { key1: 'oldValue' };
|
||||
updateQueryParam(qs, 'key1', 'newValue');
|
||||
expect(qs).toEqual({ key1: 'newValue' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('version >= 4.3 (array behavior)', () => {
|
||||
const updateQueryParam = updadeQueryParameterConfig(4.3);
|
||||
|
||||
it('should set initial value when key does not exist', () => {
|
||||
const qs = {};
|
||||
updateQueryParam(qs, 'key1', 'value1');
|
||||
expect(qs).toEqual({ key1: 'value1' });
|
||||
});
|
||||
|
||||
it('should create array when adding second value', () => {
|
||||
const qs = { key1: 'value1' };
|
||||
updateQueryParam(qs, 'key1', 'value2');
|
||||
expect(qs).toEqual({ key1: ['value1', 'value2'] });
|
||||
});
|
||||
|
||||
it('should append to existing array', () => {
|
||||
const qs = { key1: ['value1', 'value2'] };
|
||||
updateQueryParam(qs, 'key1', 'value3');
|
||||
expect(qs).toEqual({ key1: ['value1', 'value2', 'value3'] });
|
||||
});
|
||||
|
||||
it('should handle undefined values correctly', () => {
|
||||
const qs = {};
|
||||
updateQueryParam(qs, 'newKey', 'value');
|
||||
expect(qs).toEqual({ newKey: 'value' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('version boundary', () => {
|
||||
it('should use legacy behavior for version 4.2', () => {
|
||||
const updateQueryParam = updadeQueryParameterConfig(4.2);
|
||||
const qs = { key: 'first' };
|
||||
updateQueryParam(qs, 'key', 'second');
|
||||
expect(qs.key).toBe('second');
|
||||
});
|
||||
|
||||
it('should use array behavior for version 4.3', () => {
|
||||
const updateQueryParam = updadeQueryParameterConfig(4.3);
|
||||
const qs = { key: 'first' };
|
||||
updateQueryParam(qs, 'key', 'second');
|
||||
expect(qs.key).toEqual(['first', 'second']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Test Binary Data Download', () => {
|
||||
const baseUrl = 'https://dummy.domain';
|
||||
|
||||
beforeAll(async () => {
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/path/to/image.png')
|
||||
.reply(200, Buffer.from('test'), { 'content-type': 'image/png' });
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/path/to/text.txt')
|
||||
.reply(200, Buffer.from('test'), { 'content-type': 'text/plain; charset=utf-8' });
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/redirect-to-image')
|
||||
.reply(302, {}, { location: baseUrl + '/path/to/image.png' });
|
||||
|
||||
nock(baseUrl).persist().get('/custom-content-disposition').reply(200, Buffer.from('testing'), {
|
||||
'content-disposition': 'attachment; filename="testing.jpg"',
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ assertBinaryData: true });
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"name": "Download as Binary Data",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"parameters": {},
|
||||
"position": [580, 300]
|
||||
},
|
||||
{
|
||||
"name": "HTTP Request (v1)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 1,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/path/to/image.png",
|
||||
"responseFormat": "file"
|
||||
},
|
||||
"position": [1020, -100]
|
||||
},
|
||||
{
|
||||
"name": "HTTP Request (v2)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/path/to/image.png",
|
||||
"responseFormat": "file",
|
||||
"options": {}
|
||||
},
|
||||
"position": [1020, 80]
|
||||
},
|
||||
{
|
||||
"name": "HTTP Request (v3)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/path/to/image.png",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [1020, 240]
|
||||
},
|
||||
{
|
||||
"name": "HTTP Request (v4)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/path/to/image.png",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [1020, 400]
|
||||
},
|
||||
{
|
||||
"name": "Follow Redirect",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/redirect-to-image",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [1020, 560]
|
||||
},
|
||||
{
|
||||
"name": "Content Disposition",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/custom-content-disposition",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [1020, 720]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/path/to/text.txt",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "HTTP Request (v4)2",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4,
|
||||
"position": [1680, 600],
|
||||
"id": "665fef5c-6380-4bc0-be2a-430446b140ca"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request (v1)": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "image/png",
|
||||
"fileType": "image",
|
||||
"fileExtension": "png",
|
||||
"fileName": "image.png",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v2)": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "image/png",
|
||||
"fileType": "image",
|
||||
"fileExtension": "png",
|
||||
"fileName": "image.png",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v3)": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "image/png",
|
||||
"fileType": "image",
|
||||
"fileExtension": "png",
|
||||
"fileName": "image.png",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v4)": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "image/png",
|
||||
"fileType": "image",
|
||||
"fileExtension": "png",
|
||||
"fileName": "image.png",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"Follow Redirect": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "image/png",
|
||||
"fileType": "image",
|
||||
"fileExtension": "png",
|
||||
"fileName": "image.png",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"Content Disposition": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdGluZw==",
|
||||
"mimeType": "image/jpeg",
|
||||
"fileType": "image",
|
||||
"fileExtension": "jpg",
|
||||
"fileName": "testing.jpg",
|
||||
"fileSize": "7 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v4)2": [
|
||||
{
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "dGVzdA==",
|
||||
"mimeType": "text/plain",
|
||||
"fileType": "text",
|
||||
"fileExtension": "txt",
|
||||
"fileName": "text.txt",
|
||||
"fileSize": "4 B"
|
||||
}
|
||||
},
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request (v1)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v2)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v3)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v4)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Follow Redirect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Content Disposition",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v4)2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Test Response Encoding', () => {
|
||||
const baseUrl = 'https://dummy.domain';
|
||||
const payload = Buffer.from(
|
||||
'El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade.',
|
||||
'latin1',
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/index.html')
|
||||
.reply(200, payload, { 'content-type': 'text/plain; charset=latin1' });
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "Response Encoding Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
180,
|
||||
820
|
||||
],
|
||||
"id": "635fb102-a760-4b9e-836c-82e71bba7974"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/index.html",
|
||||
"options": {}
|
||||
},
|
||||
"name": "HTTP Request (v3)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
520,
|
||||
720
|
||||
],
|
||||
"id": "eb243cfd-fbd6-41ef-935d-4ea98617355f"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/index.html",
|
||||
"options": {}
|
||||
},
|
||||
"name": "HTTP Request (v4)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4,
|
||||
"position": [
|
||||
520,
|
||||
920
|
||||
],
|
||||
"id": "cc2f185d-df6a-4fa3-b7f4-29f0dbad0f9b"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request (v3)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v4)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"HTTP Request (v3)": [
|
||||
{
|
||||
"json": {
|
||||
"data": "El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade."
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v4)": [
|
||||
{
|
||||
"json": {
|
||||
"data": "El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Test Quoted Response Encoding', () => {
|
||||
const baseUrl = 'https://dummy.domain';
|
||||
const payload = Buffer.from(
|
||||
'El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade.',
|
||||
'latin1',
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/index.html')
|
||||
.reply(200, payload, { 'content-type': 'text/plain; charset="latin1"' });
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "Response Encoding Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [180, 820],
|
||||
"id": "635fb102-a760-4b9e-836c-82e71bba7974"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/index.html",
|
||||
"options": {}
|
||||
},
|
||||
"name": "HTTP Request (v3)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [520, 720],
|
||||
"id": "eb243cfd-fbd6-41ef-935d-4ea98617355f"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummy.domain/index.html",
|
||||
"options": {}
|
||||
},
|
||||
"name": "HTTP Request (v4)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4,
|
||||
"position": [520, 920],
|
||||
"id": "cc2f185d-df6a-4fa3-b7f4-29f0dbad0f9b"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request (v3)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request (v4)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"HTTP Request (v3)": [
|
||||
{
|
||||
"json": {
|
||||
"data": "El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade."
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request (v4)": [
|
||||
{
|
||||
"json": {
|
||||
"data": "El rápido zorro marrón salta sobre el perro perezoso. ¡Qué bello día en París! Árbol, cañón, façade."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
import { parse as parseUrl } from 'url';
|
||||
|
||||
describe('Test HTTP Request Node', () => {
|
||||
const baseUrl = 'https://dummyjson.com';
|
||||
|
||||
beforeAll(async () => {
|
||||
function getPaginationReturnData(this: nock.ReplyFnContext, limit = 10, skip = 0) {
|
||||
const nextUrl = `${baseUrl}/users?skip=${skip + limit}&limit=${limit}`;
|
||||
|
||||
const response = [];
|
||||
for (let i = skip; i < skip + limit; i++) {
|
||||
if (i > 14) {
|
||||
break;
|
||||
}
|
||||
response.push({
|
||||
id: i,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.length) {
|
||||
return [
|
||||
404,
|
||||
response,
|
||||
{
|
||||
'next-url': nextUrl,
|
||||
'content-type': this.req.headers['content-type'] || 'application/json',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
200,
|
||||
response,
|
||||
{
|
||||
'next-url': nextUrl,
|
||||
'content-type': this.req.headers['content-type'] || 'application/json',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
//GET
|
||||
nock(baseUrl).get('/todos/1').reply(200, {
|
||||
id: 1,
|
||||
todo: 'Do something nice for someone I care about',
|
||||
completed: true,
|
||||
userId: 26,
|
||||
});
|
||||
nock(baseUrl).get('/todos/1').reply(200, {
|
||||
id: 1,
|
||||
todo: 'Do something nice for someone I care about',
|
||||
completed: true,
|
||||
userId: 26,
|
||||
});
|
||||
nock(baseUrl).matchHeader('Authorization', 'Bearer 12345').get('/todos/3').reply(200, {
|
||||
id: 3,
|
||||
todo: 'Watch a classic movie',
|
||||
completed: false,
|
||||
userId: 4,
|
||||
});
|
||||
nock(baseUrl)
|
||||
.get('/todos?limit=2&skip=10')
|
||||
.reply(200, {
|
||||
todos: [
|
||||
{
|
||||
id: 11,
|
||||
todo: "Text a friend I haven't talked to in a long time",
|
||||
completed: false,
|
||||
userId: 39,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
todo: 'Organize pantry',
|
||||
completed: true,
|
||||
userId: 39,
|
||||
},
|
||||
],
|
||||
total: 150,
|
||||
skip: 10,
|
||||
limit: 2,
|
||||
});
|
||||
|
||||
//POST
|
||||
nock(baseUrl)
|
||||
.post('/todos/add', {
|
||||
todo: 'Use DummyJSON in the project',
|
||||
completed: false,
|
||||
userId: '5',
|
||||
})
|
||||
.reply(200, {
|
||||
id: 151,
|
||||
todo: 'Use DummyJSON in the project',
|
||||
completed: false,
|
||||
userId: '5',
|
||||
});
|
||||
nock(baseUrl)
|
||||
.post('/todos/add2', {
|
||||
todo: 'Use DummyJSON in the project',
|
||||
completed: false,
|
||||
userId: 15,
|
||||
})
|
||||
.reply(200, {
|
||||
id: 151,
|
||||
todo: 'Use DummyJSON in the project',
|
||||
completed: false,
|
||||
userId: 15,
|
||||
});
|
||||
nock(baseUrl).get('/html').reply(200, '<html><body><h1>Test</h1></body></html>');
|
||||
|
||||
//PUT
|
||||
nock(baseUrl).put('/todos/10', { userId: '42' }).reply(200, {
|
||||
id: 10,
|
||||
todo: 'Have a football scrimmage with some friends',
|
||||
completed: false,
|
||||
userId: '42',
|
||||
});
|
||||
|
||||
//PATCH
|
||||
nock(baseUrl)
|
||||
.patch('/products/1', '{"title":"iPhone 12"}')
|
||||
.reply(200, {
|
||||
id: 1,
|
||||
title: 'iPhone 12',
|
||||
price: 549,
|
||||
stock: 94,
|
||||
rating: 4.69,
|
||||
images: [
|
||||
'https://i.dummyjson.com/data/products/1/1.jpg',
|
||||
'https://i.dummyjson.com/data/products/1/2.jpg',
|
||||
'https://i.dummyjson.com/data/products/1/3.jpg',
|
||||
'https://i.dummyjson.com/data/products/1/4.jpg',
|
||||
'https://i.dummyjson.com/data/products/1/thumbnail.jpg',
|
||||
],
|
||||
thumbnail: 'https://i.dummyjson.com/data/products/1/thumbnail.jpg',
|
||||
description: 'An apple mobile which is nothing like apple',
|
||||
brand: 'Apple',
|
||||
category: 'smartphones',
|
||||
});
|
||||
|
||||
//DELETE
|
||||
nock(baseUrl).delete('/todos/1').reply(200, {
|
||||
id: 1,
|
||||
todo: 'Do something nice for someone I care about',
|
||||
completed: true,
|
||||
userId: 26,
|
||||
isDeleted: true,
|
||||
deletedOn: '2023-02-09T05:37:31.720Z',
|
||||
});
|
||||
|
||||
// Pagination - GET
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/users')
|
||||
.query(true)
|
||||
.reply(function (uri) {
|
||||
const data = parseUrl(uri, true);
|
||||
const limit = parseInt((data.query.limit as string) || '10', 10);
|
||||
const skip = parseInt((data.query.skip as string) || '0', 10);
|
||||
return getPaginationReturnData.call(this, limit, skip);
|
||||
});
|
||||
|
||||
// Pagination - POST
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/users')
|
||||
.reply(function (_uri, body) {
|
||||
let skip = 0;
|
||||
let limit = 10;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
// Form data
|
||||
skip = parseInt(body.split('name="skip"')[1].split('---')[0] ?? '0', 10);
|
||||
limit = parseInt(body.split('name="limit"')[1].split('---')[0] ?? '0', 10);
|
||||
} else {
|
||||
skip = parseInt(body.skip ?? '0', 10);
|
||||
limit = parseInt(body.limit ?? '10', 10);
|
||||
}
|
||||
|
||||
return getPaginationReturnData.call(this, limit, skip);
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { IExecuteFunctions, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
|
||||
import { HttpRequestV2 } from '../../V2/HttpRequestV2.node';
|
||||
|
||||
describe('HttpRequestV2', () => {
|
||||
let node: HttpRequestV2;
|
||||
let executeFunctions: IExecuteFunctions;
|
||||
|
||||
const baseUrl = 'http://example.com';
|
||||
const options = {
|
||||
redirect: '',
|
||||
batching: { batch: { batchSize: 1, batchInterval: 1 } },
|
||||
proxy: '',
|
||||
timeout: '',
|
||||
allowUnauthorizedCerts: '',
|
||||
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 HttpRequestV2(baseDescription);
|
||||
executeFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => {
|
||||
return {
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 2,
|
||||
};
|
||||
}),
|
||||
getCredentials: jest.fn(),
|
||||
helpers: {
|
||||
request: jest.fn(),
|
||||
requestOAuth1: jest.fn(
|
||||
async () =>
|
||||
await Promise.resolve({
|
||||
success: true,
|
||||
}),
|
||||
),
|
||||
requestOAuth2: jest.fn(
|
||||
async () =>
|
||||
await Promise.resolve({
|
||||
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;
|
||||
});
|
||||
|
||||
describe('Authentication Handling', () => {
|
||||
const authenticationTypes = [
|
||||
{
|
||||
genericCredentialType: 'httpBasicAuth',
|
||||
credentials: { user: 'username', password: 'password' },
|
||||
authField: 'auth',
|
||||
authValue: { user: 'username', pass: 'password' },
|
||||
},
|
||||
{
|
||||
genericCredentialType: 'httpBearerAuth',
|
||||
credentials: { token: 'bearerToken123' },
|
||||
authField: 'headers',
|
||||
authValue: { Authorization: 'Bearer bearerToken123' },
|
||||
},
|
||||
{
|
||||
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 $genericCredentialType 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;
|
||||
case 'bodyParametersUi':
|
||||
case 'headerParametersUi':
|
||||
case 'queryParametersUi':
|
||||
return { parameter: [] };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
(executeFunctions.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
const response = {
|
||||
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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,427 @@
|
||||
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: 'httpBearerAuth',
|
||||
credentials: { token: 'bearerToken123' },
|
||||
authField: 'headers',
|
||||
authValue: { Authorization: 'Bearer bearerToken123' },
|
||||
},
|
||||
{
|
||||
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 $genericCredentialType 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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('URL Parameter Validation', () => {
|
||||
it('should throw error when URL is undefined', 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 undefined;
|
||||
case 'authentication':
|
||||
return 'none';
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
|
||||
'URL parameter must be a string, got undefined',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when URL is null', 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 null;
|
||||
case 'authentication':
|
||||
return 'none';
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
|
||||
'URL parameter must be a string, got null',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when URL is a number', 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 42;
|
||||
case 'authentication':
|
||||
return 'none';
|
||||
case 'options':
|
||||
return options;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
|
||||
'URL parameter must be a string, got number',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-Origin Redirects', () => {
|
||||
it('should pass sendCredentialsOnCrossOriginRedirect = true to the request by default for node versions < 4.4', async () => {
|
||||
(executeFunctions.getNode as jest.Mock).mockReturnValue({
|
||||
typeVersion: 4.3,
|
||||
});
|
||||
(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({
|
||||
sendCredentialsOnCrossOriginRedirect: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass sendCredentialsOnCrossOriginRedirect = false to the request by default for node versions >= 4.4', async () => {
|
||||
(executeFunctions.getNode as jest.Mock).mockReturnValue({
|
||||
typeVersion: 4.4,
|
||||
});
|
||||
(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({
|
||||
sendCredentialsOnCrossOriginRedirect: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the sendCredentialsOnCrossOriginRedirect parameter to the request if provided', async () => {
|
||||
(executeFunctions.getNode as jest.Mock).mockReturnValue({
|
||||
typeVersion: 4.4,
|
||||
});
|
||||
(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, sendCredentialsOnCrossOriginRedirect: true };
|
||||
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({
|
||||
sendCredentialsOnCrossOriginRedirect: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "http request test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "12433cfb-74d9-4bf1-9afd-0ab9afc9ef19",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [820, 360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "DELETE",
|
||||
"url": "https://dummyjson.com/todos/1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "312e64ca-00bf-40e6-b21d-1f73930ef98c",
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1100, 360]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"todo": "Do something nice for someone I care about",
|
||||
"completed": true,
|
||||
"userId": 26,
|
||||
"isDeleted": true,
|
||||
"deletedOn": "2023-02-09T05:37:31.720Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "b1c4f6ef-0d15-49f3-b46d-447671b1583e",
|
||||
"id": "108",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"name": "HTTP Request test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "3db51d12-a71b-4d0d-84db-1d4c46454c40",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
160,
|
||||
720
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummyjson.com/todos/1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "96f38d87-0bdd-420c-b981-26fd55d11cb2",
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
460,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummyjson.com/todos/3",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer 12345"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "85ca0a5b-3ff4-491d-ba51-990fdf2b757f",
|
||||
"name": "HTTP Request fake header",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
460,
|
||||
800
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummyjson.com/todos",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"value": "10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "68d6e51a-66ea-45bf-928c-55efd2493cf0",
|
||||
"name": "HTTP Request with query",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
460,
|
||||
980
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummyjson.com/todos/1",
|
||||
"sendHeaders": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "38ec1a50-7f0e-4749-822d-f26370b00694",
|
||||
"name": "HTTP Request empty header",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
460,
|
||||
640
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"todo": "Do something nice for someone I care about",
|
||||
"completed": true,
|
||||
"userId": 26
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request with query": [
|
||||
{
|
||||
"json": {
|
||||
"todos": [
|
||||
{
|
||||
"id": 11,
|
||||
"todo": "Text a friend I haven't talked to in a long time",
|
||||
"completed": false,
|
||||
"userId": 39
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"todo": "Organize pantry",
|
||||
"completed": true,
|
||||
"userId": 39
|
||||
}
|
||||
],
|
||||
"total": 150,
|
||||
"skip": 10,
|
||||
"limit": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request fake header": [
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"todo": "Watch a classic movie",
|
||||
"completed": false,
|
||||
"userId": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request empty header": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"todo": "Do something nice for someone I care about",
|
||||
"completed": true,
|
||||
"userId": 26
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request with query",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request fake header",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request empty header",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "http request test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "12433cfb-74d9-4bf1-9afd-0ab9afc9ef19",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [820, 360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "https://dummyjson.com/products/1",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"title\":\"iPhone 12\"}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "312e64ca-00bf-40e6-b21d-1f73930ef98c",
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1100, 360]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"title": "iPhone 12",
|
||||
"price": 549,
|
||||
"stock": 94,
|
||||
"rating": 4.69,
|
||||
"images": [
|
||||
"https://i.dummyjson.com/data/products/1/1.jpg",
|
||||
"https://i.dummyjson.com/data/products/1/2.jpg",
|
||||
"https://i.dummyjson.com/data/products/1/3.jpg",
|
||||
"https://i.dummyjson.com/data/products/1/4.jpg",
|
||||
"https://i.dummyjson.com/data/products/1/thumbnail.jpg"
|
||||
],
|
||||
"thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg",
|
||||
"description": "An apple mobile which is nothing like apple",
|
||||
"brand": "Apple",
|
||||
"category": "smartphones"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "a49ffcc8-e61f-4fcd-93c0-c1c422d14b6c",
|
||||
"id": "108",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "http request test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "12433cfb-74d9-4bf1-9afd-0ab9afc9ef19",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [820, 360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dummyjson.com/todos/add",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "todo",
|
||||
"value": "Use DummyJSON in the project"
|
||||
},
|
||||
{
|
||||
"name": "completed",
|
||||
"value": "={{ false }}"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"value": "5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "07670093-862f-403f-96a5-ddf7fdb0d225",
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1140, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dummyjson.com/todos/add2",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"todo\":\"Use DummyJSON in the project\",\"completed\":false,\"userId\":15}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "db088210-2204-422c-823a-101afa464384",
|
||||
"name": "HTTP Request1",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1140, 440]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request": [
|
||||
{
|
||||
"json": {
|
||||
"id": 151,
|
||||
"todo": "Use DummyJSON in the project",
|
||||
"completed": false,
|
||||
"userId": "5"
|
||||
}
|
||||
}
|
||||
],
|
||||
"HTTP Request1": [
|
||||
{
|
||||
"json": {
|
||||
"id": 151,
|
||||
"todo": "Use DummyJSON in the project",
|
||||
"completed": false,
|
||||
"userId": 15
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTTP Request1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "c5d9075a-6d1e-49d8-b16b-7df985ebda69",
|
||||
"id": "108",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "http request test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "12433cfb-74d9-4bf1-9afd-0ab9afc9ef19",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
820,
|
||||
360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PUT",
|
||||
"url": "https://dummyjson.com/todos/10",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"value": "42"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "07670093-862f-403f-96a5-ddf7fdb0d225",
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1100,
|
||||
360
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"HTTP Request": [
|
||||
{
|
||||
"json": {
|
||||
"id": 10,
|
||||
"todo": "Have a football scrimmage with some friends",
|
||||
"completed": false,
|
||||
"userId": "42"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "209dd43e-fa03-4da7-94fb-cecf1974c5fe",
|
||||
"id": "108",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"name": "HTTP Request Node: Continue using error output not working",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "6707decf-7ae3-46f1-8603-b0fe4844f240",
|
||||
"name": "When clicking ‘Execute workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [16, 560]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "09b63a84-789d-4a31-b546-849ba47ed689",
|
||||
"name": "Success path",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [464, 272]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dummyjson.com/todos/1",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\n \"q\": \"abc\",\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c6841eb1-7913-4c8d-8c9d-b88a908125ed",
|
||||
"name": "Invalid JSON Body",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [240, 368],
|
||||
"alwaysOutputData": false,
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "21750b7e-c18a-43a5-a068-f8aa85d1cadf",
|
||||
"name": "Success path1",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [464, 656]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://dummyjson.com/html",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"responseFormat": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "ae2891f0-1968-4f57-a1a0-87d6f6dab57d",
|
||||
"name": "Invalid JSON Response",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [240, 752],
|
||||
"alwaysOutputData": false,
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "baa75f00-e0dd-40b2-b718-1e8311549a05",
|
||||
"name": "Request body error",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [464, 464]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "4733c47f-38c8-44a8-9d77-d9e733777cbf",
|
||||
"name": "Response body error",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [464, 848]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Request body error": [
|
||||
{
|
||||
"json": {
|
||||
"error": "JSON parameter needs to be valid JSON"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Response body error": [
|
||||
{
|
||||
"json": {
|
||||
"error": "Response body is not valid JSON. Change \"Response Format\" to \"Text\""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Invalid JSON Body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Invalid JSON Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Invalid JSON Body": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Success path",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Request body error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Invalid JSON Response": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Success path1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Response body error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "c7026310-8c4f-4889-a45b-befebacc7dde",
|
||||
"meta": {
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"id": "2Zd3If2j9PglrHri",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IBinaryData, IRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { setFilename } from '../../V3/utils/binaryData';
|
||||
|
||||
describe('setFilename', () => {
|
||||
it('returns filename from URI if fileName is missing and URI ends with fileExtension', () => {
|
||||
const preparedBinaryData = { fileExtension: 'png' } as IBinaryData;
|
||||
const requestOptions = { uri: 'https://example.com/image.png' } as IRequestOptions;
|
||||
expect(setFilename(preparedBinaryData, requestOptions, undefined)).toBe('image.png');
|
||||
});
|
||||
|
||||
it('returns constructed filename if fileName is missing and URI does not end with fileExtension', () => {
|
||||
const preparedBinaryData = { fileExtension: 'jpg' } as IBinaryData;
|
||||
const requestOptions = { uri: 'https://example.com/image.png' } as IRequestOptions;
|
||||
expect(setFilename(preparedBinaryData, requestOptions, 'response')).toBe('response.jpg');
|
||||
});
|
||||
|
||||
it('returns constructed filename with default "data" if responseFileName is undefined', () => {
|
||||
const preparedBinaryData = { fileExtension: 'txt' } as IBinaryData;
|
||||
const requestOptions = { uri: 'https://example.com/file' } as IRequestOptions;
|
||||
expect(setFilename(preparedBinaryData, requestOptions, undefined)).toBe('data.txt');
|
||||
});
|
||||
|
||||
it('returns fileName if it exists', () => {
|
||||
const preparedBinaryData = { fileName: 'myfile.pdf', fileExtension: 'pdf' } as IBinaryData;
|
||||
const requestOptions = { uri: 'https://example.com/file.pdf' } as IRequestOptions;
|
||||
expect(setFilename(preparedBinaryData, requestOptions, 'response')).toBe('myfile.pdf');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { mimeTypeFromResponse } from '../../V3/utils/parse';
|
||||
|
||||
describe('mimeTypeFromResponse', () => {
|
||||
it('should return undefined if input is undefined', () => {
|
||||
expect(mimeTypeFromResponse(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the mime type for a simple type', () => {
|
||||
expect(mimeTypeFromResponse('image/png')).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should strip charset from content type', () => {
|
||||
expect(mimeTypeFromResponse('text/html; charset=utf-8')).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should strip charset from content type', () => {
|
||||
expect(mimeTypeFromResponse('text/plain; charset=utf-8')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('should strip boundary from multipart content type', () => {
|
||||
expect(mimeTypeFromResponse('multipart/form-data; boundary=ExampleBoundaryString')).toBe(
|
||||
'multipart/form-data',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle content type with extra spaces', () => {
|
||||
expect(mimeTypeFromResponse('application/json ; charset=utf-8')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle content type with space before semicolon', () => {
|
||||
expect(mimeTypeFromResponse('application/xml ;charset=utf-8')).toBe('application/xml');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,330 @@
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
REDACTED,
|
||||
prepareRequestBody,
|
||||
sanitizeUiMessage,
|
||||
setAgentOptions,
|
||||
replaceNullValues,
|
||||
getSecrets,
|
||||
} from '../../GenericFunctions';
|
||||
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';
|
||||
|
||||
describe('HTTP Node Utils', () => {
|
||||
describe('prepareRequestBody', () => {
|
||||
it('should call default reducer', async () => {
|
||||
const bodyParameters: BodyParameter[] = [
|
||||
{
|
||||
name: 'foo.bar',
|
||||
value: 'baz',
|
||||
},
|
||||
];
|
||||
const defaultReducer: BodyParametersReducer = jest.fn();
|
||||
|
||||
await prepareRequestBody(bodyParameters, 'json', 3, defaultReducer);
|
||||
|
||||
expect(defaultReducer).toBeCalledTimes(1);
|
||||
expect(defaultReducer).toBeCalledWith({}, { name: 'foo.bar', value: 'baz' });
|
||||
});
|
||||
|
||||
it('should call process dot notations', async () => {
|
||||
const bodyParameters: BodyParameter[] = [
|
||||
{
|
||||
name: 'foo.bar.spam',
|
||||
value: 'baz',
|
||||
},
|
||||
];
|
||||
const defaultReducer: BodyParametersReducer = jest.fn();
|
||||
|
||||
const result = await prepareRequestBody(bodyParameters, 'json', 4, defaultReducer);
|
||||
|
||||
expect(defaultReducer).toBeCalledTimes(0);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toEqual({ foo: { bar: { spam: 'baz' } } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAgentOptions', () => {
|
||||
it("should not have agentOptions as it's undefined", async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'GET',
|
||||
uri: 'https://example.com',
|
||||
};
|
||||
|
||||
const sslCertificates = undefined;
|
||||
|
||||
setAgentOptions(requestOptions, sslCertificates);
|
||||
|
||||
expect(requestOptions).toEqual({
|
||||
method: 'GET',
|
||||
uri: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should have agentOptions set', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'GET',
|
||||
uri: 'https://example.com',
|
||||
};
|
||||
|
||||
const sslCertificates = {
|
||||
ca: 'mock-ca',
|
||||
};
|
||||
|
||||
setAgentOptions(requestOptions, sslCertificates);
|
||||
|
||||
expect(requestOptions).toStrictEqual({
|
||||
method: 'GET',
|
||||
uri: 'https://example.com',
|
||||
agentOptions: {
|
||||
ca: 'mock-ca',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUiMessage', () => {
|
||||
it('should remove large Buffers', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: Buffer.alloc(900000),
|
||||
};
|
||||
|
||||
expect(sanitizeUiMessage(requestOptions, {}).body).toEqual(
|
||||
'Binary data got replaced with this text. Original was a Buffer with a size of 900000 bytes.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove keys that contain sensitive data and do not modify requestOptions', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { sessionToken: 'secret', other: 'foo' },
|
||||
headers: { authorization: 'secret', other: 'foo' },
|
||||
auth: { user: 'user', password: 'secret' },
|
||||
};
|
||||
|
||||
expect(
|
||||
sanitizeUiMessage(requestOptions, {
|
||||
headers: ['authorization'],
|
||||
body: ['sessionToken'],
|
||||
auth: ['password'],
|
||||
}),
|
||||
).toEqual({
|
||||
body: { sessionToken: REDACTED, other: 'foo' },
|
||||
headers: { other: 'foo', authorization: REDACTED },
|
||||
auth: { user: 'user', password: REDACTED },
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
});
|
||||
|
||||
expect(requestOptions).toEqual({
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { sessionToken: 'secret', other: 'foo' },
|
||||
headers: { authorization: 'secret', other: 'foo' },
|
||||
auth: { user: 'user', password: 'secret' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove secrets', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { nested: { secret: 'secretAccessToken' } },
|
||||
headers: { authorization: 'secretAccessToken', other: 'foo' },
|
||||
};
|
||||
|
||||
const sanitizedRequest = sanitizeUiMessage(requestOptions, {}, ['secretAccessToken']);
|
||||
|
||||
expect(sanitizedRequest).toEqual({
|
||||
body: {
|
||||
nested: {
|
||||
secret: REDACTED,
|
||||
},
|
||||
},
|
||||
headers: { authorization: REDACTED, other: 'foo' },
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
const headersToTest = [
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
'cookie',
|
||||
'proxy-authorization',
|
||||
'sslclientcert',
|
||||
];
|
||||
|
||||
headersToTest.forEach((header) => {
|
||||
it(`should redact the ${header} header when the key is lowercase`, () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { sessionToken: 'secret', other: 'foo' },
|
||||
headers: { [header]: 'some-sensitive-token', other: 'foo' },
|
||||
auth: { user: 'user', password: 'secret' },
|
||||
};
|
||||
|
||||
const sanitizedRequest = sanitizeUiMessage(requestOptions, {});
|
||||
|
||||
expect(sanitizedRequest.headers).toEqual({ [header]: REDACTED, other: 'foo' });
|
||||
});
|
||||
|
||||
it(`should redact the ${header} header when the key is uppercase`, () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { sessionToken: 'secret', other: 'foo' },
|
||||
headers: { [header.toUpperCase()]: 'some-sensitive-token', other: 'foo' },
|
||||
auth: { user: 'user', password: 'secret' },
|
||||
};
|
||||
|
||||
const sanitizedRequest = sanitizeUiMessage(requestOptions, {});
|
||||
|
||||
expect(sanitizedRequest.headers).toEqual({
|
||||
[header.toUpperCase()]: REDACTED,
|
||||
other: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should leave headers unchanged if Authorization header is not present', () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
method: 'POST',
|
||||
uri: 'https://example.com',
|
||||
body: { sessionToken: 'secret', other: 'foo' },
|
||||
headers: { other: 'foo' },
|
||||
auth: { user: 'user', password: 'secret' },
|
||||
};
|
||||
const sanitizedRequest = sanitizeUiMessage(requestOptions, {});
|
||||
|
||||
expect(sanitizedRequest.headers).toEqual({ other: 'foo' });
|
||||
});
|
||||
|
||||
it('should handle case when headers are undefined', () => {
|
||||
const requestOptions: IRequestOptions = {};
|
||||
|
||||
const sanitizedRequest = sanitizeUiMessage(requestOptions, {});
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user