From 1d1655685fc0410e1098d7a2280bad0721edd3c8 Mon Sep 17 00:00:00 2001
From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:44:04 +0000
Subject: [PATCH] test: Add tests for nodes (no-changelog) (#20732)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---
.../nodes/Aws/ELB/test/AwsElb.node.test.ts | 295 ++
.../Aws/ELB/test/GenericFunctions.test.ts | 289 ++
.../nodes-base/nodes/Aws/ELB/test/README.md | 29 +
.../add-listener-certificate.workflow.json | 60 +
.../test/create-load-balancer.workflow.json | 78 +
.../test/delete-load-balancer.workflow.json | 57 +
.../ELB/test/get-load-balancer.workflow.json | 63 +
...ny-listener-certificates-all.workflow.json | 66 +
...t-many-listener-certificates.workflow.json | 67 +
...ny-load-balancers-with-names.workflow.json | 71 +
.../get-many-load-balancers.workflow.json | 69 +
.../remove-listener-certificate.workflow.json | 59 +
.../Aws/S3/test/V1/GenericFunctions.test.ts | 339 ++
.../Aws/__tests__/AwsLambda.node.test.ts | 629 +++
.../Aws/__tests__/GenericFunctions.test.ts | 643 +++
.../test/GoogleDriveTrigger.node.test.ts | 610 +++
.../__test__/GenericFunctions.test.ts | 565 +++
.../__test__/collection-getAll.workflow.json | 75 +
.../__test__/collection.getAll.test.ts | 49 +
.../__test__/document-delete.workflow.json | 61 +
.../__test__/document-get.workflow.json | 68 +
.../__test__/document-query.workflow.json | 77 +
.../__test__/document-update.workflow.json | 65 +
.../__test__/document.delete.test.ts | 25 +
.../__test__/document.get.test.ts | 37 +
.../__test__/document.query.test.ts | 50 +
.../__test__/document.update.test.ts | 34 +
.../__test__/GenericFunctions.test.ts | 242 +
.../Sheet/test/v2/node/appendOrUpdate.test.ts | 617 ++-
.../test/GenericFunctions.test.ts | 15 -
.../__test__/GenericFunctions.test.ts | 550 ++-
.../__test__/Salesforce.node.test.ts | 3904 +++++++++++++++++
.../__test__/SalesforceTrigger.node.test.ts | 586 +++
.../Slack/test/v2/GenericFunctions.test.ts | 479 +-
.../nodes/Slack/test/v2/Slack.node.test.ts | 1547 +++++++
35 files changed, 12448 insertions(+), 22 deletions(-)
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/AwsElb.node.test.ts
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/GenericFunctions.test.ts
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/README.md
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/add-listener-certificate.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/create-load-balancer.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/delete-load-balancer.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/get-load-balancer.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates-all.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers-with-names.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/ELB/test/remove-listener-certificate.workflow.json
create mode 100644 packages/nodes-base/nodes/Aws/S3/test/V1/GenericFunctions.test.ts
create mode 100644 packages/nodes-base/nodes/Aws/__tests__/AwsLambda.node.test.ts
create mode 100644 packages/nodes-base/nodes/Aws/__tests__/GenericFunctions.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Drive/test/GoogleDriveTrigger.node.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/GenericFunctions.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection-getAll.workflow.json
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection.getAll.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-delete.workflow.json
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-get.workflow.json
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-query.workflow.json
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-update.workflow.json
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.delete.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.get.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.query.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.update.test.ts
create mode 100644 packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/__test__/GenericFunctions.test.ts
create mode 100644 packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts
create mode 100644 packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts
create mode 100644 packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/AwsElb.node.test.ts b/packages/nodes-base/nodes/Aws/ELB/test/AwsElb.node.test.ts
new file mode 100644
index 00000000000..11e3447cf63
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/AwsElb.node.test.ts
@@ -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,
+ `
+
+
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456
+ test-lb
+ internet-facing
+ application
+
+ provisioning
+
+
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456
+ test-lb
+ internet-facing
+ application
+
+ active
+
+
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456
+ test-lb-1
+ internet-facing
+ application
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567
+ test-lb-2
+ internal
+ network
+
+
+
+
+ `,
+ );
+
+ mock
+ .get(
+ '/?Action=DescribeLoadBalancers&Version=2015-12-01&Names.member.1=test-lb-1&Names.member.2=test-lb-2',
+ )
+ .reply(
+ 200,
+ `
+
+
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456
+ test-lb-1
+ internet-facing
+ application
+
+
+ arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567
+ test-lb-2
+ internal
+ network
+
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+
+ arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012
+ false
+
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+
+ arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012
+ false
+
+
+ arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321
+ true
+
+
+
+
+ `,
+ );
+
+ mock
+ .get(
+ '/?Action=DescribeListenerCertificates&Version=2015-12-01&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
+ )
+ .reply(
+ 200,
+ `
+
+
+
+
+ arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012
+ false
+
+
+ arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321
+ true
+
+
+
+
+ `,
+ );
+ });
+
+ 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,
+ `
+
+
+
+ `,
+ );
+ });
+
+ new NodeTestHarness().setupTests({
+ credentials,
+ workflowFiles: ['remove-listener-certificate.workflow.json'],
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Aws/ELB/test/GenericFunctions.test.ts
new file mode 100644
index 00000000000..f026c67d245
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/GenericFunctions.test.ts
@@ -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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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;
+ const mockParseXml = parseXml as jest.MockedFunction;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ jest.clearAllMocks();
+ });
+
+ it('should parse XML response correctly', async () => {
+ const xmlResponse =
+ 'test-bucket';
+ 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;
+ const mockParseXml = parseXml as jest.MockedFunction;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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(
+ 'response',
+ );
+ 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('first-response')
+ .mockResolvedValueOnce('second-response');
+
+ 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');
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/README.md b/packages/nodes-base/nodes/Aws/ELB/test/README.md
new file mode 100644
index 00000000000..19ad80545b2
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/README.md
@@ -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
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/add-listener-certificate.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/add-listener-certificate.workflow.json
new file mode 100644
index 00000000000..54030e08020
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/add-listener-certificate.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/create-load-balancer.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/create-load-balancer.workflow.json
new file mode 100644
index 00000000000..b2663a9b922
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/create-load-balancer.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/delete-load-balancer.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/delete-load-balancer.workflow.json
new file mode 100644
index 00000000000..d753bb5fd10
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/delete-load-balancer.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/get-load-balancer.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/get-load-balancer.workflow.json
new file mode 100644
index 00000000000..2099b5b1a44
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/get-load-balancer.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates-all.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates-all.workflow.json
new file mode 100644
index 00000000000..dcf4e3cc5f6
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates-all.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates.workflow.json
new file mode 100644
index 00000000000..ce57c80ec8e
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/get-many-listener-certificates.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers-with-names.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers-with-names.workflow.json
new file mode 100644
index 00000000000..c33e202bce3
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers-with-names.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers.workflow.json
new file mode 100644
index 00000000000..3b8c7a7ad5f
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/get-many-load-balancers.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/ELB/test/remove-listener-certificate.workflow.json b/packages/nodes-base/nodes/Aws/ELB/test/remove-listener-certificate.workflow.json
new file mode 100644
index 00000000000..1674facc881
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/ELB/test/remove-listener-certificate.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Aws/S3/test/V1/GenericFunctions.test.ts b/packages/nodes-base/nodes/Aws/S3/test/V1/GenericFunctions.test.ts
new file mode 100644
index 00000000000..73b55e2db82
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/S3/test/V1/GenericFunctions.test.ts
@@ -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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ jest.clearAllMocks();
+ });
+
+ it('should parse valid XML response', async () => {
+ const xmlResponse =
+ 'test-bucket';
+
+ 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;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ jest.clearAllMocks();
+ });
+
+ it('should collect all items from single page response', async () => {
+ const xmlResponse =
+ 'file1.txt1024file2.txt2048false';
+
+ 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 =
+ 'false';
+
+ 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 =
+ 'file1.txt1024truetoken123';
+
+ const secondPageResponse =
+ 'file2.txt2048false';
+
+ 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 =
+ 'file1.txt1024file2.txt2048truetoken123';
+
+ const secondPageResponse =
+ 'file3.txt3072false';
+
+ 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 =
+ 'single-file.txt512false';
+
+ 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();
+ const xmlResponse =
+ 'file1.txtfalse';
+
+ 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);
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/__tests__/AwsLambda.node.test.ts b/packages/nodes-base/nodes/Aws/__tests__/AwsLambda.node.test.ts
new file mode 100644
index 00000000000..87e2299fdd0
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/__tests__/AwsLambda.node.test.ts
@@ -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;
+ let mockLoadOptionsFunctions: jest.Mocked;
+ const awsApiRequestRESTSpy = jest.spyOn(GenericFunctions, 'awsApiRequestREST');
+
+ beforeEach(() => {
+ node = new AwsLambda();
+ mockExecuteFunctions = mockDeep();
+ mockLoadOptionsFunctions = mockDeep();
+ 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();
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Aws/__tests__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Aws/__tests__/GenericFunctions.test.ts
new file mode 100644
index 00000000000..7391462c2fd
--- /dev/null
+++ b/packages/nodes-base/nodes/Aws/__tests__/GenericFunctions.test.ts
@@ -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;
+ let mockLoadOptionsFunctions: jest.Mocked;
+ let mockWebhookFunctions: jest.Mocked;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ mockLoadOptionsFunctions = mockDeep();
+ mockWebhookFunctions = mockDeep();
+ 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 = 'successtest';
+
+ mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
+ (mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ xmlResponse,
+ );
+
+ const result = await awsApiRequestSOAP.call(
+ mockExecuteFunctions,
+ 'ses',
+ 'POST',
+ '/send-email',
+ 'test',
+ );
+
+ 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 =
+ 'url1url2';
+
+ 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 = '- opt1
- opt2
';
+
+ 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');
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Drive/test/GoogleDriveTrigger.node.test.ts b/packages/nodes-base/nodes/Google/Drive/test/GoogleDriveTrigger.node.test.ts
new file mode 100644
index 00000000000..865637e21bc
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Drive/test/GoogleDriveTrigger.node.test.ts
@@ -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;
+ 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();
+ 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();
+ 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();
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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');
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/GenericFunctions.test.ts
new file mode 100644
index 00000000000..3820d03f3ea
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/GenericFunctions.test.ts
@@ -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;
+ let mockLoadOptionsFunctions: jest.Mocked;
+ let mockNode: INode;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ mockLoadOptionsFunctions = mockDeep();
+ mockNode = mock({
+ 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'],
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection-getAll.workflow.json b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection-getAll.workflow.json
new file mode 100644
index 00000000000..bf50bb2732e
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection-getAll.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection.getAll.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection.getAll.test.ts
new file mode 100644
index 00000000000..c97a6c27ea0
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/collection.getAll.test.ts
@@ -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'],
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-delete.workflow.json b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-delete.workflow.json
new file mode 100644
index 00000000000..0a56c40d0fa
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-delete.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-get.workflow.json b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-get.workflow.json
new file mode 100644
index 00000000000..76dc7013292
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-get.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-query.workflow.json b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-query.workflow.json
new file mode 100644
index 00000000000..36a47c2d087
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-query.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-update.workflow.json b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-update.workflow.json
new file mode 100644
index 00000000000..5030d875a36
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document-update.workflow.json
@@ -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"
+ }
+}
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.delete.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.delete.test.ts
new file mode 100644
index 00000000000..0d6379240f3
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.delete.test.ts
@@ -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'],
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.get.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.get.test.ts
new file mode 100644
index 00000000000..af61ab143e4
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.get.test.ts
@@ -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'],
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.query.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.query.test.ts
new file mode 100644
index 00000000000..39a2dab998a
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.query.test.ts
@@ -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'],
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.update.test.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.update.test.ts
new file mode 100644
index 00000000000..426986d3db9
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/__test__/document.update.test.ts
@@ -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'],
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/__test__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/__test__/GenericFunctions.test.ts
new file mode 100644
index 00000000000..745f8c0282d
--- /dev/null
+++ b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/__test__/GenericFunctions.test.ts
@@ -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;
+ let mockLoadOptionsFunctions: jest.Mocked;
+ let mockNode: INode;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ mockLoadOptionsFunctions = mockDeep();
+ mockNode = mock({
+ 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',
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts
index fb39624f1dc..5d586ef96e2 100644
--- a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts
@@ -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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
+ const params: Record = {
+ '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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
+ const params: Record = {
+ '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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
+ const params: Record = {
+ '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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
+ const params: Record = {
+ '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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, itemIndex, fallback) => {
+ const params: Record = {
+ '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({ typeVersion: 4.5 }));
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName, _itemIndex, fallback) => {
+ const params: Record = {
+ '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({ 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 = {
+ '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 = {
'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 = {
'options.cellFormat': 'USER_ENTERED',
options: {},
'columns.mappingMode': 'defineBelow',
diff --git a/packages/nodes-base/nodes/Microsoft/GraphSecurity/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Microsoft/GraphSecurity/test/GenericFunctions.test.ts
index dfbcbbf4086..274ab00d6a9 100644
--- a/packages/nodes-base/nodes/Microsoft/GraphSecurity/test/GenericFunctions.test.ts
+++ b/packages/nodes-base/nodes/Microsoft/GraphSecurity/test/GenericFunctions.test.ts
@@ -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: {
diff --git a/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts
index 0ff5d38e764..b6ad3ec1631 100644
--- a/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts
+++ b/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts
@@ -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;
+
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;
+ let mockRequest: jest.Mock;
+
+ beforeEach(() => {
+ mockExecuteFunctions = mockDeep();
+ 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');
+ });
+ });
+ });
});
diff --git a/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts
new file mode 100644
index 00000000000..321edb3341f
--- /dev/null
+++ b/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts
@@ -0,0 +1,3904 @@
+import { mockDeep } from 'jest-mock-extended';
+import type { IExecuteFunctions, INode, ILoadOptionsFunctions } from 'n8n-workflow';
+import { jsonParse } from 'n8n-workflow';
+
+import * as GenericFunctions from '../GenericFunctions';
+import { Salesforce } from '../Salesforce.node';
+
+jest.mock('../GenericFunctions', () => ({
+ getQuery: jest.fn(),
+ salesforceApiRequest: jest.fn(),
+ salesforceApiRequestAllItems: jest.fn(),
+ sortOptions: jest.fn(),
+}));
+
+describe('Salesforce', () => {
+ let node: Salesforce;
+ let mockExecuteFunctions: jest.Mocked;
+ let mockNode: INode;
+
+ const salesforceApiRequestSpy = jest.spyOn(GenericFunctions, 'salesforceApiRequest');
+ const salesforceApiRequestAllItemsSpy = jest.spyOn(
+ GenericFunctions,
+ 'salesforceApiRequestAllItems',
+ );
+ const sortOptionsSpy = jest.spyOn(GenericFunctions, 'sortOptions');
+
+ beforeEach(() => {
+ node = new Salesforce();
+ mockExecuteFunctions = mockDeep();
+ mockNode = {
+ id: 'test-node-id',
+ name: 'Salesforce Test',
+ type: 'n8n-nodes-base.salesforce',
+ typeVersion: 1,
+ position: [0, 0],
+ parameters: {},
+ };
+
+ jest.clearAllMocks();
+
+ mockExecuteFunctions.getNode.mockReturnValue(mockNode);
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
+ mockExecuteFunctions.continueOnFail.mockReturnValue(false);
+ mockExecuteFunctions.logger.debug = jest.fn();
+ (mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
+ (data: unknown[], meta: unknown) =>
+ data.map((item: unknown, index: number) => ({
+ ...(typeof item === 'object' && item !== null ? item : {}),
+ pairedItem: (meta as any)?.itemData?.item || index,
+ })),
+ );
+ (mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data) =>
+ Array.isArray(data) ? data.map((item) => ({ json: item })) : [{ json: data }],
+ );
+
+ // Set up common mock implementations that are used in most tests
+ sortOptionsSpy.mockImplementation((options) => options);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('LoadOptions Methods', () => {
+ let mockLoadOptionsFunctions: jest.Mocked;
+
+ beforeEach(() => {
+ mockLoadOptionsFunctions = mockDeep();
+ });
+
+ describe('getLeadStatuses', () => {
+ it('should return lead statuses with sorted options', async () => {
+ const mockStatuses = [
+ { Id: '1', MasterLabel: 'Qualified' },
+ { Id: '2', MasterLabel: 'New' },
+ { Id: '3', MasterLabel: 'Working' },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockStatuses);
+ sortOptionsSpy.mockImplementation((options) => {
+ return options.sort((a, b) => a.name.localeCompare(b.name));
+ });
+
+ const result =
+ await node.methods.loadOptions.getLeadStatuses.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT id, MasterLabel FROM LeadStatus' },
+ );
+ expect(sortOptionsSpy).toHaveBeenCalled();
+
+ expect(result).toEqual([
+ { name: 'New', value: 'New' },
+ { name: 'Qualified', value: 'Qualified' },
+ { name: 'Working', value: 'Working' },
+ ]);
+ });
+
+ it('should handle empty statuses response', async () => {
+ salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
+
+ const result =
+ await node.methods.loadOptions.getLeadStatuses.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('getUsers', () => {
+ it('should return users with sorted options', async () => {
+ const mockUsers = [
+ { Id: 'user1', Name: 'John Doe' },
+ { Id: 'user2', Name: 'Jane Smith' },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockUsers);
+ sortOptionsSpy.mockImplementation((options) => {
+ return options.sort((a, b) => a.name.localeCompare(b.name));
+ });
+
+ const result = await node.methods.loadOptions.getUsers.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT id, Name FROM User' },
+ );
+
+ expect(result).toEqual([
+ { name: 'Jane Smith', value: 'user2' },
+ { name: 'John Doe', value: 'user1' },
+ ]);
+ });
+ });
+
+ describe('getCaseOwners', () => {
+ it('should return case owners with queues and users', async () => {
+ const mockQueues = [
+ { Queue: { Id: 'queue1', Name: 'Support Queue' } },
+ { Queue: { Id: 'queue2', Name: 'Sales Queue' } },
+ ];
+ const mockUsers = [
+ { Id: 'user1', Name: 'John Doe' },
+ { Id: 'user2', Name: 'Jane Smith' },
+ ];
+
+ salesforceApiRequestAllItemsSpy
+ .mockResolvedValueOnce(mockQueues)
+ .mockResolvedValueOnce(mockUsers);
+
+ const result = await node.methods.loadOptions.getCaseOwners.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Case'",
+ },
+ );
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT id, Name FROM User' },
+ );
+
+ expect(result).toEqual([
+ { name: 'Queue: Support Queue', value: 'queue1' },
+ { name: 'Queue: Sales Queue', value: 'queue2' },
+ { name: 'User: John Doe', value: 'user1' },
+ { name: 'User: Jane Smith', value: 'user2' },
+ ]);
+ });
+
+ it('should handle users without queue prefix when no queues exist', async () => {
+ salesforceApiRequestAllItemsSpy
+ .mockResolvedValueOnce([])
+ .mockResolvedValueOnce([{ Id: 'user1', Name: 'John Doe' }]);
+
+ const result = await node.methods.loadOptions.getCaseOwners.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([{ name: 'John Doe', value: 'user1' }]);
+ });
+ });
+
+ describe('getLeadOwners', () => {
+ it('should return lead owners with queues and users', async () => {
+ const mockQueues = [{ Queue: { Id: 'queue1', Name: 'Lead Queue' } }];
+ const mockUsers = [{ Id: 'user1', Name: 'John Doe' }];
+
+ salesforceApiRequestAllItemsSpy
+ .mockResolvedValueOnce(mockQueues)
+ .mockResolvedValueOnce(mockUsers);
+
+ const result = await node.methods.loadOptions.getLeadOwners.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Lead'",
+ },
+ );
+
+ expect(result).toEqual([
+ { name: 'Queue: Lead Queue', value: 'queue1' },
+ { name: 'User: John Doe', value: 'user1' },
+ ]);
+ });
+ });
+
+ describe('getLeadSources', () => {
+ it('should return lead sources from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'LeadSource',
+ picklistValues: [
+ { label: 'Web', value: 'Web' },
+ { label: 'Phone Inquiry', value: 'Phone Inquiry' },
+ { label: 'Partner Referral', value: 'Partner Referral' },
+ ],
+ },
+ { name: 'OtherField', picklistValues: [] },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getLeadSources.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/lead/describe');
+ expect(result).toEqual([
+ { name: 'Web', value: 'Web' },
+ { name: 'Phone Inquiry', value: 'Phone Inquiry' },
+ { name: 'Partner Referral', value: 'Partner Referral' },
+ ]);
+ });
+ });
+
+ describe('getCustomFields', () => {
+ it('should return custom fields for a resource', async () => {
+ mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('Account');
+ const mockDescribe = {
+ fields: [
+ { name: 'CustomField1__c', label: 'Custom Field 1', custom: true },
+ { name: 'StandardField', label: 'Standard Field', custom: false },
+ { name: 'CustomField2__c', label: 'Custom Field 2', custom: true },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getCustomFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/Account/describe');
+ expect(result).toEqual([
+ { name: 'Custom Field 1', value: 'CustomField1__c' },
+ { name: 'Custom Field 2', value: 'CustomField2__c' },
+ ]);
+ });
+ });
+
+ describe('getRecordTypes', () => {
+ it('should return active record types for a resource', async () => {
+ mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('Account');
+ const mockTypes = [
+ { Id: 'rt1', Name: 'Business Account', SobjectType: 'Account', IsActive: true },
+ { Id: 'rt2', Name: 'Personal Account', SobjectType: 'Account', IsActive: true },
+ { Id: 'rt3', Name: 'Inactive Type', SobjectType: 'Account', IsActive: false },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockTypes);
+
+ const result = await node.methods.loadOptions.getRecordTypes.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: "SELECT Id, Name, SobjectType, IsActive FROM RecordType WHERE SobjectType = 'Account'",
+ },
+ );
+ expect(result).toEqual([
+ { name: 'Business Account', value: 'rt1' },
+ { name: 'Personal Account', value: 'rt2' },
+ ]);
+ });
+
+ it('should handle custom objects for record types', async () => {
+ mockLoadOptionsFunctions.getNodeParameter.mockImplementation((param) => {
+ if (param === 'resource') return 'customObject';
+ if (param === 'customObject') return 'CustomObject__c';
+ return '';
+ });
+
+ const mockTypes = [
+ { Id: 'rt1', Name: 'Custom Type', SobjectType: 'CustomObject__c', IsActive: true },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockTypes);
+
+ const result = await node.methods.loadOptions.getRecordTypes.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: "SELECT Id, Name, SobjectType, IsActive FROM RecordType WHERE SobjectType = 'CustomObject__c'",
+ },
+ );
+ expect(result).toEqual([{ name: 'Custom Type', value: 'rt1' }]);
+ });
+ });
+
+ describe('getExternalIdFields', () => {
+ it('should return external ID and lookup fields', async () => {
+ mockLoadOptionsFunctions.getCurrentNodeParameter.mockReturnValue('Account');
+ const mockDescribe = {
+ fields: [
+ { name: 'ExternalId__c', label: 'External ID', externalId: true, idLookup: false },
+ { name: 'LookupField__c', label: 'Lookup Field', externalId: false, idLookup: true },
+ { name: 'RegularField', label: 'Regular Field', externalId: false, idLookup: false },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getExternalIdFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/Account/describe');
+ expect(result).toEqual([
+ { name: 'External ID', value: 'ExternalId__c' },
+ { name: 'Lookup Field', value: 'LookupField__c' },
+ ]);
+ });
+
+ it('should handle custom object for external ID fields', async () => {
+ mockLoadOptionsFunctions.getCurrentNodeParameter.mockImplementation((param) => {
+ if (param === 'resource') return 'customObject';
+ if (param === 'customObject') return 'CustomObject__c';
+ return '';
+ });
+
+ const mockDescribe = {
+ fields: [
+ { name: 'External__c', label: 'External Field', externalId: true, idLookup: false },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getExternalIdFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/CustomObject__c/describe',
+ );
+ expect(result).toEqual([{ name: 'External Field', value: 'External__c' }]);
+ });
+ });
+
+ describe('getAccounts', () => {
+ it('should return accounts with sorted options', async () => {
+ const mockAccounts = [
+ { Id: 'acc1', Name: 'ACME Corp' },
+ { Id: 'acc2', Name: 'ABC Inc' },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockAccounts);
+
+ const result = await node.methods.loadOptions.getAccounts.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT id, Name FROM Account' },
+ );
+ expect(result).toEqual([
+ { name: 'ACME Corp', value: 'acc1' },
+ { name: 'ABC Inc', value: 'acc2' },
+ ]);
+ });
+ });
+
+ describe('getCampaigns', () => {
+ it('should return campaigns with sorted options', async () => {
+ const mockCampaigns = [
+ { Id: 'camp1', Name: 'Spring Campaign' },
+ { Id: 'camp2', Name: 'Summer Campaign' },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockCampaigns);
+
+ const result = await node.methods.loadOptions.getCampaigns.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT id, Name FROM Campaign' },
+ );
+ expect(result).toEqual([
+ { name: 'Spring Campaign', value: 'camp1' },
+ { name: 'Summer Campaign', value: 'camp2' },
+ ]);
+ });
+ });
+
+ describe('getStages', () => {
+ it('should return opportunity stages from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'StageName',
+ picklistValues: [
+ { label: 'Prospecting', value: 'Prospecting' },
+ { label: 'Qualification', value: 'Qualification' },
+ { label: 'Closed Won', value: 'Closed Won' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getStages.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/opportunity/describe',
+ );
+ expect(result).toEqual([
+ { name: 'Prospecting', value: 'Prospecting' },
+ { name: 'Qualification', value: 'Qualification' },
+ { name: 'Closed Won', value: 'Closed Won' },
+ ]);
+ });
+ });
+
+ describe('getAccountTypes', () => {
+ it('should return account types from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Type',
+ picklistValues: [
+ { label: 'Customer - Direct', value: 'Customer - Direct' },
+ { label: 'Customer - Channel', value: 'Customer - Channel' },
+ { label: 'Prospect', value: 'Prospect' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getAccountTypes.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/account/describe');
+ expect(result).toEqual([
+ { name: 'Customer - Direct', value: 'Customer - Direct' },
+ { name: 'Customer - Channel', value: 'Customer - Channel' },
+ { name: 'Prospect', value: 'Prospect' },
+ ]);
+ });
+ });
+
+ describe('getAccountSources', () => {
+ it('should return account sources from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'AccountSource',
+ picklistValues: [
+ { label: 'Web', value: 'Web' },
+ { label: 'Phone Inquiry', value: 'Phone Inquiry' },
+ { label: 'Partner Referral', value: 'Partner Referral' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getAccountSources.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/account/describe');
+ expect(result).toEqual([
+ { name: 'Web', value: 'Web' },
+ { name: 'Phone Inquiry', value: 'Phone Inquiry' },
+ { name: 'Partner Referral', value: 'Partner Referral' },
+ ]);
+ });
+ });
+
+ describe('getCaseTypes', () => {
+ it('should return case types from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Type',
+ picklistValues: [
+ { label: 'Feature Request', value: 'Feature Request' },
+ { label: 'Problem', value: 'Problem' },
+ { label: 'Question', value: 'Question' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getCaseTypes.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/case/describe');
+ expect(result).toEqual([
+ { name: 'Feature Request', value: 'Feature Request' },
+ { name: 'Problem', value: 'Problem' },
+ { name: 'Question', value: 'Question' },
+ ]);
+ });
+ });
+
+ describe('getCaseStatuses', () => {
+ it('should return case statuses from picklist values', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Status',
+ picklistValues: [
+ { label: 'New', value: 'New' },
+ { label: 'Working', value: 'Working' },
+ { label: 'Closed', value: 'Closed' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getCaseStatuses.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/case/describe');
+ expect(result).toEqual([
+ { name: 'New', value: 'New' },
+ { name: 'Working', value: 'Working' },
+ { name: 'Closed', value: 'Closed' },
+ ]);
+ });
+ });
+
+ describe('getCustomObjects', () => {
+ it('should return custom objects only', async () => {
+ 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);
+
+ const result =
+ await node.methods.loadOptions.getCustomObjects.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects');
+ expect(result).toEqual([
+ { name: 'Custom Object 1', value: 'CustomObject1__c' },
+ { name: 'Custom Object 2', value: 'CustomObject2__c' },
+ ]);
+ });
+ });
+
+ describe('getCustomObjectFields', () => {
+ it('should return all fields for a custom object', async () => {
+ mockLoadOptionsFunctions.getCurrentNodeParameter.mockReturnValue('CustomObject__c');
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'ID' },
+ { name: 'Name', label: 'Name' },
+ { name: 'CustomField__c', label: 'Custom Field' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getCustomObjectFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/CustomObject__c/describe',
+ );
+ expect(result).toEqual([
+ { name: 'ID', value: 'Id' },
+ { name: 'Name', value: 'Name' },
+ { name: 'Custom Field', value: 'CustomField__c' },
+ ]);
+ });
+ });
+
+ describe('Field Methods', () => {
+ it('should return account fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Account ID' },
+ { name: 'Name', label: 'Account Name' },
+ { name: 'Type', label: 'Account Type' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getAccountFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/account/describe');
+ expect(result).toEqual([
+ { name: 'Account ID', value: 'Id' },
+ { name: 'Account Name', value: 'Name' },
+ { name: 'Account Type', value: 'Type' },
+ ]);
+ });
+
+ it('should return case fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Case ID' },
+ { name: 'Subject', label: 'Subject' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getCaseFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/case/describe');
+ expect(result).toEqual([
+ { name: 'Case ID', value: 'Id' },
+ { name: 'Subject', value: 'Subject' },
+ ]);
+ });
+
+ it('should return lead fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Lead ID' },
+ { name: 'FirstName', label: 'First Name' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getLeadFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/lead/describe');
+ expect(result).toEqual([
+ { name: 'Lead ID', value: 'Id' },
+ { name: 'First Name', value: 'FirstName' },
+ ]);
+ });
+
+ it('should return opportunity fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Opportunity ID' },
+ { name: 'Name', label: 'Opportunity Name' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getOpportunityFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/opportunity/describe',
+ );
+ expect(result).toEqual([
+ { name: 'Opportunity ID', value: 'Id' },
+ { name: 'Opportunity Name', value: 'Name' },
+ ]);
+ });
+
+ it('should return task fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Task ID' },
+ { name: 'Subject', label: 'Subject' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getTaskFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/task/describe');
+ expect(result).toEqual([
+ { name: 'Task ID', value: 'Id' },
+ { name: 'Subject', value: 'Subject' },
+ ]);
+ });
+
+ it('should return user fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'User ID' },
+ { name: 'Name', label: 'Full Name' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getUserFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/user/describe');
+ expect(result).toEqual([
+ { name: 'User ID', value: 'Id' },
+ { name: 'Full Name', value: 'Name' },
+ ]);
+ });
+
+ it('should return contact fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Contact ID' },
+ { name: 'FirstName', label: 'First Name' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getContactFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/contact/describe');
+ expect(result).toEqual([
+ { name: 'Contact ID', value: 'Id' },
+ { name: 'First Name', value: 'FirstName' },
+ ]);
+ });
+
+ it('should return attachment fields', async () => {
+ const mockDescribe = {
+ fields: [
+ { name: 'Id', label: 'Attachment ID' },
+ { name: 'Name', label: 'File Name' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getAtachmentFields.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/attachment/describe',
+ );
+ expect(result).toEqual([
+ { name: 'Attachment ID', value: 'Id' },
+ { name: 'File Name', value: 'Name' },
+ ]);
+ });
+ });
+
+ describe('Task Picklist Methods', () => {
+ it('should return task statuses', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Status',
+ picklistValues: [
+ { label: 'Not Started', value: 'Not Started' },
+ { label: 'In Progress', value: 'In Progress' },
+ { label: 'Completed', value: 'Completed' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskStatuses.call(mockLoadOptionsFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/task/describe');
+ expect(result).toEqual([
+ { name: 'Not Started', value: 'Not Started' },
+ { name: 'In Progress', value: 'In Progress' },
+ { name: 'Completed', value: 'Completed' },
+ ]);
+ });
+
+ it('should return task types', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'TaskSubtype',
+ picklistValues: [
+ { label: 'Call', value: 'Call' },
+ { label: 'Email', value: 'Email' },
+ { label: 'Meeting', value: 'Meeting' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getTaskTypes.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Call', value: 'Call' },
+ { name: 'Email', value: 'Email' },
+ { name: 'Meeting', value: 'Meeting' },
+ ]);
+ });
+
+ it('should return task subjects', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Subject',
+ picklistValues: [
+ { label: 'Call', value: 'Call' },
+ { label: 'Send Letter/Quote', value: 'Send Letter/Quote' },
+ { label: 'Send Proposal/Price Quote', value: 'Send Proposal/Price Quote' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskSubjects.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Call', value: 'Call' },
+ { name: 'Send Letter/Quote', value: 'Send Letter/Quote' },
+ { name: 'Send Proposal/Price Quote', value: 'Send Proposal/Price Quote' },
+ ]);
+ });
+
+ it('should return task call types', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'CallType',
+ picklistValues: [
+ { label: 'Inbound', value: 'Inbound' },
+ { label: 'Outbound', value: 'Outbound' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskCallTypes.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Inbound', value: 'Inbound' },
+ { name: 'Outbound', value: 'Outbound' },
+ ]);
+ });
+
+ it('should return task priorities', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Priority',
+ picklistValues: [
+ { label: 'Low', value: 'Low' },
+ { label: 'Normal', value: 'Normal' },
+ { label: 'High', value: 'High' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskPriorities.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Low', value: 'Low' },
+ { name: 'Normal', value: 'Normal' },
+ { name: 'High', value: 'High' },
+ ]);
+ });
+
+ it('should return task recurrence types', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'RecurrenceType',
+ picklistValues: [
+ { label: 'RecursDaily', value: 'RecursDaily' },
+ { label: 'RecursWeekly', value: 'RecursWeekly' },
+ { label: 'RecursMonthly', value: 'RecursMonthly' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskRecurrenceTypes.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'RecursDaily', value: 'RecursDaily' },
+ { name: 'RecursWeekly', value: 'RecursWeekly' },
+ { name: 'RecursMonthly', value: 'RecursMonthly' },
+ ]);
+ });
+
+ it('should return task recurrence instances', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'RecurrenceInstance',
+ picklistValues: [
+ { label: 'First', value: 'First' },
+ { label: 'Second', value: 'Second' },
+ { label: 'Third', value: 'Third' },
+ { label: 'Fourth', value: 'Fourth' },
+ { label: 'Last', value: 'Last' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getTaskRecurrenceInstances.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'First', value: 'First' },
+ { name: 'Second', value: 'Second' },
+ { name: 'Third', value: 'Third' },
+ { name: 'Fourth', value: 'Fourth' },
+ { name: 'Last', value: 'Last' },
+ ]);
+ });
+ });
+
+ describe('Case Picklist Methods', () => {
+ it('should return case reasons', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Reason',
+ picklistValues: [
+ { label: 'Installation', value: 'Installation' },
+ { label: 'Equipment Complexity', value: 'Equipment Complexity' },
+ { label: 'Performance', value: 'Performance' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getCaseReasons.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Installation', value: 'Installation' },
+ { name: 'Equipment Complexity', value: 'Equipment Complexity' },
+ { name: 'Performance', value: 'Performance' },
+ ]);
+ });
+
+ it('should return case origins', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Origin',
+ picklistValues: [
+ { label: 'Phone', value: 'Phone' },
+ { label: 'Email', value: 'Email' },
+ { label: 'Web', value: 'Web' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result = await node.methods.loadOptions.getCaseOrigins.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Phone', value: 'Phone' },
+ { name: 'Email', value: 'Email' },
+ { name: 'Web', value: 'Web' },
+ ]);
+ });
+
+ it('should return case priorities', async () => {
+ const mockDescribe = {
+ fields: [
+ {
+ name: 'Priority',
+ picklistValues: [
+ { label: 'Low', value: 'Low' },
+ { label: 'Medium', value: 'Medium' },
+ { label: 'High', value: 'High' },
+ ],
+ },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockDescribe);
+
+ const result =
+ await node.methods.loadOptions.getCasePriorities.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual([
+ { name: 'Low', value: 'Low' },
+ { name: 'Medium', value: 'Medium' },
+ { name: 'High', value: 'High' },
+ ]);
+ });
+ });
+ });
+
+ describe('Execute Method - Lead Resource', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ mockExecuteFunctions.getNodeParameter.mockImplementation(
+ (param: string, _index?: number): any => {
+ return mockExecuteFunctions.getNodeParameter.mock.results[0]?.value?.[param] || '';
+ },
+ );
+ });
+
+ describe('Lead Create Operation', () => {
+ it('should handle lead create with minimal fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'create',
+ company: 'Test Company',
+ lastname: 'Test Lead',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'lead123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/lead',
+ expect.objectContaining({
+ Company: 'Test Company',
+ LastName: 'Test Lead',
+ }),
+ );
+ });
+
+ it('should handle lead create with all additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'create',
+ company: 'ACME Corp',
+ lastname: 'Doe',
+ additionalFields: {
+ hasOptedOutOfEmail: true,
+ hasOptedOutOfFax: false,
+ email: 'test@example.com',
+ city: 'New York',
+ phone: '+1234567890',
+ state: 'NY',
+ title: 'Manager',
+ jigsaw: 'JIGSAW123',
+ rating: 'Hot',
+ status: 'Qualified',
+ street: '123 Main St',
+ country: 'USA',
+ owner: 'user123',
+ website: 'https://example.com',
+ industry: 'Technology',
+ fax: 1234567890,
+ firstname: 'John',
+ leadSource: 'Web',
+ postalCode: '10001',
+ salutation: 'Mr.',
+ description: 'Test lead description',
+ annualRevenue: 50000,
+ isUnreadByOwner: true,
+ numberOfEmployees: 100,
+ mobilePhone: '+1987654321',
+ recordTypeId: 'rt123',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Field1__c', value: 'Custom Value 1' },
+ { fieldId: 'Custom_Field2__c', value: 'Custom Value 2' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'lead456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/lead',
+ expect.objectContaining({
+ Company: 'ACME Corp',
+ LastName: 'Doe',
+ HasOptedOutOfEmail: true,
+ HasOptedOutOfFax: false,
+ Email: 'test@example.com',
+ City: 'New York',
+ Phone: '+1234567890',
+ State: 'NY',
+ Title: 'Manager',
+ Jigsaw: 'JIGSAW123',
+ Rating: 'Hot',
+ Status: 'Qualified',
+ Street: '123 Main St',
+ Country: 'USA',
+ OwnerId: 'user123',
+ Website: 'https://example.com',
+ Industry: 'Technology',
+ Fax: 1234567890,
+ FirstName: 'John',
+ LeadSource: 'Web',
+ PostalCode: '10001',
+ Salutation: 'Mr.',
+ Description: 'Test lead description',
+ AnnualRevenue: 50000,
+ IsUnreadByOwner: true,
+ NumberOfEmployees: 100,
+ MobilePhone: '+1987654321',
+ RecordTypeId: 'rt123',
+ Custom_Field1__c: 'Custom Value 1',
+ Custom_Field2__c: 'Custom Value 2',
+ }),
+ );
+ });
+
+ it('should handle lead upsert operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation(
+ (param: string, index?: number): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'upsert',
+ company: 'Upsert Company',
+ lastname: 'Upsert Lead',
+ externalId: 'External_Id__c',
+ externalIdValue: 'EXT123',
+ additionalFields: {
+ email: 'upsert@example.com',
+ External_Id__c: 'EXT123', // Should be removed from body
+ },
+ };
+ if (param === 'externalId' && index === 0) {
+ return params.externalId;
+ }
+ if (param === 'externalIdValue') {
+ return params.externalIdValue;
+ }
+ return params[param];
+ },
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'lead789', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/lead/External_Id__c/EXT123',
+ expect.objectContaining({
+ Company: 'Upsert Company',
+ LastName: 'Upsert Lead',
+ Email: 'upsert@example.com',
+ }),
+ );
+
+ // Ensure external ID field is removed from body
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ expect(callArgs[2]).not.toHaveProperty('External_Id__c');
+ });
+ });
+
+ describe('Lead Update Operation', () => {
+ it('should handle lead update with all fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'update',
+ leadId: 'lead123',
+ updateFields: {
+ hasOptedOutOfEmail: false,
+ hasOptedOutOfFax: true,
+ lastname: 'Updated Doe',
+ company: 'Updated Company',
+ email: 'updated@example.com',
+ city: 'Updated City',
+ phone: '+1111111111',
+ state: 'CA',
+ title: 'Senior Manager',
+ jigsaw: 'UPDATED123',
+ rating: 'Warm',
+ status: 'Working',
+ street: '456 Updated St',
+ country: 'Canada',
+ owner: 'user456',
+ website: 'https://updated.com',
+ industry: 'Healthcare',
+ firstname: 'Jane',
+ fax: 9876543210,
+ leadSource: 'Referral',
+ postalCode: '90210',
+ salutation: 'Ms.',
+ description: 'Updated description',
+ annualRevenue: 75000,
+ isUnreadByOwner: false,
+ numberOfEmployees: 200,
+ mobilePhone: '+1555555555',
+ recordTypeId: 'rt456',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Field3__c', value: 'Updated Custom Value' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/lead/lead123',
+ expect.objectContaining({
+ HasOptedOutOfEmail: false,
+ hasOptedOutOfFax: true, // Note: This is a bug in the original code (should be HasOptedOutOfFax)
+ LastName: 'Updated Doe',
+ Company: 'Updated Company',
+ Email: 'updated@example.com',
+ City: 'Updated City',
+ Phone: '+1111111111',
+ State: 'CA',
+ Title: 'Senior Manager',
+ Jigsaw: 'UPDATED123',
+ Rating: 'Warm',
+ Status: 'Working',
+ Street: '456 Updated St',
+ Country: 'Canada',
+ OwnerId: 'user456',
+ Website: 'https://updated.com',
+ Industry: 'Healthcare',
+ FirstName: 'Jane',
+ Fax: 9876543210,
+ LeadSource: 'Referral',
+ PostalCode: '90210',
+ Salutation: 'Ms.',
+ Description: 'Updated description',
+ AnnualRevenue: 75000,
+ IsUnreadByOwner: false,
+ NumberOfEmployees: 200,
+ MobilePhone: '+1555555555',
+ RecordTypeId: 'rt456',
+ Custom_Field3__c: 'Updated Custom Value',
+ }),
+ );
+ });
+
+ it('should throw error when no update fields provided', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'update',
+ leadId: 'lead123',
+ updateFields: {},
+ };
+ return params[param];
+ });
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'You must add at least one update field',
+ );
+ });
+ });
+
+ describe('Lead Other Operations', () => {
+ it('should handle lead get operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'get',
+ leadId: 'lead123',
+ };
+ return params[param];
+ });
+
+ const mockLead = { Id: 'lead123', FirstName: 'John', LastName: 'Doe' };
+ salesforceApiRequestSpy.mockResolvedValue(mockLead);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/lead/lead123');
+ });
+
+ it('should handle lead getAll operation with returnAll true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'getAll',
+ returnAll: true,
+ options: { fields: 'Id,FirstName,LastName' },
+ };
+ return params[param];
+ });
+
+ const mockLeads = [
+ { Id: 'lead1', FirstName: 'John', LastName: 'Doe' },
+ { Id: 'lead2', FirstName: 'Jane', LastName: 'Smith' },
+ ];
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,FirstName,LastName FROM Lead');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockLeads);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,FirstName,LastName' }, 'Lead', true);
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT Id,FirstName,LastName FROM Lead' },
+ );
+ });
+
+ it('should handle lead getAll operation with limit', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 50,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT * FROM Lead LIMIT 50');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({}, 'Lead', false, 50);
+ });
+
+ it('should handle lead delete operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'delete',
+ leadId: 'lead123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('DELETE', '/sobjects/lead/lead123');
+ });
+
+ it('should handle lead getSummary operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'getSummary',
+ };
+ return params[param];
+ });
+
+ const mockSummary = { objectDescribe: { name: 'Lead', fields: [] } };
+ salesforceApiRequestSpy.mockResolvedValue(mockSummary);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/lead');
+ });
+
+ it('should handle lead addToCampaign operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'addToCampaign',
+ leadId: 'lead123',
+ campaignId: 'campaign456',
+ options: { status: 'Sent' },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'cm123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/CampaignMember',
+ expect.objectContaining({
+ LeadId: 'lead123',
+ CampaignId: 'campaign456',
+ Status: 'Sent',
+ }),
+ );
+ });
+
+ it('should handle lead addNote operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'addNote',
+ leadId: 'lead123',
+ title: 'Important Note',
+ options: {
+ body: 'This is the note body',
+ owner: 'user789',
+ isPrivate: true,
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'note123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/note',
+ expect.objectContaining({
+ Title: 'Important Note',
+ ParentId: 'lead123',
+ Body: 'This is the note body',
+ OwnerId: 'user789',
+ IsPrivate: true,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('Execute Method - Case Resource', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ });
+
+ describe('Case Create Operation', () => {
+ it('should handle case create with minimal fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'create',
+ type: 'Problem',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'case123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/case',
+ expect.objectContaining({
+ Type: 'Problem',
+ }),
+ );
+ });
+
+ it('should handle case create with all additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'create',
+ type: 'Feature Request',
+ additionalFields: {
+ origin: 'Web',
+ reason: 'Installation',
+ status: 'New',
+ owner: 'user123',
+ subject: 'Test Case Subject',
+ parentId: 'parent123',
+ priority: 'High',
+ accountId: 'acc123',
+ contactId: 'contact123',
+ description: 'Test case description',
+ isEscalated: true,
+ suppliedName: 'John Doe',
+ suppliedEmail: 'john@example.com',
+ suppliedPhone: '+1234567890',
+ suppliedCompany: 'Test Company',
+ recordTypeId: 'rt123',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'case456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/case',
+ expect.objectContaining({
+ Type: 'Feature Request',
+ Origin: 'Web',
+ Reason: 'Installation',
+ Status: 'New',
+ OwnerId: 'user123',
+ Subject: 'Test Case Subject',
+ ParentId: 'parent123',
+ Priority: 'High',
+ AccountId: 'acc123',
+ ContactId: 'contact123',
+ Description: 'Test case description',
+ IsEscalated: true,
+ SuppliedName: 'John Doe',
+ SuppliedEmail: 'john@example.com',
+ SuppliedPhone: '+1234567890',
+ SuppliedCompany: 'Test Company',
+ RecordTypeId: 'rt123',
+ }),
+ );
+ });
+
+ it('should handle case create with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'create',
+ type: 'Question',
+ additionalFields: {
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Field1__c', value: 'Custom Value 1' },
+ { fieldId: 'Custom_Field2__c', value: 'Custom Value 2' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'case789', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/case',
+ expect.objectContaining({
+ Type: 'Question',
+ Custom_Field1__c: 'Custom Value 1',
+ Custom_Field2__c: 'Custom Value 2',
+ }),
+ );
+ });
+ });
+
+ describe('Case Update Operation', () => {
+ it('should handle case update with all fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'update',
+ caseId: 'case123',
+ updateFields: {
+ type: 'Updated Problem',
+ origin: 'Email',
+ reason: 'Performance',
+ status: 'Working',
+ owner: 'user456',
+ subject: 'Updated Case Subject',
+ parentId: 'parent456',
+ priority: 'Medium',
+ accountId: 'acc456',
+ recordTypeId: 'rt456',
+ contactId: 'contact456',
+ description: 'Updated case description',
+ isEscalated: false,
+ suppliedName: 'Jane Smith',
+ suppliedEmail: 'jane@example.com',
+ suppliedPhone: '+1987654321',
+ suppliedCompany: 'Updated Company',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/case/case123',
+ expect.objectContaining({
+ Type: 'Updated Problem',
+ Origin: 'Email',
+ Reason: 'Performance',
+ Status: 'Working',
+ OwnerId: 'user456',
+ Subject: 'Updated Case Subject',
+ ParentId: 'parent456',
+ Priority: 'Medium',
+ AccountId: 'acc456',
+ RecordTypeId: 'rt456',
+ ContactId: 'contact456',
+ Description: 'Updated case description',
+ IsEscalated: false,
+ SuppliedName: 'Jane Smith',
+ SuppliedEmail: 'jane@example.com',
+ SuppliedPhone: '+1987654321',
+ SuppliedCompany: 'Updated Company',
+ }),
+ );
+ });
+
+ it('should handle case update with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'update',
+ caseId: 'case789',
+ updateFields: {
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Updated_Custom_Field__c', value: 'Updated Custom Value' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/case/case789',
+ expect.objectContaining({
+ Updated_Custom_Field__c: 'Updated Custom Value',
+ }),
+ );
+ });
+ });
+
+ describe('Case Other Operations', () => {
+ it('should handle case get operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'get',
+ caseId: 'case123',
+ };
+ return params[param];
+ });
+
+ const mockCase = { Id: 'case123', Subject: 'Test Case', Type: 'Problem' };
+ salesforceApiRequestSpy.mockResolvedValue(mockCase);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/case/case123');
+ });
+
+ it('should handle case getAll operation with returnAll true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'getAll',
+ returnAll: true,
+ options: { fields: 'Id,Subject,Type' },
+ };
+ return params[param];
+ });
+
+ const mockCases = [
+ { Id: 'case1', Subject: 'Case 1', Type: 'Problem' },
+ { Id: 'case2', Subject: 'Case 2', Type: 'Question' },
+ ];
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,Subject,Type FROM Case');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockCases);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Subject,Type' }, 'Case', true);
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT Id,Subject,Type FROM Case' },
+ );
+ });
+
+ it('should handle case getAll operation with limit', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 25,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT * FROM Case LIMIT 25');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue([]);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({}, 'Case', false, 25);
+ });
+
+ it('should handle case getAll operation error handling', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'getAll',
+ returnAll: true,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT * FROM Case');
+ salesforceApiRequestAllItemsSpy.mockRejectedValue(new Error('API Error'));
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow();
+ });
+
+ it('should handle case delete operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'delete',
+ caseId: 'case123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('DELETE', '/sobjects/case/case123');
+ });
+
+ it('should handle case delete operation error handling', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'delete',
+ caseId: 'case123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockRejectedValue(new Error('Delete error'));
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow();
+ });
+
+ it('should handle case getSummary operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'getSummary',
+ };
+ return params[param];
+ });
+
+ const mockSummary = { objectDescribe: { name: 'Case', fields: [] } };
+ salesforceApiRequestSpy.mockResolvedValue(mockSummary);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/case');
+ });
+
+ it('should handle case addComment operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'addComment',
+ caseId: 'case123',
+ options: {
+ commentBody: 'This is a test comment',
+ isPublished: true,
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'comment123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/casecomment',
+ expect.objectContaining({
+ ParentId: 'case123',
+ CommentBody: 'This is a test comment',
+ IsPublished: true,
+ }),
+ );
+ });
+
+ it('should handle case addComment operation with minimal options', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'case',
+ operation: 'addComment',
+ caseId: 'case456',
+ options: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'comment456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/casecomment',
+ expect.objectContaining({
+ ParentId: 'case456',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('Execute Method - Contact Resource', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ });
+
+ describe('Contact Create Operation', () => {
+ it('should handle contact create with minimal fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'create',
+ lastname: 'Test Contact',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'contact123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/contact',
+ expect.objectContaining({
+ LastName: 'Test Contact',
+ }),
+ );
+ });
+
+ it('should handle contact create with all additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'create',
+ lastname: 'Smith',
+ additionalFields: {
+ fax: '1234567890',
+ email: 'contact@example.com',
+ phone: '+1234567890',
+ title: 'Director',
+ jigsaw: 'JIGSAW456',
+ recordTypeId: 'rt789',
+ owner: 'user456',
+ acconuntId: 'acc123', // Note: This is a typo in the original code
+ birthdate: '1990-01-01',
+ firstName: 'Jane',
+ homePhone: '+1987654321',
+ otherCity: 'Other City',
+ department: 'Sales',
+ leadSource: 'Cold Call',
+ otherPhone: '+1555555555',
+ otherState: 'TX',
+ salutation: 'Mrs.',
+ description: 'Contact description',
+ mailingCity: 'Mailing City',
+ mobilePhone: '+1777777777',
+ otherStreet: '789 Other St',
+ mailingState: 'FL',
+ otherCountry: 'Mexico',
+ assistantName: 'Assistant Name',
+ mailingStreet: '456 Mailing St',
+ assistantPhone: '+1666666666',
+ mailingCountry: 'USA',
+ otherPostalCode: '54321',
+ emailBouncedDate: '2023-01-01',
+ mailingPostalCode: '12345',
+ emailBouncedReason: 'Mailbox full',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Contact_Custom__c', value: 'Contact Custom Value' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'contact456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/contact',
+ expect.objectContaining({
+ LastName: 'Smith',
+ Fax: '1234567890',
+ Email: 'contact@example.com',
+ Phone: '+1234567890',
+ Title: 'Director',
+ Jigsaw: 'JIGSAW456',
+ RecordTypeId: 'rt789',
+ OwnerId: 'user456',
+ AccountId: 'acc123',
+ Birthdate: '1990-01-01',
+ FirstName: 'Jane',
+ HomePhone: '+1987654321',
+ OtherCity: 'Other City',
+ Department: 'Sales',
+ LeadSource: 'Cold Call',
+ OtherPhone: '+1555555555',
+ OtherState: 'TX',
+ Salutation: 'Mrs.',
+ Description: 'Contact description',
+ MailingCity: 'Mailing City',
+ MobilePhone: '+1777777777',
+ OtherStreet: '789 Other St',
+ MailingState: 'FL',
+ OtherCountry: 'Mexico',
+ AssistantName: 'Assistant Name',
+ MailingStreet: '456 Mailing St',
+ AssistantPhone: '+1666666666',
+ MailingCountry: 'USA',
+ OtherPostalCode: '54321',
+ EmailBouncedDate: '2023-01-01',
+ MailingPostalCode: '12345',
+ EmailBouncedReason: 'Mailbox full',
+ Contact_Custom__c: 'Contact Custom Value',
+ }),
+ );
+ });
+
+ it('should handle contact upsert operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation(
+ (param: string, index?: number): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'upsert',
+ lastname: 'Upsert Contact',
+ externalId: 'External_Contact_Id__c',
+ externalIdValue: 'EXT_CONTACT_123',
+ additionalFields: {
+ email: 'upsert@contact.com',
+ External_Contact_Id__c: 'EXT_CONTACT_123', // Should be removed from body
+ },
+ };
+ if (param === 'externalId' && index === 0) {
+ return params.externalId;
+ }
+ if (param === 'externalIdValue') {
+ return params.externalIdValue;
+ }
+ return params[param];
+ },
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'contact789', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/contact/External_Contact_Id__c/EXT_CONTACT_123',
+ expect.objectContaining({
+ LastName: 'Upsert Contact',
+ Email: 'upsert@contact.com',
+ }),
+ );
+
+ // Ensure external ID field is removed from body
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ expect(callArgs[2]).not.toHaveProperty('External_Contact_Id__c');
+ });
+ });
+
+ describe('Contact Update Operation', () => {
+ it('should handle contact update with all fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'update',
+ contactId: 'contact123',
+ updateFields: {
+ lastName: 'Updated Contact',
+ fax: '9876543210',
+ email: 'updated@contact.com',
+ recordTypeId: 'rt999',
+ phone: '+1999999999',
+ title: 'Updated Director',
+ jigsaw: 'UPDATED_JIGSAW',
+ owner: 'user999',
+ acconuntId: 'acc999', // Note: This is a typo in the original code
+ birthdate: '1985-05-05',
+ firstName: 'Updated Jane',
+ homePhone: '+1888888888',
+ otherCity: 'Updated Other City',
+ department: 'Updated Sales',
+ leadSource: 'Updated Cold Call',
+ otherPhone: '+1777777777',
+ otherState: 'Updated TX',
+ salutation: 'Dr.',
+ description: 'Updated contact description',
+ mailingCity: 'Updated Mailing City',
+ mobilePhone: '+1666666666',
+ otherStreet: 'Updated 789 Other St',
+ mailingState: 'Updated FL',
+ otherCountry: 'Updated Mexico',
+ assistantName: 'Updated Assistant Name',
+ mailingStreet: 'Updated 456 Mailing St',
+ assistantPhone: '+1555555555',
+ mailingCountry: 'Updated USA',
+ otherPostalCode: 'Updated 54321',
+ emailBouncedDate: '2024-01-01',
+ mailingPostalCode: 'Updated 12345',
+ emailBouncedReason: 'Updated Mailbox full',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Updated_Contact_Custom__c', value: 'Updated Contact Custom Value' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/contact/contact123',
+ expect.objectContaining({
+ LastName: 'Updated Contact',
+ Fax: '9876543210',
+ Email: 'updated@contact.com',
+ RecordTypeId: 'rt999',
+ Phone: '+1999999999',
+ Title: 'Updated Director',
+ Jigsaw: 'UPDATED_JIGSAW',
+ OwnerId: 'user999',
+ AccountId: 'acc999',
+ Birthdate: '1985-05-05',
+ FirstName: 'Updated Jane',
+ HomePhone: '+1888888888',
+ OtherCity: 'Updated Other City',
+ Department: 'Updated Sales',
+ LeadSource: 'Updated Cold Call',
+ OtherPhone: '+1777777777',
+ OtherState: 'Updated TX',
+ Salutation: 'Dr.',
+ Description: 'Updated contact description',
+ MailingCity: 'Updated Mailing City',
+ MobilePhone: '+1666666666',
+ OtherStreet: 'Updated 789 Other St',
+ MailingState: 'Updated FL',
+ OtherCountry: 'Updated Mexico',
+ AssistantName: 'Updated Assistant Name',
+ MailingStreet: 'Updated 456 Mailing St',
+ AssistantPhone: '+1555555555',
+ MailingCountry: 'Updated USA',
+ OtherPostalCode: 'Updated 54321',
+ EmailBouncedDate: '2024-01-01',
+ MailingPostalCode: 'Updated 12345',
+ EmailBouncedReason: 'Updated Mailbox full',
+ Updated_Contact_Custom__c: 'Updated Contact Custom Value',
+ }),
+ );
+ });
+
+ it('should throw error when no update fields provided for contact', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'update',
+ contactId: 'contact123',
+ updateFields: {},
+ };
+ return params[param];
+ });
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'You must add at least one update field',
+ );
+ });
+ });
+
+ describe('Contact Other Operations', () => {
+ it('should handle contact get operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'get',
+ contactId: 'contact123',
+ };
+ return params[param];
+ });
+
+ const mockContact = { Id: 'contact123', FirstName: 'John', LastName: 'Doe' };
+ salesforceApiRequestSpy.mockResolvedValue(mockContact);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/contact/contact123');
+ });
+
+ it('should handle contact getAll operation with returnAll true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'getAll',
+ returnAll: true,
+ options: { fields: 'Id,FirstName,LastName' },
+ };
+ return params[param];
+ });
+
+ const mockContacts = [
+ { Id: 'contact1', FirstName: 'John', LastName: 'Doe' },
+ { Id: 'contact2', FirstName: 'Jane', LastName: 'Smith' },
+ ];
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,FirstName,LastName FROM Contact');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockContacts);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith(
+ { fields: 'Id,FirstName,LastName' },
+ 'Contact',
+ true,
+ );
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT Id,FirstName,LastName FROM Contact' },
+ );
+ });
+
+ it('should handle contact delete operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'delete',
+ contactId: 'contact123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'DELETE',
+ '/sobjects/contact/contact123',
+ );
+ });
+
+ it('should handle contact getSummary operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'getSummary',
+ };
+ return params[param];
+ });
+
+ const mockSummary = { objectDescribe: { name: 'Contact', fields: [] } };
+ salesforceApiRequestSpy.mockResolvedValue(mockSummary);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/contact');
+ });
+
+ it('should handle contact addToCampaign operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'addToCampaign',
+ contactId: 'contact123',
+ campaignId: 'campaign456',
+ options: { status: 'Responded' },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'cm456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/CampaignMember',
+ expect.objectContaining({
+ ContactId: 'contact123',
+ CampaignId: 'campaign456',
+ Status: 'Responded',
+ }),
+ );
+ });
+
+ it('should handle contact addNote operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'contact',
+ operation: 'addNote',
+ contactId: 'contact123',
+ title: 'Contact Note',
+ options: {
+ body: 'This is the contact note body',
+ owner: 'user888',
+ isPrivate: false,
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'note456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/note',
+ expect.objectContaining({
+ Title: 'Contact Note',
+ ParentId: 'contact123',
+ Body: 'This is the contact note body',
+ OwnerId: 'user888',
+ IsPrivate: false,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('Execute Method - CustomObject Resource', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ });
+
+ describe('CustomObject Create Operation', () => {
+ it('should handle customObject create with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'create',
+ customObject: 'CustomObject__c',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Name', value: 'Test Custom Object' },
+ { fieldId: 'Custom_Field1__c', value: 'Custom Value 1' },
+ { fieldId: 'Custom_Field2__c', value: 'Custom Value 2' },
+ ],
+ },
+ additionalFields: {
+ recordTypeId: 'rt123',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'custom123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/CustomObject__c',
+ expect.objectContaining({
+ Name: 'Test Custom Object',
+ Custom_Field1__c: 'Custom Value 1',
+ Custom_Field2__c: 'Custom Value 2',
+ RecordTypeId: 'rt123',
+ }),
+ );
+ });
+
+ it('should handle customObject upsert operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation(
+ (param: string, index?: number): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'upsert',
+ customObject: 'CustomObject__c',
+ externalId: 'External_Id__c',
+ externalIdValue: 'EXT_CUSTOM_123',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Name', value: 'Upsert Custom Object' },
+ { fieldId: 'External_Id__c', value: 'EXT_CUSTOM_123' }, // Should be removed
+ ],
+ },
+ additionalFields: {},
+ };
+ if (param === 'externalId' && index === 0) {
+ return params.externalId;
+ }
+ if (param === 'externalIdValue') {
+ return params.externalIdValue;
+ }
+ return params[param];
+ },
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'custom456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/CustomObject__c/External_Id__c/EXT_CUSTOM_123',
+ expect.objectContaining({
+ Name: 'Upsert Custom Object',
+ }),
+ );
+
+ // Ensure external ID field is removed from body
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ expect(callArgs[2]).not.toHaveProperty('External_Id__c');
+ });
+ });
+
+ describe('CustomObject Update Operation', () => {
+ it('should handle customObject update operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'update',
+ recordId: 'custom123',
+ customObject: 'CustomObject__c',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Name', value: 'Updated Custom Object' },
+ { fieldId: 'Custom_Field1__c', value: 'Updated Custom Value 1' },
+ ],
+ },
+ updateFields: {
+ recordTypeId: 'rt456',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/CustomObject__c/custom123',
+ expect.objectContaining({
+ Name: 'Updated Custom Object',
+ Custom_Field1__c: 'Updated Custom Value 1',
+ RecordTypeId: 'rt456',
+ }),
+ );
+ });
+ });
+
+ describe('CustomObject Other Operations', () => {
+ it('should handle customObject get operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'get',
+ customObject: 'CustomObject__c',
+ recordId: 'custom123',
+ };
+ return params[param];
+ });
+
+ const mockCustomObject = { Id: 'custom123', Name: 'Test Custom Object' };
+ salesforceApiRequestSpy.mockResolvedValue(mockCustomObject);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/sobjects/CustomObject__c/custom123',
+ );
+ });
+
+ it('should handle customObject getAll operation with returnAll true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'getAll',
+ customObject: 'CustomObject__c',
+ returnAll: true,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const mockCustomObjects = [
+ { Id: 'custom1', Name: 'Custom Object 1' },
+ { Id: 'custom2', Name: 'Custom Object 2' },
+ ];
+
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT * FROM CustomObject__c');
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockCustomObjects);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({}, 'CustomObject__c', true);
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT * FROM CustomObject__c' },
+ );
+ });
+
+ it('should handle customObject delete operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'customObject',
+ operation: 'delete',
+ customObject: 'CustomObject__c',
+ recordId: 'custom123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'DELETE',
+ '/sobjects/CustomObject__c/custom123',
+ );
+ });
+ });
+ });
+
+ describe('Execute Method - Document Resource', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ });
+
+ describe('Document Upload Operation', () => {
+ it('should handle document upload operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'document',
+ operation: 'upload',
+ title: 'Test Document',
+ binaryPropertyName: 'data',
+ additionalFields: {
+ ownerId: 'user123',
+ linkToObjectId: 'record456',
+ fileExtension: 'pdf',
+ },
+ };
+ return params[param];
+ });
+
+ const mockBinaryData = {
+ data: Buffer.from('test file content'),
+ mimeType: 'application/pdf',
+ fileExtension: 'pdf',
+ fileName: 'test.pdf',
+ };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(
+ mockBinaryData,
+ );
+ (mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
+ mockBinaryData.data,
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'cv123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
+ expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data');
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/ContentVersion',
+ {},
+ {},
+ undefined,
+ expect.objectContaining({
+ formData: expect.objectContaining({
+ entity_content: expect.objectContaining({
+ value: expect.any(String),
+ options: { contentType: 'application/json' },
+ }),
+ VersionData: expect.objectContaining({
+ value: mockBinaryData.data,
+ options: { filename: 'Test Document.pdf' },
+ }),
+ }),
+ }),
+ );
+
+ // Check entity_content JSON structure
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ const formData = (callArgs[5] as { formData: any }).formData;
+ const entityContent = jsonParse(formData.entity_content.value as string);
+
+ expect(entityContent).toEqual({
+ Title: 'Test Document',
+ ContentLocation: 'S',
+ ownerId: 'user123',
+ FirstPublishLocationId: 'record456',
+ PathOnClient: 'Test Document.pdf',
+ });
+ });
+
+ it('should handle document upload with minimal fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'document',
+ operation: 'upload',
+ title: 'Minimal Document',
+ binaryPropertyName: 'data',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ const mockBinaryData = {
+ data: Buffer.from('minimal content'),
+ mimeType: 'text/plain',
+ fileExtension: 'txt',
+ fileName: 'test.txt',
+ };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(
+ mockBinaryData,
+ );
+ (mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
+ mockBinaryData.data,
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'cv456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ expect(callArgs).toBeDefined();
+ expect(callArgs[5]).toBeDefined();
+ expect((callArgs[5] as { formData: any }).formData).toBeDefined();
+
+ const formData = (callArgs[5] as { formData: any }).formData;
+ const entityContent = jsonParse(formData.entity_content.value as string);
+
+ expect(entityContent).toEqual({
+ Title: 'Minimal Document',
+ ContentLocation: 'S',
+ PathOnClient: 'Minimal Document.txt',
+ });
+
+ expect(entityContent).not.toHaveProperty('ownerId');
+ expect(entityContent).not.toHaveProperty('FirstPublishLocationId');
+ });
+ });
+ });
+
+ describe('Execute Method', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: { testData: 'value' } }]);
+ });
+
+ describe('Flow Resource', () => {
+ it('should execute flow invoke operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'flow',
+ operation: 'invoke',
+ apiName: 'TestFlow',
+ jsonParameters: true,
+ variablesJson: { input1: 'value1', input2: 'value2' },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/actions/custom/flow/TestFlow',
+ {
+ inputs: [{ input1: 'value1', input2: 'value2' }],
+ },
+ );
+ });
+
+ it('should execute flow invoke operation with UI variables', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'flow',
+ operation: 'invoke',
+ apiName: 'TestFlow',
+ jsonParameters: false,
+ variablesUi: {
+ variablesValues: [
+ { name: 'VAR1', value: 'val1' },
+ { name: 'VAR2', value: 'val2' },
+ ],
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/actions/custom/flow/TestFlow',
+ {
+ inputs: [{ VAR1: 'val1', VAR2: 'val2' }],
+ },
+ );
+ });
+
+ it('should execute flow getAll operation with returnAll true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'flow',
+ operation: 'getAll',
+ returnAll: true,
+ };
+ return params[param];
+ });
+
+ const mockFlows = {
+ actions: [
+ { name: 'Flow1', label: 'Flow 1' },
+ { name: 'Flow2', label: 'Flow 2' },
+ { name: 'Flow3', label: 'Flow 3' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockFlows);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/actions/custom/flow');
+ });
+
+ it('should execute flow getAll operation with limit', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'flow',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 2,
+ };
+ return params[param];
+ });
+
+ const mockFlows = {
+ actions: [
+ { name: 'Flow1', label: 'Flow 1' },
+ { name: 'Flow2', label: 'Flow 2' },
+ { name: 'Flow3', label: 'Flow 3' },
+ ],
+ };
+
+ salesforceApiRequestSpy.mockResolvedValue(mockFlows);
+
+ await node.execute.call(mockExecuteFunctions);
+ });
+ });
+
+ describe('Search Resource', () => {
+ it('should execute search query operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'search',
+ operation: 'query',
+ query: 'SELECT Id, Name FROM Account',
+ };
+ return params[param];
+ });
+
+ const mockResults = [
+ { Id: '001000001', Name: 'Account 1' },
+ { Id: '001000002', Name: 'Account 2' },
+ ];
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockResults);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ { q: 'SELECT Id, Name FROM Account' },
+ );
+ });
+ });
+
+ describe('Opportunity Resource', () => {
+ it('should handle opportunity create operation with all fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'opportunity',
+ operation: 'create',
+ name: 'Test Opportunity',
+ closeDate: '2024-12-31',
+ stageName: 'Qualification',
+ additionalFields: {
+ type: 'New Customer',
+ amount: 50000,
+ owner: 'user123',
+ nextStep: 'Follow up call',
+ accountId: 'acc123',
+ campaignId: 'camp123',
+ leadSource: 'Web',
+ description: 'Test opportunity',
+ probability: 75,
+ pricebook2Id: 'pb123',
+ forecastCategoryName: 'Pipeline',
+ customFieldsUi: {
+ customFieldsValues: [{ fieldId: 'Custom_Field__c', value: 'Custom Value' }],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'opp123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/sobjects/opportunity',
+ expect.objectContaining({
+ Name: 'Test Opportunity',
+ CloseDate: '2024-12-31',
+ StageName: 'Qualification',
+ Type: 'New Customer',
+ Amount: 50000,
+ OwnerId: 'user123',
+ NextStep: 'Follow up call',
+ AccountId: 'acc123',
+ CampaignId: 'camp123',
+ LeadSource: 'Web',
+ Description: 'Test opportunity',
+ Probability: 75,
+ Pricebook2Id: 'pb123',
+ ForecastCategoryName: 'Pipeline',
+ Custom_Field__c: 'Custom Value',
+ }),
+ );
+ });
+
+ it('should handle opportunity upsert operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'opportunity',
+ operation: 'upsert',
+ name: 'Upsert Opportunity',
+ closeDate: '2024-12-31',
+ stageName: 'Negotiation',
+ externalId: 'External_Id__c',
+ externalIdValue: 'EXT123',
+ additionalFields: {
+ type: 'Existing Customer',
+ External_Id__c: 'EXT123', // This should be removed from body
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'opp123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/opportunity/External_Id__c/EXT123',
+ expect.objectContaining({
+ Name: 'Upsert Opportunity',
+ CloseDate: '2024-12-31',
+ StageName: 'Negotiation',
+ Type: 'Existing Customer',
+ }),
+ );
+
+ // Ensure external ID field is removed from body
+ const callArgs = salesforceApiRequestSpy.mock.calls[0];
+ expect(callArgs[2]).not.toHaveProperty('External_Id__c');
+ });
+
+ it('should handle opportunity update operation with all fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'opportunity',
+ operation: 'update',
+ opportunityId: 'opp123',
+ updateFields: {
+ name: 'Updated Opportunity',
+ closeDate: '2025-01-31',
+ stageName: 'Closed Won',
+ type: 'Existing Customer',
+ amount: 75000,
+ owner: 'user456',
+ nextStep: 'Implementation',
+ accountId: 'acc456',
+ campaignId: 'camp456',
+ leadSource: 'Referral',
+ description: 'Updated description',
+ probability: 90,
+ pricebook2Id: 'pb456',
+ forecastCategoryName: 'Closed',
+ customFieldsUi: {
+ customFieldsValues: [{ fieldId: 'Custom_Field__c', value: 'Updated Value' }],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/opportunity/opp123',
+ expect.objectContaining({
+ Name: 'Updated Opportunity',
+ CloseDate: '2025-01-31',
+ StageName: 'Closed Won',
+ Type: 'Existing Customer',
+ Amount: 75000,
+ OwnerId: 'user456',
+ NextStep: 'Implementation',
+ AccountId: 'acc456',
+ CampaignId: 'camp456',
+ LeadSource: 'Referral',
+ Description: 'Updated description',
+ Probability: 90,
+ Pricebook2Id: 'pb456',
+ ForecastCategoryName: 'Closed',
+ Custom_Field__c: 'Updated Value',
+ }),
+ );
+ });
+ });
+
+ describe('Default Response Handling', () => {
+ it('should return default response when responseData is undefined', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'create',
+ company: 'Test Company',
+ lastname: 'Test',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ // Mock API to return undefined
+ salesforceApiRequestSpy.mockResolvedValue(undefined);
+
+ await node.execute.call(mockExecuteFunctions);
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should handle errors with continueOnFail true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'create',
+ company: 'Test Company',
+ lastname: 'Test',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ mockExecuteFunctions.continueOnFail.mockReturnValue(true);
+ salesforceApiRequestSpy.mockRejectedValue(new Error('API Error'));
+
+ await node.execute.call(mockExecuteFunctions);
+ });
+
+ it('should throw error with continueOnFail false', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'lead',
+ operation: 'create',
+ company: 'Test Company',
+ lastname: 'Test',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ mockExecuteFunctions.continueOnFail.mockReturnValue(false);
+ const testError = new Error('API Error');
+ salesforceApiRequestSpy.mockRejectedValue(testError);
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow('API Error');
+ });
+ });
+
+ describe('Multiple Items Processing', () => {
+ it('should process multiple input items', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ { json: { testData: 'value1' } },
+ { json: { testData: 'value2' } },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'search',
+ operation: 'query',
+ query: 'SELECT Id FROM Account',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestAllItemsSpy
+ .mockResolvedValueOnce([{ Id: '001000001', Name: 'Account 1' }])
+ .mockResolvedValueOnce([{ Id: '001000002', Name: 'Account 2' }]);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
+
+ describe('Execute Method - Account Resource Extended Fields', () => {
+ describe('Account Create Operation - Additional Fields', () => {
+ it('should handle account create with all additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'account',
+ operation: 'create',
+ name: 'Test Account',
+ additionalFields: {
+ fax: '555-0199',
+ type: 'Customer - Direct',
+ jigsaw: 'JIG123',
+ phone: '555-0100',
+ owner: 'user123',
+ sicDesc: 'Technology Services',
+ website: 'https://test.com',
+ industry: 'Technology',
+ parentId: 'parent123',
+ billingCity: 'San Francisco',
+ description: 'Test account description',
+ billingState: 'CA',
+ shippingCity: 'San Francisco',
+ accountNumber: 'ACC-001',
+ accountSource: 'Web',
+ annualRevenue: 1000000,
+ billingStreet: '123 Main St',
+ shippingState: 'CA',
+ billingCountry: 'USA',
+ shippingStreet: '456 Oak Ave',
+ shippingCountry: 'USA',
+ billingPostalCode: '94105',
+ numberOfEmployees: '50',
+ shippingPostalCode: '94105',
+ recordTypeId: 'rt123',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'acc123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/account', {
+ Name: 'Test Account',
+ Fax: '555-0199',
+ Type: 'Customer - Direct',
+ Jigsaw: 'JIG123',
+ Phone: '555-0100',
+ OwnerId: 'user123',
+ SicDesc: 'Technology Services',
+ Website: 'https://test.com',
+ Industry: 'Technology',
+ ParentId: 'parent123',
+ BillingCity: 'San Francisco',
+ Description: 'Test account description',
+ BillingState: 'CA',
+ ShippingCity: 'San Francisco',
+ AccountNumber: 'ACC-001',
+ AccountSource: 'Web',
+ AnnualRevenue: 1000000,
+ BillingStreet: '123 Main St',
+ ShippingState: 'CA',
+ BillingCountry: 'USA',
+ ShippingStreet: '456 Oak Ave',
+ ShippingCountry: 'USA',
+ BillingPostalCode: '94105',
+ NumberOfEmployees: '50',
+ ShippingPostalCode: '94105',
+ RecordTypeId: 'rt123',
+ });
+
+ expect(result).toEqual([[{ json: { id: 'acc123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle account create with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'account',
+ operation: 'create',
+ name: 'Test Account',
+ additionalFields: {
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Field__c', value: 'Custom Value 1' },
+ { fieldId: 'Another_Field__c', value: 'Custom Value 2' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'acc123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/account', {
+ Name: 'Test Account',
+ Custom_Field__c: 'Custom Value 1',
+ Another_Field__c: 'Custom Value 2',
+ });
+ });
+
+ it('should handle account upsert operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation(
+ (param: string, index?: number): any => {
+ if (param === 'externalId' && index === 0) return 'AccountNumber';
+ if (param === 'externalIdValue') return 'ACC-001';
+ const params: Record = {
+ resource: 'account',
+ operation: 'upsert',
+ name: 'Test Account',
+ additionalFields: {
+ fax: '555-0199',
+ type: 'Customer - Direct',
+ },
+ };
+ return params[param];
+ },
+ );
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'acc123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/account/AccountNumber/ACC-001',
+ {
+ Name: 'Test Account',
+ Fax: '555-0199',
+ Type: 'Customer - Direct',
+ },
+ );
+ });
+ });
+ });
+
+ describe('Execute Method - Task Resource Extended Fields', () => {
+ describe('Task Create Operation - Additional Fields', () => {
+ it('should handle task create with basic additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'create',
+ status: 'Not Started',
+ additionalFields: {
+ type: 'Call',
+ whoId: 'contact123',
+ whatId: 'account123',
+ owner: 'user123',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/task', {
+ Status: 'Not Started',
+ TaskSubtype: 'Call',
+ WhoId: 'contact123',
+ WhatId: 'account123',
+ OwnerId: 'user123',
+ });
+
+ expect(result).toEqual([[{ json: { id: 'task123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle task create with all extended additional fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'create',
+ status: 'In Progress',
+ additionalFields: {
+ activityDate: '2025-01-15',
+ isReminderSet: true,
+ recurrenceType: 'RecursDaily',
+ callDisposition: 'Completed',
+ reminderDateTime: '2025-01-15T10:00:00Z',
+ recurrenceInstance: 'First',
+ recurrenceInterval: 1,
+ recurrenceDayOfMonth: 15,
+ callDurationInSeconds: 1800,
+ recurrenceEndDateOnly: '2025-12-31',
+ recurrenceMonthOfYear: 'January',
+ recurrenceDayOfWeekMask: 'Monday',
+ recurrenceStartDateOnly: '2025-01-01',
+ recurrenceTimeZoneSidKey: 'America/Los_Angeles',
+ recurrenceRegeneratedType: 'RecurEvery',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/task', {
+ Status: 'In Progress',
+ ActivityDate: '2025-01-15',
+ IsReminderSet: true,
+ RecurrenceType: 'RecursDaily',
+ CallDisposition: 'Completed',
+ ReminderDateTime: '2025-01-15T10:00:00Z',
+ RecurrenceInstance: 'First',
+ RecurrenceInterval: 1,
+ RecurrenceDayOfMonth: 15,
+ CallDurationInSeconds: 1800,
+ RecurrenceEndDateOnly: '2025-12-31',
+ RecurrenceMonthOfYear: 'January',
+ RecurrenceDayOfWeekMask: 'Monday',
+ RecurrenceStartDateOnly: '2025-01-01',
+ RecurrenceTimeZoneSidKey: 'America/Los_Angeles',
+ RecurrenceRegeneratedType: 'RecurEvery',
+ });
+ });
+
+ it('should handle task create with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'create',
+ status: 'Not Started',
+ additionalFields: {
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Priority__c', value: 'High' },
+ { fieldId: 'Custom_Category__c', value: 'Sales' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/task', {
+ Status: 'Not Started',
+ Custom_Priority__c: 'High',
+ Custom_Category__c: 'Sales',
+ });
+ });
+ });
+
+ describe('Task Update Operation - Update Fields', () => {
+ it('should handle task update with basic fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'update',
+ taskId: 'task123',
+ updateFields: {
+ type: 'Email',
+ whoId: 'contact456',
+ status: 'Completed',
+ whatId: 'account456',
+ owner: 'user456',
+ subject: 'Updated Task',
+ callType: 'Outbound',
+ priority: 'High',
+ callObject: 'Lead',
+ description: 'Updated description',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('PATCH', '/sobjects/task/task123', {
+ TaskSubtype: 'Email',
+ WhoId: 'contact456',
+ Status: 'Completed',
+ WhatId: 'account456',
+ OwnerId: 'user456',
+ Subject: 'Updated Task',
+ CallType: 'Outbound',
+ Priority: 'High',
+ CallObject: 'Lead',
+ Description: 'Updated description',
+ });
+
+ expect(result).toEqual([[{ json: { id: 'task123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle task update with all extended fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'update',
+ taskId: 'task123',
+ updateFields: {
+ activityDate: '2025-01-20',
+ isReminderSet: false,
+ recurrenceType: 'RecursWeekly',
+ callDisposition: 'No Answer',
+ reminderDateTime: '2025-01-20T14:00:00Z',
+ recurrenceInstance: 'Second',
+ recurrenceInterval: 2,
+ recurrenceDayOfMonth: 20,
+ callDurationInSeconds: 900,
+ recurrenceEndDateOnly: '2025-06-30',
+ recurrenceMonthOfYear: 'June',
+ recurrenceDayOfWeekMask: 'Friday',
+ recurrenceStartDateOnly: '2025-01-20',
+ recurrenceTimeZoneSidKey: 'America/New_York',
+ recurrenceRegeneratedType: 'RecurChild',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('PATCH', '/sobjects/task/task123', {
+ ActivityDate: '2025-01-20',
+ IsReminderSet: false,
+ RecurrenceType: 'RecursWeekly',
+ CallDisposition: 'No Answer',
+ ReminderDateTime: '2025-01-20T14:00:00Z',
+ RecurrenceInstance: 'Second',
+ RecurrenceInterval: 2,
+ RecurrenceDayOfMonth: 20,
+ CallDurationInSeconds: 900,
+ RecurrenceEndDateOnly: '2025-06-30',
+ RecurrenceMonthOfYear: 'June',
+ RecurrenceDayOfWeekMask: 'Friday',
+ RecurrenceStartDateOnly: '2025-01-20',
+ RecurrenceTimeZoneSidKey: 'America/New_York',
+ RecurrenceRegeneratedType: 'RecurChild',
+ });
+ });
+
+ it('should handle task update with custom fields', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'update',
+ taskId: 'task123',
+ updateFields: {
+ subject: 'Updated Task',
+ customFieldsUi: {
+ customFieldsValues: [
+ { fieldId: 'Custom_Status__c', value: 'In Review' },
+ { fieldId: 'Custom_Notes__c', value: 'Updated notes' },
+ ],
+ },
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'task123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('PATCH', '/sobjects/task/task123', {
+ Subject: 'Updated Task',
+ Custom_Status__c: 'In Review',
+ Custom_Notes__c: 'Updated notes',
+ });
+ });
+ });
+
+ describe('Task GetAll Operation - Query and Error Handling', () => {
+ it('should handle task getAll with limit', async () => {
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,Subject,Status FROM Task LIMIT 10');
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 10,
+ options: { fields: 'Id,Subject,Status' },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue([
+ { Id: 'task1', Subject: 'Task 1', Status: 'Open' },
+ { Id: 'task2', Subject: 'Task 2', Status: 'Completed' },
+ ]);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith(
+ { fields: 'Id,Subject,Status' },
+ 'Task',
+ false,
+ 10,
+ );
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: 'SELECT Id,Subject,Status FROM Task LIMIT 10',
+ },
+ );
+
+ expect(result).toEqual([
+ [
+ { json: { Id: 'task1', Subject: 'Task 1', Status: 'Open' }, pairedItem: 0 },
+ { json: { Id: 'task2', Subject: 'Task 2', Status: 'Completed' }, pairedItem: 1 },
+ ],
+ ]);
+ });
+
+ it('should handle task getAll operation error handling', async () => {
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id FROM Task LIMIT 5');
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'task',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 5,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const testError = new Error('Query failed');
+ salesforceApiRequestAllItemsSpy.mockRejectedValue(testError);
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow('Query failed');
+ });
+ });
+ });
+
+ describe('Execute Method - Attachment Resource', () => {
+ describe('Attachment Create Operation', () => {
+ it('should handle attachment create with minimal fields', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ binary: {
+ data: {
+ data: 'SGVsbG8gV29ybGQ=',
+ mimeType: 'text/plain',
+ fileName: 'test.txt',
+ },
+ },
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'create',
+ name: 'Test File',
+ parentId: 'parent123',
+ binaryPropertyName: 'data',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'att123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/attachment', {
+ Name: 'Test File',
+ ParentId: 'parent123',
+ Body: 'SGVsbG8gV29ybGQ=',
+ ContentType: 'text/plain',
+ });
+
+ expect(result).toEqual([[{ json: { id: 'att123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle attachment create with all additional fields', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ binary: {
+ document: {
+ data: 'UERGIGRhdGE=',
+ mimeType: 'application/pdf',
+ fileName: 'document.pdf',
+ },
+ },
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'create',
+ name: 'Important Document',
+ parentId: 'parent456',
+ binaryPropertyName: 'document',
+ additionalFields: {
+ description: 'Important business document',
+ owner: 'user123',
+ isPrivate: true,
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'att456', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('POST', '/sobjects/attachment', {
+ Name: 'Important Document',
+ ParentId: 'parent456',
+ Body: 'UERGIGRhdGE=',
+ ContentType: 'application/pdf',
+ Description: 'Important business document',
+ OwnerId: 'user123',
+ IsPrivate: true,
+ });
+ });
+
+ it('should throw error when binary property does not exist', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ binary: {},
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'create',
+ name: 'Test File',
+ parentId: 'parent123',
+ binaryPropertyName: 'nonexistent',
+ additionalFields: {},
+ };
+ return params[param];
+ });
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'The property nonexistent does not exist',
+ );
+ });
+ });
+
+ describe('Attachment Update Operation', () => {
+ it('should handle attachment update with binary data', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ binary: {
+ newFile: {
+ data: 'VXBkYXRlZCBkYXRh',
+ mimeType: 'text/plain',
+ fileName: 'updated.txt',
+ },
+ },
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'update',
+ attachmentId: 'att123',
+ updateFields: {
+ name: 'Updated File Name',
+ binaryPropertyName: 'newFile',
+ description: 'Updated description',
+ owner: 'user456',
+ isPrivate: false,
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'att123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/attachment/att123',
+ {
+ Name: 'Updated File Name',
+ Body: 'VXBkYXRlZCBkYXRh',
+ ContentType: 'text/plain',
+ Description: 'Updated description',
+ OwnerId: 'user456',
+ IsPrivate: false,
+ },
+ );
+
+ expect(result).toEqual([[{ json: { id: 'att123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle attachment update without binary data', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'update',
+ attachmentId: 'att123',
+ updateFields: {
+ name: 'Updated Name Only',
+ description: 'Updated description',
+ },
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'att123', success: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'PATCH',
+ '/sobjects/attachment/att123',
+ {
+ Name: 'Updated Name Only',
+ Description: 'Updated description',
+ },
+ );
+ });
+
+ it('should throw error when binary property does not exist for update', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([
+ {
+ json: {},
+ binary: {},
+ },
+ ]);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'update',
+ attachmentId: 'att123',
+ updateFields: {
+ binaryPropertyName: 'missing',
+ },
+ };
+ return params[param];
+ });
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'The property missing does not exist',
+ );
+ });
+ });
+
+ describe('Attachment Get Operation', () => {
+ it('should handle attachment get operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'get',
+ attachmentId: 'att123',
+ };
+ return params[param];
+ });
+
+ const mockAttachment = {
+ Id: 'att123',
+ Name: 'Test File',
+ ParentId: 'parent123',
+ ContentType: 'text/plain',
+ };
+ salesforceApiRequestSpy.mockResolvedValue(mockAttachment);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/attachment/att123');
+
+ expect(result).toEqual([[{ json: mockAttachment, pairedItem: 0 }]]);
+ });
+ });
+
+ describe('Attachment GetAll Operation', () => {
+ it('should handle attachment getAll with returnAll true', async () => {
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,Name,ParentId FROM Attachment');
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'getAll',
+ returnAll: true,
+ options: { fields: 'Id,Name,ParentId' },
+ };
+ return params[param];
+ });
+
+ const mockAttachments = [
+ { Id: 'att1', Name: 'File 1', ParentId: 'parent1' },
+ { Id: 'att2', Name: 'File 2', ParentId: 'parent2' },
+ ];
+ salesforceApiRequestAllItemsSpy.mockResolvedValue(mockAttachments);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith(
+ { fields: 'Id,Name,ParentId' },
+ 'Attachment',
+ true,
+ );
+ expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'records',
+ 'GET',
+ '/query',
+ {},
+ {
+ q: 'SELECT Id,Name,ParentId FROM Attachment',
+ },
+ );
+
+ expect(result).toEqual([
+ [
+ { json: { Id: 'att1', Name: 'File 1', ParentId: 'parent1' }, pairedItem: 0 },
+ { json: { Id: 'att2', Name: 'File 2', ParentId: 'parent2' }, pairedItem: 1 },
+ ],
+ ]);
+ });
+
+ it('should handle attachment getAll with limit', async () => {
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id,Name FROM Attachment LIMIT 5');
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 5,
+ options: {},
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestAllItemsSpy.mockResolvedValue([{ Id: 'att1', Name: 'File 1' }]);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(getQuerySpy).toHaveBeenCalledWith({}, 'Attachment', false, 5);
+ });
+
+ it('should handle attachment getAll operation error handling', async () => {
+ const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery');
+ getQuerySpy.mockReturnValue('SELECT Id FROM Attachment');
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'getAll',
+ returnAll: true,
+ options: {},
+ };
+ return params[param];
+ });
+
+ const testError = new Error('Query execution failed');
+ salesforceApiRequestAllItemsSpy.mockRejectedValue(testError);
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'Query execution failed',
+ );
+ });
+ });
+
+ describe('Attachment Delete Operation', () => {
+ it('should handle attachment delete operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'delete',
+ attachmentId: 'att123',
+ };
+ return params[param];
+ });
+
+ salesforceApiRequestSpy.mockResolvedValue({ id: 'att123', success: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
+ 'DELETE',
+ '/sobjects/attachment/att123',
+ );
+
+ expect(result).toEqual([[{ json: { id: 'att123', success: true }, pairedItem: 0 }]]);
+ });
+
+ it('should handle attachment delete operation error handling', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'delete',
+ attachmentId: 'att123',
+ };
+ return params[param];
+ });
+
+ const testError = new Error('Delete operation failed');
+ salesforceApiRequestSpy.mockRejectedValue(testError);
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'Delete operation failed',
+ );
+ });
+ });
+
+ describe('Attachment GetSummary Operation', () => {
+ it('should handle attachment getSummary operation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
+ const params: Record = {
+ resource: 'attachment',
+ operation: 'getSummary',
+ };
+ return params[param];
+ });
+
+ const mockSummary = {
+ objectDescribe: {
+ name: 'Attachment',
+ label: 'Attachment',
+ fields: [{ name: 'Id' }, { name: 'Name' }],
+ },
+ };
+ salesforceApiRequestSpy.mockResolvedValue(mockSummary);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/sobjects/attachment');
+
+ expect(result).toEqual([[{ json: mockSummary, pairedItem: 0 }]]);
+ });
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts
new file mode 100644
index 00000000000..66d46e61647
--- /dev/null
+++ b/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts
@@ -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;
+ 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();
+ 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();
+ 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();
+ 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();
+ 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 = {
+ 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);
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Slack/test/v2/GenericFunctions.test.ts b/packages/nodes-base/nodes/Slack/test/v2/GenericFunctions.test.ts
index bb0f0e465f1..37008800fc4 100644
--- a/packages/nodes-base/nodes/Slack/test/v2/GenericFunctions.test.ts
+++ b/packages/nodes-base/nodes/Slack/test/v2/GenericFunctions.test.ts
@@ -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;
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;
});
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 _',
+ },
+ });
+ 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 _',
+ );
+ });
+
+ 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 _',
+ });
+ });
+
+ 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 _',
+ });
+ });
+ });
+
+ 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!');
+ });
+ });
});
diff --git a/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts b/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts
new file mode 100644
index 00000000000..d8d7f05386e
--- /dev/null
+++ b/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts
@@ -0,0 +1,1547 @@
+import { mockDeep } from 'jest-mock-extended';
+import type {
+ IExecuteFunctions,
+ INode,
+ INodeExecutionData,
+ INodeParameterResourceLocator,
+} from 'n8n-workflow';
+import { NodeOperationError } from 'n8n-workflow';
+
+import { SlackV2 } from '../../V2/SlackV2.node';
+import * as GenericFunctions from '../../V2/GenericFunctions';
+
+describe('SlackV2', () => {
+ let node: SlackV2;
+ let mockExecuteFunctions: jest.Mocked;
+ let slackApiRequestSpy: jest.SpyInstance;
+ let slackApiRequestAllItemsSpy: jest.SpyInstance;
+
+ const mockNode: INode = {
+ id: 'test-node-id',
+ name: 'Slack Test',
+ type: 'n8n-nodes-base.slack',
+ typeVersion: 2,
+ position: [0, 0],
+ parameters: {},
+ };
+
+ beforeEach(() => {
+ node = new SlackV2({
+ name: 'Slack',
+ displayName: 'Slack',
+ description: 'Slack node test',
+ group: ['input'],
+ });
+
+ mockExecuteFunctions = mockDeep();
+ slackApiRequestSpy = jest.spyOn(GenericFunctions, 'slackApiRequest');
+ slackApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'slackApiRequestAllItems');
+
+ jest.clearAllMocks();
+
+ // Default mocks
+ mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
+ mockExecuteFunctions.getNode.mockReturnValue(mockNode);
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const mockParams: Record = {
+ authentication: 'accessToken',
+ resource: 'channel',
+ operation: 'kick',
+ };
+ return mockParams[paramName];
+ });
+ mockExecuteFunctions.continueOnFail.mockReturnValue(false);
+ // Mock helper functions
+ (mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
+ (data: any, options: any) => {
+ return data.map((item: any, index: number) => ({
+ ...item,
+ pairedItem: { item: options?.itemData?.item ?? index },
+ }));
+ },
+ );
+ (mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: any) => {
+ // returnJsonArray should always return array with single item containing the data
+ return [{ json: data }];
+ });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('Channel Operations - Kick and Join', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ authentication: 'accessToken',
+ resource: 'channel',
+ operation: 'kick',
+ channelId: 'C123456789',
+ userId: 'U987654321',
+ };
+ return params[paramName];
+ });
+ });
+
+ it('should kick user from channel successfully', async () => {
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.kick',
+ {
+ channel: 'C123456789',
+ user: 'U987654321',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+
+ it('should join channel successfully', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ authentication: 'accessToken',
+ resource: 'channel',
+ operation: 'join',
+ channelId: 'C123456789',
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'C123456789',
+ name: 'general',
+ is_channel: true,
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.join',
+ {
+ channel: 'C123456789',
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should handle API error when kicking user', async () => {
+ slackApiRequestSpy.mockRejectedValue(new Error('Channel not found'));
+ mockExecuteFunctions.continueOnFail.mockReturnValue(true);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([[{ json: { error: 'Channel not found' } }]]);
+ });
+ });
+
+ describe('Channel Operations - History, Invite, Leave, Members, etc.', () => {
+ it('should get channel history with pagination', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'history',
+ channelId: 'C123456789',
+ returnAll: false,
+ limit: 50,
+ filters: {},
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ messages: [
+ { type: 'message', text: 'Hello', ts: '1234567890.123456' },
+ { type: 'message', text: 'World', ts: '1234567891.123456' },
+ ],
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/conversations.history',
+ {},
+ {
+ channel: 'C123456789',
+ limit: 50,
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.messages,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should get all channel history', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'history',
+ channelId: 'C123456789',
+ returnAll: true,
+ filters: {
+ latest: '2023-12-01',
+ oldest: '2023-11-01',
+ inclusive: true,
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockMessages = [
+ { type: 'message', text: 'Message 1', ts: '1234567890.123456' },
+ { type: 'message', text: 'Message 2', ts: '1234567891.123456' },
+ ];
+ slackApiRequestAllItemsSpy.mockResolvedValue(mockMessages);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'messages',
+ 'GET',
+ '/conversations.history',
+ {},
+ {
+ channel: 'C123456789',
+ inclusive: true,
+ latest: new Date('2023-12-01').getTime() / 1000,
+ oldest: new Date('2023-11-01').getTime() / 1000,
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockMessages,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should invite users to channel', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'invite',
+ channelId: 'C123456789',
+ userIds: ['U111111111', 'U222222222'],
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'C123456789',
+ name: 'general',
+ members: ['U111111111', 'U222222222'],
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.invite',
+ {
+ channel: 'C123456789',
+ users: 'U111111111,U222222222',
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should leave channel', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'leave',
+ channelId: 'C123456789',
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.leave',
+ {
+ channel: 'C123456789',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+
+ it('should get channel members with resolve data', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'member',
+ channelId: 'C123456789',
+ returnAll: true,
+ resolveData: true,
+ };
+ return params[paramName];
+ });
+
+ const mockMembers = ['U111111111', 'U222222222'];
+ slackApiRequestAllItemsSpy.mockResolvedValue(mockMembers);
+
+ // Mock user info responses
+ slackApiRequestSpy
+ .mockResolvedValueOnce({
+ user: {
+ id: 'U111111111',
+ name: 'john.doe',
+ real_name: 'John Doe',
+ },
+ })
+ .mockResolvedValueOnce({
+ user: {
+ id: 'U222222222',
+ name: 'jane.smith',
+ real_name: 'Jane Smith',
+ },
+ });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'members',
+ 'GET',
+ '/conversations.members',
+ {},
+ {
+ channel: 'C123456789',
+ },
+ );
+ expect(slackApiRequestSpy).toHaveBeenCalledTimes(2);
+ expect(result[0][0].json).toEqual([
+ {
+ id: 'U111111111',
+ name: 'john.doe',
+ real_name: 'John Doe',
+ },
+ {
+ id: 'U222222222',
+ name: 'jane.smith',
+ real_name: 'Jane Smith',
+ },
+ ]);
+ });
+
+ it('should open conversation', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'open',
+ options: {
+ users: ['U111111111', 'U222222222'],
+ returnIm: true,
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'D123456789',
+ created: 1234567890,
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.open',
+ {
+ users: 'U111111111,U222222222',
+ return_im: true,
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should rename channel', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'rename',
+ channelId: 'C123456789',
+ name: 'new-channel-name',
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'C123456789',
+ name: 'new-channel-name',
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.rename',
+ {
+ channel: 'C123456789',
+ name: 'new-channel-name',
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should get conversation replies', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'replies',
+ channelId: 'C123456789',
+ ts: '1234567890.123456',
+ returnAll: false,
+ limit: 100,
+ filters: {
+ inclusive: true,
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ messages: [
+ { type: 'message', text: 'Original message', ts: '1234567890.123456' },
+ {
+ type: 'message',
+ text: 'Reply 1',
+ ts: '1234567891.123456',
+ thread_ts: '1234567890.123456',
+ },
+ ],
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/conversations.replies',
+ {},
+ {
+ channel: 'C123456789',
+ ts: '1234567890.123456',
+ limit: 100,
+ inclusive: true,
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.messages,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should set channel purpose', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'setPurpose',
+ channelId: 'C123456789',
+ purpose: 'New channel purpose',
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'C123456789',
+ purpose: {
+ value: 'New channel purpose',
+ creator: 'U123456789',
+ last_set: 1234567890,
+ },
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.setPurpose',
+ {
+ channel: 'C123456789',
+ purpose: 'New channel purpose',
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should set channel topic', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'setTopic',
+ channelId: 'C123456789',
+ topic: 'New channel topic',
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ channel: {
+ id: 'C123456789',
+ topic: {
+ value: 'New channel topic',
+ creator: 'U123456789',
+ last_set: 1234567890,
+ },
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.setTopic',
+ {
+ channel: 'C123456789',
+ topic: 'New channel topic',
+ },
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.channel,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should unarchive channel', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'unarchive',
+ channelId: 'C123456789',
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/conversations.unarchive',
+ {
+ channel: 'C123456789',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+ });
+
+ describe('Message Operations - Ephemeral Messages', () => {
+ beforeEach(() => {
+ jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('C123456789');
+ jest.spyOn(GenericFunctions, 'getMessageContent').mockReturnValue({
+ text: 'Test ephemeral message',
+ });
+ jest.spyOn(GenericFunctions, 'processThreadOptions').mockReturnValue({});
+ });
+
+ it('should send ephemeral message to channel', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'message',
+ operation: 'post',
+ select: 'channel',
+ otherOptions: {
+ ephemeral: {
+ ephemeralValues: {
+ user: {
+ mode: 'id',
+ value: 'U123456789',
+ } as INodeParameterResourceLocator,
+ },
+ },
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ ok: true,
+ message: {
+ type: 'message',
+ text: 'Test ephemeral message',
+ user: 'U123456789',
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/chat.postEphemeral',
+ expect.objectContaining({
+ channel: 'C123456789',
+ text: 'Test ephemeral message',
+ user: 'U123456789',
+ }),
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should send ephemeral message to user with username format', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'message',
+ operation: 'post',
+ select: 'channel',
+ otherOptions: {
+ ephemeral: {
+ ephemeralValues: {
+ user: {
+ mode: 'username',
+ value: 'john.doe',
+ } as INodeParameterResourceLocator,
+ },
+ },
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = { ok: true };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/chat.postEphemeral',
+ expect.objectContaining({
+ user: '@john.doe',
+ }),
+ {},
+ );
+ });
+
+ it('should send ephemeral message to user directly', async () => {
+ jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('U123456789');
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'message',
+ operation: 'post',
+ select: 'user',
+ otherOptions: {
+ ephemeral: {},
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = { ok: true };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/chat.postEphemeral',
+ expect.objectContaining({
+ channel: 'U123456789',
+ user: 'U123456789',
+ }),
+ {},
+ );
+ });
+ });
+
+ describe('Reaction and Star Operations', () => {
+ describe('Reaction Operations', () => {
+ it('should add reaction to message', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'reaction',
+ operation: 'add',
+ channelId: 'C123456789',
+ timestamp: '1234567890.123456',
+ name: 'thumbs_up',
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/reactions.add',
+ {
+ channel: 'C123456789',
+ name: 'thumbs_up',
+ timestamp: '1234567890.123456',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+
+ it('should remove reaction from message', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'reaction',
+ operation: 'remove',
+ channelId: 'C123456789',
+ timestamp: '1234567890.123456',
+ name: 'thumbs_up',
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/reactions.remove',
+ {
+ channel: 'C123456789',
+ name: 'thumbs_up',
+ timestamp: '1234567890.123456',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+
+ it('should get reactions for message', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'reaction',
+ operation: 'get',
+ channelId: 'C123456789',
+ timestamp: '1234567890.123456',
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ message: {
+ reactions: [
+ {
+ name: 'thumbs_up',
+ count: 2,
+ users: ['U111111111', 'U222222222'],
+ },
+ ],
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/reactions.get',
+ {},
+ {
+ channel: 'C123456789',
+ timestamp: '1234567890.123456',
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('Star Operations', () => {
+ it('should add star to message', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'star',
+ operation: 'add',
+ target: 'message',
+ channelId: 'C123456789',
+ timestamp: '1234567890.123456',
+ options: {},
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/stars.add',
+ {
+ channel: 'C123456789',
+ timestamp: '1234567890.123456',
+ },
+ {},
+ );
+ expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
+ });
+
+ it('should add star to file', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'star',
+ operation: 'add',
+ target: 'file',
+ channelId: 'C123456789',
+ fileId: 'F123456789',
+ options: {
+ fileComment: 'FC123456789',
+ },
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/stars.add',
+ {
+ channel: 'C123456789',
+ file: 'F123456789',
+ file_comment: 'FC123456789',
+ },
+ {},
+ );
+ });
+
+ it('should remove star', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'star',
+ operation: 'delete',
+ options: {
+ channelId: 'C123456789',
+ timestamp: '1234567890.123456',
+ fileId: 'F123456789',
+ fileComment: 'FC123456789',
+ },
+ };
+ return params[paramName];
+ });
+
+ slackApiRequestSpy.mockResolvedValue({ ok: true });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/stars.remove',
+ {
+ channel: 'C123456789',
+ file: 'F123456789',
+ file_comment: 'FC123456789',
+ timestamp: '1234567890.123456',
+ },
+ {},
+ );
+ });
+
+ it('should get all stars', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'star',
+ operation: 'getAll',
+ returnAll: true,
+ };
+ return params[paramName];
+ });
+
+ const mockStars = [
+ {
+ type: 'message',
+ message: {
+ text: 'Starred message',
+ ts: '1234567890.123456',
+ },
+ },
+ ];
+ slackApiRequestAllItemsSpy.mockResolvedValue(mockStars);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
+ 'items',
+ 'GET',
+ '/stars.list',
+ {},
+ {},
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockStars,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should get stars with limit', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'star',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 10,
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ items: [
+ {
+ type: 'message',
+ message: {
+ text: 'Starred message',
+ ts: '1234567890.123456',
+ },
+ },
+ ],
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'GET',
+ '/stars.list',
+ {},
+ {
+ limit: 10,
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.items,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+ });
+ });
+
+ describe('File Upload Operations - Setup', () => {
+ it('should set up file upload options correctly', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'file',
+ operation: 'upload',
+ options: {
+ channelIds: ['C111111111', 'C222222222'],
+ channelId: 'C123456789',
+ initialComment: 'File upload test',
+ threadTs: '1234567890.123456',
+ },
+ };
+ return params[paramName];
+ });
+
+ // We'll test the parameter extraction logic
+ expect(mockExecuteFunctions.getNodeParameter('options', 0)).toEqual({
+ channelIds: ['C111111111', 'C222222222'],
+ channelId: 'C123456789',
+ initialComment: 'File upload test',
+ threadTs: '1234567890.123456',
+ });
+ });
+ });
+
+ describe('File Upload Operations - Binary Data Handling', () => {
+ beforeEach(() => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'file',
+ operation: 'upload',
+ binaryData: true,
+ binaryPropertyName: 'data',
+ options: {
+ title: 'Test File',
+ channelIds: ['C123456789'],
+ initialComment: 'Test upload',
+ },
+ };
+ return params[paramName];
+ });
+ });
+
+ it('should upload file with binary data (node version <= 2.1)', async () => {
+ mockExecuteFunctions.getNode.mockReturnValue({
+ ...mockNode,
+ typeVersion: 2.1,
+ });
+
+ const mockBinaryData = {
+ data: 'base64encodeddata', // cspell:disable-line
+ fileName: 'test.txt',
+ mimeType: 'text/plain',
+ };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
+
+ const mockResponse = {
+ file: {
+ id: 'F123456789',
+ name: 'test.txt',
+ mimetype: 'text/plain',
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/files.upload',
+ {},
+ {},
+ { 'Content-Type': 'multipart/form-data' },
+ {
+ formData: expect.objectContaining({
+ channels: 'C123456789',
+ initial_comment: 'Test upload',
+ title: 'Test File',
+ file: expect.objectContaining({
+ value: expect.any(Buffer),
+ options: {
+ filename: 'test.txt',
+ contentType: 'text/plain',
+ },
+ }),
+ }),
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.file,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should upload file with external upload (node version > 2.1)', async () => {
+ mockExecuteFunctions.getNode.mockReturnValue({
+ ...mockNode,
+ typeVersion: 2.2,
+ });
+
+ const mockBinaryData = {
+ id: 'binary-data-id',
+ fileName: 'test.txt',
+ mimeType: 'text/plain',
+ };
+
+ const mockStream = Buffer.from('file content');
+ const mockMetadata = { fileSize: 12 };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
+ (mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(mockStream);
+ (mockExecuteFunctions.helpers.getBinaryMetadata as jest.Mock).mockResolvedValue(mockMetadata);
+
+ // Mock the upload URL response
+ const mockUploadUrlResponse = {
+ upload_url: 'https://files.slack.com/upload/v1',
+ file_id: 'F123456789',
+ };
+ slackApiRequestSpy
+ .mockResolvedValueOnce(mockUploadUrlResponse) // getUploadURLExternal
+ .mockResolvedValueOnce({ ok: true }) // upload to external URL
+ .mockResolvedValueOnce({
+ // completeUploadExternal
+ files: [
+ {
+ id: 'F123456789',
+ title: 'Test File',
+ },
+ ],
+ });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledTimes(3);
+
+ // Check getUploadURLExternal call
+ expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
+ 1,
+ 'GET',
+ '/files.getUploadURLExternal',
+ {},
+ {
+ filename: 'test.txt',
+ length: 12,
+ },
+ );
+
+ // Check external upload call
+ expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
+ 2,
+ 'POST',
+ 'https://files.slack.com/upload/v1',
+ {},
+ {},
+ { 'Content-Type': 'multipart/form-data' },
+ {
+ formData: expect.objectContaining({
+ file: expect.objectContaining({
+ value: mockStream,
+ }),
+ }),
+ },
+ );
+
+ // Check completeUploadExternal call
+ expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
+ 3,
+ 'POST',
+ '/files.completeUploadExternal',
+ {
+ channels: 'C123456789',
+ initial_comment: 'Test upload',
+ files: [
+ {
+ id: 'F123456789',
+ title: 'Test File',
+ },
+ ],
+ },
+ );
+
+ expect(result).toEqual([
+ [
+ {
+ json: [
+ {
+ id: 'F123456789',
+ title: 'Test File',
+ },
+ ],
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should upload file content as text', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'file',
+ operation: 'upload',
+ binaryData: false,
+ fileContent: 'This is text file content',
+ options: {
+ channelIds: ['C123456789'],
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockResponse = {
+ file: {
+ id: 'F123456789',
+ name: 'content.txt',
+ },
+ };
+ slackApiRequestSpy.mockResolvedValue(mockResponse);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenCalledWith(
+ 'POST',
+ '/files.upload',
+ {
+ channels: 'C123456789',
+ content: 'This is text file content',
+ },
+ {},
+ { 'Content-Type': 'application/x-www-form-urlencoded' },
+ {
+ form: expect.objectContaining({
+ channels: 'C123456789',
+ content: 'This is text file content',
+ }),
+ },
+ );
+ expect(result).toEqual([
+ [
+ {
+ json: mockResponse.file,
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should handle binary data without id (legacy format)', async () => {
+ mockExecuteFunctions.getNode.mockReturnValue({
+ ...mockNode,
+ typeVersion: 2.2,
+ });
+
+ const mockBinaryData = {
+ data: 'YmFzZTY0ZGF0YQ==', // base64 encoded data
+ fileName: 'test.txt',
+ mimeType: 'text/plain',
+ };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
+
+ const mockUploadUrlResponse = {
+ upload_url: 'https://files.slack.com/upload/v1',
+ file_id: 'F123456789',
+ };
+
+ slackApiRequestSpy
+ .mockResolvedValueOnce(mockUploadUrlResponse)
+ .mockResolvedValueOnce({ ok: true })
+ .mockResolvedValueOnce({
+ files: [{ id: 'F123456789', title: 'test.txt' }],
+ });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
+ 1,
+ 'GET',
+ '/files.getUploadURLExternal',
+ {},
+ {
+ filename: 'test.txt',
+ length: 10, // length of decoded 'base64data'
+ },
+ );
+
+ expect(result).toEqual([
+ [
+ {
+ json: [{ id: 'F123456789', title: 'test.txt' }],
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should use custom filename when provided', async () => {
+ mockExecuteFunctions.getNode.mockReturnValue({
+ ...mockNode,
+ typeVersion: 2.2,
+ });
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'file',
+ operation: 'upload',
+ binaryData: true,
+ binaryPropertyName: 'data',
+ options: {
+ fileName: 'custom-name.txt',
+ title: 'Custom Title',
+ },
+ };
+ return params[paramName];
+ });
+
+ const mockBinaryData = {
+ id: 'binary-data-id',
+ fileName: 'original.txt',
+ mimeType: 'text/plain',
+ };
+
+ (mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
+ (mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(
+ Buffer.from('test'),
+ );
+ (mockExecuteFunctions.helpers.getBinaryMetadata as jest.Mock).mockResolvedValue({
+ fileSize: 4,
+ });
+
+ slackApiRequestSpy
+ .mockResolvedValueOnce({ upload_url: 'https://test.com', file_id: 'F123' })
+ .mockResolvedValueOnce({ ok: true })
+ .mockResolvedValueOnce({ files: [{ id: 'F123', title: 'Custom Title' }] });
+
+ await node.execute.call(mockExecuteFunctions);
+
+ expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
+ 1,
+ 'GET',
+ '/files.getUploadURLExternal',
+ {},
+ {
+ filename: 'custom-name.txt',
+ length: 4,
+ },
+ );
+ });
+ });
+
+ describe('Error Handling with continueOnFail', () => {
+ it('should continue execution on API error when continueOnFail is true', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'kick',
+ channelId: 'C123456789',
+ userId: 'U987654321',
+ };
+ return params[paramName];
+ });
+
+ mockExecuteFunctions.continueOnFail.mockReturnValue(true);
+ slackApiRequestSpy.mockRejectedValue(new Error('Channel not found'));
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([
+ [
+ {
+ json: { error: 'Channel not found' },
+ },
+ ],
+ ]);
+ });
+
+ it('should throw error when continueOnFail is false', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'channel',
+ operation: 'kick',
+ channelId: 'C123456789',
+ userId: 'U987654321',
+ };
+ return params[paramName];
+ });
+
+ mockExecuteFunctions.continueOnFail.mockReturnValue(false);
+ const apiError = new NodeOperationError(mockNode, 'API Error');
+ slackApiRequestSpy.mockRejectedValue(apiError);
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(apiError);
+ });
+
+ it('should handle multiple items with mixed success and failure', async () => {
+ const inputData: INodeExecutionData[] = [
+ { json: { id: 1 } },
+ { json: { id: 2 } },
+ { json: { id: 3 } },
+ ];
+
+ mockExecuteFunctions.getInputData.mockReturnValue(inputData);
+ mockExecuteFunctions.continueOnFail.mockReturnValue(true);
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ return {
+ resource: 'channel',
+ operation: 'kick',
+ channelId: 'C123456789',
+ userId: 'U987654321',
+ }[paramName];
+ });
+
+ // First call succeeds, second fails, third succeeds
+ slackApiRequestSpy
+ .mockResolvedValueOnce({ ok: true })
+ .mockRejectedValueOnce(new Error('User not found'))
+ .mockResolvedValueOnce({ ok: true });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([
+ [
+ { json: { ok: true }, pairedItem: { item: 0 } },
+ { json: { error: 'User not found' } },
+ { json: { ok: true }, pairedItem: { item: 2 } },
+ ],
+ ]);
+ });
+
+ it('should handle JSON errors correctly', async () => {
+ mockExecuteFunctions.continueOnFail.mockReturnValue(true);
+
+ const jsonError = {
+ message: 'Invalid JSON response',
+ code: 'INVALID_JSON',
+ };
+
+ slackApiRequestSpy.mockRejectedValue(jsonError);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([
+ [
+ {
+ json: { error: 'Invalid JSON response' },
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('Edge Cases and Error Scenarios', () => {
+ it('should handle empty input data', async () => {
+ mockExecuteFunctions.getInputData.mockReturnValue([]);
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([[]]);
+ });
+
+ it('should handle unknown resource/operation combination', async () => {
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ const params: Record = {
+ resource: 'unknown',
+ operation: 'unknown',
+ };
+ return params[paramName];
+ });
+
+ const result = await node.execute.call(mockExecuteFunctions);
+
+ expect(result).toEqual([
+ [
+ {
+ json: {
+ error: 'Resource unknown / operation unknown not found!',
+ },
+ pairedItem: { item: 0 },
+ },
+ ],
+ ]);
+ });
+
+ it('should throw error for ephemeral message with username when select is user', async () => {
+ jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('U123456789');
+ jest.spyOn(GenericFunctions, 'getMessageContent').mockReturnValue({
+ text: 'Test message',
+ });
+ jest.spyOn(GenericFunctions, 'processThreadOptions').mockReturnValue({});
+
+ mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
+ if (paramName === 'resource') return 'message';
+ if (paramName === 'operation') return 'post';
+ if (paramName === 'select') return 'user';
+ if (paramName === 'user') {
+ return {
+ mode: 'username',
+ value: 'john.doe',
+ } as INodeParameterResourceLocator;
+ }
+ if (paramName === 'otherOptions') {
+ return {
+ ephemeral: {},
+ };
+ }
+ return undefined;
+ });
+
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(NodeOperationError);
+ await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
+ 'You cannot send ephemeral messages using User type "By username". Please use "From List" or "By ID".',
+ );
+ });
+ });
+});