diff --git a/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.test.ts b/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.test.ts index 7da2620b797..3e095da90b0 100644 --- a/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.test.ts +++ b/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.test.ts @@ -41,5 +41,25 @@ ruleTester.run('no-error-instance-in-to-throw', NoErrorInstanceInToThrowRule, { code: 'expect(() => foo()).toThrow(new TypeError())', errors: [{ messageId: 'noErrorInstance', data: { className: 'TypeError' } }], }, + { + code: "expect(() => foo()).toThrowError(new Error('bad'))", + errors: [{ messageId: 'noErrorInstance', data: { className: 'Error' } }], + }, + { + code: "expect(() => foo()).toThrowError(new NodeOperationError(node, 'bad'))", + errors: [{ messageId: 'noErrorInstance', data: { className: 'NodeOperationError' } }], + }, + { + code: "await expect(foo()).rejects.toThrowError(new Error('bad'))", + errors: [{ messageId: 'noErrorInstance', data: { className: 'Error' } }], + }, + { + code: "await expect(foo()).rejects.toThrowError(new NodeOperationError(node, 'message'))", + errors: [{ messageId: 'noErrorInstance', data: { className: 'NodeOperationError' } }], + }, + { + code: 'expect(() => foo()).toThrowError(new TypeError())', + errors: [{ messageId: 'noErrorInstance', data: { className: 'TypeError' } }], + }, ], }); diff --git a/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.ts b/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.ts index 03c33e9fc44..f8ed4d05313 100644 --- a/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.ts +++ b/packages/@n8n/eslint-config/src/rules/no-error-instance-in-to-throw.ts @@ -5,11 +5,11 @@ export const NoErrorInstanceInToThrowRule = ESLintUtils.RuleCreator.withoutDocs( type: 'problem', docs: { description: - 'Disallow passing error instances to `.toThrow()`. Jest compares by reference, not message, making assertions flaky. Pass the error class and message string separately instead.', + 'Disallow passing error instances to `.toThrow()` and `.toThrowError`. Jest compares by reference, not message, making assertions flaky. Pass the error class and message string separately instead.', }, messages: { noErrorInstance: - "Do not pass an error instance to `.toThrow()`. Use `.toThrow({{className}})` for type checking and `.toThrow('message')` for message matching.", + "Do not pass an error instance to `.toThrow()` or `.toThrowError`. Use `.toThrow({{className}})` for type checking and `.toThrow('message')` for message matching.", }, schema: [], }, @@ -19,10 +19,11 @@ export const NoErrorInstanceInToThrowRule = ESLintUtils.RuleCreator.withoutDocs( CallExpression(node) { // Match: expect(...).toThrow(new Foo(...)) // Match: expect(...).rejects.toThrow(new Foo(...)) + // Match: expect(...).rejects.toThrowError(new Foo(...)) if ( node.callee.type !== TSESTree.AST_NODE_TYPES.MemberExpression || node.callee.property.type !== TSESTree.AST_NODE_TYPES.Identifier || - node.callee.property.name !== 'toThrow' + (node.callee.property.name !== 'toThrow' && node.callee.property.name !== 'toThrowError') ) { return; } diff --git a/packages/cli/src/controllers/__tests__/auth.controller.test.ts b/packages/cli/src/controllers/__tests__/auth.controller.test.ts index efecbd86b0f..8bfdefbd775 100644 --- a/packages/cli/src/controllers/__tests__/auth.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/auth.controller.test.ts @@ -142,9 +142,9 @@ describe('AuthController', () => { // Act - await expect(controller.login(req, res, body)).rejects.toThrowError( - new AuthError('SSO is enabled, please log in with SSO'), - ); + const execution = controller.login(req, res, body); + await expect(execution).rejects.toThrow(AuthError); + await expect(execution).rejects.toThrow('SSO is enabled, please log in with SSO'); // Assert @@ -174,9 +174,9 @@ describe('AuthController', () => { emailAuthHandler.handleLogin.mockResolvedValue(undefined); config.set('userManagement.authenticationMethod', 'oidc'); - await expect(controller.login(req, res, body)).rejects.toThrowError( - new AuthError('SSO is enabled, please log in with SSO'), - ); + const execution = controller.login(req, res, body); + await expect(execution).rejects.toThrow(AuthError); + await expect(execution).rejects.toThrow('SSO is enabled, please log in with SSO'); expect(eventsService.emit).toHaveBeenCalledWith('user-login-failed', { authenticationMethod: 'email', diff --git a/packages/cli/src/controllers/__tests__/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts index b06111ccc98..73f848aebb7 100644 --- a/packages/cli/src/controllers/__tests__/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -115,13 +115,13 @@ describe('MeController', () => { } }); - await expect( - controller.updateCurrentUser( - req, - mock(), - mock({ email: user.email, firstName: 'John', lastName: 'Potato' }), - ), - ).rejects.toThrowError(new BadRequestError('Invalid email address')); + const execution = controller.updateCurrentUser( + req, + mock(), + mock({ email: user.email, firstName: 'John', lastName: 'Potato' }), + ); + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Invalid email address'); }); describe('when user is authenticated via LDAP or OIDC', () => { @@ -143,18 +143,19 @@ describe('MeController', () => { } as unknown as AuthIdentity); getCurrentAuthenticationMethodMock.mockReturnValue('ldap'); - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'ldap@email.com', - firstName: 'Jane', - lastName: 'Doe', - }), - ), - ).rejects.toThrowError( - new BadRequestError('LDAP user may not change their profile information'), + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'ldap@email.com', + firstName: 'Jane', + lastName: 'Doe', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow( + 'LDAP user may not change their profile information', ); }); @@ -176,18 +177,19 @@ describe('MeController', () => { } as unknown as AuthIdentity); getCurrentAuthenticationMethodMock.mockReturnValue('oidc'); - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'new-oidc@email.com', - firstName: 'John', - lastName: 'Doe', - }), - ), - ).rejects.toThrowError( - new BadRequestError('OIDC user may not change their profile information'), + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'new-oidc@email.com', + firstName: 'John', + lastName: 'Doe', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow( + 'OIDC user may not change their profile information', ); }); @@ -237,18 +239,19 @@ describe('MeController', () => { } as unknown as AuthIdentity); getCurrentAuthenticationMethodMock.mockReturnValue('ldap'); - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'multi@email.com', - firstName: 'Jane', - lastName: 'Doe', - }), - ), - ).rejects.toThrowError( - new BadRequestError('LDAP user may not change their profile information'), + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'multi@email.com', + firstName: 'Jane', + lastName: 'Doe', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow( + 'LDAP user may not change their profile information', ); }); }); @@ -281,18 +284,19 @@ describe('MeController', () => { } as unknown as AuthIdentity); getCurrentAuthenticationMethodMock.mockReturnValue('saml'); - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'saml@email.com', - firstName: 'Jane', - lastName: 'Doe', - }), - ), - ).rejects.toThrowError( - new BadRequestError('SAML user may not change their profile information'), + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'saml@email.com', + firstName: 'Jane', + lastName: 'Doe', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow( + 'SAML user may not change their profile information', ); }); @@ -504,17 +508,18 @@ describe('MeController', () => { } as unknown as User; const req = { user, browserId } as unknown as AuthenticatedRequest; - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'new@email.com', - firstName: 'John', - lastName: 'Potato', - }), - ), - ).rejects.toThrowError(new BadRequestError('Two-factor code is required to change email')); + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'new@email.com', + firstName: 'John', + lastName: 'Potato', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Two-factor code is required to change email'); }); it('should throw InvalidMfaCodeError if mfa code is invalid', async () => { @@ -589,17 +594,18 @@ describe('MeController', () => { } as unknown as User; const req = { user, browserId } as unknown as AuthenticatedRequest; - await expect( - controller.updateCurrentUser( - req, - mock(), - new UserUpdateRequestDto({ - email: 'michel-new@email.com', - firstName: 'Michel', - lastName: 'n8n', - }), - ), - ).rejects.toThrowError(new BadRequestError('Current password is required to change email')); + const execution = controller.updateCurrentUser( + req, + mock(), + new UserUpdateRequestDto({ + email: 'michel-new@email.com', + firstName: 'Michel', + lastName: 'n8n', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Current password is required to change email'); }); it('should throw BadRequestError if currentPassword is not a string', async () => { @@ -611,14 +617,15 @@ describe('MeController', () => { } as unknown as User; const req = { user, browserId } as unknown as AuthenticatedRequest; - await expect( - controller.updateCurrentUser(req, mock(), { - email: 'michel-new@email.com', - firstName: 'Michel', - lastName: 'n8n', - currentPassword: 123 as any, - } as any), - ).rejects.toThrowError(new BadRequestError('Current password is required to change email')); + const execution = controller.updateCurrentUser(req, mock(), { + email: 'michel-new@email.com', + firstName: 'Michel', + lastName: 'n8n', + currentPassword: 123 as any, + } as any); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Current password is required to change email'); }); it('should throw BadRequestError if currentPassword is incorrect', async () => { @@ -629,21 +636,20 @@ describe('MeController', () => { } as unknown as User; const req = { user, browserId } as unknown as AuthenticatedRequest; - await expect( - controller.updateCurrentUser( - req, - mock(), - mock({ - email: 'michel-new@email.com', - firstName: 'Michel', - lastName: 'n8n', - currentPassword: 'wrong-password', - }), - ), - ).rejects.toThrowError( - new BadRequestError( - 'Unable to update profile. Please check your credentials and try again.', - ), + const execution = controller.updateCurrentUser( + req, + mock(), + mock({ + email: 'michel-new@email.com', + firstName: 'Michel', + lastName: 'n8n', + currentPassword: 'wrong-password', + }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow( + 'Unable to update profile. Please check your credentials and try again.', ); }); @@ -728,16 +734,15 @@ describe('MeController', () => { } as unknown as User; const req = { user, browserId } as unknown as AuthenticatedRequest; - await expect( - controller.updateCurrentUser( - req, - mock(), - mock({ email: user.email, firstName: 'John', lastName: 'Doe' }), - ), - ).rejects.toThrowError( - new ForbiddenError( - 'This account is managed via environment variables and cannot be modified through the API', - ), + const execution = controller.updateCurrentUser( + req, + mock(), + mock({ email: user.email, firstName: 'John', lastName: 'Doe' }), + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'This account is managed via environment variables and cannot be modified through the API', ); }); @@ -749,16 +754,15 @@ describe('MeController', () => { } as unknown as User, } as unknown as AuthenticatedRequest; - await expect( - controller.updatePassword( - req, - mock(), - mock({ currentPassword: 'old_password', newPassword: 'NewPassword123' }), - ), - ).rejects.toThrowError( - new ForbiddenError( - 'This account is managed via environment variables and cannot be modified through the API', - ), + const execution = controller.updatePassword( + req, + mock(), + mock({ currentPassword: 'old_password', newPassword: 'NewPassword123' }), + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'This account is managed via environment variables and cannot be modified through the API', ); }); @@ -795,22 +799,30 @@ describe('MeController', () => { const req = { user: mock({ password: undefined }), } as unknown as AuthenticatedRequest; - await expect( - controller.updatePassword(req, mock(), mock({ currentPassword: '', newPassword: '' })), - ).rejects.toThrowError(new BadRequestError('Requesting user not set up.')); + + const execution = controller.updatePassword( + req, + mock(), + mock({ currentPassword: '', newPassword: '' }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Requesting user not set up.'); }); it("should throw if currentPassword does not match the user's password", async () => { const req = { user: mock({ password: passwordHash }), } as unknown as AuthenticatedRequest; - await expect( - controller.updatePassword( - req, - mock(), - mock({ currentPassword: 'not_old_password', newPassword: '' }), - ), - ).rejects.toThrowError(new BadRequestError('Provided current password is incorrect.')); + + const execution = controller.updatePassword( + req, + mock(), + mock({ currentPassword: 'not_old_password', newPassword: '' }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Provided current password is incorrect.'); }); describe('should throw if newPassword is not valid', () => { @@ -820,13 +832,15 @@ describe('MeController', () => { user: mock({ password: passwordHash }), browserId, } as unknown as AuthenticatedRequest; - await expect( - controller.updatePassword( - req, - mock(), - mock({ currentPassword: 'old_password', newPassword }), - ), - ).rejects.toThrowError(new BadRequestError(errorMessage)); + + const execution = controller.updatePassword( + req, + mock(), + mock({ currentPassword: 'old_password', newPassword }), + ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow(errorMessage); }); }); }); @@ -876,15 +890,14 @@ describe('MeController', () => { user: mock({ password: passwordHash, mfaEnabled: true }), } as unknown as AuthenticatedRequest; - await expect( - controller.updatePassword( - req, - mock(), - mock({ currentPassword: 'old_password', newPassword: 'NewPassword123' }), - ), - ).rejects.toThrowError( - new BadRequestError('Two-factor code is required to change password.'), + const execution = controller.updatePassword( + req, + mock(), + mock({ currentPassword: 'old_password', newPassword: 'NewPassword123' }), ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Two-factor code is required to change password.'); }); it('should throw InvalidMfaCodeError if invalid mfa code is given', async () => { @@ -937,9 +950,11 @@ describe('MeController', () => { const req = mock({ body: undefined, }); - await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError( - new BadRequestError('Personalization answers are mandatory'), - ); + + const execution = controller.storeSurveyAnswers(req); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Personalization answers are mandatory'); }); it('should not flag XSS attempt for `<` sign in company size', async () => { @@ -977,7 +992,7 @@ describe('MeController', () => { [fieldName]: [''], }; - await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); + await expect(controller.storeSurveyAnswers(req)).rejects.toThrow(BadRequestError); }); test.each([ @@ -997,7 +1012,7 @@ describe('MeController', () => { [fieldName]: '', }; - await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); + await expect(controller.storeSurveyAnswers(req)).rejects.toThrow(BadRequestError); }); }); }); diff --git a/packages/cli/src/controllers/__tests__/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts index 1b4f5b9fafd..264d79d0656 100644 --- a/packages/cli/src/controllers/__tests__/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -30,9 +30,10 @@ describe('OwnerController', () => { .spyOn(ownershipService, 'setupOwner') .mockRejectedValueOnce(new BadRequestError('Instance owner already setup')); - await expect(controller.setupOwner(mock(), mock(), mock())).rejects.toThrowError( - new BadRequestError('Instance owner already setup'), - ); + const execution = controller.setupOwner(mock(), mock(), mock()); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Instance owner already setup'); expect(authService.issueCookie).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/controllers/__tests__/translation.controller.test.ts b/packages/cli/src/controllers/__tests__/translation.controller.test.ts index 9119fefb85b..4d797d04fd5 100644 --- a/packages/cli/src/controllers/__tests__/translation.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/translation.controller.test.ts @@ -22,9 +22,10 @@ describe('TranslationController', () => { const req = mock({ query: { credentialType } }); credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(false); - await expect(controller.getCredentialTranslation(req)).rejects.toThrowError( - new BadRequestError(`Invalid Credential type: "${credentialType}"`), - ); + const execution = controller.getCredentialTranslation(req); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow(`Invalid Credential type: "${credentialType}"`); }); it('should return translation json on valid credential types', async () => { diff --git a/packages/cli/src/license/__tests__/license.service.test.ts b/packages/cli/src/license/__tests__/license.service.test.ts index d700632b8d9..6bc5a2e1b44 100644 --- a/packages/cli/src/license/__tests__/license.service.test.ts +++ b/packages/cli/src/license/__tests__/license.service.test.ts @@ -98,9 +98,10 @@ describe('LicenseService', () => { Object.entries(LicenseErrors).forEach(([errorId, message]) => it(`should handle ${errorId} error`, async () => { license.activate.mockRejectedValueOnce(new LicenseError(errorId)); - await expect(licenseService.activateLicense('')).rejects.toThrowError( - new BadRequestError(message), - ); + const execution = licenseService.activateLicense(''); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow(message); }), ); }); @@ -126,9 +127,10 @@ describe('LicenseService', () => { test('on failure', async () => { license.renew.mockRejectedValueOnce(new LicenseError('RESERVATION_EXPIRED')); - await expect(licenseService.renewLicense()).rejects.toThrowError( - new BadRequestError('Activation key has expired'), - ); + + const execution = licenseService.renewLicense(); + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Activation key has expired'); expect(eventService.emit).toHaveBeenCalledWith('license-renewal-attempted', { success: false, diff --git a/packages/cli/src/modules/insights/__tests__/insights.controller.test.ts b/packages/cli/src/modules/insights/__tests__/insights.controller.test.ts index d314cec9089..646898e39fc 100644 --- a/packages/cli/src/modules/insights/__tests__/insights.controller.test.ts +++ b/packages/cli/src/modules/insights/__tests__/insights.controller.test.ts @@ -242,15 +242,18 @@ describe('InsightsController', () => { }); it('should throw a BadRequestError when endDate is before startDate', async () => { - await expect( - controller.getInsightsSummary(mock(), mock(), { + const execution = controller.getInsightsSummary( + mock(), + mock(), + { startDate: new Date('2025-06-10'), endDate: new Date('2025-06-01'), projectId: 'test-project', - }), - ).rejects.toThrowError( - new BadRequestError('endDate must be the same as or after startDate'), + }, ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('endDate must be the same as or after startDate'); }); }); @@ -266,14 +269,19 @@ describe('InsightsController', () => { ]); // ACT & ASSERT - await expect( - controller.getInsightsSummary(mock(), mock(), { + const execution = controller.getInsightsSummary( + mock(), + mock(), + { startDate: new Date('2025-06-01T00:00:00Z'), // same day as startDate to force 'hour' granularity endDate: new Date('2025-06-01T00:00:00Z'), - }), - ).rejects.toThrowError( - new ForbiddenError('Hourly data is not available with your current license'), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'Hourly data is not available with your current license', ); }); @@ -291,15 +299,18 @@ describe('InsightsController', () => { ]); // ACT & ASSERT - await expect( - controller.getInsightsSummary(mock(), mock(), { + const execution = controller.getInsightsSummary( + mock(), + mock(), + { startDate: outOfRangeStart, endDate, - }), - ).rejects.toThrowError( - new ForbiddenError( - 'The selected date range exceeds the maximum history allowed by your license', - ), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'The selected date range exceeds the maximum history allowed by your license', ); }); }); @@ -489,17 +500,20 @@ describe('InsightsController', () => { const startDate = DateTime.now().startOf('day').minus({ days: 10 }).toJSDate(); const endDate = DateTime.now().startOf('day').minus({ days: 12 }).toJSDate(); - await expect( - controller.getInsightsByWorkflow(mock(), mock(), { + const execution = controller.getInsightsByWorkflow( + mock(), + mock(), + { startDate, endDate, skip: 0, take: 5, sortBy: 'total:desc', - }), - ).rejects.toThrowError( - new BadRequestError('endDate must be the same as or after startDate'), + }, ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('endDate must be the same as or after startDate'); }); }); @@ -515,16 +529,21 @@ describe('InsightsController', () => { }); // ACT & ASSERT - await expect( - controller.getInsightsByWorkflow(mock(), mock(), { + const execution = controller.getInsightsByWorkflow( + mock(), + mock(), + { startDate: new Date('2025-06-01T00:00:00Z'), endDate: new Date('2025-06-01T00:00:00Z'), skip: 0, take: 5, sortBy: 'total:desc', - }), - ).rejects.toThrowError( - new ForbiddenError('Hourly data is not available with your current license'), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'Hourly data is not available with your current license', ); }); @@ -542,18 +561,21 @@ describe('InsightsController', () => { }); // ACT & ASSERT - await expect( - controller.getInsightsByWorkflow(mock(), mock(), { + const execution = controller.getInsightsByWorkflow( + mock(), + mock(), + { startDate: outOfRangeStart, endDate, skip: 0, take: 5, sortBy: 'total:desc', - }), - ).rejects.toThrowError( - new ForbiddenError( - 'The selected date range exceeds the maximum history allowed by your license', - ), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'The selected date range exceeds the maximum history allowed by your license', ); }); }); @@ -718,14 +740,17 @@ describe('InsightsController', () => { }); it('should throw a BadRequestError when endDate is before startDate', async () => { - await expect( - controller.getInsightsByTime(mock(), mock(), { + const execution = controller.getInsightsByTime( + mock(), + mock(), + { startDate: new Date('2025-06-10'), endDate: new Date('2025-06-01'), - }), - ).rejects.toThrowError( - new BadRequestError('endDate must be the same as or after startDate'), + }, ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('endDate must be the same as or after startDate'); }); }); @@ -736,13 +761,18 @@ describe('InsightsController', () => { insightsByPeriodRepository.getInsightsByTime.mockResolvedValue([]); - await expect( - controller.getInsightsByTime(mock(), mock(), { + const execution = controller.getInsightsByTime( + mock(), + mock(), + { startDate: new Date('2025-06-01T00:00:00Z'), endDate: new Date('2025-06-01T00:00:00Z'), - }), - ).rejects.toThrowError( - new ForbiddenError('Hourly data is not available with your current license'), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'Hourly data is not available with your current license', ); }); @@ -755,15 +785,18 @@ describe('InsightsController', () => { insightsByPeriodRepository.getInsightsByTime.mockResolvedValue([]); - await expect( - controller.getInsightsByTime(mock(), mock(), { + const execution = controller.getInsightsByTime( + mock(), + mock(), + { startDate: outOfRangeStart, endDate, - }), - ).rejects.toThrowError( - new ForbiddenError( - 'The selected date range exceeds the maximum history allowed by your license', - ), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'The selected date range exceeds the maximum history allowed by your license', ); }); }); @@ -866,14 +899,17 @@ describe('InsightsController', () => { }); it('should throw a BadRequestError when endDate is before startDate', async () => { - await expect( - controller.getTimeSavedInsightsByTime(mock(), mock(), { + const execution = controller.getTimeSavedInsightsByTime( + mock(), + mock(), + { startDate: new Date('2025-06-10'), endDate: new Date('2025-06-01'), - }), - ).rejects.toThrowError( - new BadRequestError('endDate must be the same as or after startDate'), + }, ); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('endDate must be the same as or after startDate'); }); }); @@ -884,13 +920,18 @@ describe('InsightsController', () => { insightsByPeriodRepository.getInsightsByTime.mockResolvedValue([]); - await expect( - controller.getTimeSavedInsightsByTime(mock(), mock(), { + const execution = controller.getTimeSavedInsightsByTime( + mock(), + mock(), + { startDate: new Date('2025-06-01T00:00:00Z'), endDate: new Date('2025-06-01T00:00:00Z'), - }), - ).rejects.toThrowError( - new ForbiddenError('Hourly data is not available with your current license'), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'Hourly data is not available with your current license', ); }); @@ -903,15 +944,18 @@ describe('InsightsController', () => { insightsByPeriodRepository.getInsightsByTime.mockResolvedValue([]); - await expect( - controller.getTimeSavedInsightsByTime(mock(), mock(), { + const execution = controller.getTimeSavedInsightsByTime( + mock(), + mock(), + { startDate: outOfRangeStart, endDate, - }), - ).rejects.toThrowError( - new ForbiddenError( - 'The selected date range exceeds the maximum history allowed by your license', - ), + }, + ); + + await expect(execution).rejects.toThrow(ForbiddenError); + await expect(execution).rejects.toThrow( + 'The selected date range exceeds the maximum history allowed by your license', ); }); }); diff --git a/packages/cli/src/modules/insights/__tests__/insights.service.integration.test.ts b/packages/cli/src/modules/insights/__tests__/insights.service.integration.test.ts index 2ce5e90ef3c..52740de3ead 100644 --- a/packages/cli/src/modules/insights/__tests__/insights.service.integration.test.ts +++ b/packages/cli/src/modules/insights/__tests__/insights.service.integration.test.ts @@ -994,12 +994,13 @@ describe('InsightsService (Integration)', () => { const startDate = DateTime.now().minus({ days: 3 }).startOf('day'); const endDate = startDate.plus({ hours: 10 }); - expect(() => + const execution = () => insightsService.validateDateFiltersLicense({ startDate: startDate.toJSDate(), endDate: endDate.toJSDate(), - }), - ).toThrowError(new UserError('Hourly data is not available with your current license')); + }); + expect(execution).toThrow(UserError); + expect(execution).toThrow('Hourly data is not available with your current license'); }); test('does not throw if granularity is hour and hourly data is licensed', () => { @@ -1025,10 +1026,10 @@ describe('InsightsService (Integration)', () => { const startDate = today.minus({ days: 8 }).toJSDate(); const endDate = today.toJSDate(); - expect(() => insightsService.validateDateFiltersLicense({ startDate, endDate })).toThrowError( - new UserError( - 'The selected date range exceeds the maximum history allowed by your license', - ), + const execution = () => insightsService.validateDateFiltersLicense({ startDate, endDate }); + expect(execution).toThrow(UserError); + expect(execution).toThrow( + 'The selected date range exceeds the maximum history allowed by your license', ); }); diff --git a/packages/cli/src/modules/sso-saml/__tests__/saml.service.ee.test.ts b/packages/cli/src/modules/sso-saml/__tests__/saml.service.ee.test.ts index 3e67158789e..8f1a40f801b 100644 --- a/packages/cli/src/modules/sso-saml/__tests__/saml.service.ee.test.ts +++ b/packages/cli/src/modules/sso-saml/__tests__/saml.service.ee.test.ts @@ -429,9 +429,10 @@ describe('SamlService', () => { raw: {}, }); - await expect( - samlService.handleSamlLogin(mock(), 'post'), - ).rejects.toThrowError(new BadRequestError('Invalid email format')); + const execution = samlService.handleSamlLogin(mock(), 'post'); + + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Invalid email format'); }); it('logs in user that has already completed onboarding', async () => { diff --git a/packages/cli/src/services/__tests__/ownership.service.test.ts b/packages/cli/src/services/__tests__/ownership.service.test.ts index a1e5a34e0de..26d0c7ab5fb 100644 --- a/packages/cli/src/services/__tests__/ownership.service.test.ts +++ b/packages/cli/src/services/__tests__/ownership.service.test.ts @@ -272,9 +272,9 @@ describe('OwnershipService', () => { it('should throw a BadRequestError if the instance owner is already setup', async () => { jest.spyOn(userRepository, 'exists').mockResolvedValueOnce(true); - await expect(ownershipService.setupOwner(mock())).rejects.toThrowError( - new BadRequestError('Instance owner already setup'), - ); + const execution = ownershipService.setupOwner(mock()); + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Instance owner already setup'); expect(userRepository.save).not.toHaveBeenCalled(); expect(eventService.emit).not.toHaveBeenCalled(); @@ -287,9 +287,9 @@ describe('OwnershipService', () => { userRepository.exists.mockResolvedValueOnce(false); userRepository.findOne.mockResolvedValueOnce(null); - await expect(ownershipService.setupOwner(mock())).rejects.toThrowError( - new BadRequestError('Instance owner shell user not found'), - ); + const execution = ownershipService.setupOwner(mock()); + await expect(execution).rejects.toThrow(BadRequestError); + await expect(execution).rejects.toThrow('Instance owner shell user not found'); expect(userRepository.save).not.toHaveBeenCalled(); }); diff --git a/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts b/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts index 083bff8ca51..6ecab26166f 100644 --- a/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts +++ b/packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts @@ -8,7 +8,12 @@ import type { INodeExecutionData, INodeType, } from 'n8n-workflow'; -import { NodeConnectionTypes, createRunExecutionData } from 'n8n-workflow'; +import { + ApplicationError, + NodeConnectionTypes, + UnexpectedError, + createRunExecutionData, +} from 'n8n-workflow'; import { mock } from 'vitest-mock-extended'; import { NodeTypes } from '@test/helpers'; @@ -62,9 +67,10 @@ describe('processRunExecutionData', () => { // ACT & ASSERT // The function returns a Promise, but throws synchronously, so we can't await it. // eslint-disable-next-line @typescript-eslint/promise-function-async - expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError( - 'Failed to run workflow due to missing execution data', - ); + const execution = () => workflowExecute.processRunExecutionData(workflow); + + expect(execution).toThrow(UnexpectedError); + expect(execution).toThrow('Failed to run workflow due to missing execution data'); }); test('returns input data verbatim', async () => { @@ -246,7 +252,10 @@ describe('processRunExecutionData', () => { // ACT & ASSERT // The function returns a Promise, but throws synchronously, so we can't await it. // eslint-disable-next-line @typescript-eslint/promise-function-async - expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError( + const execution = () => workflowExecute.processRunExecutionData(workflow); + + expect(execution).toThrow(ApplicationError); + expect(execution).toThrow( 'The workflow has issues and cannot be executed for that reason. Please fix them first.', ); }); diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/ensure-type.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/ensure-type.test.ts index 8114a39bc26..804385ae249 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/ensure-type.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/ensure-type.test.ts @@ -54,9 +54,10 @@ describe('ensureType', () => { it('throws error for invalid conversion to number', () => { const value = 'invalid'; - expect(() => ensureType('number', value, 'myParam')).toThrowError( - new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"), - ); + const execution = () => ensureType('number', value, 'myParam'); + + expect(execution).toThrow(ExpressionError); + expect(execution).toThrow("Parameter 'myParam' must be a number, but we got 'invalid'"); }); it('parses valid JSON string to object if toType is object', () => { @@ -68,9 +69,10 @@ describe('ensureType', () => { it('throws error for invalid JSON string to object conversion', () => { const value = 'invalid_json'; - expect(() => ensureType('object', value, 'myParam')).toThrowError( - "Parameter 'myParam' could not be parsed", - ); + const execution = () => ensureType('object', value, 'myParam'); + + expect(execution).toThrow(ExpressionError); + expect(execution).toThrow("Parameter 'myParam' could not be parsed"); }); it('throws error for non-array value if toType is array', () => { diff --git a/packages/nodes-base/nodes/Aws/Cognito/test/helpers/utils.test.ts b/packages/nodes-base/nodes/Aws/Cognito/test/helpers/utils.test.ts index b23ea9bd4d9..abade01aa16 100644 --- a/packages/nodes-base/nodes/Aws/Cognito/test/helpers/utils.test.ts +++ b/packages/nodes-base/nodes/Aws/Cognito/test/helpers/utils.test.ts @@ -460,13 +460,9 @@ describe('AWS Cognito - Helpers functions', () => { return undefined; }); - await expect( - preSendAttributes.call(loadOptionsFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError(loadOptionsFunctions.getNode(), 'No user attributes provided', { - description: 'At least one user attribute must be provided for the update operation.', - }), - ); + const execution = preSendAttributes.call(loadOptionsFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('No user attributes provided'); }); it('should throw an error if a user attribute is invalid (empty value) (create operation)', async () => { @@ -478,13 +474,9 @@ describe('AWS Cognito - Helpers functions', () => { return undefined; }); - await expect( - preSendAttributes.call(loadOptionsFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError(loadOptionsFunctions.getNode(), 'Invalid User Attribute', { - description: 'Each attribute must have a valid name and value.', - }), - ); + const execution = preSendAttributes.call(loadOptionsFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Invalid User Attribute'); }); it('should throw an error if email_verified is true but email is missing (create operation)', async () => { @@ -496,18 +488,9 @@ describe('AWS Cognito - Helpers functions', () => { return undefined; }); - await expect( - preSendAttributes.call(loadOptionsFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError( - loadOptionsFunctions.getNode(), - 'Missing required "email" attribute', - { - description: - '"email_verified" is set to true, but the corresponding "email" attribute is not provided.', - }, - ), - ); + const execution = preSendAttributes.call(loadOptionsFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Missing required "email" attribute'); }); it('should throw an error if phone_number_verified is true but phone_number is missing (create operation)', async () => { @@ -521,18 +504,9 @@ describe('AWS Cognito - Helpers functions', () => { return undefined; }); - await expect( - preSendAttributes.call(loadOptionsFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError( - loadOptionsFunctions.getNode(), - 'Missing required "phone_number" attribute', - { - description: - '"phone_number_verified" is set to true, but the corresponding "phone_number" attribute is not provided.', - }, - ), - ); + const execution = preSendAttributes.call(loadOptionsFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Missing required "phone_number" attribute'); }); it('should add the user attribute to the body when valid (create operation)', async () => { @@ -594,13 +568,9 @@ describe('AWS Cognito - Helpers functions', () => { return undefined; }); - await expect( - preSendAttributes.call(loadOptionsFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError(loadOptionsFunctions.getNode(), 'Invalid User Attribute', { - description: 'Each attribute must have a valid name and value.', - }), - ); + const execution = preSendAttributes.call(loadOptionsFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Invalid User Attribute'); }); }); diff --git a/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts index bfca99388ab..842c05ffecd 100644 --- a/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts +++ b/packages/nodes-base/nodes/Aws/IAM/test/helpers/errorHandler.test.ts @@ -1,7 +1,6 @@ import { NodeApiError } from 'n8n-workflow'; import type { INodeExecutionData, IN8nHttpFullResponse, JsonObject } from 'n8n-workflow'; -import { ERROR_DESCRIPTIONS } from '../../helpers/constants'; import { handleError } from '../../helpers/errorHandler'; const mockExecuteSingleFunctions = { @@ -49,12 +48,9 @@ describe('handleError', () => { Error: { Code: 'NoSuchEntity', Message: 'User "nonExistentUser" does not exist' }, } as JsonObject; - await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, { - message: 'User "nonExistentUser" does not exist', - description: ERROR_DESCRIPTIONS.NoSuchEntity.User, - }), - ); + const promise = handleError.call(mockExecuteSingleFunctions, data, response); + await expect(promise).rejects.toThrow(NodeApiError); + await expect(promise).rejects.toThrow('User "nonExistentUser" does not exist'); }); test('should throw generic error if no specific mapping exists', async () => { diff --git a/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts b/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts index a413a034e50..08f51eac7e7 100644 --- a/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts +++ b/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts @@ -220,9 +220,9 @@ describe('AWS IAM - Helper Functions', () => { return ''; }); - await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError( - new NodeOperationError(mockNode.getNode(), 'User name should not contain spaces.'), - ); + const execution = validateName.call(mockNode, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('User name should not contain spaces.'); }); it('should throw an error if userName contains invalid characters', async () => { @@ -232,11 +232,10 @@ describe('AWS IAM - Helper Functions', () => { return ''; }); - await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError( - new NodeOperationError( - mockNode.getNode(), - 'User name can have up to 64 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).', - ), + const execution = validateName.call(mockNode, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow( + 'User name can have up to 64 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).', ); }); @@ -257,9 +256,9 @@ describe('AWS IAM - Helper Functions', () => { return ''; }); - await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError( - new NodeOperationError(mockNode.getNode(), 'Group name should not contain spaces.'), - ); + const execution = validateName.call(mockNode, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Group name should not contain spaces.'); }); it('should throw an error if groupName contains invalid characters', async () => { @@ -269,11 +268,10 @@ describe('AWS IAM - Helper Functions', () => { return ''; }); - await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError( - new NodeOperationError( - mockNode.getNode(), - 'Group name can have up to 128 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).', - ), + const execution = validateName.call(mockNode, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow( + 'Group name can have up to 128 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).', ); }); diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/credentials/sharedKey.test.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/credentials/sharedKey.test.ts index 5c0e28ed45b..3472d32803d 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/credentials/sharedKey.test.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/credentials/sharedKey.test.ts @@ -63,11 +63,13 @@ describe('Azure Cosmos DB', () => { expect(foundResource).toBeUndefined(); - expect(() => { + const throwFn = () => { 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')); + }; + expect(throwFn).toThrow(OperationalError); + expect(throwFn).toThrow('Unable to determine the resource type from the URL'); }); it('should throw OperationalError if no resource type found in URL path', async () => { @@ -100,11 +102,13 @@ describe('Azure Cosmos DB', () => { expect(foundResource).toBeUndefined(); - expect(() => { + const throwFn = () => { 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')); + }; + expect(throwFn).toThrow(OperationalError); + expect(throwFn).toThrow('Unable to determine the resource type from the URL'); }); it('should properly construct the resourceId and payload', async () => { diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts index 80f8c8db028..370167926eb 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts @@ -52,11 +52,9 @@ describe('getPartitionKey', () => { 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', - }), - ); + const execution = getPartitionKey.call(mockExecuteSingleFunctions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Partition key not found'); }); test('should throw NodeApiError for 404 error', async () => { @@ -77,16 +75,9 @@ describe('getPartitionKey', () => { azureCosmosDbApiRequest.mockRejectedValue(mockError); - await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError( - new NodeApiError( - mockExecuteSingleFunctions.getNode(), - {}, - { - message: errorMessage, - description: ErrorMap.Container.NotFound.description, - }, - ), - ); + const execution = getPartitionKey.call(mockExecuteSingleFunctions); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow(errorMessage); }); }); @@ -110,18 +101,9 @@ describe('validatePartitionKey', () => { }; 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'.", - }, - ), - ); + const execution = validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow("Partition key not found in 'Item Contents'"); }); test('should throw NodeOperationError when partition key is missing for "update" operation', async () => { @@ -135,17 +117,9 @@ describe('validatePartitionKey', () => { }; 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.', - }, - ), - ); + const execution = validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Partition key is missing or empty'); }); test('should throw NodeOperationError when partition key is missing for "get" operation', async () => { @@ -159,17 +133,9 @@ describe('validatePartitionKey', () => { }; 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.', - }, - ), - ); + const execution = validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Partition key is missing or empty'); }); test('should throw NodeOperationError when invalid JSON is provided for customProperties', async () => { @@ -183,17 +149,9 @@ describe('validatePartitionKey', () => { }; 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', - }, - ), - ); + const execution = validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Invalid JSON format in "Item Contents"'); }); }); @@ -229,17 +187,9 @@ describe('validateQueryParameters', () => { .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', - }, - ), - ); + const execution = validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Empty parameter value provided'); }); test('should successfully map parameters when they match', async () => { @@ -394,18 +344,9 @@ describe('validateQueryParameters', () => { .mockReturnValueOnce('$1') .mockReturnValueOnce({ queryParametersJson: '{"a": 1}' }); - await expect( - validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions), - ).rejects.toThrowError( - new NodeOperationError( - mockExecuteSingleFunctions.getNode(), - 'Query Parameters (JSON) must be a JSON array', - { - description: - 'Provide values as a JSON array, e.g. [1737062400000, "01234", true, null]', - }, - ), - ); + const execution = validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + await expect(execution).rejects.toThrow(NodeOperationError); + await expect(execution).rejects.toThrow('Query Parameters (JSON) must be a JSON array'); }); }); @@ -437,16 +378,16 @@ describe('processJsonInput', () => { 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' }), - ); + const throwFn = () => processJsonInput(invalidJson); + expect(throwFn).toThrow(OperationalError); + expect(throwFn).toThrow('Input must contain a valid JSON'); }); 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' }), - ); + const throwFn = () => processJsonInput(invalidInput, 'testInput'); + expect(throwFn).toThrow(OperationalError); + expect(throwFn).toThrow("Input 'testInput' must contain a valid JSON"); }); }); @@ -470,23 +411,21 @@ describe('validateCustomProperties', () => { 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 emptyExecution = validateCustomProperties.call( + mockExecuteSingleFunctions, + requestOptions, ); + await expect(emptyExecution).rejects.toThrow(NodeOperationError); + await expect(emptyExecution).rejects.toThrow('Item contents are empty'); 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.', - }), + const invalidExecution = validateCustomProperties.call( + mockExecuteSingleFunctions, + requestOptions, ); + await expect(invalidExecution).rejects.toThrow(NodeOperationError); + await expect(invalidExecution).rejects.toThrow('Item contents are empty'); }); }); diff --git a/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts b/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts index 3379889f436..0db2bafe3bc 100644 --- a/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts +++ b/packages/nodes-base/nodes/Perplexity/test/GenericFunction.test.ts @@ -65,19 +65,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'Invalid model', - description: - 'The model is not valid. Permitted models can be found in the documentation at https://docs.perplexity.ai/guides/model-cards.', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('Invalid model'); }); it('should throw NodeApiError with "Invalid parameter" message if error type is invalid_parameter', async () => { @@ -91,19 +85,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'Invalid parameter provided.', - description: - 'Please check all input parameters and ensure they are correctly formatted. Valid values can be found in the documentation at https://docs.perplexity.ai/api-reference/chat-completions.', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('Invalid parameter provided.'); }); it('should handle "invalid_model" error with itemIndex', async () => { @@ -118,18 +106,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'Invalid model', - description: 'Permitted models documentation...', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('Invalid model'); }); it('should handle "invalid_parameter" error with non-string message', async () => { @@ -143,18 +126,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'An unexpected issue occurred.', - description: 'Please check parameters...', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('An unexpected issue occurred.'); }); it('should throw generic error for unknown error type', async () => { @@ -168,18 +146,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'Internal server error.', - description: 'Refer to API documentation...', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('Internal server error.'); }); it('should include itemIndex in error message when present', async () => { @@ -194,17 +167,13 @@ describe('Generic Functions', () => { }, }; - await expect( - sendErrorPostReceive.call( - mockExecuteSingleFunctions, - testData, - errorResponse as unknown as IN8nHttpFullResponse, - ), - ).rejects.toThrowError( - new NodeApiError(mockExecuteSingleFunctions.getNode(), errorResponse.body, { - message: 'Error with item [Item 2].', - }), + const execution = sendErrorPostReceive.call( + mockExecuteSingleFunctions, + testData, + errorResponse as unknown as IN8nHttpFullResponse, ); + await expect(execution).rejects.toThrow(NodeApiError); + await expect(execution).rejects.toThrow('Error with item [Item 2].'); }); });