2023-09-20 05:40:06 -07:00
import {
2023-10-06 07:25:58 -07:00
copyInputItems ,
2023-09-20 05:40:06 -07:00
getBinaryDataBuffer ,
parseIncomingMessage ,
proxyRequestToAxios ,
setBinaryDataBuffer ,
} from '@/NodeExecuteFunctions' ;
import { mkdtempSync , readFileSync } from 'fs' ;
import type { IncomingMessage } from 'http' ;
2023-04-12 05:58:05 -07:00
import { mock } from 'jest-mock-extended' ;
import type {
IBinaryData ,
INode ,
ITaskDataConnections ,
IWorkflowExecuteAdditionalData ,
Workflow ,
WorkflowHooks ,
} from 'n8n-workflow' ;
2023-09-22 08:22:12 -07:00
import { BinaryDataService } from '@/BinaryData/BinaryData.service' ;
2023-09-20 05:40:06 -07:00
import nock from 'nock' ;
import { tmpdir } from 'os' ;
import { join } from 'path' ;
2023-06-21 00:38:28 -07:00
import { initLogger } from './helpers/utils' ;
2023-09-22 08:22:12 -07:00
import Container from 'typedi' ;
2022-09-11 07:42:09 -07:00
const temporaryDir = mkdtempSync ( join ( tmpdir ( ) , 'n8n' ) ) ;
describe ( 'NodeExecuteFunctions' , ( ) = > {
2023-04-12 05:58:05 -07:00
describe ( 'test binary data helper methods' , ( ) = > {
2023-05-02 01:37:19 -07:00
test ( "test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode" , async ( ) = > {
2022-09-11 07:42:09 -07:00
// Setup a 'default' binary data manager instance
2023-09-22 08:22:12 -07:00
Container . set ( BinaryDataService , new BinaryDataService ( ) ) ;
await Container . get ( BinaryDataService ) . init ( {
2022-09-11 07:42:09 -07:00
mode : 'default' ,
2023-09-22 08:22:12 -07:00
availableModes : [ 'default' ] ,
2022-09-11 07:42:09 -07:00
localStoragePath : temporaryDir ,
} ) ;
// Set our binary data buffer
2023-05-02 01:37:19 -07:00
const inputData : Buffer = Buffer . from ( 'This is some binary data' , 'utf8' ) ;
const setBinaryDataBufferResponse : IBinaryData = await setBinaryDataBuffer (
2022-09-11 07:42:09 -07:00
{
mimeType : 'txt' ,
data : 'This should be overwritten by the actual payload in the response' ,
} ,
inputData ,
2023-09-27 00:42:35 -07:00
'workflowId' ,
2022-09-11 07:42:09 -07:00
'executionId' ,
) ;
// Expect our return object to contain the base64 encoding of the input data, as it should be stored in memory.
expect ( setBinaryDataBufferResponse . data ) . toEqual ( inputData . toString ( 'base64' ) ) ;
// Now, re-fetch our data.
// An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node.
2023-05-02 01:37:19 -07:00
const taskDataConnectionsInput : ITaskDataConnections = {
2022-09-11 07:42:09 -07:00
main : [ ] ,
} ;
// We add an input set, with one item at index 0, to this input. It contains an empty json payload and our binary data.
taskDataConnectionsInput . main . push ( [
{
json : { } ,
binary : {
data : setBinaryDataBufferResponse ,
} ,
} ,
] ) ;
// Now, lets fetch our data! The item will be item index 0.
2023-05-02 01:37:19 -07:00
const getBinaryDataBufferResponse : Buffer = await getBinaryDataBuffer (
2022-09-11 07:42:09 -07:00
taskDataConnectionsInput ,
0 ,
'data' ,
0 ,
) ;
expect ( getBinaryDataBufferResponse ) . toEqual ( inputData ) ;
} ) ;
2023-05-02 01:37:19 -07:00
test ( "test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode" , async ( ) = > {
2023-09-22 08:22:12 -07:00
Container . set ( BinaryDataService , new BinaryDataService ( ) ) ;
2022-09-11 07:42:09 -07:00
// Setup a 'filesystem' binary data manager instance
2023-09-22 08:22:12 -07:00
await Container . get ( BinaryDataService ) . init ( {
2022-09-11 07:42:09 -07:00
mode : 'filesystem' ,
2023-09-22 08:22:12 -07:00
availableModes : [ 'filesystem' ] ,
2022-09-11 07:42:09 -07:00
localStoragePath : temporaryDir ,
} ) ;
// Set our binary data buffer
2023-05-02 01:37:19 -07:00
const inputData : Buffer = Buffer . from ( 'This is some binary data' , 'utf8' ) ;
const setBinaryDataBufferResponse : IBinaryData = await setBinaryDataBuffer (
2022-09-11 07:42:09 -07:00
{
mimeType : 'txt' ,
data : 'This should be overwritten with the name of the configured data manager' ,
} ,
inputData ,
2023-09-27 00:42:35 -07:00
'workflowId' ,
2022-09-11 07:42:09 -07:00
'executionId' ,
) ;
// Expect our return object to contain the name of the configured data manager.
2023-10-10 01:06:06 -07:00
expect ( setBinaryDataBufferResponse . data ) . toEqual ( 'filesystem-v2' ) ;
2022-09-11 07:42:09 -07:00
// Ensure that the input data was successfully persisted to disk.
expect (
readFileSync (
2023-10-10 01:06:06 -07:00
` ${ temporaryDir } / ${ setBinaryDataBufferResponse . id ? . replace ( 'filesystem-v2:' , '' ) } ` ,
2022-09-11 07:42:09 -07:00
) ,
) . toEqual ( inputData ) ;
// Now, re-fetch our data.
// An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node.
2023-05-02 01:37:19 -07:00
const taskDataConnectionsInput : ITaskDataConnections = {
2022-09-11 07:42:09 -07:00
main : [ ] ,
} ;
// We add an input set, with one item at index 0, to this input. It contains an empty json payload and our binary data.
taskDataConnectionsInput . main . push ( [
{
json : { } ,
binary : {
data : setBinaryDataBufferResponse ,
} ,
} ,
] ) ;
// Now, lets fetch our data! The item will be item index 0.
2023-05-02 01:37:19 -07:00
const getBinaryDataBufferResponse : Buffer = await getBinaryDataBuffer (
2022-09-11 07:42:09 -07:00
taskDataConnectionsInput ,
0 ,
'data' ,
0 ,
) ;
expect ( getBinaryDataBufferResponse ) . toEqual ( inputData ) ;
} ) ;
} ) ;
2023-04-12 05:58:05 -07:00
2023-09-20 05:40:06 -07:00
describe ( 'parseIncomingMessage' , ( ) = > {
it ( 'parses valid content-type header' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : { 'content-type' : 'application/json' , 'content-disposition' : undefined } ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentType ) . toEqual ( 'application/json' ) ;
} ) ;
it ( 'parses valid content-type header with parameters' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : {
'content-type' : 'application/json; charset=utf-8' ,
'content-disposition' : undefined ,
} ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentType ) . toEqual ( 'application/json' ) ;
} ) ;
it ( 'parses valid content-disposition header with filename*' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : {
'content-type' : undefined ,
'content-disposition' :
'attachment; filename="screenshot%20(1).png"; filename*=UTF-8\'\'screenshot%20(1).png' ,
} ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentDisposition ) . toEqual ( {
filename : 'screenshot (1).png' ,
type : 'attachment' ,
} ) ;
} ) ;
2023-09-21 05:54:10 -07:00
it ( 'parses valid content-disposition header with filename* (quoted)' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : {
'content-type' : undefined ,
'content-disposition' : ' attachment;filename*="utf-8\' \'test-unsplash.jpg"' ,
} ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentDisposition ) . toEqual ( {
filename : 'test-unsplash.jpg' ,
type : 'attachment' ,
} ) ;
} ) ;
2023-09-20 05:40:06 -07:00
it ( 'parses valid content-disposition header with filename and trailing ";"' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : {
'content-type' : undefined ,
'content-disposition' : 'inline; filename="screenshot%20(1).png";' ,
} ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentDisposition ) . toEqual ( {
filename : 'screenshot (1).png' ,
type : 'inline' ,
} ) ;
} ) ;
it ( 'parses non standard content-disposition with missing type' , ( ) = > {
const message = mock < IncomingMessage > ( {
headers : {
'content-type' : undefined ,
'content-disposition' : 'filename="screenshot%20(1).png";' ,
} ,
} ) ;
parseIncomingMessage ( message ) ;
expect ( message . contentDisposition ) . toEqual ( {
filename : 'screenshot (1).png' ,
type : 'attachment' ,
} ) ;
} ) ;
} ) ;
2023-04-12 05:58:05 -07:00
describe ( 'proxyRequestToAxios' , ( ) = > {
const baseUrl = 'http://example.de' ;
const workflow = mock < Workflow > ( ) ;
const hooks = mock < WorkflowHooks > ( ) ;
const additionalData = mock < IWorkflowExecuteAdditionalData > ( { hooks } ) ;
const node = mock < INode > ( ) ;
beforeEach ( ( ) = > {
initLogger ( ) ;
hooks . executeHookFunctions . mockClear ( ) ;
} ) ;
test ( 'should not throw if the response status is 200' , async ( ) = > {
nock ( baseUrl ) . get ( '/test' ) . reply ( 200 ) ;
await proxyRequestToAxios ( workflow , additionalData , node , ` ${ baseUrl } /test ` ) ;
expect ( hooks . executeHookFunctions ) . toHaveBeenCalledWith ( 'nodeFetchedData' , [
workflow . id ,
node ,
] ) ;
} ) ;
test ( 'should throw if the response status is 403' , async ( ) = > {
const headers = { 'content-type' : 'text/plain' } ;
nock ( baseUrl ) . get ( '/test' ) . reply ( 403 , 'Forbidden' , headers ) ;
try {
await proxyRequestToAxios ( workflow , additionalData , node , ` ${ baseUrl } /test ` ) ;
} catch ( error ) {
expect ( error . statusCode ) . toEqual ( 403 ) ;
expect ( error . request ) . toBeUndefined ( ) ;
expect ( error . response ) . toMatchObject ( { headers , status : 403 } ) ;
expect ( error . options ) . toMatchObject ( {
headers : { Accept : '*/*' } ,
method : 'get' ,
url : 'http://example.de/test' ,
} ) ;
expect ( error . config ) . toBeUndefined ( ) ;
expect ( error . message ) . toEqual ( '403 - "Forbidden"' ) ;
}
expect ( hooks . executeHookFunctions ) . not . toHaveBeenCalled ( ) ;
} ) ;
test ( 'should not throw if the response status is 404, but `simple` option is set to `false`' , async ( ) = > {
nock ( baseUrl ) . get ( '/test' ) . reply ( 404 , 'Not Found' ) ;
const response = await proxyRequestToAxios ( workflow , additionalData , node , {
url : ` ${ baseUrl } /test ` ,
simple : false ,
} ) ;
expect ( response ) . toEqual ( 'Not Found' ) ;
expect ( hooks . executeHookFunctions ) . toHaveBeenCalledWith ( 'nodeFetchedData' , [
workflow . id ,
node ,
] ) ;
} ) ;
test ( 'should return full response when `resolveWithFullResponse` is set to true' , async ( ) = > {
nock ( baseUrl ) . get ( '/test' ) . reply ( 404 , 'Not Found' ) ;
const response = await proxyRequestToAxios ( workflow , additionalData , node , {
url : ` ${ baseUrl } /test ` ,
resolveWithFullResponse : true ,
simple : false ,
} ) ;
expect ( response ) . toMatchObject ( {
body : 'Not Found' ,
headers : { } ,
statusCode : 404 ,
statusMessage : null ,
} ) ;
expect ( hooks . executeHookFunctions ) . toHaveBeenCalledWith ( 'nodeFetchedData' , [
workflow . id ,
node ,
] ) ;
} ) ;
} ) ;
2023-10-06 07:25:58 -07:00
describe ( 'copyInputItems' , ( ) = > {
it ( 'should pick only selected properties' , ( ) = > {
const output = copyInputItems (
[
{
json : {
a : 1 ,
b : true ,
c : { } ,
} ,
} ,
] ,
[ 'a' ] ,
) ;
expect ( output ) . toEqual ( [ { a : 1 } ] ) ;
} ) ;
it ( 'should convert undefined to null' , ( ) = > {
const output = copyInputItems (
[
{
json : {
a : undefined ,
} ,
} ,
] ,
[ 'a' ] ,
) ;
expect ( output ) . toEqual ( [ { a : null } ] ) ;
} ) ;
it ( 'should clone objects' , ( ) = > {
const input = {
a : { b : 5 } ,
} ;
const output = copyInputItems (
[
{
json : input ,
} ,
] ,
[ 'a' ] ,
) ;
expect ( output [ 0 ] . a ) . toEqual ( input . a ) ;
expect ( output [ 0 ] . a === input . a ) . toEqual ( false ) ;
} ) ;
} ) ;
2022-09-11 07:42:09 -07:00
} ) ;