diff --git a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts index a4a429b67f..66c4248df7 100644 --- a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts @@ -75,15 +75,22 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType { requestOptions.headers['x-ms-session-token'] = credentials.sessionToken; } - // const url = new URL (requestOptions.uri); + let url; - const url = new URL(requestOptions.baseURL + requestOptions.url); - const pathSegments = url.pathname.split('/').filter((segment) => segment); + if (requestOptions.url) { + url = new URL(requestOptions.baseURL + requestOptions.url); + //@ts-ignore + } else if (requestOptions.uri) { + //@ts-ignore + url = new URL(requestOptions.uri); + } + + const pathSegments = url?.pathname.split('/').filter((segment) => segment); let resourceType = ''; let resourceId = ''; - if (pathSegments.includes('docs')) { + if (pathSegments?.includes('docs')) { const docsIndex = pathSegments.lastIndexOf('docs'); resourceType = 'docs'; if (pathSegments[docsIndex + 1]) { @@ -92,7 +99,7 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType { } else { resourceId = pathSegments.slice(0, docsIndex).join('/'); } - } else if (pathSegments.includes('colls')) { + } else if (pathSegments?.includes('colls')) { const collsIndex = pathSegments.lastIndexOf('colls'); resourceType = 'colls'; if (pathSegments[collsIndex + 1]) { @@ -101,7 +108,7 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType { } else { resourceId = pathSegments.slice(0, collsIndex).join('/'); } - } else if (pathSegments.includes('dbs')) { + } else if (pathSegments?.includes('dbs')) { const dbsIndex = pathSegments.lastIndexOf('dbs'); resourceType = 'dbs'; resourceId = pathSegments.slice(0, dbsIndex + 2).join('/'); diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts index 554909e8b4..ae66d76aca 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-argument import * as crypto from 'crypto'; import type { DeclarativeRestApiSettings, @@ -71,6 +75,7 @@ export async function microsoftCosmosDbRequest( const requestOptions: IHttpRequestOptions = { ...opts, + // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions baseURL: `${credentials.baseUrl}`, headers: { ...opts.headers, @@ -99,8 +104,6 @@ export async function microsoftCosmosDbRequest( }; try { - console.log('Final Request Options before Request:', requestOptions); - return (await this.helpers.requestWithAuthentication.call( this, 'microsoftCosmosDbSharedKeyApi', @@ -442,14 +445,15 @@ export async function validateOperations( ); } - //To-Do-check to not send properties it doesn't need return { op: operation.op, path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value, ...(operation.from ? { from: operation.from.value } : {}), ...(operation.op === 'incr' ? { value: Number(operation.value) } - : { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) }), + : operation.value !== undefined + ? { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) } + : {}), }; }); @@ -458,12 +462,11 @@ export async function validateOperations( return requestOptions; } -export async function validateFields( +export async function validateContainerFields( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; - const indexingPolicy = additionalFields.indexingPolicy; const manualThroughput = additionalFields.offerThroughput; const autoscaleThroughput = additionalFields.maxThroughput; @@ -473,32 +476,28 @@ export async function validateFields( {}, { message: 'Bad parameter', - description: - 'Please choose only one of Max RU/s (Autoscale) and Max RU/s (Manual Throughput)', + description: 'Please choose only one of Max RU/s (Autoscale) and Manual Throughput RU/s', }, ); } - if (autoscaleThroughput && requestOptions?.qs) { - requestOptions.qs['x-ms-cosmos-offer-autopilot-settings'] = { - maxThroughput: autoscaleThroughput, + + if (autoscaleThroughput) { + requestOptions.headers = { + ...requestOptions.headers, + 'x-ms-cosmos-offer-autopilot-setting': { maxThroughput: autoscaleThroughput }, }; } - if (!indexingPolicy || Object.keys(indexingPolicy).length === 0) { - throw new NodeApiError( - this.getNode(), - {}, - { - message: 'Invalid Indexing Policy', - description: 'Please provide a valid indexingPolicy JSON.', - }, - ); + if (manualThroughput) { + requestOptions.headers = { + ...requestOptions.headers, + 'x-ms-offer-throughput': manualThroughput, + }; } return requestOptions; } -//WIP export async function handlePagination( this: IExecutePaginationFunctions, resultOptions: DeclarativeRestApiSettings.ResultOptions, @@ -533,7 +532,6 @@ export async function handlePagination( } } - //TO-DO-check-if-works if (responseData.length > 0) { const lastItem = responseData[responseData.length - 1]; @@ -554,40 +552,34 @@ export async function handlePagination( return aggregatedResult.map((result) => ({ json: result })); } -//WIP export async function handleErrorPostReceive( this: IExecuteSingleFunctions, data: INodeExecutionData[], response: IN8nHttpFullResponse, ): Promise { - console.log('Status code❌', response.statusCode); - if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) { const responseBody = response.body as IDataObject; - console.log('Got here ❌', responseBody); + let errorMessage = 'Unknown error occurred'; + let errorDescription = 'An unexpected error was encountered.'; - if (typeof responseBody.message === 'string') { - try { - const jsonMatch = responseBody.message.match(/Message: (\{.*\})/); - - if (jsonMatch && jsonMatch[1]) { - const parsedMessage = JSON.parse(jsonMatch[1]); - - if ( - parsedMessage.Errors && - Array.isArray(parsedMessage.Errors) && - parsedMessage.Errors.length > 0 - ) { - errorMessage = parsedMessage.Errors[0].split(' Learn more:')[0].trim(); - } - } - } catch (error) { - errorMessage = 'Failed to extract error message'; + if (typeof responseBody === 'object' && responseBody !== null) { + if (typeof responseBody.code === 'string') { + errorMessage = responseBody.code; + } + if (typeof responseBody.message === 'string') { + errorDescription = responseBody.message; } } - throw new ApplicationError(errorMessage); + throw new NodeApiError( + this.getNode(), + {}, + { + message: errorMessage, + description: errorDescription, + }, + ); } return data; } @@ -675,7 +667,7 @@ export async function searchItems( }; } -function extractFieldPaths(obj: any, prefix = ''): string[] { +function extractFieldPaths(obj: IDataObject, prefix = ''): string[] { let paths: string[] = []; Object.entries(obj).forEach(([key, value]) => { @@ -692,7 +684,7 @@ function extractFieldPaths(obj: any, prefix = ''): string[] { } }); } else if (typeof value === 'object' && value !== null) { - paths = paths.concat(extractFieldPaths(value, newPath)); + paths = paths.concat(extractFieldPaths(value as IDataObject, newPath)); } else { paths.push(newPath); } @@ -783,6 +775,34 @@ export async function getProperties(this: ILoadOptionsFunctions): Promise { + const returnAll = this.getNodeParameter('returnAll'); + let limit; + if (!returnAll) { + limit = this.getNodeParameter('limit'); + if (!limit) { + throw new NodeApiError( + this.getNode(), + {}, + { + message: 'Limit value not found', + description: + ' Please provide a value for "Limit" or set "Return All" to true to return all results', + }, + ); + } + } + requestOptions.headers = { + ...requestOptions.headers, + 'x-ms-max-item-count': limit, + }; + + return requestOptions; +} + export async function formatCustomProperties( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, @@ -803,7 +823,6 @@ export async function formatCustomProperties( let parsedProperties: Record; try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment parsedProperties = JSON.parse(rawCustomProperties); } catch (error) { throw new NodeApiError( @@ -841,11 +860,9 @@ export async function formatJSONFields( let parsedIndexPolicy: Record | undefined; try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment parsedPartitionKey = JSON.parse(rawPartitionKey); if (indexingPolicy) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment parsedIndexPolicy = JSON.parse(indexingPolicy); } } catch (error) { @@ -880,32 +897,32 @@ export async function processResponseItems( this: IExecuteSingleFunctions, items: INodeExecutionData[], response: IN8nHttpFullResponse, -): Promise { +): Promise { if (!response || typeof response !== 'object' || !Array.isArray(items)) { throw new ApplicationError('Invalid response format from Cosmos DB.'); } - const extractedDocuments: IDataObject[] = items.flatMap((item) => { + const extractedDocuments: INodeExecutionData[] = items.flatMap((item) => { if ( item.json && typeof item.json === 'object' && 'Documents' in item.json && Array.isArray(item.json.Documents) ) { - return item.json.Documents as IDataObject[]; + return item.json.Documents.map((doc) => ({ json: doc })); } return []; }); - return extractedDocuments; + return extractedDocuments.length ? extractedDocuments : [{ json: {} }]; } export async function processResponseContainers( this: IExecuteSingleFunctions, items: INodeExecutionData[], response: IN8nHttpFullResponse, -): Promise { +): Promise { if (!response || typeof response !== 'object' || !Array.isArray(items)) { throw new ApplicationError('Invalid response format from Cosmos DB.'); } diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts index 8c91e6659c..487356b376 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts @@ -1,6 +1,11 @@ import type { INodeProperties } from 'n8n-workflow'; -import { formatJSONFields, processResponseContainers, validateFields } from '../GenericFunctions'; +import { + formatJSONFields, + handleErrorPostReceive, + processResponseContainers, + validateContainerFields, +} from '../GenericFunctions'; export const containerOperations: INodeProperties[] = [ { @@ -20,13 +25,16 @@ export const containerOperations: INodeProperties[] = [ description: 'Create a container', routing: { send: { - preSend: [formatJSONFields, validateFields], + preSend: [formatJSONFields, validateContainerFields], }, request: { ignoreHttpStatusErrors: true, method: 'POST', url: '/colls', }, + output: { + postReceive: [handleErrorPostReceive], + }, }, action: 'Create container', }, @@ -42,6 +50,7 @@ export const containerOperations: INodeProperties[] = [ }, output: { postReceive: [ + handleErrorPostReceive, { type: 'set', properties: { @@ -63,6 +72,9 @@ export const containerOperations: INodeProperties[] = [ method: 'GET', url: '=/colls/{{ $parameter["collId"] }}', }, + output: { + postReceive: [handleErrorPostReceive], + }, }, action: 'Get container', }, @@ -77,7 +89,7 @@ export const containerOperations: INodeProperties[] = [ url: '/colls', }, output: { - postReceive: [processResponseContainers], + postReceive: [handleErrorPostReceive, processResponseContainers], }, }, action: 'Get many containers', @@ -156,19 +168,15 @@ export const createFields: INodeProperties[] = [ description: 'The user specified autoscale max RU/s', }, { - displayName: 'Max RU/s (for Manual Throughput)', + displayName: 'Manual Throughput RU/s', name: 'offerThroughput', type: 'number', default: 400, + typeOptions: { + minValue: 400, + }, description: 'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second', - routing: { - send: { - type: 'query', - property: 'x-ms-offer-throughput', - value: '={{$value}}', - }, - }, }, ], placeholder: 'Add Option', diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts index a42552ffd1..9102831d89 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts @@ -4,6 +4,7 @@ import { formatCustomProperties, handleErrorPostReceive, handlePagination, + presendLimitField, processResponseItems, validateOperations, validatePartitionKey, @@ -97,6 +98,7 @@ export const itemOperations: INodeProperties[] = [ routing: { send: { paginate: true, + preSend: [presendLimitField], }, operations: { pagination: handlePagination, @@ -128,7 +130,7 @@ export const itemOperations: INodeProperties[] = [ }, }, output: { - postReceive: [handleErrorPostReceive], + postReceive: [processResponseItems, handleErrorPostReceive], }, }, action: 'Query items', @@ -539,13 +541,6 @@ export const getAllFields: INodeProperties[] = [ returnAll: [false], }, }, - routing: { - send: { - property: 'x-ms-max-item-count', - type: 'query', - value: '={{ $value }}', - }, - }, type: 'number', typeOptions: { minValue: 1, @@ -611,7 +606,7 @@ export const queryFields: INodeProperties[] = [ operation: ['query'], }, }, - placeholder: 'SELECT * FROM c WHERE c.name = @name', + placeholder: 'SELECT * FROM c WHERE c.name = @Name', routing: { send: { type: 'body', diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/FetchPartitionKeyField.test.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/FetchPartitionKeyField.test.ts new file mode 100644 index 0000000000..7845580f3a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/FetchPartitionKeyField.test.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; + +import { fetchPartitionKeyField } from '../GenericFunctions'; + +describe('GenericFunctions - fetchPartitionKeyField', () => { + const mockMicrosoftCosmosDbRequest = jest.fn(); + + const mockContext = { + helpers: { + requestWithAuthentication: mockMicrosoftCosmosDbRequest, + }, + getNodeParameter: jest.fn(), + getCredentials: jest.fn(), + } as unknown as ILoadOptionsFunctions; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext.getNode = jest.fn().mockReturnValue({}); + + (mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ + account: 'us-east-1', + database: 'test_database', + baseUrl: 'https://us-east-1.documents.azure.com', + }); + }); + + it('should fetch the partition key successfully', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + + mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({ + partitionKey: { + paths: ['/PartitionKey'], + kind: 'Hash', + version: 2, + }, + }); + + const response = await fetchPartitionKeyField.call(mockContext); + + expect(mockMicrosoftCosmosDbRequest).toHaveBeenCalledWith( + 'microsoftCosmosDbSharedKeyApi', + expect.objectContaining({ + method: 'GET', + url: '/colls/coll-1', + }), + ); + + expect(response).toEqual({ + results: [ + { + name: 'PartitionKey', + value: 'PartitionKey', + }, + ], + }); + }); + + it('should throw an error when container ID is missing', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' }); + + await expect(fetchPartitionKeyField.call(mockContext)).rejects.toThrowError( + expect.objectContaining({ + message: 'Container is required to determine the partition key.', + }), + ); + }); + + it('should return an empty array if no partition key is found', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + + mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({ + partitionKey: { + paths: [], + kind: 'Hash', + version: 2, + }, + }); + + const response = await fetchPartitionKeyField.call(mockContext); + + expect(response).toEqual({ results: [] }); + }); + + it('should handle unexpected response format gracefully', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + + mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({ unexpectedKey: 'value' }); + + const response = await fetchPartitionKeyField.call(mockContext); + + expect(response).toEqual({ results: [] }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/MicrosoftCosmosDbRequest.test.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/MicrosoftCosmosDbRequest.test.ts index 523cfea187..788d23886d 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/MicrosoftCosmosDbRequest.test.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/MicrosoftCosmosDbRequest.test.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { microsoftCosmosDbRequest } from '../GenericFunctions'; describe('GenericFunctions - microsoftCosmosDbRequest', () => { diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchCollections.test.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchContainers.test.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchCollections.test.ts rename to packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchContainers.test.ts diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItemById.test.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItemById.test.ts new file mode 100644 index 0000000000..76fea195c0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItemById.test.ts @@ -0,0 +1,119 @@ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; + +import { searchItemById } from '../GenericFunctions'; + +describe('GenericFunctions - searchItemById', () => { + const mockRequestWithAuthentication = jest.fn(); + + const mockContext = { + helpers: { + requestWithAuthentication: mockRequestWithAuthentication, + }, + getNodeParameter: jest.fn(), + getCredentials: jest.fn(), + } as unknown as ILoadOptionsFunctions; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext.getNode = jest.fn().mockReturnValue({}); + }); + + it('should fetch the item successfully', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + const itemId = 'item-123'; + + (mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ + account: 'us-east-1', + database: 'first_database_1', + baseUrl: 'https://us-east-1.documents.azure.com', + }); + + mockRequestWithAuthentication.mockResolvedValueOnce({ + id: itemId, + name: 'Test Item', + }); + + const response = await searchItemById.call(mockContext, itemId); + + expect(mockRequestWithAuthentication).toHaveBeenCalledWith( + 'microsoftCosmosDbSharedKeyApi', + expect.objectContaining({ + method: 'GET', + url: '/colls/coll-1/docs/item-123', + }), + ); + + expect(response).toEqual({ + id: itemId, + name: 'Test Item', + }); + }); + + it('should throw an error when container ID is missing', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' }); + + await expect(searchItemById.call(mockContext, 'item-123')).rejects.toThrowError( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect.objectContaining({ + message: 'Container is required', + }), + ); + }); + + it('should throw an error when item ID is missing', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + + await expect(searchItemById.call(mockContext, '')).rejects.toThrowError( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect.objectContaining({ + message: 'Item is required', + }), + ); + }); + + it('should return null if the response is empty', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + const itemId = 'item-123'; + + (mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ + account: 'us-east-1', + database: 'first_database_1', + baseUrl: 'https://us-east-1.documents.azure.com', + }); + + mockRequestWithAuthentication.mockResolvedValueOnce(null); + + const response = await searchItemById.call(mockContext, itemId); + + expect(response).toBeNull(); + }); + + it('should handle unexpected response format gracefully', async () => { + (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ + mode: 'list', + value: 'coll-1', + }); + const itemId = 'item-123'; + + (mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ + account: 'us-east-1', + database: 'first_database_1', + baseUrl: 'https://us-east-1.documents.azure.com', + }); + + mockRequestWithAuthentication.mockResolvedValueOnce({ unexpectedKey: 'value' }); + + const response = await searchItemById.call(mockContext, itemId); + + expect(response).toEqual({ unexpectedKey: 'value' }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItems.test.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItems.test.ts index 71633f8c8a..9fc1f1af4f 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItems.test.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/test/SearchItems.test.ts @@ -15,6 +15,7 @@ describe('GenericFunctions - searchItems', () => { beforeEach(() => { jest.clearAllMocks(); + mockContext.getNode = jest.fn().mockReturnValue({}); }); it('should fetch documents and return formatted results', async () => { @@ -45,7 +46,7 @@ describe('GenericFunctions - searchItems', () => { expect(response).toEqual({ results: [ - { name: 'Item1', value: 'Item 1' }, // Space removed from 'Item 1' + { name: 'Item1', value: 'Item 1' }, { name: 'Item2', value: 'Item 2' }, ], }); @@ -119,6 +120,11 @@ describe('GenericFunctions - searchItems', () => { it('should throw an error when container ID is missing', async () => { (mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' }); - await expect(searchItems.call(mockContext)).rejects.toThrow('Container is required'); + await expect(searchItems.call(mockContext)).rejects.toThrowError( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect.objectContaining({ + message: 'Container is required', + }), + ); }); });