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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -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 });
});
});