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
630 lines
17 KiB
TypeScript
630 lines
17 KiB
TypeScript
import { mockDeep } from 'jest-mock-extended';
|
|
import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
|
|
import { NodeApiError } from 'n8n-workflow';
|
|
|
|
import { AwsLambda } from '../AwsLambda.node';
|
|
import * as GenericFunctions from '../GenericFunctions';
|
|
|
|
describe('AwsLambda', () => {
|
|
let node: AwsLambda;
|
|
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
|
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
|
const awsApiRequestRESTSpy = jest.spyOn(GenericFunctions, 'awsApiRequestREST');
|
|
|
|
beforeEach(() => {
|
|
node = new AwsLambda();
|
|
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
|
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
|
jest.clearAllMocks();
|
|
|
|
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
|
mockExecuteFunctions.getNode.mockReturnValue({
|
|
id: 'test-node',
|
|
name: 'AWS Lambda',
|
|
type: 'n8n-nodes-base.awsLambda',
|
|
typeVersion: 1,
|
|
position: [0, 0],
|
|
parameters: {},
|
|
});
|
|
(mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
|
|
(data: any, meta: any) => {
|
|
return [
|
|
{
|
|
...data[0],
|
|
pairedItem: meta?.itemData?.item ?? 0,
|
|
},
|
|
];
|
|
},
|
|
);
|
|
(mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: any) => [
|
|
{ json: data },
|
|
]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
describe('Load Options Methods', () => {
|
|
describe('getFunctions', () => {
|
|
it('should load functions without pagination', async () => {
|
|
const mockFunctions = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'test-function-1',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-1',
|
|
},
|
|
{
|
|
FunctionName: 'test-function-2',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-2',
|
|
},
|
|
],
|
|
};
|
|
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockFunctions);
|
|
|
|
const result = await node.methods.loadOptions.getFunctions.call(mockLoadOptionsFunctions);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
name: 'test-function-1',
|
|
value: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-1',
|
|
},
|
|
{
|
|
name: 'test-function-2',
|
|
value: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-2',
|
|
},
|
|
]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'GET',
|
|
'/2015-03-31/functions/',
|
|
);
|
|
});
|
|
|
|
it('should handle paginated function list', async () => {
|
|
const mockFirstPage = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'test-function-1',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-1',
|
|
},
|
|
],
|
|
NextMarker: 'marker123',
|
|
};
|
|
|
|
const mockSecondPage = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'test-function-2',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-2',
|
|
},
|
|
],
|
|
};
|
|
|
|
awsApiRequestRESTSpy
|
|
.mockResolvedValueOnce(mockFirstPage)
|
|
.mockResolvedValueOnce(mockSecondPage);
|
|
|
|
const result = await node.methods.loadOptions.getFunctions.call(mockLoadOptionsFunctions);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
name: 'test-function-1',
|
|
value: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-1',
|
|
},
|
|
{
|
|
name: 'test-function-2',
|
|
value: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-2',
|
|
},
|
|
]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledTimes(2);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(
|
|
2,
|
|
'lambda',
|
|
'GET',
|
|
'/2015-03-31/functions/?MaxItems=50&Marker=marker123',
|
|
);
|
|
});
|
|
|
|
it('should handle multiple pages with NextMarker', async () => {
|
|
const mockFirstPage = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'function-1',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:function-1',
|
|
},
|
|
],
|
|
NextMarker: 'marker1',
|
|
};
|
|
|
|
const mockSecondPage = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'function-2',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:function-2',
|
|
},
|
|
],
|
|
NextMarker: 'marker2',
|
|
};
|
|
|
|
const mockThirdPage = {
|
|
Functions: [
|
|
{
|
|
FunctionName: 'function-3',
|
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:function-3',
|
|
},
|
|
],
|
|
};
|
|
|
|
awsApiRequestRESTSpy
|
|
.mockResolvedValueOnce(mockFirstPage)
|
|
.mockResolvedValueOnce(mockSecondPage)
|
|
.mockResolvedValueOnce(mockThirdPage);
|
|
|
|
const result = await node.methods.loadOptions.getFunctions.call(mockLoadOptionsFunctions);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledTimes(3);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenNthCalledWith(
|
|
3,
|
|
'lambda',
|
|
'GET',
|
|
'/2015-03-31/functions/?MaxItems=50&Marker=marker2',
|
|
);
|
|
});
|
|
|
|
it('should handle empty function list', async () => {
|
|
const mockEmptyResponse = {
|
|
Functions: [],
|
|
};
|
|
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockEmptyResponse);
|
|
|
|
const result = await node.methods.loadOptions.getFunctions.call(mockLoadOptionsFunctions);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'GET',
|
|
'/2015-03-31/functions/',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Execute Method', () => {
|
|
beforeEach(() => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'arn:aws:lambda:us-east-1:123456789012:function:test-function',
|
|
qualifier: '$LATEST',
|
|
invocationType: 'RequestResponse',
|
|
payload: JSON.stringify({ test: 'data' }),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
});
|
|
|
|
describe('Successful Lambda Invocation', () => {
|
|
it('should invoke lambda function with RequestResponse type', async () => {
|
|
const mockResponse = {
|
|
StatusCode: 200,
|
|
result: { success: true, data: 'processed' },
|
|
};
|
|
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { result: mockResponse },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:test-function/invocations?Qualifier=$LATEST',
|
|
JSON.stringify({ test: 'data' }),
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should invoke lambda function with Event type', async () => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: 'PROD',
|
|
invocationType: 'Event',
|
|
payload: JSON.stringify({ async: true }),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = {
|
|
StatusCode: 202,
|
|
};
|
|
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { result: mockResponse },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=PROD',
|
|
JSON.stringify({ async: true }),
|
|
{
|
|
'X-Amz-Invocation-Type': 'Event',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should handle empty payload', async () => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: '$LATEST',
|
|
invocationType: 'RequestResponse',
|
|
payload: '',
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = { result: 'success' };
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { result: mockResponse },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=$LATEST',
|
|
'',
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should process multiple input items', async () => {
|
|
mockExecuteFunctions.getInputData.mockReturnValue([
|
|
{ json: { item: 1 } },
|
|
{ json: { item: 2 } },
|
|
]);
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, itemIndex) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: '$LATEST',
|
|
invocationType: 'RequestResponse',
|
|
payload: JSON.stringify({ item: itemIndex + 1 }),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse1 = { result: 'item1-processed' };
|
|
const mockResponse2 = { result: 'item2-processed' };
|
|
awsApiRequestRESTSpy
|
|
.mockResolvedValueOnce(mockResponse1)
|
|
.mockResolvedValueOnce(mockResponse2);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result[0]).toHaveLength(1);
|
|
expect(result[0][0].json).toHaveLength(2);
|
|
expect(result[0][0].json[0]).toEqual({ json: { result: mockResponse1 }, pairedItem: 0 });
|
|
expect(result[0][0].json[1]).toEqual({ json: { result: mockResponse2 }, pairedItem: 1 });
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('Lambda Function Errors', () => {
|
|
it('should handle lambda function execution errors', async () => {
|
|
const mockErrorResponse = {
|
|
errorMessage: 'Function execution failed',
|
|
errorType: 'Runtime.HandlerNotFound',
|
|
stackTrace: ['at /var/task/index.js:10:5'],
|
|
};
|
|
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockErrorResponse);
|
|
|
|
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(NodeApiError);
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:test-function/invocations?Qualifier=$LATEST',
|
|
JSON.stringify({ test: 'data' }),
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should handle API errors when continueOnFail is true', async () => {
|
|
const apiError = new Error('Function not found');
|
|
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
|
awsApiRequestRESTSpy.mockRejectedValue(apiError);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { error: 'Function not found' },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should throw API errors when continueOnFail is false', async () => {
|
|
const apiError = new Error('Invalid function name');
|
|
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
|
awsApiRequestRESTSpy.mockRejectedValue(apiError);
|
|
|
|
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
|
'Invalid function name',
|
|
);
|
|
});
|
|
|
|
it('should handle mixed success and error scenarios with continueOnFail', async () => {
|
|
mockExecuteFunctions.getInputData.mockReturnValue([
|
|
{ json: { item: 1 } },
|
|
{ json: { item: 2 } },
|
|
{ json: { item: 3 } },
|
|
]);
|
|
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
|
|
|
const mockSuccessResponse = { result: 'success' };
|
|
const apiError = new Error('Function error');
|
|
|
|
awsApiRequestRESTSpy
|
|
.mockResolvedValueOnce(mockSuccessResponse)
|
|
.mockRejectedValueOnce(apiError)
|
|
.mockResolvedValueOnce(mockSuccessResponse);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result[0]).toHaveLength(1);
|
|
expect(result[0][0].json).toHaveLength(3);
|
|
expect(result[0][0].json[0]).toEqual({
|
|
json: { result: mockSuccessResponse },
|
|
pairedItem: 0,
|
|
});
|
|
expect(result[0][0].json[1]).toEqual({ json: { error: 'Function error' }, pairedItem: 1 });
|
|
expect(result[0][0].json[2]).toEqual({
|
|
json: { result: mockSuccessResponse },
|
|
pairedItem: 2,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Different Qualifier Values', () => {
|
|
it('should handle custom version qualifier', async () => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: '1',
|
|
invocationType: 'RequestResponse',
|
|
payload: JSON.stringify({ version: 1 }),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = { result: 'version-1-response' };
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=1',
|
|
JSON.stringify({ version: 1 }),
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should handle alias qualifier', async () => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: 'PROD',
|
|
invocationType: 'RequestResponse',
|
|
payload: JSON.stringify({ environment: 'production' }),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = { result: 'prod-response' };
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=PROD',
|
|
JSON.stringify({ environment: 'production' }),
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Payload Handling', () => {
|
|
it('should handle complex JSON payload', async () => {
|
|
const complexPayload = {
|
|
user: { id: 123, name: 'John Doe' },
|
|
data: [1, 2, 3],
|
|
metadata: { timestamp: '2023-01-01T00:00:00Z' },
|
|
};
|
|
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: '$LATEST',
|
|
invocationType: 'RequestResponse',
|
|
payload: JSON.stringify(complexPayload),
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = { result: 'complex-processed' };
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=$LATEST',
|
|
JSON.stringify(complexPayload),
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should handle string payload', async () => {
|
|
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
|
|
const mockParams = {
|
|
operation: 'invoke',
|
|
function: 'test-function',
|
|
qualifier: '$LATEST',
|
|
invocationType: 'RequestResponse',
|
|
payload: 'plain string payload',
|
|
};
|
|
return mockParams[paramName as keyof typeof mockParams];
|
|
});
|
|
|
|
const mockResponse = { result: 'string-processed' };
|
|
awsApiRequestRESTSpy.mockResolvedValue(mockResponse);
|
|
|
|
await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(awsApiRequestRESTSpy).toHaveBeenCalledWith(
|
|
'lambda',
|
|
'POST',
|
|
'/2015-03-31/functions/test-function/invocations?Qualifier=$LATEST',
|
|
'plain string payload',
|
|
{
|
|
'X-Amz-Invocation-Type': 'RequestResponse',
|
|
'Content-Type': 'application/x-amz-json-1.0',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle null response from Lambda', async () => {
|
|
awsApiRequestRESTSpy.mockResolvedValue(null);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { result: null },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should handle undefined response from Lambda', async () => {
|
|
awsApiRequestRESTSpy.mockResolvedValue(undefined);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [
|
|
{
|
|
json: { result: undefined },
|
|
pairedItem: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should handle empty input data', async () => {
|
|
mockExecuteFunctions.getInputData.mockReturnValue([]);
|
|
|
|
const result = await node.execute.call(mockExecuteFunctions);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{
|
|
json: [],
|
|
},
|
|
],
|
|
]);
|
|
expect(awsApiRequestRESTSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
});
|