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,101 @@
|
||||
{
|
||||
"name": "Test S3 upload",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8f35d24b-1493-43a4-846f-bacb577bfcb2",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [540, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "jsonToBinary",
|
||||
"options": {}
|
||||
},
|
||||
"id": "eae2946a-1a1e-47e9-9fd6-e32119b13ec0",
|
||||
"name": "Move Binary Data",
|
||||
"type": "n8n-nodes-base.moveBinaryData",
|
||||
"typeVersion": 1,
|
||||
"position": [900, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "upload",
|
||||
"bucketName": "bucket",
|
||||
"fileName": "binary.json",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "6f21fa3f-ede1-44b1-8182-a2c07152f666",
|
||||
"name": "AWS S3",
|
||||
"type": "n8n-nodes-base.awsS3",
|
||||
"typeVersion": 1,
|
||||
"position": [1080, 340],
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "1",
|
||||
"name": "AWS account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": "e12f1876-cfd1-47a4-a21b-d478452683bc",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [720, 340]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Move Binary Data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "AWS S3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Move Binary Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"AWS S3": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../__tests__/credentials';
|
||||
|
||||
describe('Test S3 V1 Node', () => {
|
||||
describe('File Upload', () => {
|
||||
let mock: nock.Scope;
|
||||
const now = 1683028800000;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
||||
|
||||
mock = nock('https://bucket.s3.eu-central-1.amazonaws.com');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
mock.get('/?location').reply(
|
||||
200,
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<LocationConstraint>
|
||||
<LocationConstraint>eu-central-1</LocationConstraint>
|
||||
</LocationConstraint>`,
|
||||
{
|
||||
'content-type': 'application/xml',
|
||||
},
|
||||
);
|
||||
|
||||
mock
|
||||
.put('/binary.json')
|
||||
.matchHeader(
|
||||
'X-Amz-Content-Sha256',
|
||||
'e43abcf3375244839c012f9633f95862d232a95b00d5bc7348b3098b9fed7f32',
|
||||
)
|
||||
.once()
|
||||
.reply(200, { success: true });
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,339 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
awsApiRequest,
|
||||
awsApiRequestREST,
|
||||
awsApiRequestSOAP,
|
||||
awsApiRequestSOAPAllItems,
|
||||
} from '../../V1/GenericFunctions';
|
||||
|
||||
describe('AWS S3 V1 GenericFunctions', () => {
|
||||
describe('awsApiRequest', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should make AWS API request with basic parameters', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await awsApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
'us-east-1',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
qs: expect.objectContaining({
|
||||
service: 's3',
|
||||
path: '/bucket',
|
||||
}),
|
||||
method: 'GET',
|
||||
body: '',
|
||||
url: '',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle query parameters correctly', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
const queryParams = { 'list-type': '2', 'max-keys': '10' };
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await awsApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
'',
|
||||
queryParams,
|
||||
{},
|
||||
{},
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
qs: expect.objectContaining({
|
||||
...queryParams,
|
||||
service: 's3',
|
||||
path: '/bucket',
|
||||
query: queryParams,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestREST', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should parse valid JSON response', async () => {
|
||||
const jsonString = JSON.stringify({ id: '123', name: 'test' });
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(jsonString);
|
||||
|
||||
const result = await awsApiRequestREST.call(mockExecuteFunctions, 's3', 'GET', '/bucket');
|
||||
|
||||
expect(result).toEqual({ id: '123', name: 'test' });
|
||||
});
|
||||
|
||||
it('should return raw response when JSON parsing fails', async () => {
|
||||
const rawResponse = 'not valid json';
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(rawResponse);
|
||||
|
||||
const result = await awsApiRequestREST.call(mockExecuteFunctions, 's3', 'GET', '/bucket');
|
||||
|
||||
expect(result).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestSOAP', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should parse valid XML response', async () => {
|
||||
const xmlResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Name>test-bucket</Name></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(xmlResponse);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(mockExecuteFunctions, 's3', 'GET', '/bucket');
|
||||
|
||||
expect(result).toHaveProperty('ListBucketResult.Name', 'test-bucket');
|
||||
});
|
||||
|
||||
it('should return error when XML parsing fails', async () => {
|
||||
const invalidXml = 'not valid xml';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(invalidXml);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(mockExecuteFunctions, 's3', 'GET', '/bucket');
|
||||
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestSOAPAllItems', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should collect all items from single page response', async () => {
|
||||
const xmlResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file1.txt</Key><Size>1024</Size></Contents><Contents><Key>file2.txt</Key><Size>2048</Size></Contents><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(xmlResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([
|
||||
{ Key: 'file1.txt', Size: '1024' },
|
||||
{ Key: 'file2.txt', Size: '2048' },
|
||||
]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle empty response', async () => {
|
||||
const xmlResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(xmlResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle pagination with NextContinuationToken', async () => {
|
||||
const firstPageResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file1.txt</Key><Size>1024</Size></Contents><IsTruncated>true</IsTruncated><NextContinuationToken>token123</NextContinuationToken></ListBucketResult>';
|
||||
|
||||
const secondPageResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file2.txt</Key><Size>2048</Size></Contents><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(firstPageResponse)
|
||||
.mockResolvedValueOnce(secondPageResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
'',
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([
|
||||
{ Key: 'file1.txt', Size: '1024' },
|
||||
{ Key: 'file2.txt', Size: '2048' },
|
||||
]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Verify that continuation token was passed in the second call
|
||||
expect(mockRequestWithAuth).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
qs: expect.objectContaining({
|
||||
'continuation-token': 'token123',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect limit parameter and stop early', async () => {
|
||||
const firstPageResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file1.txt</Key><Size>1024</Size></Contents><Contents><Key>file2.txt</Key><Size>2048</Size></Contents><IsTruncated>true</IsTruncated><NextContinuationToken>token123</NextContinuationToken></ListBucketResult>';
|
||||
|
||||
const secondPageResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file3.txt</Key><Size>3072</Size></Contents><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth
|
||||
.mockResolvedValueOnce(firstPageResponse)
|
||||
.mockResolvedValueOnce(secondPageResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
'',
|
||||
{ limit: 2 },
|
||||
);
|
||||
|
||||
// Should stop after collecting 2 items, even though there are more pages
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([
|
||||
{ Key: 'file1.txt', Size: '1024' },
|
||||
{ Key: 'file2.txt', Size: '2048' },
|
||||
]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle single item response (not an array)', async () => {
|
||||
const xmlResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>single-file.txt</Key><Size>512</Size></Contents><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(xmlResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ Key: 'single-file.txt', Size: '512' }]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle error responses from SOAP parsing', async () => {
|
||||
const invalidXml = 'invalid xml response';
|
||||
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(invalidXml);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
// When XML parsing fails, awsApiRequestSOAP returns an Error object
|
||||
// and awsApiRequestSOAPAllItems should handle this gracefully
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should work with different function contexts', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
const xmlResponse =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Contents><Key>file1.txt</Key></Contents><IsTruncated>false</IsTruncated></ListBucketResult>';
|
||||
|
||||
const mockRequestWithAuth = mockLoadOptionsFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(xmlResponse);
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'ListBucketResult.Contents',
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ Key: 'file1.txt' }]);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "Test S3 upload",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8f35d24b-1493-43a4-846f-bacb577bfcb2",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [540, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "jsonToBinary",
|
||||
"options": {}
|
||||
},
|
||||
"id": "eae2946a-1a1e-47e9-9fd6-e32119b13ec0",
|
||||
"name": "Move Binary Data",
|
||||
"type": "n8n-nodes-base.moveBinaryData",
|
||||
"typeVersion": 1,
|
||||
"position": [900, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "upload",
|
||||
"bucketName": "buc.ket",
|
||||
"fileName": "binary.json",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "6f21fa3f-ede1-44b1-8182-a2c07152f666",
|
||||
"name": "AWS S3",
|
||||
"type": "n8n-nodes-base.awsS3",
|
||||
"typeVersion": 2,
|
||||
"position": [1080, 340],
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "1",
|
||||
"name": "AWS account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": "e12f1876-cfd1-47a4-a21b-d478452683bc",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [720, 340]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Move Binary Data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "AWS S3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Move Binary Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"AWS S3": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { AwsS3V2 } from '../../V2/AwsS3V2.node';
|
||||
import * as GenericFunctions from '../../V2/GenericFunctions';
|
||||
|
||||
const mockLocationResponse = {
|
||||
LocationConstraint: {
|
||||
_: 'eu-central-1',
|
||||
},
|
||||
};
|
||||
|
||||
const mockFileResponse = {
|
||||
body: Buffer.from('test file content'),
|
||||
headers: {
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
};
|
||||
|
||||
describe('AWS S3 V2 Node - File Download', () => {
|
||||
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
const awsApiRequestRESTSpy = jest.spyOn(GenericFunctions, 'awsApiRequestREST');
|
||||
let node: AwsS3V2;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
node = new AwsS3V2({
|
||||
displayName: 'AWS S3',
|
||||
name: 'awsS3',
|
||||
icon: 'file:s3.svg',
|
||||
group: ['output'],
|
||||
description: 'Sends data to AWS S3',
|
||||
});
|
||||
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({
|
||||
accessKeyId: 'test-key',
|
||||
secretAccessKey: 'test-secret',
|
||||
region: 'eu-central-1',
|
||||
});
|
||||
|
||||
executeFunctionsMock.getNode.mockReturnValue({
|
||||
typeVersion: 2,
|
||||
} as INode);
|
||||
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: { test: 'data' } }]);
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(false);
|
||||
|
||||
executeFunctionsMock.helpers.returnJsonArray.mockImplementation((data) =>
|
||||
Array.isArray(data) ? data.map((item) => ({ json: item })) : [{ json: data }],
|
||||
);
|
||||
|
||||
executeFunctionsMock.helpers.constructExecutionMetaData.mockImplementation(
|
||||
(data) => data as any,
|
||||
);
|
||||
|
||||
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
|
||||
data: 'mock-binary-data-id',
|
||||
mimeType: 'text/plain',
|
||||
fileName: 'test.txt',
|
||||
});
|
||||
});
|
||||
|
||||
describe('successful file download', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test-bucket';
|
||||
case 'fileKey':
|
||||
return 'path/to/test.txt';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
awsApiRequestRESTSpy
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockResolvedValueOnce(mockFileResponse);
|
||||
});
|
||||
|
||||
it('should successfully download a file and return binary data', async () => {
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(awsApiRequestRESTSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(1, 'test-bucket.s3', 'GET', '', '', {
|
||||
location: '',
|
||||
});
|
||||
|
||||
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'test-bucket.s3',
|
||||
'GET',
|
||||
'/path/to/test.txt',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
{ encoding: null, resolveWithFullResponse: true },
|
||||
'eu-central-1',
|
||||
);
|
||||
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
'test.txt',
|
||||
'text/plain',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0]).toHaveProperty('json');
|
||||
expect(result[0][0]).toHaveProperty('binary');
|
||||
});
|
||||
|
||||
it('should handle bucket names with dots correctly', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test.bucket.com';
|
||||
case 'fileKey':
|
||||
return 'path/to/test.txt';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(1, 's3', 'GET', '/test.bucket.com', '', {
|
||||
location: '',
|
||||
});
|
||||
|
||||
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
's3',
|
||||
'GET',
|
||||
'/test.bucket.com/path/to/test.txt',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
{ encoding: null, resolveWithFullResponse: true },
|
||||
'eu-central-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should extract filename correctly from different file key formats', async () => {
|
||||
const testCases = [
|
||||
{ fileKey: 'simple.txt', expectedFileName: 'simple.txt' },
|
||||
{ fileKey: 'path/to/file.pdf', expectedFileName: 'file.pdf' },
|
||||
{ fileKey: 'deep/nested/path/document.docx', expectedFileName: 'document.docx' },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
jest.clearAllMocks();
|
||||
awsApiRequestRESTSpy
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockResolvedValueOnce(mockFileResponse);
|
||||
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test-bucket';
|
||||
case 'fileKey':
|
||||
return testCase.fileKey;
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
testCase.expectedFileName,
|
||||
'text/plain',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test-bucket';
|
||||
case 'fileKey':
|
||||
return 'path/to/directory/';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when trying to download a directory', async () => {
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(NodeOperationError);
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
|
||||
'Downloading a whole directory is not yet supported, please provide a file key',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('continueOnFail logic', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test-bucket';
|
||||
case 'fileKey':
|
||||
return 'path/to/test.txt';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should continue execution and return error data when continueOnFail is true', async () => {
|
||||
const testError = new Error('AWS API Error');
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(true);
|
||||
awsApiRequestRESTSpy.mockRejectedValue(testError);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0].json).toEqual({ error: 'AWS API Error' });
|
||||
|
||||
expect(executeFunctionsMock.helpers.constructExecutionMetaData).toHaveBeenCalledWith(
|
||||
[{ json: { error: 'AWS API Error' } }],
|
||||
{ itemData: { item: 0 } },
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when continueOnFail is false', async () => {
|
||||
const testError = new Error('AWS API Error');
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(false);
|
||||
awsApiRequestRESTSpy.mockRejectedValue(testError);
|
||||
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow('AWS API Error');
|
||||
});
|
||||
|
||||
it('should handle multiple items with mixed success/failure when continueOnFail is true', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{ json: { test: 'data1' } },
|
||||
{ json: { test: 'data2' } },
|
||||
{ json: { test: 'data3' } },
|
||||
]);
|
||||
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(true);
|
||||
|
||||
awsApiRequestRESTSpy
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockResolvedValueOnce(mockFileResponse)
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockRejectedValueOnce(new Error('File not found'))
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockResolvedValueOnce(mockFileResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(3);
|
||||
|
||||
expect(result[0][0]).toHaveProperty('binary');
|
||||
expect(result[0][1].json).toEqual({ error: 'File not found' });
|
||||
expect(result[0][2]).toHaveProperty('binary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('binary data handling', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'download';
|
||||
case 'bucketName':
|
||||
return 'test-bucket';
|
||||
case 'fileKey':
|
||||
return 'path/to/test.txt';
|
||||
case 'binaryPropertyName':
|
||||
return 'customData';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
awsApiRequestRESTSpy
|
||||
.mockResolvedValueOnce(mockLocationResponse)
|
||||
.mockResolvedValueOnce(mockFileResponse);
|
||||
});
|
||||
|
||||
it('should handle custom binary property name', async () => {
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
'test.txt',
|
||||
'text/plain',
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve existing binary data when adding new binary data', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: { test: 'data' },
|
||||
binary: {
|
||||
existingFile: {
|
||||
data: 'existing-data',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'existing.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result[0][0].binary).toHaveProperty('existingFile');
|
||||
expect(result[0][0].binary).toHaveProperty('customData');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../__tests__/credentials';
|
||||
|
||||
describe('Test S3 V2 Node', () => {
|
||||
describe('File Upload', () => {
|
||||
let mock: nock.Scope;
|
||||
const now = 1683028800000;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
||||
|
||||
mock = nock('https://s3.eu-central-1.amazonaws.com/buc.ket');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
mock.get('?location').reply(
|
||||
200,
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<LocationConstraint>
|
||||
<LocationConstraint>eu-central-1</LocationConstraint>
|
||||
</LocationConstraint>`,
|
||||
{
|
||||
'content-type': 'application/xml',
|
||||
},
|
||||
);
|
||||
|
||||
mock
|
||||
.put('/binary.json')
|
||||
.matchHeader(
|
||||
'X-Amz-Content-Sha256',
|
||||
'e43abcf3375244839c012f9633f95862d232a95b00d5bc7348b3098b9fed7f32',
|
||||
)
|
||||
.once()
|
||||
.reply(200, { success: true });
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user