first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,77 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Create Container', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.post('/colls', {
id: 'container1',
partitionKey: {
paths: ['/id'],
kind: 'Hash',
version: 2,
},
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [
{
path: '/*',
},
],
excludedPaths: [],
},
})
.matchHeader('x-ms-offer-throughput', '400')
.reply(201, {
id: 'container1',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [
{
path: '/*',
},
],
excludedPaths: [
{
path: '/"_etag"/?',
},
],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
version: 2,
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAOKH+8U=',
_ts: 1742313299,
_self: 'dbs/4PVyAA==/colls/4PVyAOKH+8U=/',
_etag: '"00005702-0000-0300-0000-67d997530000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['create.workflow.json'],
});
});
@@ -0,0 +1,96 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "create",
"containerCreate": "container1",
"additionalFields": {
"partitionKey": "{\n\t\"paths\": [\n\t\t\"/id\"\n\t],\n\t\"kind\": \"Hash\",\n\t\"version\": 2\n}",
"indexingPolicy": "{\n\t\"indexingMode\": \"consistent\",\n\t\"automatic\": true,\n\t\"includedPaths\": [\n\t\t{\n\t\t\t\"path\": \"/*\"\n\t\t}\n\t],\n\t\"excludedPaths\": []\n}",
"offerThroughput": 400
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
"name": "Azure Cosmos DB",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos DB",
"type": "main",
"index": 0
}
]
]
},
"Azure Cosmos DB": {
"main": [[]]
}
},
"pinData": {
"Azure Cosmos DB": [
{
"json": {
"id": "container1",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
]
},
"partitionKey": {
"paths": ["/id"],
"kind": "Hash",
"version": 2
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
},
"_rid": "4PVyAOKH+8U=",
"_ts": 1742313299,
"_self": "dbs/4PVyAA==/colls/4PVyAOKH+8U=/",
"_etag": "\"00005702-0000-0300-0000-67d997530000\"",
"_docs": "docs/",
"_sprocs": "sprocs/",
"_triggers": "triggers/",
"_udfs": "udfs/",
"_conflicts": "conflicts/"
}
}
]
}
}
@@ -0,0 +1,21 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Delete Container', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.delete('/colls/container1')
.reply(204, {});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['delete.workflow.json'],
});
});
@@ -0,0 +1,60 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "delete",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
"name": "Azure Cosmos DB",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos DB",
"type": "main",
"index": 0
}
]
]
},
"Azure Cosmos DB": {
"main": [[]]
}
},
"pinData": {
"Azure Cosmos DB": [
{
"json": {
"deleted": true
}
}
]
}
}
@@ -0,0 +1,59 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Get Container', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/container1')
.reply(200, {
id: 'container1',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [
{
path: '/*',
},
],
excludedPaths: [
{
path: '/"_etag"/?',
},
],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
version: 2,
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAMPuBto=',
_ts: 1742298418,
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/',
_etag: '"00004402-0000-0300-0000-67d95d320000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['get.workflow.json'],
});
});
@@ -0,0 +1,87 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "get",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
"name": "Azure Cosmos DB",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos DB",
"type": "main",
"index": 0
}
]
]
},
"Azure Cosmos DB": {
"main": [[]]
}
},
"pinData": {
"Azure Cosmos DB": [
{
"json": {
"id": "container1",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
]
},
"partitionKey": {
"paths": ["/id"],
"kind": "Hash",
"version": 2
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
}
}
}
]
}
}
@@ -0,0 +1,161 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Get All Containers', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls')
.reply(
200,
{
_rid: '4PVyAA==',
DocumentCollections: [
{
id: 'newOne3',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
fullTextIndexes: [],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
version: 2,
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: { type: 'Geography' },
},
{
id: 'newId',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
fullTextIndexes: [],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
version: 2,
},
uniqueKeyPolicy: {
uniqueKeys: [],
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: { type: 'Geography' },
fullTextPolicy: {
defaultLanguage: 'en-US',
fullTextPaths: [],
},
computedProperties: [],
},
{
id: 'ContainerWithNameAsKey',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
fullTextIndexes: [],
},
partitionKey: {
paths: ['/Name'],
kind: 'Hash',
version: 2,
},
uniqueKeyPolicy: {
uniqueKeys: [],
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: { type: 'Geography' },
fullTextPolicy: {
defaultLanguage: 'en-US',
fullTextPaths: [],
},
computedProperties: [],
},
{
id: 'ContainerWithPhoneNrAsKey',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
fullTextIndexes: [],
},
partitionKey: {
paths: ['/PhoneNumber'],
kind: 'Hash',
version: 2,
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: { type: 'Geography' },
},
],
_count: 4,
},
{
'x-ms-continuation': '4PVyAKoVaBQ=',
},
)
.get('/colls')
.matchHeader('x-ms-continuation', '4PVyAKoVaBQ=')
.reply(200, {
_rid: '4PVyAA==',
DocumentCollections: [
{
id: 'ContainerWithPhoneNrAsKey',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
fullTextIndexes: [],
},
partitionKey: {
paths: ['/PhoneNumber'],
kind: 'Hash',
version: 2,
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: { type: 'Geography' },
},
],
_count: 1,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getAll.workflow.json'],
});
});
@@ -0,0 +1,196 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-180, -520],
"id": "8218dacc-3b5f-460a-a773-817faf012ba9",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [20, -520],
"id": "4568cad5-4e55-4370-9fbe-7f19e03faa67",
"name": "getAllContainers",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "iTUZNEdLdLW0RpF9",
"name": "Azure Cosmos DB account "
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "getAllContainers",
"type": "main",
"index": 0
}
]
]
},
"getAllContainers": {
"main": [[]]
}
},
"pinData": {
"getAllContainers": [
{
"json": {
"id": "newOne3",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
],
"fullTextIndexes": []
},
"partitionKey": {
"paths": ["/id"],
"kind": "Hash",
"version": 2
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
}
}
},
{
"json": {
"id": "newId",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
],
"fullTextIndexes": []
},
"partitionKey": {
"paths": ["/id"],
"kind": "Hash",
"version": 2
},
"uniqueKeyPolicy": {
"uniqueKeys": []
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
},
"fullTextPolicy": {
"defaultLanguage": "en-US",
"fullTextPaths": []
},
"computedProperties": []
}
},
{
"json": {
"id": "ContainerWithNameAsKey",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
],
"fullTextIndexes": []
},
"partitionKey": {
"paths": ["/Name"],
"kind": "Hash",
"version": 2
},
"uniqueKeyPolicy": {
"uniqueKeys": []
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
},
"fullTextPolicy": {
"defaultLanguage": "en-US",
"fullTextPaths": []
},
"computedProperties": []
}
},
{
"json": {
"id": "ContainerWithPhoneNrAsKey",
"indexingPolicy": {
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*"
}
],
"excludedPaths": [
{
"path": "/\"_etag\"/?"
}
],
"fullTextIndexes": []
},
"partitionKey": {
"paths": ["/PhoneNumber"],
"kind": "Hash",
"version": 2
},
"conflictResolutionPolicy": {
"mode": "LastWriterWins",
"conflictResolutionPath": "/_ts",
"conflictResolutionProcedure": ""
},
"geospatialConfig": {
"type": "Geography"
}
}
}
]
}
}
@@ -0,0 +1,8 @@
export const credentials = {
microsoftAzureCosmosDbSharedKeyApi: {
account: 'n8n-us-east-account',
key: 'I3rwpzP0XoFpNzJ7hRIUXjwgpD1qaVKi71NZBbk7oOHUXrbd80WAoIAAoRaT47W9hHO3b6us1yABACDbVdilag==',
database: 'database_1',
baseUrl: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1',
},
};
@@ -0,0 +1,123 @@
import { OperationalError } from 'n8n-workflow';
import type { IHttpRequestOptions, IRequestOptions } from 'n8n-workflow';
import { MicrosoftAzureCosmosDbSharedKeyApi } from '@credentials/MicrosoftAzureCosmosDbSharedKeyApi.credentials';
import { credentials } from '../credentials';
jest.mock('crypto', () => ({
createHmac: jest.fn(() => ({
update: jest.fn(() => ({
digest: jest.fn(() => 'fake-signature'),
})),
})),
}));
describe('Azure Cosmos DB', () => {
const { account, key, baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
describe('authenticate', () => {
const azureCosmosDbSharedKeyApi = new MicrosoftAzureCosmosDbSharedKeyApi();
it('should generate a valid authorization header', async () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
const requestOptions: IHttpRequestOptions = {
url: `${baseUrl}/colls/container1/docs/item1`,
method: 'GET',
};
const result = await azureCosmosDbSharedKeyApi.authenticate({ account, key }, requestOptions);
expect(result.headers?.authorization).toBe(
'type%3Dmaster%26ver%3D1.0%26sig%3Dfake-signature',
);
});
it('should throw an error when unable to determine the resource type from the URL', async () => {
const requestOptions: IRequestOptions = {
uri: 'https://invalid-url.com/',
method: 'GET',
};
const urlString =
requestOptions.uri ??
(requestOptions.baseURL && requestOptions.url
? requestOptions.baseURL + requestOptions.url
: '');
if (!urlString) {
throw new OperationalError('Invalid URL: Both uri and baseURL+url are missing');
}
const url = new URL(urlString);
const pathSegments = url.pathname.split('/').filter(Boolean);
const RESOURCE_TYPES = ['dbs', 'colls', 'docs', 'sprocs', 'udfs', 'triggers'];
const foundResource = RESOURCE_TYPES.map((type) => ({
type,
index: pathSegments.lastIndexOf(type),
}))
.filter(({ index }) => index !== -1)
.sort((a, b) => b.index - a.index)
.shift();
expect(foundResource).toBeUndefined();
expect(() => {
if (!foundResource) {
throw new OperationalError('Unable to determine the resource type from the URL');
}
}).toThrowError(new OperationalError('Unable to determine the resource type from the URL'));
});
it('should throw OperationalError if no resource type found in URL path', async () => {
const requestOptions: IRequestOptions = {
uri: 'https://example.com/invalidpath',
method: 'GET',
};
const urlString =
requestOptions.uri ??
(requestOptions.baseURL && requestOptions.url
? requestOptions.baseURL + requestOptions.url
: '');
if (!urlString) {
throw new OperationalError('Invalid URL: Both uri and baseURL+url are missing');
}
const url = new URL(urlString);
const pathSegments = url.pathname.split('/').filter(Boolean);
const RESOURCE_TYPES = ['dbs', 'colls', 'docs', 'sprocs', 'udfs', 'triggers'];
const foundResource = RESOURCE_TYPES.map((type) => ({
type,
index: pathSegments.lastIndexOf(type),
}))
.filter(({ index }) => index !== -1)
.sort((a, b) => b.index - a.index)
.shift();
expect(foundResource).toBeUndefined();
expect(() => {
if (!foundResource) {
throw new OperationalError('Unable to determine the resource type from the URL');
}
}).toThrowError(new OperationalError('Unable to determine the resource type from the URL'));
});
it('should properly construct the resourceId and payload', async () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
const requestOptions: IHttpRequestOptions = {
url: 'https://example.com/dbs/mydb/colls/mycoll/docs/mydoc',
method: 'GET',
};
const result = await azureCosmosDbSharedKeyApi.authenticate({ account, key }, requestOptions);
expect(result.headers?.authorization).toBe(
'type%3Dmaster%26ver%3D1.0%26sig%3Dfake-signature',
);
});
});
});
@@ -0,0 +1,177 @@
import type { IN8nHttpFullResponse, INodeExecutionData, JsonObject } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { handleError, ErrorMap } from '../../helpers/errorHandler';
const mockExecuteSingleFunctions = {
getNode: jest.fn(() => ({ name: 'MockNode' })),
getNodeParameter: jest.fn(),
} as any;
describe('handleError', () => {
let response: IN8nHttpFullResponse;
let data: INodeExecutionData[];
beforeEach(() => {
data = [{}] as INodeExecutionData[];
response = { statusCode: 200, body: {} } as IN8nHttpFullResponse;
});
test('should return data when no error occurs', async () => {
const result = await handleError.call(mockExecuteSingleFunctions, data, response);
expect(result).toBe(data);
});
test('should throw NodeApiError for container conflict', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 409;
response.body = { code: 'Conflict', message: 'Container already exists' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Container.Conflict.getMessage('container'),
description: ErrorMap.Container.Conflict.description,
}),
);
});
test('should throw NodeApiError for container not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 404;
response.body = { code: 'NotFound', message: 'Container not found' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Container.NotFound.getMessage('container'),
description: ErrorMap.Container.NotFound.description,
}),
);
});
test('should throw NodeApiError for item not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('item');
response.statusCode = 404;
response.body = { code: 'NotFound', message: 'Item not found' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Item.NotFound.getMessage('item'),
description: ErrorMap.Item.NotFound.description,
}),
);
});
test('should throw generic error if no specific mapping exists', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 400;
response.body = { code: 'BadRequest', message: 'Invalid request' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: 'BadRequest',
description: 'Invalid request',
}),
);
});
test('should handle error details correctly when match is successful', async () => {
const errorMessage = 'Message: {"Errors":["Error 1", "Error 2"]}';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
expect(errorDetails).toEqual(['Error 1', 'Error 2']);
});
test('should handle error when match does not return expected format', async () => {
const errorMessage = 'Message: Invalid format';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
expect(errorDetails).toEqual([]);
});
test('should throw NodeApiError with proper details if error details are present', async () => {
const errorMessage = 'Message: {"Errors":["Specific error occurred"]}';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
if (errorDetails && errorDetails.length > 0) {
await expect(
handleError.call(mockExecuteSingleFunctions, data, {
statusCode: 500,
body: { code: 'InternalServerError', message: errorMessage },
headers: {},
}),
).rejects.toThrow(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{
code: 'InternalServerError',
message: errorMessage,
} as JsonObject,
{
message: 'InternalServerError',
description: errorDetails.join('\n'),
},
),
);
}
});
test('should throw NodeApiError with fallback message if no details found', async () => {
const errorMessage = 'Message: {"Errors":[] }';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
if (errorDetails && errorDetails.length > 0) {
await expect(
handleError.call(mockExecuteSingleFunctions, data, {
statusCode: 500,
body: { code: 'InternalServerError', message: errorMessage },
headers: {},
}),
).rejects.toThrow(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{
code: 'InternalServerError',
message: errorMessage,
} as JsonObject,
{
message: 'InternalServerError',
description: 'Internal Server Error',
},
),
);
}
});
});
@@ -0,0 +1,360 @@
import { mock } from 'jest-mock-extended';
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
INode,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError, OperationalError } from 'n8n-workflow';
const azureCosmosDbApiRequest = jest.fn();
jest.mock('../../transport', () => ({ azureCosmosDbApiRequest }));
import { ErrorMap } from '../../helpers/errorHandler';
import {
getPartitionKey,
simplifyData,
validateQueryParameters,
processJsonInput,
validatePartitionKey,
validateCustomProperties,
} from '../../helpers/utils';
interface RequestBodyWithParameters extends IDataObject {
parameters: Array<{ name: string; value: string }>;
}
const mockExecuteSingleFunctions = mock<IExecuteSingleFunctions>();
beforeEach(() => {
jest.resetAllMocks();
mockExecuteSingleFunctions.getNode.mockReturnValue({ name: 'MockNode' } as INode);
});
describe('getPartitionKey', () => {
test('should return partition key when found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
const result = await getPartitionKey.call(mockExecuteSingleFunctions);
expect(result).toBe('partitionKeyPath');
});
test('should throw NodeOperationError if partition key is not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
const mockApiResponse = {};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Partition key not found', {
description: 'Failed to determine the partition key for this collection',
}),
);
});
test('should throw NodeApiError for 404 error', async () => {
const containerName = 'containerName';
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(containerName);
const errorMessage = ErrorMap.Container.NotFound.getMessage(containerName);
const mockError = new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{},
{
httpCode: '404',
message: errorMessage,
description: ErrorMap.Container.NotFound.description,
},
);
azureCosmosDbApiRequest.mockRejectedValue(mockError);
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{},
{
message: errorMessage,
description: ErrorMap.Container.NotFound.description,
},
),
);
});
});
describe('validatePartitionKey', () => {
let requestOptions: any;
beforeEach(() => {
requestOptions = { body: {}, headers: {} };
azureCosmosDbApiRequest.mockClear();
});
test('should throw NodeOperationError when partition key is missing for "create" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({});
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
"Partition key not found in 'Item Contents'",
{
description:
"Partition key 'partitionKey' must be present and have a valid, non-empty value in 'Item Contents'.",
},
),
);
});
test('should throw NodeOperationError when partition key is missing for "update" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('update');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({ partitionKey: '' });
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Partition key is missing or empty',
{
description: 'Ensure the "Partition Key" field has a valid, non-empty value.',
},
),
);
});
test('should throw NodeOperationError when partition key is missing for "get" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('get');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce(undefined);
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Partition key is missing or empty',
{
description: 'Ensure the "Partition Key" field exists and has a valid, non-empty value.',
},
),
);
});
test('should throw NodeOperationError when invalid JSON is provided for customProperties', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('invalidJson');
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Invalid JSON format in "Item Contents"',
{
description: 'Ensure the "Item Contents" field contains a valid JSON object',
},
),
);
});
});
describe('simplifyData', () => {
test('should return the same data when "simple" parameter is false', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(false);
const items = [{ json: { foo: 'bar' } }] as INodeExecutionData[];
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
expect(result).toEqual(items);
});
test('should simplify the data when "simple" parameter is true', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(true);
const items = [{ json: { _internalKey: 'value', foo: 'bar' } }] as INodeExecutionData[];
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
expect(result).toEqual([{ json: { foo: 'bar' } }]);
});
});
describe('validateQueryParameters', () => {
let requestOptions: IHttpRequestOptions;
beforeEach(() => {
requestOptions = { body: {}, headers: {} } as IHttpRequestOptions;
});
test('should throw NodeOperationError when parameter values do not match', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1')
.mockReturnValueOnce({ queryParameters: 'param1, param2' });
await expect(
validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Empty parameter value provided',
{
description: 'Please provide non-empty values for the query parameters',
},
),
);
});
test('should successfully map parameters when they match', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1, $2')
.mockReturnValueOnce({ queryParameters: 'value1, value2' });
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
{ name: '@Param1', value: 'value1' },
{ name: '@Param2', value: 'value2' },
]);
} else {
throw new OperationalError('Expected result.body to contain a parameters array');
}
});
test('should correctly map parameters when query contains multiple dynamic values', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1, $2, $3')
.mockReturnValueOnce({ queryParameters: 'firstValue, secondValue, thirdValue' });
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
{ name: '@Param1', value: 'firstValue' },
{ name: '@Param2', value: 'secondValue' },
{ name: '@Param3', value: 'thirdValue' },
]);
} else {
throw new OperationalError('Expected result.body to contain a parameters array');
}
});
test('should extract and map parameter names correctly using regex', async () => {
const query = '$1, $2, $3';
const queryParamsString = 'value1, value2, value3';
const parameterNames = query.replace(/\$(\d+)/g, '@param$1').match(/@\w+/g) ?? [];
const parameterValues = queryParamsString.split(',').map((val) => val.trim());
expect(parameterNames).toEqual(['@param1', '@param2', '@param3']);
expect(parameterValues).toEqual(['value1', 'value2', 'value3']);
});
});
describe('processJsonInput', () => {
test('should return parsed JSON when input is a valid JSON string', () => {
const result = processJsonInput('{"key": "value"}');
expect(result).toEqual({ key: 'value' });
});
test('should return input data when it is already an object', () => {
const result = processJsonInput({ key: 'value' });
expect(result).toEqual({ key: 'value' });
});
test('should throw OperationalError for invalid JSON string', () => {
const invalidJson = '{key: value}';
expect(() => processJsonInput(invalidJson)).toThrowError(
new OperationalError('Input must contain a valid JSON', { level: 'warning' }),
);
});
test('should throw OperationalError for invalid non-string and non-object input', () => {
const invalidInput = 123;
expect(() => processJsonInput(invalidInput, 'testInput')).toThrowError(
new OperationalError("Input 'testInput' must contain a valid JSON", { level: 'warning' }),
);
});
});
describe('validateCustomProperties', () => {
let requestOptions: any;
beforeEach(() => {
requestOptions = { body: {}, headers: {}, url: 'http://mock.url' };
});
test('should merge custom properties into requestOptions.body for valid input', async () => {
const validCustomProperties = { property1: 'value1', property2: 'value2' };
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(validCustomProperties);
const result = await validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions);
expect(result.body).toEqual({ property1: 'value1', property2: 'value2' });
});
test('should throw NodeOperationError when customProperties are empty, undefined, null, or contain only invalid values', async () => {
const emptyCustomProperties = {};
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(emptyCustomProperties);
await expect(
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
description: 'Ensure the "Item Contents" field contains at least one valid property.',
}),
);
const invalidValues = { property1: null, property2: '' };
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(invalidValues);
await expect(
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
description: 'Ensure the "Item Contents" field contains at least one valid property.',
}),
);
});
});
@@ -0,0 +1,59 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Create Item', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/container1')
.reply(200, {
id: 'containerFromSDK',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAMLVz0c=',
_ts: 1739178329,
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
})
.post('/colls/container1/docs')
.reply(201, {
id: 'item1',
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
_etag: '"bb000143-0000-0300-0000-67d9a2430000"',
_attachments: 'attachments/',
_ts: 1742316099,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['create.workflow.json'],
});
});
@@ -0,0 +1,64 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"operation": "create",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"customProperties": "{\n\t\"id\": \"item1\"\n}",
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
"name": "Azure Cosmos Db",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos Db",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Azure Cosmos Db": [
{
"json": {
"id": "item1",
"_rid": "4PVyAMPuBtoEAAAAAAAAAA==",
"_self": "dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/",
"_etag": "\"bb000143-0000-0300-0000-67d9a2430000\"",
"_attachments": "attachments/",
"_ts": 1742316099
}
}
]
}
}
@@ -0,0 +1,52 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Delete Item', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/container1')
.reply(200, {
id: 'containerFromSDK',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAMLVz0c=',
_ts: 1739178329,
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
})
.delete('/colls/container1/docs/item1')
.reply(204, '');
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['delete.workflow.json'],
});
});
@@ -0,0 +1,65 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"operation": "delete",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"item": {
"__rl": true,
"value": "item1",
"mode": "list",
"cachedResultName": "item1"
},
"additionalFields": {},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
"name": "Azure Cosmos Db",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos Db",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Azure Cosmos Db": [
{
"json": {
"deleted": true
}
}
]
}
}
@@ -0,0 +1,60 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Get Item', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/container1')
.reply(200, {
id: 'containerFromSDK',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAMLVz0c=',
_ts: 1739178329,
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
})
.get('/colls/container1/docs/item1')
.matchHeader('x-ms-documentdb-partitionkey', '["item1"]')
.reply(200, {
id: 'item1',
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
_etag: '"bb000143-0000-0300-0000-67d9a2430000"',
_attachments: 'attachments/',
_ts: 1742316099,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['get.workflow.json'],
});
});
@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"operation": "get",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"item": {
"__rl": true,
"value": "item1",
"mode": "list",
"cachedResultName": "item1"
},
"additionalFields": {
"partitionKey": "item1"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
"name": "Azure Cosmos Db",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos Db",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Azure Cosmos Db": [
{
"json": {
"id": "item1"
}
}
]
}
}
@@ -0,0 +1,71 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Get All Items', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/newOne3/docs')
.reply(
200,
{
_rid: '4PVyAMPuBto=',
Documents: [
{
id: 'John',
FamilyName: 'NewNameAdded',
Parents: [
88,
{ FirstName: 'Thomas', FamilyName: 'Bob' },
{ FamilyName: null, FirstName: 'Mary Kay' },
],
ExtraField: 'nothing serious',
Otherdetails: 'male',
This: 'male',
},
{
FamilyName: 'NewName',
id: 'NewId',
},
{
id: 'this',
},
],
_count: 3,
},
{
'x-ms-continuation': '4PVyAKoVaBQ=',
},
)
.get('/colls/newOne3/docs')
.matchHeader('x-ms-continuation', '4PVyAKoVaBQ=')
.reply(200, {
_rid: '4PVyAMPuBto=',
Documents: [
{
id: 'John',
FamilyName: 'NewNameAdded',
Parents: [
88,
{ FirstName: 'Thomas', FamilyName: 'Bob' },
{ FamilyName: null, FirstName: 'Mary Kay' },
],
ExtraField: 'nothing serious',
Otherdetails: 'male',
This: 'male',
},
],
_count: 1,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getAll.workflow.json'],
});
});
@@ -0,0 +1,84 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-180, -520],
"id": "8218dacc-3b5f-460a-a773-817faf012ba9",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"container": {
"__rl": true,
"value": "newOne3",
"mode": "list",
"cachedResultName": "newOne3"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [-80, -360],
"id": "4f60d430-3c90-4838-b9d1-8c9637e745b6",
"name": "getAllItems",
"alwaysOutputData": true,
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account "
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "getAllItems",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"getAllItems": [
{
"json": {
"id": "John",
"FamilyName": "NewNameAdded",
"Parents": [
88,
{
"FirstName": "Thomas",
"FamilyName": "Bob"
},
{
"FamilyName": null,
"FirstName": "Mary Kay"
}
],
"ExtraField": "nothing serious",
"Otherdetails": "male",
"This": "male"
}
},
{
"json": {
"FamilyName": "NewName",
"id": "NewId"
}
},
{
"json": {
"id": "this"
}
}
]
}
}
@@ -0,0 +1,36 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Query Items', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.post('/colls/newId/docs', {
query: 'SELECT * FROM c WHERE c.id = @Param1',
parameters: [
{
name: '@Param1',
value: 'User1',
},
],
})
.reply(200, {
Documents: [
{
id: 'User1',
key1: 'value',
},
],
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['query.workflow.json'],
});
});
@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"operation": "query",
"container": {
"__rl": true,
"value": "newId",
"mode": "list",
"cachedResultName": "newId"
},
"query": "SELECT * FROM c WHERE c.id = $1",
"options": {
"queryOptions": {
"queryParameters": "User1"
}
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [160, -100],
"id": "0dc90797-8211-457c-a673-b7df28139de8",
"name": "queryItems",
"retryOnFail": false,
"alwaysOutputData": true,
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account "
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "queryItems",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"queryItems": [
{
"json": {
"id": "User1",
"key1": "value"
}
}
]
}
}
@@ -0,0 +1,64 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../credentials';
describe('Azure Cosmos DB - Update Item', () => {
beforeEach(() => {
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/colls/container1')
.reply(200, {
id: 'containerFromSDK',
indexingPolicy: {
indexingMode: 'consistent',
automatic: true,
includedPaths: [{ path: '/*' }],
excludedPaths: [{ path: '/"_etag"/?' }],
},
partitionKey: {
paths: ['/id'],
kind: 'Hash',
},
conflictResolutionPolicy: {
mode: 'LastWriterWins',
conflictResolutionPath: '/_ts',
conflictResolutionProcedure: '',
},
geospatialConfig: {
type: 'Geography',
},
_rid: '4PVyAMLVz0c=',
_ts: 1739178329,
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
_docs: 'docs/',
_sprocs: 'sprocs/',
_triggers: 'triggers/',
_udfs: 'udfs/',
_conflicts: 'conflicts/',
})
.put('/colls/container1/docs/item1', {
id: 'item1',
key1: 'value1',
})
.matchHeader('x-ms-documentdb-partitionkey', '["item1"]')
.reply(200, {
id: 'item1',
key1: 'value1',
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
_etag: '"bb002b70-0000-0300-0000-67d9a3c70000"',
_attachments: 'attachments/',
_ts: 1742316487,
});
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['update.workflow.json'],
});
});
@@ -0,0 +1,76 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "item",
"operation": "update",
"container": {
"__rl": true,
"value": "container1",
"mode": "list",
"cachedResultName": "container1"
},
"item": {
"__rl": true,
"value": "item1",
"mode": "list",
"cachedResultName": "item1"
},
"customProperties": "{\n \"key1\": \"value1\"\n}",
"additionalFields": {},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureCosmosDb",
"typeVersion": 1,
"position": [220, 0],
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
"name": "Azure Cosmos Db",
"credentials": {
"microsoftAzureCosmosDbSharedKeyApi": {
"id": "exampleId",
"name": "Azure Cosmos DB account"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Azure Cosmos Db",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Azure Cosmos Db": [
{
"json": {
"id": "item1",
"key1": "value1",
"_rid": "4PVyAMPuBtoEAAAAAAAAAA==",
"_self": "dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/",
"_etag": "\"bb002b70-0000-0300-0000-67d9a3c70000\"",
"_attachments": "attachments/",
"_ts": 1742316487
}
}
]
},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "7047d2700c0010a2580ba6b2649861ef0a567bfcd1671650a6ca242150e686b8"
}
}
@@ -0,0 +1,149 @@
import {
OperationalError,
type IGetNodeParameterOptions,
type ILoadOptionsFunctions,
} from 'n8n-workflow';
import { AzureCosmosDb } from '../../AzureCosmosDb.node';
import { HeaderConstants } from '../../helpers/constants';
import { credentials } from '../credentials';
describe('Azure Cosmos DB', () => {
describe('List search', () => {
it('should list search containers', async () => {
const mockResponse = {
body: {
DocumentCollections: [
{
id: 'Container2',
},
{
id: 'Container1',
},
],
},
headers: {
'x-ms-continuation': '4PVyAKoVaBQ=',
},
};
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
if (type === 'microsoftAzureCosmosDbSharedKeyApi') {
return credentials.microsoftAzureCosmosDbSharedKeyApi;
}
throw new OperationalError('Unknown credentials');
});
const mockContext = {
getCredentials: mockGetCredentials,
helpers: {
httpRequestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
const node = new AzureCosmosDb();
const paginationToken = '4PVyAKoVaBQ=';
const listSearchResult = await node.methods.listSearch.searchContainers.call(
mockContext,
'',
paginationToken,
);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftAzureCosmosDbSharedKeyApi',
expect.objectContaining({
method: 'GET',
url: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1/colls',
headers: {
[HeaderConstants.X_MS_CONTINUATION]: paginationToken,
},
qs: {},
body: {},
json: true,
returnFullResponse: true,
}),
);
expect(listSearchResult).toEqual({
results: [
{ name: 'Container1', value: 'Container1' },
{ name: 'Container2', value: 'Container2' },
],
paginationToken: '4PVyAKoVaBQ=',
});
});
it('should list search items', async () => {
const mockResponse = {
body: {
Documents: [{ id: 'Item2' }, { id: 'Item1' }],
},
headers: {
'x-ms-continuation': '4PVyAKoVaBQ=',
},
};
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
const mockGetCurrentNodeParameter = jest.fn(
(parameterName, options: IGetNodeParameterOptions) => {
if (parameterName === 'container' && options.extractValue) {
return 'Container1';
}
throw new OperationalError('Unknown parameter');
},
);
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
if (type === 'microsoftAzureCosmosDbSharedKeyApi') {
return credentials.microsoftAzureCosmosDbSharedKeyApi;
}
throw new OperationalError('Unknown credentials');
});
const mockContext = {
getCredentials: mockGetCredentials,
getCurrentNodeParameter: mockGetCurrentNodeParameter,
helpers: {
httpRequestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
const node = new AzureCosmosDb();
const paginationToken = '4PVyAKoVaBQ=';
const listSearchResult = await node.methods.listSearch.searchItems.call(
mockContext,
'',
paginationToken,
);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
'microsoftAzureCosmosDbSharedKeyApi',
expect.objectContaining({
method: 'GET',
url: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1/colls/Container1/docs',
headers: {
[HeaderConstants.X_MS_CONTINUATION]: paginationToken,
},
qs: {},
body: {},
json: true,
returnFullResponse: true,
}),
);
expect(listSearchResult).toEqual({
results: [
{ name: 'Item1', value: 'Item1' },
{ name: 'Item2', value: 'Item2' },
],
paginationToken: '4PVyAKoVaBQ=',
});
});
});
});