mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
test: Add tests for nodes (no-changelog) (#20732)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
parent
941a54e723
commit
1d1655685f
@@ -0,0 +1,295 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('AWS ELB', () => {
|
||||
const credentials = {
|
||||
aws: {
|
||||
region: 'us-east-1',
|
||||
accessKeyId: 'test-access-key',
|
||||
secretAccessKey: 'test-secret-key',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Load Balancer Operations', () => {
|
||||
describe('Create Load Balancer', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=CreateLoadBalancer&Version=2015-12-01&IpAddressType=ipv4&Name=test-lb&Scheme=internet-facing&Type=application&Subnets.member.1=subnet-12345&Subnets.member.2=subnet-67890&SecurityGroups.member.1=sg-12345&Tags.member.1.Key=Environment&Tags.member.1.Value=test',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<CreateLoadBalancerResponse>
|
||||
<CreateLoadBalancerResult>
|
||||
<LoadBalancers>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb</LoadBalancerName>
|
||||
<Scheme>internet-facing</Scheme>
|
||||
<Type>application</Type>
|
||||
<State>
|
||||
<Code>provisioning</Code>
|
||||
</State>
|
||||
</member>
|
||||
</LoadBalancers>
|
||||
</CreateLoadBalancerResult>
|
||||
</CreateLoadBalancerResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['create-load-balancer.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get Load Balancer', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=DescribeLoadBalancers&Version=2015-12-01&LoadBalancerArns.member.1=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<DescribeLoadBalancersResponse>
|
||||
<DescribeLoadBalancersResult>
|
||||
<LoadBalancers>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb</LoadBalancerName>
|
||||
<Scheme>internet-facing</Scheme>
|
||||
<Type>application</Type>
|
||||
<State>
|
||||
<Code>active</Code>
|
||||
</State>
|
||||
</member>
|
||||
</LoadBalancers>
|
||||
</DescribeLoadBalancersResult>
|
||||
</DescribeLoadBalancersResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['get-load-balancer.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get Many Load Balancers', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock.get('/?Action=DescribeLoadBalancers&Version=2015-12-01&PageSize=10').reply(
|
||||
200,
|
||||
`
|
||||
<DescribeLoadBalancersResponse>
|
||||
<DescribeLoadBalancersResult>
|
||||
<LoadBalancers>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb-1</LoadBalancerName>
|
||||
<Scheme>internet-facing</Scheme>
|
||||
<Type>application</Type>
|
||||
</member>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb-2</LoadBalancerName>
|
||||
<Scheme>internal</Scheme>
|
||||
<Type>network</Type>
|
||||
</member>
|
||||
</LoadBalancers>
|
||||
</DescribeLoadBalancersResult>
|
||||
</DescribeLoadBalancersResponse>
|
||||
`,
|
||||
);
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=DescribeLoadBalancers&Version=2015-12-01&Names.member.1=test-lb-1&Names.member.2=test-lb-2',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<DescribeLoadBalancersResponse>
|
||||
<DescribeLoadBalancersResult>
|
||||
<LoadBalancers>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb-1</LoadBalancerName>
|
||||
<Scheme>internet-facing</Scheme>
|
||||
<Type>application</Type>
|
||||
</member>
|
||||
<member>
|
||||
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567</LoadBalancerArn>
|
||||
<LoadBalancerName>test-lb-2</LoadBalancerName>
|
||||
<Scheme>internal</Scheme>
|
||||
<Type>network</Type>
|
||||
</member>
|
||||
</LoadBalancers>
|
||||
</DescribeLoadBalancersResult>
|
||||
</DescribeLoadBalancersResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'get-many-load-balancers.workflow.json',
|
||||
'get-many-load-balancers-with-names.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete Load Balancer', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=DeleteLoadBalancer&Version=2015-12-01&LoadBalancerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<DeleteLoadBalancerResponse>
|
||||
<DeleteLoadBalancerResult/>
|
||||
</DeleteLoadBalancerResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['delete-load-balancer.workflow.json'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Listener Certificate Operations', () => {
|
||||
describe('Add Listener Certificate', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=AddListenerCertificates&Version=2015-12-01&Certificates.member.1.CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<AddListenerCertificatesResponse>
|
||||
<AddListenerCertificatesResult>
|
||||
<Certificates>
|
||||
<member>
|
||||
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
|
||||
<IsDefault>false</IsDefault>
|
||||
</member>
|
||||
</Certificates>
|
||||
</AddListenerCertificatesResult>
|
||||
</AddListenerCertificatesResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['add-listener-certificate.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get Many Listener Certificates', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=DescribeListenerCertificates&Version=2015-12-01&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2&PageSize=10',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<DescribeListenerCertificatesResponse>
|
||||
<DescribeListenerCertificatesResult>
|
||||
<Certificates>
|
||||
<member>
|
||||
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
|
||||
<IsDefault>false</IsDefault>
|
||||
</member>
|
||||
<member>
|
||||
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321</CertificateArn>
|
||||
<IsDefault>true</IsDefault>
|
||||
</member>
|
||||
</Certificates>
|
||||
</DescribeListenerCertificatesResult>
|
||||
</DescribeListenerCertificatesResponse>
|
||||
`,
|
||||
);
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=DescribeListenerCertificates&Version=2015-12-01&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<DescribeListenerCertificatesResponse>
|
||||
<DescribeListenerCertificatesResult>
|
||||
<Certificates>
|
||||
<member>
|
||||
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
|
||||
<IsDefault>false</IsDefault>
|
||||
</member>
|
||||
<member>
|
||||
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321</CertificateArn>
|
||||
<IsDefault>true</IsDefault>
|
||||
</member>
|
||||
</Certificates>
|
||||
</DescribeListenerCertificatesResult>
|
||||
</DescribeListenerCertificatesResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'get-many-listener-certificates.workflow.json',
|
||||
'get-many-listener-certificates-all.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove Listener Certificate', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
|
||||
|
||||
mock
|
||||
.get(
|
||||
'/?Action=RemoveListenerCertificates&Version=2015-12-01&Certificates.member.1.CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
`
|
||||
<RemoveListenerCertificatesResponse>
|
||||
<RemoveListenerCertificatesResult/>
|
||||
</RemoveListenerCertificatesResponse>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['remove-listener-certificate.workflow.json'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
awsApiRequest,
|
||||
awsApiRequestREST,
|
||||
awsApiRequestSOAP,
|
||||
awsApiRequestSOAPAllItems,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
jest.mock('xml2js', () => ({
|
||||
parseString: jest.fn(),
|
||||
}));
|
||||
|
||||
import { parseString as parseXml } from 'xml2js';
|
||||
|
||||
describe('ELB GenericFunctions', () => {
|
||||
describe('awsApiRequest', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test ELB Node',
|
||||
type: 'n8n-nodes-base.awsElb',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should make successful 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,
|
||||
'elasticloadbalancing',
|
||||
'GET',
|
||||
'/test-path',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
qs: expect.objectContaining({
|
||||
service: 'elasticloadbalancing',
|
||||
path: '/test-path',
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const apiError = new Error('API Error');
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
awsApiRequest.call(mockExecuteFunctions, 'elasticloadbalancing', 'GET', '/test-path'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestREST', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should parse valid JSON response', async () => {
|
||||
const jsonResponse = '{"result": "success", "data": [1,2,3]}';
|
||||
const expectedResult = { result: 'success', data: [1, 2, 3] };
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(jsonResponse);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'elasticloadbalancing',
|
||||
'GET',
|
||||
'/test-path',
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should return raw response when JSON parsing fails', async () => {
|
||||
const rawResponse = 'not json data';
|
||||
const mockRequestWithAuth = mockExecuteFunctions.helpers
|
||||
.requestWithAuthentication as jest.Mock;
|
||||
mockRequestWithAuth.mockResolvedValue(rawResponse);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'elasticloadbalancing',
|
||||
'GET',
|
||||
'/test-path',
|
||||
);
|
||||
|
||||
expect(result).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestSOAP', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
const mockParseXml = parseXml as jest.MockedFunction<typeof parseXml>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should parse XML response correctly', async () => {
|
||||
const xmlResponse =
|
||||
'<ListBucketsResult><Buckets><Bucket><Name>test-bucket</Name></Bucket></Buckets></ListBucketsResult>';
|
||||
const expectedParsedData = {
|
||||
ListBucketsResult: {
|
||||
Buckets: {
|
||||
Bucket: {
|
||||
Name: 'test-bucket',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
xmlResponse,
|
||||
);
|
||||
mockParseXml.mockImplementation((_xml, _options, callback) => {
|
||||
callback(null, expectedParsedData);
|
||||
});
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockExecuteFunctions,
|
||||
'elasticloadbalancing',
|
||||
'GET',
|
||||
'/test-path',
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedParsedData);
|
||||
});
|
||||
|
||||
it('should handle XML parsing errors', async () => {
|
||||
const xmlResponse = 'invalid xml';
|
||||
const xmlError = new Error('Invalid XML');
|
||||
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
xmlResponse,
|
||||
);
|
||||
mockParseXml.mockImplementation((_xml, _options, callback) => {
|
||||
callback(xmlError, null);
|
||||
});
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockExecuteFunctions,
|
||||
'elasticloadbalancing',
|
||||
'GET',
|
||||
'/test-path',
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestSOAPAllItems', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
const mockParseXml = parseXml as jest.MockedFunction<typeof parseXml>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('pagination patterns', () => {
|
||||
it('should handle DescribeLoadBalancers pagination', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const xmlResponse = {
|
||||
DescribeLoadBalancersResponse: {
|
||||
DescribeLoadBalancersResult: {
|
||||
LoadBalancerDescriptions: [
|
||||
{ LoadBalancerName: 'elb-1' },
|
||||
{ LoadBalancerName: 'elb-2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
'<xml>response</xml>',
|
||||
);
|
||||
mockParseXml.mockImplementation((_xml, _options, callback) => {
|
||||
callback(null, xmlResponse);
|
||||
});
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancerDescriptions',
|
||||
'elasticloadbalancing',
|
||||
'POST',
|
||||
'/describe-load-balancers',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ LoadBalancerName: 'elb-1' }, { LoadBalancerName: 'elb-2' }]);
|
||||
});
|
||||
|
||||
it('should handle DescribeTargetGroups with nested properties', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const firstResponse = {
|
||||
DescribeTargetGroupsResponse: {
|
||||
DescribeTargetGroupsResult: {
|
||||
TargetGroups: [
|
||||
{ TargetGroupName: 'tg-1', Protocol: 'HTTP' },
|
||||
{ TargetGroupName: 'tg-2', Protocol: 'HTTPS' },
|
||||
],
|
||||
NextMarker: 'marker123',
|
||||
},
|
||||
},
|
||||
};
|
||||
const secondResponse = {
|
||||
DescribeTargetGroupsResponse: {
|
||||
DescribeTargetGroupsResult: {
|
||||
TargetGroups: [{ TargetGroupName: 'tg-3', Protocol: 'TCP' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock)
|
||||
.mockResolvedValueOnce('<xml>first-response</xml>')
|
||||
.mockResolvedValueOnce('<xml>second-response</xml>');
|
||||
|
||||
mockParseXml
|
||||
.mockImplementationOnce((_xml, _options, callback) => callback(null, firstResponse))
|
||||
.mockImplementationOnce((_xml, _options, callback) => callback(null, secondResponse));
|
||||
|
||||
const result = await awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'DescribeTargetGroupsResponse.DescribeTargetGroupsResult.TargetGroups',
|
||||
'elasticloadbalancing',
|
||||
'POST',
|
||||
'/describe-target-groups',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ TargetGroupName: 'tg-1', Protocol: 'HTTP' },
|
||||
{ TargetGroupName: 'tg-2', Protocol: 'HTTPS' },
|
||||
{ TargetGroupName: 'tg-3', Protocol: 'TCP' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle service error responses', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const elbError = new Error(
|
||||
'LoadBalancerNotFound: The specified load balancer does not exist',
|
||||
);
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
|
||||
elbError,
|
||||
);
|
||||
|
||||
await expect(
|
||||
awsApiRequestSOAPAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancerDescriptions',
|
||||
'elasticloadbalancing',
|
||||
'POST',
|
||||
'/describe-load-balancers',
|
||||
),
|
||||
).rejects.toThrow('LoadBalancerNotFound: The specified load balancer does not exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
# AWS ELB Test Implementation Notes
|
||||
|
||||
This document explains intentional test data adjustments made to match the current implementation behavior.
|
||||
|
||||
## Known Implementation Issues
|
||||
|
||||
### 1. Typo in Remove Listener Certificate Operation
|
||||
|
||||
**File**: `remove-listener-certificate.workflow.json`
|
||||
**Issue**: The response contains `"sucess": true` instead of `"success": true`
|
||||
**Root Cause**: Spelling error in the node implementation
|
||||
**Test Adjustment**: The pinData uses the misspelled version to match actual output
|
||||
**TODO**: Fix the typo in the AWS ELB node implementation and update test accordingly
|
||||
|
||||
### 2. Boolean Values Returned as Strings
|
||||
|
||||
**File**: `get-many-listener-certificates-all.workflow.json`
|
||||
**Issue**: Boolean values like `IsDefault` are returned as strings ("true"/"false") instead of actual booleans
|
||||
**Root Cause**: AWS API response parsing returns string values rather than converting to boolean types
|
||||
**Test Adjustment**: The pinData uses string values ("true"/"false") instead of boolean values (true/false)
|
||||
**Note**: This reflects how the AWS API response is actually parsed and returned by the node
|
||||
|
||||
## Test Data Maintenance
|
||||
|
||||
When updating these test files:
|
||||
|
||||
1. Verify the actual node implementation output before changing expected results
|
||||
2. If implementation bugs are fixed, update both the node code and corresponding test data
|
||||
3. Consider backward compatibility when making changes that affect output format
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "AWS ELB Add Listener Certificate Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "listenerCertificate",
|
||||
"operation": "add",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
|
||||
"certificateId": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Add Listener Certificate",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Add Listener Certificate": [
|
||||
{
|
||||
"json": {
|
||||
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
||||
"IsDefault": "false"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Add Listener Certificate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "AWS ELB Create Load Balancer Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "loadBalancer",
|
||||
"operation": "create",
|
||||
"ipAddressType": "ipv4",
|
||||
"name": "test-lb",
|
||||
"schema": "internet-facing",
|
||||
"type": "application",
|
||||
"subnets": ["subnet-12345", "subnet-67890"],
|
||||
"additionalFields": {
|
||||
"securityGroups": ["sg-12345"],
|
||||
"tagsUi": {
|
||||
"tagValues": [
|
||||
{
|
||||
"key": "Environment",
|
||||
"value": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Create Load Balancer",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Load Balancer": [
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"LoadBalancerName": "test-lb",
|
||||
"Scheme": "internet-facing",
|
||||
"Type": "application",
|
||||
"State": {
|
||||
"Code": "provisioning"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Load Balancer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "AWS ELB Delete Load Balancer Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "loadBalancer",
|
||||
"operation": "delete",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456"
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Delete Load Balancer",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Load Balancer": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Load Balancer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "AWS ELB Get Load Balancer Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "loadBalancer",
|
||||
"operation": "get",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456"
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Get Load Balancer",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Load Balancer": [
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"LoadBalancerName": "test-lb",
|
||||
"Scheme": "internet-facing",
|
||||
"Type": "application",
|
||||
"State": {
|
||||
"Code": "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Load Balancer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "AWS ELB Get Many Listener Certificates All Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "listenerCertificate",
|
||||
"operation": "getMany",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
|
||||
"returnAll": true
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Get Many Listener Certificates All",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Many Listener Certificates All": [
|
||||
{
|
||||
"json": {
|
||||
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
||||
"IsDefault": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321",
|
||||
"IsDefault": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Many Listener Certificates All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "AWS ELB Get Many Listener Certificates Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "listenerCertificate",
|
||||
"operation": "getMany",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
|
||||
"returnAll": false,
|
||||
"limit": 10
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Get Many Listener Certificates",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Many Listener Certificates": [
|
||||
{
|
||||
"json": {
|
||||
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
|
||||
"IsDefault": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321",
|
||||
"IsDefault": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Many Listener Certificates",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "AWS ELB Get Many Load Balancers with Names Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "loadBalancer",
|
||||
"operation": "getMany",
|
||||
"returnAll": true,
|
||||
"filters": {
|
||||
"names": "test-lb-1,test-lb-2"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Get Many Load Balancers with Names",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Many Load Balancers with Names": [
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456",
|
||||
"LoadBalancerName": "test-lb-1",
|
||||
"Scheme": "internet-facing",
|
||||
"Type": "application"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567",
|
||||
"LoadBalancerName": "test-lb-2",
|
||||
"Scheme": "internal",
|
||||
"Type": "network"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Many Load Balancers with Names",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "AWS ELB Get Many Load Balancers Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "loadBalancer",
|
||||
"operation": "getMany",
|
||||
"returnAll": false,
|
||||
"limit": 10
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Get Many Load Balancers",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Many Load Balancers": [
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456",
|
||||
"LoadBalancerName": "test-lb-1",
|
||||
"Scheme": "internet-facing",
|
||||
"Type": "application"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567",
|
||||
"LoadBalancerName": "test-lb-2",
|
||||
"Scheme": "internal",
|
||||
"Type": "network"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Many Load Balancers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "AWS ELB Remove Listener Certificate Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "listenerCertificate",
|
||||
"operation": "remove",
|
||||
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
|
||||
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
|
||||
"certificateId": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
|
||||
},
|
||||
"type": "n8n-nodes-base.awsElb",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"id": "node-id",
|
||||
"name": "Remove Listener Certificate",
|
||||
"credentials": {
|
||||
"aws": {
|
||||
"id": "credential-id",
|
||||
"name": "Test AWS Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Remove Listener Certificate": [
|
||||
{
|
||||
"json": {
|
||||
"sucess": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Remove Listener Certificate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -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,629 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,643 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, IWebhookFunctions } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { awsApiRequest, awsApiRequestREST, awsApiRequestSOAP } from '../GenericFunctions';
|
||||
|
||||
describe('AWS GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
||||
let mockWebhookFunctions: jest.Mocked<IWebhookFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockWebhookFunctions = mockDeep<IWebhookFunctions>();
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.aws',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('awsApiRequest', () => {
|
||||
describe('Successful Requests', () => {
|
||||
it('should make successful API request with basic parameters', async () => {
|
||||
const mockCredentials = {
|
||||
accessKeyId: 'test-access-key',
|
||||
secretAccessKey: 'test-secret-key',
|
||||
region: 'us-east-1',
|
||||
};
|
||||
const mockResponse = { success: true, data: 'test response' };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'GET',
|
||||
'/test-path',
|
||||
undefined,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('aws');
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith('aws', {
|
||||
qs: {
|
||||
service: 's3',
|
||||
path: '/test-path',
|
||||
},
|
||||
method: 'GET',
|
||||
body: undefined,
|
||||
url: '',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
region: 'us-east-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle lambda service with raw body', async () => {
|
||||
const mockCredentials = {
|
||||
region: 'us-west-2',
|
||||
};
|
||||
const testBody = '{"test": "data"}';
|
||||
const mockResponse = { result: 'lambda response' };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'lambda',
|
||||
'POST',
|
||||
'/2015-03-31/functions/test/invocations',
|
||||
testBody,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith('aws', {
|
||||
qs: {
|
||||
service: 'lambda',
|
||||
path: '/2015-03-31/functions/test/invocations',
|
||||
},
|
||||
method: 'POST',
|
||||
body: testBody,
|
||||
url: '',
|
||||
headers: undefined,
|
||||
region: 'us-west-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should stringify body for non-lambda services', async () => {
|
||||
const mockCredentials = { region: 'eu-central-1' };
|
||||
const testBody = { name: 'Test', value: 123 };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await awsApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'PUT',
|
||||
'/bucket/object',
|
||||
testBody as any,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify(testBody),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with ILoadOptionsFunctions context', async () => {
|
||||
const mockCredentials = { region: 'ca-central-1' };
|
||||
const mockResponse = ['option1', 'option2'];
|
||||
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue({
|
||||
id: 'load-node',
|
||||
name: 'Load Node',
|
||||
type: 'n8n-nodes-base.aws',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const result = await awsApiRequest.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'ec2',
|
||||
'GET',
|
||||
'/instances',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should work with IWebhookFunctions context', async () => {
|
||||
const mockCredentials = { region: 'sa-east-1' };
|
||||
const mockResponse = { webhook: 'processed' };
|
||||
|
||||
mockWebhookFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockWebhookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
mockWebhookFunctions.getNode.mockReturnValue({
|
||||
id: 'webhook-node',
|
||||
name: 'Webhook Node',
|
||||
type: 'n8n-nodes-base.aws',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const result = await awsApiRequest.call(
|
||||
mockWebhookFunctions,
|
||||
'apigateway',
|
||||
'GET',
|
||||
'/webhooks',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle undefined body parameter', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/list-buckets');
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
body: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined headers parameter', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/list-buckets');
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
headers: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle credential retrieval errors', async () => {
|
||||
const credentialError = new Error('Failed to get AWS credentials');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
|
||||
await expect(
|
||||
awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/test-path'),
|
||||
).rejects.toThrow('Failed to get AWS credentials');
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('aws');
|
||||
});
|
||||
|
||||
it('should wrap API errors in NodeApiError', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const apiError = new Error('AWS API Error');
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
|
||||
apiError,
|
||||
);
|
||||
|
||||
await expect(
|
||||
awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/test-path'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle authentication errors', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const authError = {
|
||||
message: 'Invalid AWS credentials',
|
||||
statusCode: 403,
|
||||
response: { body: 'Forbidden' },
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
|
||||
authError,
|
||||
);
|
||||
|
||||
await expect(
|
||||
awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/test-path'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty service name', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await awsApiRequest.call(mockExecuteFunctions, '', 'GET', '/test');
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
qs: {
|
||||
service: '',
|
||||
path: '/test',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null credentials region', async () => {
|
||||
const mockCredentials = { region: null };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await awsApiRequest.call(mockExecuteFunctions, 's3', 'GET', '/test');
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
region: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle various HTTP methods', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const mockResponse = { success: true };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
|
||||
|
||||
for (const method of methods) {
|
||||
await awsApiRequest.call(mockExecuteFunctions, 's3', method, '/test');
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'aws',
|
||||
expect.objectContaining({
|
||||
method,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestREST', () => {
|
||||
describe('JSON Response Handling', () => {
|
||||
it('should parse valid JSON response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const jsonResponse = '{"result": "success", "data": [1, 2, 3]}';
|
||||
const expectedParsed = { result: 'success', data: [1, 2, 3] };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
jsonResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'lambda',
|
||||
'POST',
|
||||
'/2015-03-31/functions/test/invocations',
|
||||
'{"test": "data"}',
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedParsed);
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return raw response when JSON parsing fails', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const invalidJsonResponse = 'Not valid JSON content';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
invalidJsonResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'GET',
|
||||
'/bucket/object.txt',
|
||||
);
|
||||
|
||||
expect(result).toBe(invalidJsonResponse);
|
||||
});
|
||||
|
||||
it('should handle empty string response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const emptyResponse = '';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
emptyResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'lambda',
|
||||
'POST',
|
||||
'/test',
|
||||
);
|
||||
|
||||
expect(result).toBe(emptyResponse);
|
||||
});
|
||||
|
||||
it('should handle null response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const nullResponse = null;
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
nullResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'DELETE',
|
||||
'/bucket',
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle number response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const numberResponse = 42;
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
numberResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'cloudwatch',
|
||||
'GET',
|
||||
'/metrics',
|
||||
);
|
||||
|
||||
expect(result).toBe(numberResponse);
|
||||
});
|
||||
|
||||
it('should handle boolean response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const booleanResponse = true;
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
booleanResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
's3',
|
||||
'HEAD',
|
||||
'/bucket/exists',
|
||||
);
|
||||
|
||||
expect(result).toBe(booleanResponse);
|
||||
});
|
||||
|
||||
it('should work with ILoadOptionsFunctions context', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const jsonResponse = '["option1", "option2", "option3"]';
|
||||
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
jsonResponse,
|
||||
);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue({
|
||||
id: 'load-node',
|
||||
name: 'Load Node',
|
||||
type: 'n8n-nodes-base.aws',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'ec2',
|
||||
'GET',
|
||||
'/instances',
|
||||
);
|
||||
|
||||
expect(result).toEqual(['option1', 'option2', 'option3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Propagation', () => {
|
||||
it('should propagate errors from awsApiRequest', async () => {
|
||||
const apiError = new Error('API request failed');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
awsApiRequestREST.call(mockExecuteFunctions, 's3', 'GET', '/test'),
|
||||
).rejects.toThrow('API request failed');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON gracefully', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const malformedJson = '{"incomplete": json';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
malformedJson,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestREST.call(
|
||||
mockExecuteFunctions,
|
||||
'lambda',
|
||||
'POST',
|
||||
'/test',
|
||||
);
|
||||
|
||||
expect(result).toBe(malformedJson);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsApiRequestSOAP', () => {
|
||||
describe('XML Response Handling', () => {
|
||||
it('should parse valid XML response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const xmlResponse = '<response><status>success</status><data>test</data></response>';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
xmlResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockExecuteFunctions,
|
||||
'ses',
|
||||
'POST',
|
||||
'/send-email',
|
||||
'<email>test</email>',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
response: {
|
||||
status: 'success',
|
||||
data: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return raw response when XML parsing fails', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const invalidXmlResponse = 'Not valid XML content';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
invalidXmlResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockExecuteFunctions,
|
||||
'ses',
|
||||
'POST',
|
||||
'/send-email',
|
||||
);
|
||||
|
||||
expect(result).toBe(invalidXmlResponse);
|
||||
});
|
||||
|
||||
it('should handle empty XML response', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const emptyResponse = '';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
emptyResponse,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(mockExecuteFunctions, 'ses', 'GET', '/status');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle complex nested XML with actual parsing', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const complexXml =
|
||||
'<ListQueuesResult><QueueUrl>url1</QueueUrl><QueueUrl>url2</QueueUrl></ListQueuesResult>';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
complexXml,
|
||||
);
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockExecuteFunctions,
|
||||
'sqs',
|
||||
'GET',
|
||||
'/list-queues',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ListQueuesResult: {
|
||||
QueueUrl: ['url1', 'url2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with different function contexts', async () => {
|
||||
const mockCredentials = { region: 'us-east-1' };
|
||||
const xmlResponse = '<options><item>opt1</item><item>opt2</item></options>';
|
||||
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
xmlResponse,
|
||||
);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue({
|
||||
id: 'load-node',
|
||||
name: 'Load Node',
|
||||
type: 'n8n-nodes-base.aws',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const result = await awsApiRequestSOAP.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'ses',
|
||||
'GET',
|
||||
'/options',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
options: {
|
||||
item: ['opt1', 'opt2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should propagate errors from awsApiRequest', async () => {
|
||||
const apiError = new Error('SOAP API request failed');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
awsApiRequestSOAP.call(mockExecuteFunctions, 'ses', 'POST', '/test'),
|
||||
).rejects.toThrow('SOAP API request failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,610 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import moment from 'moment-timezone';
|
||||
import type { IPollFunctions, INode, ILoadOptionsFunctions, IDataObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { GoogleDriveTrigger } from '../GoogleDriveTrigger.node';
|
||||
import * as GenericFunctions from '../v1/GenericFunctions';
|
||||
import * as listSearch from '../v2/methods/listSearch';
|
||||
|
||||
jest.mock('../v1/GenericFunctions', () => ({
|
||||
extractId: jest.fn(),
|
||||
googleApiRequest: jest.fn(),
|
||||
googleApiRequestAllItems: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../v2/methods/listSearch', () => ({
|
||||
fileSearch: jest.fn(),
|
||||
folderSearch: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('GoogleDriveTrigger', () => {
|
||||
let trigger: GoogleDriveTrigger;
|
||||
let mockPollFunctions: jest.Mocked<IPollFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
const extractIdSpy = jest.spyOn(GenericFunctions, 'extractId');
|
||||
const googleApiRequestSpy = jest.spyOn(GenericFunctions, 'googleApiRequest');
|
||||
const googleApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'googleApiRequestAllItems');
|
||||
|
||||
beforeEach(() => {
|
||||
trigger = new GoogleDriveTrigger();
|
||||
mockPollFunctions = mockDeep<IPollFunctions>();
|
||||
mockNode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Google Drive Trigger Test',
|
||||
type: 'n8n-nodes-base.googleDriveTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockPollFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue({});
|
||||
mockPollFunctions.getMode.mockReturnValue('trigger');
|
||||
(mockPollFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: unknown[]) =>
|
||||
data.map((item: unknown, index: number) => ({ json: item, pairedItem: { item: index } })),
|
||||
);
|
||||
extractIdSpy.mockImplementation((id) => id);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Methods', () => {
|
||||
it('should have correct list search methods', () => {
|
||||
expect(trigger.methods?.listSearch?.fileSearch).toBe(listSearch.fileSearch);
|
||||
expect(trigger.methods?.listSearch?.folderSearch).toBe(listSearch.folderSearch);
|
||||
});
|
||||
|
||||
it('should have correct load options methods', () => {
|
||||
expect(trigger.methods?.loadOptions?.getDrives).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getDrives', () => {
|
||||
it('should return drives with root option', async () => {
|
||||
const mockDrives = [
|
||||
{ id: 'drive1', name: 'My Drive 1' },
|
||||
{ id: 'drive2', name: 'My Drive 2' },
|
||||
];
|
||||
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockDrives);
|
||||
|
||||
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'drives',
|
||||
'GET',
|
||||
'/drive/v3/drives',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Root', value: 'root' },
|
||||
{ name: 'My Drive 1', value: 'drive1' },
|
||||
{ name: 'My Drive 2', value: 'drive2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty drives list', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual([{ name: 'Root', value: 'root' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Parameter Setup', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle specific file trigger', async () => {
|
||||
const now = moment().utc();
|
||||
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
name: 'Test File',
|
||||
modifiedTime: now.format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-file-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
q: expect.stringContaining('trashed = false'),
|
||||
fields: 'nextPageToken, files(*)',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should handle specific folder trigger for file created', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const now = moment().utc();
|
||||
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'new-file-id',
|
||||
name: 'New File',
|
||||
createdTime: now.format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("'test-folder-id' in parents"),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by file type when specified', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: { fileType: 'application/vnd.google-apps.document' },
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.document'"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle folder events with folder mime type filter', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'folderCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.folder'"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle watch folder updated event', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'watchFolderUpdated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-folder-id',
|
||||
name: 'Test Folder',
|
||||
modifiedTime: moment().utc().format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.not.stringContaining('in parents'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use createdTime for Created events and modifiedTime for Updated events', async () => {
|
||||
const now = moment().utc();
|
||||
const webhookData: IDataObject = {
|
||||
lastTimeChecked: now.clone().subtract(1, 'hour').format(),
|
||||
};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
// Test fileCreated event uses createdTime
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, unknown> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining('createdTime >'),
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset mock
|
||||
googleApiRequestAllItemsSpy.mockClear();
|
||||
|
||||
// Test fileUpdated event uses modifiedTime
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, unknown> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileUpdated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining('modifiedTime >'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Manual Mode', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch single file in manual mode', async () => {
|
||||
const mockResponse = {
|
||||
files: [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
name: 'Test File',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
googleApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should throw NodeApiError when no data found in manual mode', async () => {
|
||||
googleApiRequestSpy.mockResolvedValue({ files: [] });
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(NodeApiError);
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(
|
||||
'No data with the current filter could be found',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - State Management', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should update lastTimeChecked in webhook data', async () => {
|
||||
const mockWebhookData = { lastTimeChecked: moment().subtract(1, 'day').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWebhookData.lastTimeChecked).toBeDefined();
|
||||
expect(moment(mockWebhookData.lastTimeChecked).isValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('should use current time as startDate when no lastTimeChecked exists', async () => {
|
||||
const mockWebhookData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWebhookData.lastTimeChecked).toBeDefined();
|
||||
expect(moment(mockWebhookData.lastTimeChecked as string).isValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API request errors', async () => {
|
||||
const apiError = new Error('API Error');
|
||||
googleApiRequestAllItemsSpy.mockRejectedValue(apiError);
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
|
||||
});
|
||||
|
||||
it('should handle invalid extractId results', async () => {
|
||||
extractIdSpy.mockImplementation(() => {
|
||||
throw new Error('Invalid ID');
|
||||
});
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('Invalid ID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Edge Cases', () => {
|
||||
it('should return null when no files found in trigger mode', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty files array', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle files without required fields gracefully', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
// Missing name and other fields
|
||||
},
|
||||
];
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should skip time filtering in manual mode', async () => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockResponse = { files: [{ id: 'test-file', name: 'Test' }] };
|
||||
googleApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.not.stringMatching(/createdTime|modifiedTime/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - File Filtering', () => {
|
||||
it('should filter specific file results correctly', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'target-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{ id: 'target-file-id', name: 'Target File' },
|
||||
{ id: 'other-file-id', name: 'Other File' },
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('target-file-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('target-file-id');
|
||||
});
|
||||
|
||||
it('should filter specific folder results correctly for watchFolderUpdated', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'watchFolderUpdated',
|
||||
folderToWatch: 'target-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{ id: 'target-folder-id', name: 'Target Folder' },
|
||||
{ id: 'other-folder-id', name: 'Other Folder' },
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('target-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('target-folder-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../../GenericFunctions';
|
||||
import {
|
||||
googleApiRequest,
|
||||
jsonToDocument,
|
||||
documentToJson,
|
||||
fullDocumentToJson,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
// Mock the getGoogleAccessToken function
|
||||
jest.mock('../../../GenericFunctions', () => ({
|
||||
getGoogleAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetGoogleAccessToken = getGoogleAccessToken as jest.MockedFunction<
|
||||
typeof getGoogleAccessToken
|
||||
>;
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore > GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Test CloudFirestore Node',
|
||||
type: 'n8n-nodes-base.googleFirebaseCloudFirestore',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('googleApiRequest', () => {
|
||||
const mockRequestWithAuth = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = mockRequestWithAuth;
|
||||
mockLoadOptionsFunctions.helpers.requestWithAuthentication = mockRequestWithAuth;
|
||||
});
|
||||
|
||||
describe('OAuth2 authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
});
|
||||
|
||||
it('should make successful API request with OAuth2', async () => {
|
||||
const mockResponse = { success: true, data: 'test' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/databases/test/documents/users',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
uri: 'https://firestore.googleapis.com/v1/projects/databases/test/documents/users',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
qs: {},
|
||||
qsStringifyOptions: {
|
||||
arrayFormat: 'repeat',
|
||||
},
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty body for GET requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {});
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.not.objectContaining({ body: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include body for POST requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const requestBody = { name: 'Test', value: 'data' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'POST', '/test', requestBody);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include query parameters', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const queryParams = { pageSize: 100, orderBy: 'name' };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {}, queryParams);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
qs: queryParams,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom URI when provided', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const customUri = 'https://custom.googleapis.com/v1/test';
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test', {}, {}, customUri);
|
||||
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleFirebaseCloudFirestoreOAuth2Api',
|
||||
expect.objectContaining({
|
||||
uri: customUri,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Account authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('serviceAccount');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('serviceAccount');
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
});
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
});
|
||||
});
|
||||
|
||||
it('should make API request with service account', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const mockAccessToken = 'mock-access-token';
|
||||
mockGetGoogleAccessToken.mockResolvedValue({ access_token: mockAccessToken });
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockGetGoogleAccessToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test-key',
|
||||
}),
|
||||
'firestore',
|
||||
);
|
||||
expect(mockRequestWithAuth).toHaveBeenCalledWith(
|
||||
'googleApi',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle service account credential errors', async () => {
|
||||
const credentialError = new Error('Invalid service account');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle access token errors', async () => {
|
||||
const tokenError = new Error('Failed to get access token');
|
||||
mockGetGoogleAccessToken.mockRejectedValue(tokenError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('oAuth2Api');
|
||||
});
|
||||
|
||||
it('should throw NodeAPIError on API failure', async () => {
|
||||
const apiError = new Error('API request failed');
|
||||
mockRequestWithAuth.mockRejectedValue(apiError);
|
||||
|
||||
await expect(googleApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeApiError,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work with ILoadOptionsFunctions', async () => {
|
||||
const mockResponse = { options: ['option1', 'option2'] };
|
||||
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(mockLoadOptionsFunctions, 'GET', '/options');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonToDocument', () => {
|
||||
it('should convert boolean values', () => {
|
||||
expect(jsonToDocument(true as any)).toEqual({ booleanValue: true });
|
||||
expect(jsonToDocument(false as any)).toEqual({ booleanValue: false });
|
||||
// Note: The current implementation preserves string 'true'/'false' as strings in booleanValue
|
||||
// While Firestore API spec expects actual boolean values for booleanValue fields,
|
||||
// this behavior is maintained for backward compatibility and to avoid breaking existing workflows
|
||||
// that may depend on this string preservation behavior in certain edge cases
|
||||
expect(jsonToDocument('true')).toEqual({ booleanValue: 'true' });
|
||||
expect(jsonToDocument('false')).toEqual({ booleanValue: 'false' });
|
||||
});
|
||||
|
||||
it('should convert null values', () => {
|
||||
expect(jsonToDocument(null as any)).toEqual({ nullValue: null });
|
||||
});
|
||||
|
||||
it('should convert numeric values', () => {
|
||||
expect(jsonToDocument(42)).toEqual({ integerValue: 42 });
|
||||
expect(jsonToDocument(3.14)).toEqual({ doubleValue: 3.14 });
|
||||
// Note: The current implementation preserves numeric strings as strings in Firestore document values
|
||||
// While Firestore API spec typically expects numeric types for doubleValue/integerValue,
|
||||
// this behavior is maintained for backward compatibility and to avoid breaking existing workflows
|
||||
// that may depend on string preservation in certain edge cases
|
||||
expect(jsonToDocument('123')).toEqual({ integerValue: '123' });
|
||||
expect(jsonToDocument('123.45')).toEqual({ doubleValue: '123.45' });
|
||||
});
|
||||
|
||||
it('should convert string values', () => {
|
||||
expect(jsonToDocument('hello world')).toEqual({ stringValue: 'hello world' });
|
||||
expect(jsonToDocument('')).toEqual({ stringValue: '' });
|
||||
});
|
||||
|
||||
it('should convert date strings to timestamps', () => {
|
||||
const dateString = '2023-01-01T10:00:00.000Z';
|
||||
const result = jsonToDocument(dateString);
|
||||
expect(result).toHaveProperty('timestampValue');
|
||||
expect(result.timestampValue).toBe('2023-01-01T10:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should convert arrays', () => {
|
||||
const array = [1, 'test', true, null] as any;
|
||||
const result = jsonToDocument(array);
|
||||
expect(result).toEqual({
|
||||
arrayValue: {
|
||||
values: [
|
||||
{ integerValue: 1 },
|
||||
{ stringValue: 'test' },
|
||||
{ booleanValue: true },
|
||||
{ nullValue: null },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert objects', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
active: true,
|
||||
metadata: null,
|
||||
};
|
||||
const result = jsonToDocument(obj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
age: { integerValue: 30 },
|
||||
active: { booleanValue: true },
|
||||
metadata: { nullValue: null },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert nested objects', () => {
|
||||
const nestedObj = {
|
||||
user: {
|
||||
profile: {
|
||||
name: 'John',
|
||||
settings: {
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = jsonToDocument(nestedObj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
user: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
profile: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
settings: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
notifications: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(jsonToDocument(0)).toEqual({ integerValue: 0 });
|
||||
expect(jsonToDocument(NaN as any)).toEqual({});
|
||||
expect(jsonToDocument(undefined as any)).toEqual({});
|
||||
});
|
||||
|
||||
it('should skip unsafe object properties', () => {
|
||||
const obj = { safeProperty: 'safe' } as any;
|
||||
Object.defineProperty(obj, '__proto__', {
|
||||
value: 'unsafe',
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
const result = jsonToDocument(obj);
|
||||
expect(result).toEqual({
|
||||
mapValue: {
|
||||
fields: {
|
||||
safeProperty: { stringValue: 'safe' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('documentToJson', () => {
|
||||
it('should handle undefined input', () => {
|
||||
expect(documentToJson(undefined as any)).toEqual({});
|
||||
});
|
||||
|
||||
it('should convert primitive document values', () => {
|
||||
expect(documentToJson({ stringValue: 'hello' })).toBe('hello');
|
||||
expect(documentToJson({ integerValue: 42 })).toBe(42);
|
||||
expect(documentToJson({ doubleValue: 3.14 })).toBe(3.14);
|
||||
expect(documentToJson({ booleanValue: true })).toBe(true);
|
||||
expect(documentToJson({ nullValue: null })).toBe(null);
|
||||
expect(documentToJson({ timestampValue: '2023-01-01T00:00:00.000Z' })).toBe(
|
||||
'2023-01-01T00:00:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert map values', () => {
|
||||
const mapValue = {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
age: { integerValue: 30 },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(documentToJson(mapValue)).toEqual({
|
||||
name: 'John',
|
||||
age: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert array values', () => {
|
||||
const arrayValue = {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'item1' }, { integerValue: 42 }, { booleanValue: true }],
|
||||
},
|
||||
};
|
||||
expect(documentToJson(arrayValue)).toEqual(['item1', 42, true]);
|
||||
});
|
||||
|
||||
it('should convert nested structures', () => {
|
||||
const nested = {
|
||||
user: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
tags: {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'developer' }, { stringValue: 'javascript' }],
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
created: { timestampValue: '2023-01-01T00:00:00.000Z' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = documentToJson(nested);
|
||||
expect(result).toEqual({
|
||||
user: {
|
||||
name: 'John',
|
||||
tags: ['developer', 'javascript'],
|
||||
metadata: {
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty arrays', () => {
|
||||
const emptyArray = { arrayValue: { values: [] } };
|
||||
expect(documentToJson(emptyArray)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle arrays with undefined values', () => {
|
||||
const arrayWithUndefined = { arrayValue: {} };
|
||||
expect(documentToJson(arrayWithUndefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty map values', () => {
|
||||
const emptyMap = { mapValue: { fields: {} } };
|
||||
expect(documentToJson(emptyMap)).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle map values with undefined fields', () => {
|
||||
const mapWithUndefinedFields = { mapValue: {} };
|
||||
expect(documentToJson(mapWithUndefinedFields)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fullDocumentToJson', () => {
|
||||
it('should handle undefined input', () => {
|
||||
expect(fullDocumentToJson(undefined as any)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract metadata and fields', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/users/123',
|
||||
id: 'user123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
age: { integerValue: 30 },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/users/123',
|
||||
_id: 'user123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
age: 30,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle document without fields', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/empty/123',
|
||||
id: 'empty123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/empty/123',
|
||||
_id: 'empty123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex nested document', () => {
|
||||
const document = {
|
||||
name: 'projects/test/databases/(default)/documents/complex/123',
|
||||
id: 'complex123',
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
fields: {
|
||||
profile: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
name: { stringValue: 'John' },
|
||||
preferences: {
|
||||
mapValue: {
|
||||
fields: {
|
||||
theme: { stringValue: 'dark' },
|
||||
notifications: { booleanValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
arrayValue: {
|
||||
values: [{ stringValue: 'developer' }, { stringValue: 'admin' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = fullDocumentToJson(document);
|
||||
expect(result).toEqual({
|
||||
_name: 'projects/test/databases/(default)/documents/complex/123',
|
||||
_id: 'complex123',
|
||||
_createTime: '2023-01-01T10:00:00.000Z',
|
||||
_updateTime: '2023-01-01T11:00:00.000Z',
|
||||
profile: {
|
||||
name: 'John',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
tags: ['developer', 'admin'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "CloudFirestore Collection Get All Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "getAll",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-getAll",
|
||||
"name": "Get All Documents",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get All Documents": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user1",
|
||||
"_id": "user1",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user2",
|
||||
"_id": "user2",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get All Documents",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock
|
||||
.get('/v1/projects/test-project/databases/(default)/documents/users')
|
||||
.query({ pageSize: 100 })
|
||||
.reply(200, {
|
||||
documents: [
|
||||
{
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user1',
|
||||
id: 'user1',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user2',
|
||||
id: 'user2',
|
||||
fields: {
|
||||
name: { stringValue: 'Jane Smith' },
|
||||
email: { stringValue: 'jane@example.com' },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['collection-getAll.workflow.json'],
|
||||
});
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "delete",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"documentId": "test-doc-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-delete",
|
||||
"name": "Delete Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Document": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Get Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "get",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"documentId": "test-doc-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-get",
|
||||
"name": "Get Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Document": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/test-doc-id",
|
||||
"_id": "test-doc-id",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"age": "30",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Query Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "query",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"query": "{\"structuredQuery\": {\"where\": {\"fieldFilter\": {\"field\": {\"fieldPath\": \"active\"}, \"op\": \"EQUAL\", \"value\": {\"booleanValue\": true}}}, \"from\": [{\"collectionId\": \"users\"}]}}"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-query",
|
||||
"name": "Query Documents",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Query Documents": [
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user1",
|
||||
"_id": "user1",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"_name": "projects/test-project/databases/(default)/documents/users/user2",
|
||||
"_id": "user2",
|
||||
"_createTime": "2023-01-01T10:00:00.000Z",
|
||||
"_updateTime": "2023-01-01T10:00:00.000Z",
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Query Documents",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "CloudFirestore Document Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "googleFirebaseCloudFirestoreOAuth2Api",
|
||||
"resource": "document",
|
||||
"operation": "upsert",
|
||||
"projectId": "test-project",
|
||||
"database": "(default)",
|
||||
"collection": "users",
|
||||
"updateKey": "documentId",
|
||||
"columns": "name,email,age,active"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
|
||||
"typeVersion": 1.1,
|
||||
"position": [200, 0],
|
||||
"id": "cloudFirestore-update",
|
||||
"name": "Update Document",
|
||||
"credentials": {
|
||||
"googleFirebaseCloudFirestoreOAuth2Api": {
|
||||
"id": "cloudFirestore-oauth-id",
|
||||
"name": "CloudFirestore OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update Document": [
|
||||
{
|
||||
"json": {
|
||||
"status": {
|
||||
"code": 0
|
||||
},
|
||||
"updateTime": "2023-01-01T11:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock
|
||||
.delete('/v1/projects/test-project/databases/(default)/documents/users/test-doc-id')
|
||||
.reply(200, {});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-delete.workflow.json'],
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:batchGet').reply(200, [
|
||||
{
|
||||
found: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/test-doc-id',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
age: { integerValue: '30' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-get.workflow.json'],
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:runQuery').reply(200, [
|
||||
{
|
||||
document: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user1',
|
||||
id: 'user1',
|
||||
fields: {
|
||||
name: { stringValue: 'John Doe' },
|
||||
email: { stringValue: 'john@example.com' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
document: {
|
||||
name: 'projects/test-project/databases/(default)/documents/users/user2',
|
||||
id: 'user2',
|
||||
fields: {
|
||||
name: { stringValue: 'Jane Smith' },
|
||||
email: { stringValue: 'jane@example.com' },
|
||||
active: { booleanValue: true },
|
||||
},
|
||||
createTime: '2023-01-01T10:00:00.000Z',
|
||||
updateTime: '2023-01-01T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-query.workflow.json'],
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('GoogleFirebaseCloudFirestore', () => {
|
||||
const credentials = {
|
||||
googleFirebaseCloudFirestoreOAuth2Api: {
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://firestore.googleapis.com');
|
||||
|
||||
mock.post('/v1/projects/test-project/databases/(default)/documents:batchWrite').reply(200, {
|
||||
writeResults: [
|
||||
{
|
||||
updateTime: '2023-01-01T11:00:00.000Z',
|
||||
},
|
||||
],
|
||||
status: [
|
||||
{
|
||||
code: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['document-update.workflow.json'],
|
||||
});
|
||||
});
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { googleApiRequest } from '../GenericFunctions';
|
||||
|
||||
describe('GoogleFirebaseRealtimeDatabase > GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockLoadOptionsFunctions: jest.Mocked<ILoadOptionsFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Test RealtimeDatabase Node',
|
||||
type: 'n8n-nodes-base.googleFirebaseRealtimeDatabase',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('googleApiRequest', () => {
|
||||
const mockRequestOAuth2 = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.helpers.requestOAuth2 = mockRequestOAuth2;
|
||||
mockLoadOptionsFunctions.helpers.requestOAuth2 = mockRequestOAuth2;
|
||||
});
|
||||
|
||||
describe('successful requests', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
mockLoadOptionsFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should make successful API request with default options', async () => {
|
||||
const mockResponse = { data: { test: 'value' } };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://test-project.firebaseio.com//users.json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
qs: {},
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include body for POST requests', async () => {
|
||||
const mockResponse = { success: true };
|
||||
const requestBody = { name: 'John Doe', email: 'john@example.com' };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'POST',
|
||||
'/users',
|
||||
requestBody,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty body for GET requests', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users', {});
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expect.not.objectContaining({ body: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include query parameters', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const queryParams = { orderBy: '"$key"', limitToFirst: 10 };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
queryParams,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
qs: queryParams,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include custom headers', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const customHeaders = { 'X-Custom-Header': 'custom-value' };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
{},
|
||||
customHeaders,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Custom-Header': 'custom-value',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom URI when provided', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
const customUri = 'https://custom-project.firebaseio.com/custom/path.json';
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'test-project',
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
customUri,
|
||||
);
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: customUri,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use different regions correctly', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'europe-west1-firebase.cloudfunctions.net',
|
||||
});
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users');
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: 'https://test-project.europe-west1-firebase.cloudfunctions.net//users.json',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
region: 'firebaseio.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NodeApiError on API failure', async () => {
|
||||
const apiError = new Error('API request failed');
|
||||
mockRequestOAuth2.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.getNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle credential errors', async () => {
|
||||
const credentialError = new Error('Invalid credentials');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
|
||||
await expect(
|
||||
googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should handle missing region in credentials', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({});
|
||||
const mockResponse = { data: [] };
|
||||
mockRequestOAuth2.mockResolvedValue(mockResponse);
|
||||
|
||||
await googleApiRequest.call(mockExecuteFunctions, 'test-project', 'GET', '/users');
|
||||
|
||||
expect(mockRequestOAuth2).toHaveBeenCalledWith(
|
||||
'googleFirebaseRealtimeDatabaseOAuth2Api',
|
||||
expect.objectContaining({
|
||||
url: 'https://test-project.undefined//users.json',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/appendOrUpdate.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
@@ -95,6 +96,618 @@ describe('Google Sheet - Append or Update', () => {
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when no column names can be retrieved and sheet has data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([{ column: 'name', fieldValue: 'Test Name' }]); // fieldsUi.values
|
||||
|
||||
// Mock sheet with data but no header row at keyRowIndex
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
undefined as any, // No header row at keyRowIndex 0
|
||||
['some', 'data', 'here'], // Has data but not at header position
|
||||
]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle custom header row and first data row positions', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({
|
||||
locationDefine: {
|
||||
values: {
|
||||
headerRow: '3',
|
||||
firstDataRow: '5',
|
||||
},
|
||||
},
|
||||
}) // options with custom positions
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([{ column: 'name', fieldValue: 'Test Name' }]); // fieldsUi.values
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([
|
||||
[],
|
||||
[],
|
||||
['id', 'name'], // Header at row 3 (index 2)
|
||||
[],
|
||||
['1', 'Old Name'], // Data starts at row 5 (index 4)
|
||||
]);
|
||||
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: '1', name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({
|
||||
dataStartRowIndex: 4, // firstDataRow - 1
|
||||
keyIndex: 0,
|
||||
range: 'Sheet1!A:Z',
|
||||
sheetData: expect.any(Array),
|
||||
valueRenderMode: 'UNFORMATTED_VALUE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle data mode "nothing" by skipping processing', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { id: 1, name: 'Test' }, pairedItem: { item: 0, input: undefined } },
|
||||
{ json: { id: 2, name: 'Test2' }, pairedItem: { item: 1, input: undefined } },
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('nothing') // dataMode
|
||||
.mockReturnValueOnce('id'); // columnToMatchOn
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [],
|
||||
});
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should return original items since dataMode is 'nothing'
|
||||
expect(result).toEqual([
|
||||
{ json: { id: 1, name: 'Test' }, pairedItem: { item: 0 } },
|
||||
{ json: { id: 2, name: 'Test2' }, pairedItem: { item: 1 } },
|
||||
]);
|
||||
|
||||
// Should not call update or append operations
|
||||
expect(mockGoogleSheet.batchUpdate).not.toHaveBeenCalled();
|
||||
expect(mockGoogleSheet.appendSheetData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with ignoreIt option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', extraField: 'should be ignored' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'ignoreIt' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test', extraField: 'should be ignored' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [{ id: 1, name: 'Test', extraField: 'should be ignored' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with error option and throw on unexpected fields', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', unexpectedField: 'error' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'error' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData with insertInNewColumn option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test', newField: 'new column value' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({ handlingExtraData: 'insertInNewColumn' }) // options
|
||||
.mockReturnValueOnce('autoMapInputData'); // dataMode
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test', newField: 'new column value' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should update header row with new column
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith(
|
||||
'Sheet1',
|
||||
[['id', 'name', 'newField']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when valueToMatchOn is empty in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce(''); // empty valueToMatchOn
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should throw error when no values are provided in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([]); // empty fieldsUi.values
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle newColumn type in defineBelow mode (v3)', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 3 }));
|
||||
mockExecuteFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('USER_ENTERED') // options.cellFormat
|
||||
.mockReturnValueOnce({}) // options
|
||||
.mockReturnValueOnce('defineBelow') // dataMode
|
||||
.mockReturnValueOnce('id') // columnToMatchOn
|
||||
.mockReturnValueOnce('1') // valueToMatchOn
|
||||
.mockReturnValueOnce([
|
||||
{ column: 'name', fieldValue: 'Updated Name' },
|
||||
{ column: 'newColumn', columnName: 'description', fieldValue: 'New Description' },
|
||||
]); // fieldsUi.values with new column
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: '1', name: 'Updated Name', description: 'New Description' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
// Should update header with new column
|
||||
expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith(
|
||||
'Sheet1',
|
||||
[['id', 'name', 'description']],
|
||||
'RAW',
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle both update and append data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Updated Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [{ range: 'A2:B2', values: [['1', 'Updated Name']] }],
|
||||
appendData: [{ id: 2, name: 'New Item' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.batchUpdate).toHaveBeenCalledWith(
|
||||
[{ range: 'A2:B2', values: [['1', 'Updated Name']] }],
|
||||
'USER_ENTERED',
|
||||
);
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use useAppend option when appending data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: { useAppend: true },
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
columnNamesList: [['id', 'name']],
|
||||
inputData: [{ id: 1, name: 'Test Name' }],
|
||||
keyRowIndex: 1,
|
||||
lastRow: 2,
|
||||
range: 'Sheet1!A:Z',
|
||||
useAppend: true,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
|
||||
// Should NOT call appendEmptyRowsOrColumns when useAppend is true
|
||||
expect(mockGoogleSheet.appendEmptyRowsOrColumns).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle v4 with empty columns.value and throw error', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': {},
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle v4 with null/undefined values by converting to empty string', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: null, description: undefined },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name', 'description']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: '', description: '' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inputData: [{ id: 1, name: '', description: '' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return mapped values for v4+ in defineBelow mode', async () => {
|
||||
const mappedValues = [
|
||||
{ id: 1, name: 'Test Name' },
|
||||
{ id: 2, name: 'Another Test' },
|
||||
];
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { id: 1 }, pairedItem: { item: 0, input: undefined } },
|
||||
{ json: { id: 2 }, pairedItem: { item: 1, input: undefined } },
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value[id]': itemIndex === 0 ? 1 : 2,
|
||||
};
|
||||
|
||||
// Handle per-item columns.value calls
|
||||
if (paramName === 'columns.value' && typeof itemIndex === 'number') {
|
||||
return mappedValues[itemIndex];
|
||||
}
|
||||
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
|
||||
// Mock prepareDataForUpdateOrUpsert to be called for each item
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert
|
||||
.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [mappedValues[0]],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [mappedValues[1]],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(result).toEqual([
|
||||
{ json: mappedValues[0], pairedItem: { item: 0 } },
|
||||
{ json: mappedValues[1], pairedItem: { item: 1 } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle custom valueRenderMode option', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 4.5 }));
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: { valueRenderMode: 'FORMATTED_VALUE' },
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': [],
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
valueRenderMode: 'FORMATTED_VALUE',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
valueRenderMode: 'FORMATTED_VALUE',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call checkForSchemaChanges for node version >= 4.4', async () => {
|
||||
const mockSchema = [
|
||||
{ id: 'id', displayName: 'ID' },
|
||||
{ id: 'name', displayName: 'Name' },
|
||||
];
|
||||
const mockNode = mock<INode>({ typeVersion: 4.5 });
|
||||
|
||||
mockExecuteFunctions.getInputData.mockReturnValueOnce([
|
||||
{
|
||||
json: { id: 1, name: 'Test' },
|
||||
pairedItem: { item: 0, input: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
'columns.schema': mockSchema,
|
||||
'columns.matchingColumns': ['id'],
|
||||
'columns.value': { id: 1, name: 'Test Name' },
|
||||
'columns.value[id]': 1,
|
||||
};
|
||||
return params[paramName] ?? fallback;
|
||||
});
|
||||
|
||||
mockGoogleSheet.getData.mockResolvedValueOnce([['id', 'name']]);
|
||||
mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
|
||||
updateData: [],
|
||||
appendData: [{ id: 1, name: 'Test Name' }],
|
||||
});
|
||||
|
||||
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
|
||||
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
|
||||
|
||||
// Mock the checkForSchemaChanges import to verify it's called
|
||||
const GoogleSheetsUtils = await import('../../../v2/helpers/GoogleSheets.utils');
|
||||
jest.spyOn(GoogleSheetsUtils, 'checkForSchemaChanges').mockImplementation(() => {});
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
|
||||
|
||||
expect(GoogleSheetsUtils.checkForSchemaChanges).toHaveBeenCalledWith(
|
||||
mockNode, // node
|
||||
['id', 'name'], // columnNames
|
||||
mockSchema, // schema
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Sheet - Append or Update v4.6 vs v4.7 Behavior', () => {
|
||||
@@ -121,7 +734,7 @@ describe('Google Sheet - Append or Update v4.6 vs v4.7 Behavior', () => {
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: any } = {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
@@ -191,7 +804,7 @@ describe('Google Sheet - Append or Update v4.6 vs v4.7 Behavior', () => {
|
||||
]);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: { [key: string]: any } = {
|
||||
const params: Record<string, any> = {
|
||||
'options.cellFormat': 'USER_ENTERED',
|
||||
options: {},
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
|
||||
@@ -324,21 +324,6 @@ describe('Microsoft GraphSecurity GenericFunctions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle network timeout errors', async () => {
|
||||
const networkError = {
|
||||
error: {
|
||||
error: {
|
||||
message: 'ETIMEDOUT - Network timeout',
|
||||
},
|
||||
},
|
||||
};
|
||||
mockRequest.mockRejectedValue(networkError);
|
||||
|
||||
await expect(
|
||||
msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should handle errors without nested structure', async () => {
|
||||
const simpleError = {
|
||||
error: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { IDataObject, INodePropertyOptions } from 'n8n-workflow';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IDataObject, INodePropertyOptions, IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getValue,
|
||||
@@ -7,8 +9,44 @@ import {
|
||||
getDefaultFields,
|
||||
getQuery,
|
||||
filterAndManageProcessedItems,
|
||||
salesforceApiRequest,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
// Mock external dependencies
|
||||
jest.mock('jsonwebtoken');
|
||||
jest.mock('moment-timezone', () => {
|
||||
const mockMoment = (value?: any) => ({
|
||||
unix: jest.fn(() => 1640995200), // Mock timestamp: 2022-01-01T00:00:00Z
|
||||
isValid: jest.fn(() => {
|
||||
// Mock moment validation logic to match real moment behavior
|
||||
// Real moment considers many values "valid" even if they're not proper dates
|
||||
if (typeof value === 'string') {
|
||||
// ISO date formats
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
||||
return true;
|
||||
}
|
||||
// Only consider obviously non-date strings as invalid
|
||||
// Real moment would consider numeric strings like '123' as valid (with deprecation warning)
|
||||
if (/^[a-zA-Z\s]+$/.test(value)) {
|
||||
return false; // Pure text strings like 'foo bar baz' are invalid
|
||||
}
|
||||
// Everything else (including numeric strings) is considered "valid" by moment
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: mockMoment,
|
||||
};
|
||||
});
|
||||
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
const mockJwt = jwt as jest.Mocked<typeof jwt>;
|
||||
|
||||
describe('Salesforce -> GenericFunctions', () => {
|
||||
describe('getValue', () => {
|
||||
it('should return date string as is', () => {
|
||||
@@ -113,7 +151,7 @@ describe('Salesforce -> GenericFunctions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined when constitions is not an array', () => {
|
||||
it('should return undefined when conditions is not an array', () => {
|
||||
const options: IDataObject = {
|
||||
conditionsUi: {
|
||||
conditionValues: 'not an array',
|
||||
@@ -125,7 +163,7 @@ describe('Salesforce -> GenericFunctions', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when constitions is an empty array', () => {
|
||||
it('should return undefined when conditions is an empty array', () => {
|
||||
const options: IDataObject = {
|
||||
conditionsUi: {
|
||||
conditionValues: [],
|
||||
@@ -417,4 +455,510 @@ describe('Salesforce -> GenericFunctions', () => {
|
||||
expect(result.updatedProcessedIds).toEqual(['100', '200', '003', '001', '004']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('salesforceApiRequest - JWT Authentication', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockRequest: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockRequest = jest.fn();
|
||||
mockExecuteFunctions.helpers.request = mockRequest;
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Setup default mocks
|
||||
(mockJwt.sign as jest.Mock).mockReturnValue('mock-jwt-signature');
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((param: string) => {
|
||||
if (param === 'authentication') return 'jwt';
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('JWT Authentication Flow', () => {
|
||||
it('should authenticate using JWT with production environment', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {});
|
||||
|
||||
// Verify JWT signature generation
|
||||
expect(mockJwt.sign as jest.Mock).toHaveBeenCalledWith(
|
||||
{
|
||||
iss: 'test-client-id',
|
||||
sub: 'test@example.com',
|
||||
aud: 'https://login.salesforce.com',
|
||||
exp: 1640995200 + 3 * 60, // Current timestamp + 3 minutes
|
||||
},
|
||||
'mock-private-key',
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
header: {
|
||||
alg: 'RS256',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Verify token exchange request
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: 'POST',
|
||||
form: {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
assertion: 'mock-jwt-signature',
|
||||
},
|
||||
uri: 'https://login.salesforce.com/services/oauth2/token',
|
||||
json: true,
|
||||
});
|
||||
|
||||
// Verify API request with bearer token
|
||||
const expectedApiOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-access-token',
|
||||
},
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
uri: 'https://test.salesforce.com/services/data/v59.0/test-endpoint',
|
||||
json: true,
|
||||
};
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(expectedApiOptions);
|
||||
expect(mockExecuteFunctions.logger.debug).toHaveBeenCalledWith(
|
||||
'Authentication for "Salesforce" node is using "jwt". Invoking URI https://test.salesforce.com/services/data/v59.0/test-endpoint',
|
||||
);
|
||||
});
|
||||
|
||||
it('should authenticate using JWT with sandbox environment', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'sandbox-client-id',
|
||||
username: 'sandbox@example.com',
|
||||
privateKey: 'sandbox-private-key',
|
||||
environment: 'sandbox',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'sandbox-access-token',
|
||||
instance_url: 'https://test.my.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/sandbox-endpoint',
|
||||
{ data: 'test' },
|
||||
{ param: 'value' },
|
||||
);
|
||||
|
||||
// Verify JWT uses sandbox URL
|
||||
expect(mockJwt.sign as jest.Mock).toHaveBeenCalledWith(
|
||||
{
|
||||
iss: 'sandbox-client-id',
|
||||
sub: 'sandbox@example.com',
|
||||
aud: 'https://test.salesforce.com',
|
||||
exp: 1640995200 + 3 * 60,
|
||||
},
|
||||
'sandbox-private-key',
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
header: {
|
||||
alg: 'RS256',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Verify token exchange request uses sandbox URL
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: 'POST',
|
||||
form: {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
assertion: 'mock-jwt-signature',
|
||||
},
|
||||
uri: 'https://test.salesforce.com/services/oauth2/token',
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle JWT token exchange with body and query parameters', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const testBody = { name: 'Test Account', type: 'Customer' };
|
||||
const testQs = { fields: 'Id,Name', limit: '10' };
|
||||
|
||||
await salesforceApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/test-endpoint',
|
||||
testBody,
|
||||
testQs,
|
||||
);
|
||||
|
||||
// Verify API request includes body and query parameters
|
||||
const expectedApiOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-access-token',
|
||||
},
|
||||
method: 'POST',
|
||||
body: testBody,
|
||||
qs: testQs,
|
||||
uri: 'https://test.salesforce.com/services/data/v59.0/test-endpoint',
|
||||
json: true,
|
||||
};
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(expectedApiOptions);
|
||||
});
|
||||
|
||||
it('should handle custom URI parameter', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/default-endpoint',
|
||||
{},
|
||||
{},
|
||||
'/custom-uri',
|
||||
);
|
||||
|
||||
// Verify custom URI is used instead of endpoint
|
||||
const expectedApiOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-access-token',
|
||||
},
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
uri: 'https://test.salesforce.com/services/data/v59.0/custom-uri',
|
||||
json: true,
|
||||
};
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(expectedApiOptions);
|
||||
});
|
||||
|
||||
it('should merge additional options', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const additionalOptions = {
|
||||
timeout: 30000,
|
||||
resolveWithFullResponse: true,
|
||||
};
|
||||
|
||||
await salesforceApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/test-endpoint',
|
||||
{},
|
||||
{},
|
||||
undefined,
|
||||
additionalOptions,
|
||||
);
|
||||
|
||||
// Verify additional options are merged
|
||||
const expectedApiOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-access-token',
|
||||
},
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
uri: 'https://test.salesforce.com/services/data/v59.0/test-endpoint',
|
||||
json: true,
|
||||
timeout: 30000,
|
||||
resolveWithFullResponse: true,
|
||||
};
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(expectedApiOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JWT Authentication Error Handling', () => {
|
||||
it('should handle credential retrieval errors', async () => {
|
||||
const credentialError = new Error('Failed to get credentials');
|
||||
mockExecuteFunctions.getCredentials.mockRejectedValue(credentialError);
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.salesforce',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {}),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('salesforceJwtApi');
|
||||
});
|
||||
|
||||
it('should handle JWT token exchange errors', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const tokenError = new Error('Invalid JWT signature');
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockRejectedValue(tokenError);
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.salesforce',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {}),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should handle API request errors after successful authentication', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
const apiError = new Error('API rate limit exceeded');
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest
|
||||
.mockResolvedValueOnce(mockResponse) // First call succeeds (token exchange)
|
||||
.mockRejectedValueOnce(apiError); // Second call fails (API request)
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.salesforce',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {}),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
// Verify that token exchange succeeded but API request failed
|
||||
expect(mockRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle missing instance_url in token response', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'mock-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
// Missing instance_url
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.salesforce',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
// Should not throw error but handle gracefully
|
||||
await salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {});
|
||||
|
||||
// Verify API call was made with undefined instance_url
|
||||
expect(mockRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JWT Signature Generation', () => {
|
||||
it('should generate correct JWT payload for production environment', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'prod-client-id',
|
||||
username: 'prod@example.com',
|
||||
privateKey: 'prod-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'prod-access-token',
|
||||
instance_url: 'https://prod.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {});
|
||||
|
||||
expect(mockJwt.sign as jest.Mock).toHaveBeenCalledWith(
|
||||
{
|
||||
iss: 'prod-client-id',
|
||||
sub: 'prod@example.com',
|
||||
aud: 'https://login.salesforce.com',
|
||||
exp: 1640995200 + 180, // 3 minutes = 180 seconds
|
||||
},
|
||||
'prod-private-key',
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
header: {
|
||||
alg: 'RS256',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate correct JWT payload for sandbox environment', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'sandbox-client-id',
|
||||
username: 'sandbox@example.com',
|
||||
privateKey: 'sandbox-private-key',
|
||||
environment: 'sandbox',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'sandbox-access-token',
|
||||
instance_url: 'https://sandbox.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {});
|
||||
|
||||
expect(mockJwt.sign as jest.Mock).toHaveBeenCalledWith(
|
||||
{
|
||||
iss: 'sandbox-client-id',
|
||||
sub: 'sandbox@example.com',
|
||||
aud: 'https://test.salesforce.com', // Sandbox uses test.salesforce.com
|
||||
exp: 1640995200 + 180,
|
||||
},
|
||||
'sandbox-private-key',
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
header: {
|
||||
alg: 'RS256',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use RS256 algorithm and proper header configuration', async () => {
|
||||
const mockCredentials = {
|
||||
clientId: 'test-client-id',
|
||||
username: 'test@example.com',
|
||||
privateKey: 'test-private-key',
|
||||
environment: 'production',
|
||||
};
|
||||
const mockResponse = {
|
||||
access_token: 'test-access-token',
|
||||
instance_url: 'https://test.salesforce.com',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockRequest.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.logger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
await salesforceApiRequest.call(mockExecuteFunctions, 'GET', '/test-endpoint', {}, {});
|
||||
|
||||
const jwtOptions = (mockJwt.sign as jest.Mock).mock.calls[0][2];
|
||||
expect(jwtOptions.algorithm).toBe('RS256');
|
||||
expect(jwtOptions.header?.alg).toBe('RS256');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,586 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { IPollFunctions, INode, ILoadOptionsFunctions, IDataObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import * as GenericFunctions from '../GenericFunctions';
|
||||
import { SalesforceTrigger } from '../SalesforceTrigger.node';
|
||||
|
||||
jest.mock('../GenericFunctions', () => ({
|
||||
getQuery: jest.fn(),
|
||||
salesforceApiRequest: jest.fn(),
|
||||
salesforceApiRequestAllItems: jest.fn(),
|
||||
sortOptions: jest.fn(),
|
||||
getPollStartDate: jest.fn(),
|
||||
filterAndManageProcessedItems: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SalesforceTrigger', () => {
|
||||
let trigger: SalesforceTrigger;
|
||||
let mockPollFunctions: jest.Mocked<IPollFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
|
||||
const salesforceApiRequestSpy = jest.spyOn(GenericFunctions, 'salesforceApiRequest');
|
||||
const salesforceApiRequestAllItemsSpy = jest.spyOn(
|
||||
GenericFunctions,
|
||||
'salesforceApiRequestAllItems',
|
||||
);
|
||||
const sortOptionsSpy = jest.spyOn(GenericFunctions, 'sortOptions');
|
||||
const getPollStartDateSpy = jest.spyOn(GenericFunctions, 'getPollStartDate');
|
||||
const filterAndManageProcessedItemsSpy = jest.spyOn(
|
||||
GenericFunctions,
|
||||
'filterAndManageProcessedItems',
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
trigger = new SalesforceTrigger();
|
||||
mockPollFunctions = mockDeep<IPollFunctions>();
|
||||
mockNode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Salesforce Trigger Test',
|
||||
type: 'n8n-nodes-base.salesforceTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockPollFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue({});
|
||||
mockPollFunctions.getMode.mockReturnValue('trigger');
|
||||
mockPollFunctions.getWorkflow.mockReturnValue({ id: 'test-workflow', active: true });
|
||||
mockPollFunctions.logger.error = jest.fn();
|
||||
(mockPollFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: unknown[]) =>
|
||||
data.map((item: unknown, index: number) => ({ json: item, pairedItem: { item: index } })),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Methods', () => {
|
||||
it('should have correct load options methods', () => {
|
||||
expect(trigger.methods?.loadOptions?.getCustomObjects).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getCustomObjects', () => {
|
||||
it('should return custom objects with sorted options', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
const mockSobjects = {
|
||||
sobjects: [
|
||||
{ name: 'Account', label: 'Account', custom: false },
|
||||
{ name: 'CustomObject1__c', label: 'Custom Object 1', custom: true },
|
||||
{ name: 'Contact', label: 'Contact', custom: false },
|
||||
{ name: 'CustomObject2__c', label: 'Custom Object 2', custom: true },
|
||||
],
|
||||
};
|
||||
|
||||
salesforceApiRequestSpy.mockResolvedValue(mockSobjects);
|
||||
sortOptionsSpy.mockImplementation((options) => {
|
||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
const result =
|
||||
await trigger.methods!.loadOptions!.getCustomObjects.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects');
|
||||
expect(sortOptionsSpy).toHaveBeenCalled();
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Custom Object 1', value: 'CustomObject1__c' },
|
||||
{ name: 'Custom Object 2', value: 'CustomObject2__c' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty sobjects response', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
const mockSobjects = { sobjects: [] };
|
||||
|
||||
salesforceApiRequestSpy.mockResolvedValue(mockSobjects);
|
||||
|
||||
const result =
|
||||
await trigger.methods!.loadOptions!.getCustomObjects.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out non-custom objects', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
const mockSobjects = {
|
||||
sobjects: [
|
||||
{ name: 'Account', label: 'Account', custom: false },
|
||||
{ name: 'Contact', label: 'Contact', custom: false },
|
||||
{ name: 'Lead', label: 'Lead', custom: false },
|
||||
],
|
||||
};
|
||||
|
||||
salesforceApiRequestSpy.mockResolvedValue(mockSobjects);
|
||||
|
||||
const result =
|
||||
await trigger.methods!.loadOptions!.getCustomObjects.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Parameter Setup', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, unknown> = {
|
||||
triggerOn: 'accountCreated',
|
||||
customObject: 'CustomObject__c',
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle standard object creation trigger', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue(
|
||||
'SELECT id,name,type FROM Account WHERE CreatedDate >= 2023-01-01T00:00:00.000Z',
|
||||
);
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Id: '001000000000001', Name: 'Test Account 1' },
|
||||
{ Id: '001000000000002', Name: 'Test Account 2' },
|
||||
]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [
|
||||
{ Id: '001000000000001', Name: 'Test Account 1' },
|
||||
{ Id: '001000000000002', Name: 'Test Account 2' },
|
||||
],
|
||||
updatedProcessedIds: ['001000000000001', '001000000000002'],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getPollStartDateSpy).toHaveBeenCalledWith(undefined);
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditionsUi: {
|
||||
conditionValues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'CreatedDate',
|
||||
operation: '>=',
|
||||
value: '2023-01-01T00:00:00.000Z',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
}),
|
||||
'Account',
|
||||
true,
|
||||
);
|
||||
expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: 'SELECT id,name,type FROM Account WHERE CreatedDate >= 2023-01-01T00:00:00.000Z',
|
||||
}),
|
||||
);
|
||||
expect(filterAndManageProcessedItemsSpy).toHaveBeenCalled();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(2);
|
||||
expect(result![0][0].json).toEqual({ Id: '001000000000001', Name: 'Test Account 1' });
|
||||
});
|
||||
|
||||
it('should handle standard object update trigger', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'accountUpdated';
|
||||
return '';
|
||||
});
|
||||
|
||||
const mockWorkflowData: IDataObject = { lastTimeChecked: '2023-01-01T00:00:00.000Z' };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2022-12-31T23:45:00.000Z');
|
||||
getQuerySpy.mockReturnValue(
|
||||
'SELECT id,name,type FROM Account WHERE LastModifiedDate >= 2022-12-31T23:45:00.000Z',
|
||||
);
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Id: '001000000000001', Name: 'Updated Account' },
|
||||
]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [{ Id: '001000000000001', Name: 'Updated Account' }],
|
||||
updatedProcessedIds: ['001000000000001'],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditionsUi: {
|
||||
conditionValues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'LastModifiedDate',
|
||||
operation: '>=',
|
||||
value: '2022-12-31T23:45:00.000Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
field: 'CreatedDate',
|
||||
operation: '<',
|
||||
value: '2022-12-31T23:45:00.000Z',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
}),
|
||||
'Account',
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle custom object triggers', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'customObjectCreated';
|
||||
if (paramName === 'customObject') return 'CustomObject__c';
|
||||
return '';
|
||||
});
|
||||
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue(
|
||||
'SELECT id FROM CustomObject__c WHERE CreatedDate >= 2023-01-01T00:00:00.000Z',
|
||||
);
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Id: 'a00000000000001', Name: 'Custom Object 1' },
|
||||
]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [{ Id: 'a00000000000001', Name: 'Custom Object 1' }],
|
||||
updatedProcessedIds: ['a00000000000001'],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(expect.any(Object), 'CustomObject__c', true);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should initialize processedIds when not present', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWorkflowData.processedIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update lastTimeChecked after polling', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWorkflowData.lastTimeChecked).toBeDefined();
|
||||
expect(DateTime.fromISO(mockWorkflowData.lastTimeChecked as string).isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Manual Mode', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'accountCreated';
|
||||
return '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should use limit 1 in manual mode', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account LIMIT 1');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Id: '001000000000001', Name: 'Test Account' },
|
||||
]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [{ Id: '001000000000001', Name: 'Test Account' }],
|
||||
updatedProcessedIds: ['001000000000001'],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditionsUi: {
|
||||
conditionValues: [],
|
||||
},
|
||||
}),
|
||||
'Account',
|
||||
false,
|
||||
1,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not add date conditions in manual mode', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account LIMIT 1');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditionsUi: {
|
||||
conditionValues: [],
|
||||
},
|
||||
}),
|
||||
'Account',
|
||||
false,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'accountCreated';
|
||||
return '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NodeApiError for API request errors', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should throw error in manual mode even with lastTimeChecked', async () => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
const mockWorkflowData: IDataObject = { lastTimeChecked: '2023-01-01T00:00:00.000Z' };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account LIMIT 1');
|
||||
salesforceApiRequestAllItemsSpy.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
|
||||
});
|
||||
|
||||
it('should log error and rethrow in trigger mode with lastTimeChecked', async () => {
|
||||
const mockWorkflowData: IDataObject = { lastTimeChecked: '2023-01-01T00:00:00.000Z' };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
const testError = new Error('API Error');
|
||||
getPollStartDateSpy.mockReturnValue('2022-12-31T23:45:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockRejectedValue(testError);
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
|
||||
|
||||
expect(mockPollFunctions.logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"There was a problem in 'Salesforce Trigger Test' node in workflow 'test-workflow'",
|
||||
),
|
||||
expect.objectContaining({
|
||||
node: 'Salesforce Trigger Test',
|
||||
workflowId: 'test-workflow',
|
||||
error: testError,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error immediately when no lastTimeChecked exists', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
const testError = new Error('API Error');
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockRejectedValue(testError);
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
|
||||
|
||||
expect(mockPollFunctions.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'accountCreated';
|
||||
return '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null when no data is found', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockWorkflowData.lastTimeChecked).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return null when no new items after filtering', async () => {
|
||||
const mockWorkflowData: IDataObject = { processedIds: ['001000000000001'] };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Id: '001000000000001', Name: 'Already Processed Account' },
|
||||
]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: ['001000000000001'],
|
||||
});
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle null/undefined responseData', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id,name,type FROM Account');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue(null);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockWorkflowData.lastTimeChecked).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty string triggerOn parameter', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return '';
|
||||
return '';
|
||||
});
|
||||
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id FROM LIMIT 1');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
// When triggerOn is empty, it results in 'Updated' operation and empty resource name
|
||||
expect(getQuerySpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conditionsUi: expect.objectContaining({
|
||||
conditionValues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'LastModifiedDate',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
'', // Empty resource name
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Date Handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'triggerOn') return 'leadUpdated';
|
||||
return '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should use getPollStartDate with safety margin', async () => {
|
||||
const mockWorkflowData: IDataObject = { lastTimeChecked: '2023-01-01T12:00:00.000Z' };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T11:45:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id FROM Lead');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(getPollStartDateSpy).toHaveBeenCalledWith('2023-01-01T12:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should set endDate to current time', async () => {
|
||||
const mockWorkflowData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWorkflowData);
|
||||
|
||||
const startTime = DateTime.now();
|
||||
|
||||
getPollStartDateSpy.mockReturnValue('2023-01-01T00:00:00.000Z');
|
||||
getQuerySpy.mockReturnValue('SELECT id FROM Lead');
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
filterAndManageProcessedItemsSpy.mockReturnValue({
|
||||
newItems: [],
|
||||
updatedProcessedIds: [],
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
const endTime = DateTime.now();
|
||||
const lastTimeChecked = DateTime.fromISO(mockWorkflowData.lastTimeChecked as string);
|
||||
|
||||
expect(lastTimeChecked >= startTime).toBe(true);
|
||||
expect(lastTimeChecked <= endTime).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
slackApiRequest,
|
||||
slackApiRequestAllItems,
|
||||
processThreadOptions,
|
||||
getMessageContent,
|
||||
} from '../../V2/GenericFunctions';
|
||||
|
||||
jest.mock('n8n-workflow', () => ({
|
||||
@@ -12,7 +14,7 @@ jest.mock('n8n-workflow', () => ({
|
||||
}));
|
||||
|
||||
describe('Slack V2 > GenericFunctions', () => {
|
||||
let mockExecuteFunctions: IExecuteFunctions;
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -22,7 +24,10 @@ describe('Slack V2 > GenericFunctions', () => {
|
||||
},
|
||||
getNode: jest.fn().mockReturnValue({ type: 'n8n-nodes-base.slack', typeVersion: 2 }),
|
||||
getNodeParameter: jest.fn().mockReturnValue('accessToken'),
|
||||
} as unknown as IExecuteFunctions;
|
||||
getWorkflow: jest.fn().mockReturnValue({ id: 'workflow-123', active: true }),
|
||||
getInstanceBaseUrl: jest.fn().mockReturnValue('https://test.n8n.io/'),
|
||||
getInstanceId: jest.fn().mockReturnValue('instance-123'),
|
||||
} as unknown as jest.Mocked<IExecuteFunctions>;
|
||||
});
|
||||
|
||||
describe('slackApiRequest', () => {
|
||||
@@ -34,6 +39,142 @@ describe('Slack V2 > GenericFunctions', () => {
|
||||
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle missing_scope error with needed scopes', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
error: 'missing_scope',
|
||||
needed: 'channels:read,users:read',
|
||||
};
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
// Test that the function throws the correct error type and message
|
||||
// Using .rejects ensures the test fails if slackApiRequest doesn't throw
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
'Your Slack credential is missing required Oauth Scopes',
|
||||
);
|
||||
|
||||
// Test specific error properties by catching the thrown error
|
||||
await expect(
|
||||
slackApiRequest.call(mockExecuteFunctions, 'GET', '/test'),
|
||||
).rejects.toMatchObject({
|
||||
description: 'Add the following scope(s) to your Slack App: channels:read,users:read',
|
||||
level: 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing_scope error without needed scopes', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
error: 'missing_scope',
|
||||
};
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
|
||||
// Test detailed error properties
|
||||
try {
|
||||
await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
throw new Error('Expected slackApiRequest to throw');
|
||||
} catch (error) {
|
||||
if (error.message === 'Expected slackApiRequest to throw') {
|
||||
throw error; // Re-throw our custom error if the function didn't throw as expected
|
||||
}
|
||||
expect(error).toBeInstanceOf(NodeOperationError);
|
||||
expect(error.message).toBe('Your Slack credential is missing required Oauth Scopes');
|
||||
expect(error.description).toBe('Add the following scope(s) to your Slack App: undefined');
|
||||
expect(error.level).toBe('warning');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle not_admin error', async () => {
|
||||
const mockResponse = { ok: false, error: 'not_admin' };
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
|
||||
try {
|
||||
await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(NodeOperationError);
|
||||
expect(error.message).toBe(
|
||||
'Need higher Role Level for this Operation (e.g. Owner or Admin Rights)',
|
||||
);
|
||||
expect(error.description).toBe(
|
||||
'Hint: Check the Role of your Slack App Integration. For more information see the Slack Documentation - https://slack.com/help/articles/360018112273-Types-of-roles-in-Slack',
|
||||
);
|
||||
expect(error.level).toBe('warning');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle generic error responses', async () => {
|
||||
const mockResponse = { ok: false, error: 'some_other_error' };
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(slackApiRequest.call(mockExecuteFunctions, 'GET', '/test')).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
|
||||
try {
|
||||
await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(NodeOperationError);
|
||||
expect(error.message).toBe('Slack error response: "some_other_error"');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add message_timestamp and remove ts when response contains ts', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
ts: '1234567890.123456',
|
||||
message: 'Test message',
|
||||
};
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
message_timestamp: '1234567890.123456',
|
||||
message: 'Test message',
|
||||
});
|
||||
expect(result.ts).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not modify response when ts is not present', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
message: 'Test message',
|
||||
};
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await slackApiRequest.call(mockExecuteFunctions, 'GET', '/test');
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
message: 'Test message',
|
||||
});
|
||||
expect(result.message_timestamp).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('slackApiRequestAllItems', () => {
|
||||
@@ -370,4 +511,338 @@ describe('Slack V2 > GenericFunctions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessageContent', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getWorkflow.mockReturnValue({ id: 'workflow-123', active: true });
|
||||
mockExecuteFunctions.getInstanceBaseUrl.mockReturnValue('https://test.n8n.io/');
|
||||
});
|
||||
|
||||
describe('block message type', () => {
|
||||
beforeEach(() => {
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
const params: { [key: string]: any } = {
|
||||
messageType: 'block',
|
||||
'otherOptions.includeLinkToWorkflow': true,
|
||||
text: 'Fallback text',
|
||||
};
|
||||
return params[param] || undefined;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle block message type with includeLinkToWorkflow true', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Hello World',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return true;
|
||||
if (param === 'text') return 'Fallback text';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect((result as any).blocks).toHaveLength(2);
|
||||
expect((result as any).blocks[0]).toEqual({
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Hello World',
|
||||
},
|
||||
});
|
||||
expect((result as any).blocks[1]).toEqual({
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: '_Automated with this <https://test.n8n.io/workflow/workflow-123?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.slack_instance-123|n8n workflow>_',
|
||||
},
|
||||
});
|
||||
expect(result.text).toBe('Fallback text');
|
||||
});
|
||||
|
||||
it('should handle block message type with includeLinkToWorkflow false', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Hello World',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return false;
|
||||
if (param === 'text') return 'Fallback text';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect((result as any).blocks).toHaveLength(1);
|
||||
expect((result as any).blocks[0]).toEqual({
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: 'Hello World',
|
||||
},
|
||||
});
|
||||
expect(result.text).toBe('Fallback text');
|
||||
});
|
||||
|
||||
it('should handle block message type without instanceId', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return true;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1);
|
||||
|
||||
expect((result as any).blocks).toHaveLength(2);
|
||||
expect((result as any).blocks[1].text.text).toBe(
|
||||
'_Automated with this <https://test.n8n.io/workflow/workflow-123?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.slack|n8n workflow>_',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle block message type with non-array blocks', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: 'invalid-blocks-format',
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return true;
|
||||
if (param === 'text') return 'Test text';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
blocks: 'invalid-blocks-format',
|
||||
text: 'Test text',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add text property when text parameter is provided', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return false;
|
||||
if (param === 'text') return 'Custom fallback text';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect(result.text).toBe('Custom fallback text');
|
||||
expect(result.blocks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not add text property when text parameter is empty', () => {
|
||||
const mockBlocksUI = {
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'block';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return false;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'blocksUi') return mockBlocksUI;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
blocks: [],
|
||||
});
|
||||
expect(result.text).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachment message type', () => {
|
||||
it('should handle attachment message type with includeLinkToWorkflow true', () => {
|
||||
const mockAttachments = [
|
||||
{
|
||||
color: 'good',
|
||||
text: 'Attachment text',
|
||||
fields: {
|
||||
item: [
|
||||
{ title: 'Field 1', value: 'Value 1' },
|
||||
{ title: 'Field 2', value: 'Value 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
color: 'warning',
|
||||
title: 'Warning attachment',
|
||||
},
|
||||
];
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'attachment';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return true;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'attachments') return mockAttachments;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect((result as any).attachments).toHaveLength(3);
|
||||
expect((result as any).attachments[0]).toEqual({
|
||||
color: 'good',
|
||||
text: 'Attachment text',
|
||||
fields: [
|
||||
{ title: 'Field 1', value: 'Value 1' },
|
||||
{ title: 'Field 2', value: 'Value 2' },
|
||||
],
|
||||
});
|
||||
expect((result as any).attachments[1]).toEqual({
|
||||
color: 'warning',
|
||||
title: 'Warning attachment',
|
||||
});
|
||||
expect((result as any).attachments[2]).toEqual({
|
||||
text: '_Automated with this <https://test.n8n.io/workflow/workflow-123?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.slack_instance-123|n8n workflow>_',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle attachment message type with includeLinkToWorkflow false', () => {
|
||||
const mockAttachments = [
|
||||
{
|
||||
text: 'Simple attachment',
|
||||
},
|
||||
];
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'attachment';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return false;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'attachments') return mockAttachments;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect((result as any).attachments).toHaveLength(1);
|
||||
expect((result as any).attachments[0]).toEqual({
|
||||
text: 'Simple attachment',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle attachment with undefined fields', () => {
|
||||
const mockAttachments = [
|
||||
{
|
||||
text: 'No fields attachment',
|
||||
fields: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'attachment';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return false;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'attachments') return mockAttachments;
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
expect((result as any).attachments[0]).toEqual({
|
||||
text: 'No fields attachment',
|
||||
fields: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle attachment with non-array attachments', () => {
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'attachment';
|
||||
if (param === 'otherOptions.includeLinkToWorkflow') return true;
|
||||
if (param === 'text') return '';
|
||||
if (param === 'attachments') return 'invalid-format';
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const result = getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
|
||||
// The function will treat the string as an iterable, so each character becomes an attachment
|
||||
// Plus the attribution link is added
|
||||
expect((result as any).attachments).toHaveLength(15);
|
||||
expect((result as any).attachments[(result as any).attachments.length - 1]).toEqual({
|
||||
text: '_Automated with this <https://test.n8n.io/workflow/workflow-123?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.slack_instance-123|n8n workflow>_',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error for unknown message type', () => {
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
|
||||
(param: string, _index: number) => {
|
||||
if (param === 'messageType') return 'unknown-type';
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
}).toThrow(NodeOperationError);
|
||||
|
||||
expect(() => {
|
||||
getMessageContent.call(mockExecuteFunctions, 0, 2.1, 'instance-123');
|
||||
}).toThrow('The message type "unknown-type" is not known!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user