mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 16:57:08 +02:00
fix: Honor continueOnFail in Send and Wait operations (#30287)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
cdc12d202f
commit
f155aceed9
|
|
@ -94,4 +94,51 @@ describe('Test DiscordV2, message => sendAndWait', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
const setupErrorParameters = () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'operation') return SEND_AND_WAIT_OPERATION;
|
||||
if (key === 'resource') return 'message';
|
||||
if (key === 'authentication') return 'botToken';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'sendTo') return 'channel';
|
||||
if (key === 'channelId') return 'channelID';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { data: 'test' } }]);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&token=abc',
|
||||
);
|
||||
};
|
||||
|
||||
it('should route API errors to error output and skip putExecutionToWait when continueOnFail is true', async () => {
|
||||
setupErrorParameters();
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(transport.discordApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
Object.assign(new Error('channel_not_found'), { description: 'channel_not_found' }),
|
||||
);
|
||||
|
||||
const result = await discord.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: expect.any(String) } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
expect((result?.[0]?.[0] as { json: { error: string } }).json.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should rethrow API errors and skip putExecutionToWait when continueOnFail is false', async () => {
|
||||
setupErrorParameters();
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(transport.discordApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
Object.assign(new Error('channel_not_found'), { description: 'channel_not_found' }),
|
||||
);
|
||||
|
||||
await expect(discord.execute.call(mockExecuteFunctions)).rejects.toThrow();
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import type {
|
|||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { configureWaitTillDate } from '../../../../../utils/sendAndWait/configureWaitTillDate.util';
|
||||
import { getSendAndWaitProperties } from '../../../../../utils/sendAndWait/utils';
|
||||
import {
|
||||
createSendAndWaitMessageBody,
|
||||
parseDiscordError,
|
||||
prepareErrorData,
|
||||
sendDiscordMessage,
|
||||
} from '../../helpers/utils';
|
||||
import { sendToProperties } from '../common.description';
|
||||
|
|
@ -45,11 +45,14 @@ export async function execute(
|
|||
const err = parseDiscordError.call(this, error, 0);
|
||||
|
||||
if (this.continueOnFail()) {
|
||||
return prepareErrorData.call(this, err, 0);
|
||||
return [{ json: { error: err.message } }];
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return items;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import * as member from './member';
|
|||
import * as message from './message';
|
||||
import type { Discord } from './node.type';
|
||||
import * as webhook from './webhook';
|
||||
import { configureWaitTillDate } from '../../../../utils/sendAndWait/configureWaitTillDate.util';
|
||||
import { checkAccessToGuild } from '../helpers/utils';
|
||||
import { discordApiRequest } from '../transport';
|
||||
|
||||
|
|
@ -48,11 +47,7 @@ export async function router(this: IExecuteFunctions) {
|
|||
} as Discord;
|
||||
|
||||
if (discord.resource === 'message' && discord.operation === SEND_AND_WAIT_OPERATION) {
|
||||
returnData = await message[discord.operation].execute.call(this, guildId, userGuilds);
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
returnData = await message.sendAndWait.execute.call(this, guildId, userGuilds);
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,4 +69,56 @@ describe('Test EmailSendV2, email => sendAndWait', () => {
|
|||
to: 'to@mail.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should route SMTP errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('from@mail.com');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('to@mail.com');
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
transporter.sendMail.mockRejectedValueOnce(new Error('smtp_connection_refused'));
|
||||
|
||||
const result = await emailSendV2.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'smtp_connection_refused' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow SMTP errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('from@mail.com');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('to@mail.com');
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
transporter.sendMail.mockRejectedValueOnce(new Error('smtp_connection_refused'));
|
||||
|
||||
await expect(emailSendV2.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'smtp_connection_refused',
|
||||
);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { fromEmailProperty, toEmailProperty } from './descriptions';
|
||||
|
|
@ -52,7 +53,14 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
|
|||
const credentials = await this.getCredentials('smtp');
|
||||
const transporter = configureTransport(credentials, {});
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IRequestOptions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
|
|
@ -256,7 +257,14 @@ export class GoogleChat implements INodeType {
|
|||
const spaceId = this.getNodeParameter('spaceId', 0) as string;
|
||||
const body = createSendAndWaitMessageBody(this);
|
||||
|
||||
await googleApiRequest.call(this, 'POST', `/v1/${spaceId}/messages`, body);
|
||||
try {
|
||||
await googleApiRequest.call(this, 'POST', `/v1/${spaceId}/messages`, body);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -59,4 +59,58 @@ describe('Test GoogleChat, message => sendAndWait', () => {
|
|||
text: 'my message\n\n\n*<http://localhost/waiting-webhook/nodeID?approved=true&signature=abc|Approve>*\n\n_This_ _message_ _was_ _sent_ _automatically_ _with_ _<https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.googleChat_instanceId|n8n>_',
|
||||
});
|
||||
});
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('spaceID');
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(genericFunctions.googleApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('space_not_found'),
|
||||
);
|
||||
|
||||
const result = await googleChat.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'space_not_found' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow API errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('spaceID');
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(genericFunctions.googleApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('space_not_found'),
|
||||
);
|
||||
|
||||
await expect(googleChat.execute.call(mockExecuteFunctions)).rejects.toThrow('space_not_found');
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import {
|
||||
type INode,
|
||||
type INodeTypeBaseDescription,
|
||||
SEND_AND_WAIT_OPERATION,
|
||||
type IExecuteFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as genericFunctions from '../../GenericFunctions';
|
||||
import { GmailV2 } from '../../v2/GmailV2.node';
|
||||
|
||||
jest.mock('../../GenericFunctions', () => {
|
||||
const originalModule = jest.requireActual('../../GenericFunctions');
|
||||
return {
|
||||
...originalModule,
|
||||
googleApiRequest: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Test GmailV2, message => sendAndWait', () => {
|
||||
let gmail: GmailV2;
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
gmail = new GmailV2(mock<INodeTypeBaseDescription>());
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupParameters = () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message'); // resource
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION); // operation
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 2.2 }));
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
|
||||
// createEmail
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('to@mail.com'); // sendTo
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message'); // message
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject'); // subject
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // approvalOptions
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // options
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval'); // responseType
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // configureWaitTillDate
|
||||
};
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
setupParameters();
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(genericFunctions.googleApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('invalid_recipient'),
|
||||
);
|
||||
|
||||
const result = await gmail.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'invalid_recipient' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow API errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
setupParameters();
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(genericFunctions.googleApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('invalid_recipient'),
|
||||
);
|
||||
|
||||
await expect(gmail.execute.call(mockExecuteFunctions)).rejects.toThrow('invalid_recipient');
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
|
|
@ -181,9 +182,16 @@ export class GmailV2 implements INodeType {
|
|||
if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) {
|
||||
const email: IEmail = createEmail(this);
|
||||
|
||||
await googleApiRequest.call(this, 'POST', '/gmail/v1/users/me/messages/send', {
|
||||
raw: await encodeEmail(email),
|
||||
});
|
||||
try {
|
||||
await googleApiRequest.call(this, 'POST', '/gmail/v1/users/me/messages/send', {
|
||||
raw: await encodeEmail(email),
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -75,4 +75,58 @@ describe('Test MicrosoftOutlookV2, message => sendAndWait', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my@outlook.com');
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(transport.microsoftApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('recipient_not_found'),
|
||||
);
|
||||
|
||||
const result = await microsoftOutlook.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'recipient_not_found' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow API errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my@outlook.com');
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(transport.microsoftApiRequest as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('recipient_not_found'),
|
||||
);
|
||||
|
||||
await expect(microsoftOutlook.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'recipient_not_found',
|
||||
);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, INodeExecutionData, JsonObject } from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
import * as calendar from './calendar';
|
||||
|
|
@ -30,7 +30,14 @@ export async function router(this: IExecuteFunctions) {
|
|||
microsoftOutlook.resource === 'message' &&
|
||||
microsoftOutlook.operation === SEND_AND_WAIT_OPERATION
|
||||
) {
|
||||
await message[microsoftOutlook.operation].execute.call(this, 0, items);
|
||||
try {
|
||||
await message[microsoftOutlook.operation].execute.call(this, 0, items);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -67,4 +67,60 @@ describe('Test MicrosoftTeamsV2, chatMessage => sendAndWait', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'operation') return SEND_AND_WAIT_OPERATION;
|
||||
if (key === 'resource') return 'chatMessage';
|
||||
if (key === 'chatId') return 'chatID';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 2 }));
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(transport.microsoftApiRequest as jest.Mock).mockRejectedValueOnce(new Error('chat_not_found'));
|
||||
|
||||
const result = await microsoftTeamsV2.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'chat_not_found' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow API errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'operation') return SEND_AND_WAIT_OPERATION;
|
||||
if (key === 'resource') return 'chatMessage';
|
||||
if (key === 'chatId') return 'chatID';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 2 }));
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(transport.microsoftApiRequest as jest.Mock).mockRejectedValueOnce(new Error('chat_not_found'));
|
||||
|
||||
await expect(microsoftTeamsV2.execute.call(mockExecuteFunctions)).rejects.toThrow(
|
||||
'chat_not_found',
|
||||
);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
type IExecuteFunctions,
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type JsonObject,
|
||||
NodeOperationError,
|
||||
SEND_AND_WAIT_OPERATION,
|
||||
} from 'n8n-workflow';
|
||||
|
|
@ -33,7 +34,14 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
|
|||
microsoftTeamsTypeData.resource === 'chatMessage' &&
|
||||
microsoftTeamsTypeData.operation === SEND_AND_WAIT_OPERATION
|
||||
) {
|
||||
await chatMessage[microsoftTeamsTypeData.operation].execute.call(this, 0, instanceId);
|
||||
try {
|
||||
await chatMessage[microsoftTeamsTypeData.operation].execute.call(this, 0, instanceId);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IHttpRequestMethods,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
|
|
@ -1833,7 +1834,14 @@ export class Telegram implements INodeType {
|
|||
if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) {
|
||||
body = createSendAndWaitMessageBody(this);
|
||||
|
||||
await apiRequest.call(this, 'POST', 'sendMessage', body);
|
||||
try {
|
||||
await apiRequest.call(this, 'POST', 'sendMessage', body);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as JsonObject).message } }]];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -75,4 +75,56 @@ describe('Test Telegram, message => sendAndWait', () => {
|
|||
text: 'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('chatID');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(genericFunctions.apiRequest as jest.Mock).mockRejectedValueOnce(new Error('chat_not_found'));
|
||||
|
||||
const result = await telegram.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'chat_not_found' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rethrow API errors when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('chatID');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(genericFunctions.apiRequest as jest.Mock).mockRejectedValueOnce(new Error('chat_not_found'));
|
||||
|
||||
await expect(telegram.execute.call(mockExecuteFunctions)).rejects.toThrow('chat_not_found');
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,29 +78,32 @@ export class WhatsApp implements INodeType {
|
|||
customOperations = {
|
||||
message: {
|
||||
async [SEND_AND_WAIT_OPERATION](this: IExecuteFunctions) {
|
||||
const phoneNumberId = this.getNodeParameter('phoneNumberId', 0) as string;
|
||||
|
||||
const recipientPhoneNumber = sanitizePhoneNumber(
|
||||
this.getNodeParameter('recipientPhoneNumber', 0) as string,
|
||||
);
|
||||
|
||||
const config = getSendAndWaitConfig(this);
|
||||
const instanceId = this.getInstanceId();
|
||||
|
||||
try {
|
||||
const phoneNumberId = this.getNodeParameter('phoneNumberId', 0) as string;
|
||||
|
||||
const recipientPhoneNumber = sanitizePhoneNumber(
|
||||
this.getNodeParameter('recipientPhoneNumber', 0) as string,
|
||||
);
|
||||
|
||||
const config = getSendAndWaitConfig(this);
|
||||
const instanceId = this.getInstanceId();
|
||||
|
||||
await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
WHATSAPP_CREDENTIALS_TYPE,
|
||||
createMessage(config, phoneNumberId, recipientPhoneNumber, instanceId),
|
||||
);
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [this.getInputData()];
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), error);
|
||||
if (this.continueOnFail()) {
|
||||
return [[{ json: { error: (error as Error).message } }]];
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), error as Error);
|
||||
}
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [this.getInputData()];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,4 +66,64 @@ describe('Test WhatsApp Business Cloud, sendAndWait operation', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should route API errors to error output when continueOnFail is true', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'phoneNumberId') return '11111';
|
||||
if (key === 'recipientPhoneNumber') return '22222';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'sendTo') return 'channel';
|
||||
if (key === 'channelId') return 'channelID';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
|
||||
(mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('invalid_recipient'),
|
||||
);
|
||||
|
||||
const result = await whatsApp.customOperations.message.sendAndWait.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'invalid_recipient' } }]]);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NodeOperationError when continueOnFail is false', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'phoneNumberId') return '11111';
|
||||
if (key === 'recipientPhoneNumber') return '22222';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'sendTo') return 'channel';
|
||||
if (key === 'channelId') return 'channelID';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
(mockExecuteFunctions.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('invalid_recipient'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
whatsApp.customOperations.message.sendAndWait.call(mockExecuteFunctions),
|
||||
).rejects.toThrow('invalid_recipient');
|
||||
expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user