mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
Add ChatTrigger node tests and update TriggerHelpers
- Test webhook setup and execution - Add file upload handling tests - Test previous session loading - Mock memory and error scenarios - Update TriggerHelpers for input connection
This commit is contained in:
parent
8a240380f4
commit
201e510f10
|
@ -473,7 +473,6 @@ export class ChatTrigger implements INodeType {
|
||||||
try {
|
try {
|
||||||
await validateAuth(this);
|
await validateAuth(this);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('🚀 ~ ChatTrigger ~ webhook ~ error:', error);
|
|
||||||
if (error) {
|
if (error) {
|
||||||
res.writeHead((error as IDataObject).responseCode as number, {
|
res.writeHead((error as IDataObject).responseCode as number, {
|
||||||
'www-authenticate': 'Basic realm="Webhook"',
|
'www-authenticate': 'Basic realm="Webhook"',
|
||||||
|
@ -540,8 +539,6 @@ export class ChatTrigger implements INodeType {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
webhookResponse.error = error.message;
|
webhookResponse.error = error.message;
|
||||||
this.logger.error(`Could not retrieve memory for loadPreviousSession: ${error.message}`);
|
this.logger.error(`Could not retrieve memory for loadPreviousSession: ${error.message}`);
|
||||||
} finally {
|
|
||||||
return { webhookResponse };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
import { testWebhookTriggerNode } from 'n8n-nodes-base/test/nodes/TriggerHelpers';
|
||||||
|
import { ApplicationError, NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { ChatTrigger } from '../ChatTrigger.node';
|
||||||
|
|
||||||
|
describe('ChatTrigger', () => {
|
||||||
|
// Helper function to create mocked message JSON
|
||||||
|
const mockedMessageJson = (content: string, type: string) => ({
|
||||||
|
toJSON: () => ({ content, type }),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webhook setup (GET request)', () => {
|
||||||
|
it('should return 404 when not public', async () => {
|
||||||
|
const { response } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
webhookName: 'setup',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return chat page for public hosted mode', async () => {
|
||||||
|
const { response } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
webhookName: 'setup',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
initialMessages: 'Hello\nHow can I help?',
|
||||||
|
options: {
|
||||||
|
title: 'Test Chat',
|
||||||
|
subtitle: 'Test Subtitle',
|
||||||
|
inputPlaceholder: 'Type here...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(response.send).toHaveBeenCalledWith(expect.stringContaining('Test Chat'));
|
||||||
|
expect(response.send).toHaveBeenCalledWith(expect.stringContaining('Test Subtitle'));
|
||||||
|
expect(response.send).toHaveBeenCalledWith(expect.stringContaining('Type here...'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return chat page for webhook mode', async () => {
|
||||||
|
const { response } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
webhookName: 'setup',
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'webhook',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webhook execution (POST request)', () => {
|
||||||
|
it('should handle regular message', async () => {
|
||||||
|
const { responseData } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bodyData: {
|
||||||
|
message: 'Test message',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(responseData?.workflowData?.[0]).toEqual([{ json: { message: 'Test message' } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file uploads', async () => {
|
||||||
|
const mockFile = {
|
||||||
|
filepath: '/tmp/test.txt',
|
||||||
|
originalFilename: 'test.txt',
|
||||||
|
newFilename: 'test.txt',
|
||||||
|
mimetype: 'text/plain',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { responseData } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
options: {
|
||||||
|
allowFileUploads: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'multipart/form-data',
|
||||||
|
body: {
|
||||||
|
data: { message: 'Test with file' },
|
||||||
|
files: {
|
||||||
|
upload: mockFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnData = responseData?.workflowData?.[0]?.[0];
|
||||||
|
|
||||||
|
expect(returnData?.json?.files).toBeDefined();
|
||||||
|
const returnDataFiles = returnData?.json?.files;
|
||||||
|
expect(Array.isArray(returnDataFiles)).toBe(true);
|
||||||
|
if (Array.isArray(returnDataFiles)) {
|
||||||
|
expect(returnDataFiles.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
expect(returnData?.binary).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load previous session from memory when requested', async () => {
|
||||||
|
// Mock memory with chat history
|
||||||
|
const mockMemory = {
|
||||||
|
chatHistory: {
|
||||||
|
getMessages: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue([
|
||||||
|
mockedMessageJson('My name is Nathan.', 'human'),
|
||||||
|
mockedMessageJson('K.', 'ai'),
|
||||||
|
mockedMessageJson('What is my name?', 'human'),
|
||||||
|
mockedMessageJson('Nathan.', 'ai'),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
options: {
|
||||||
|
loadPreviousSession: 'memory',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { responseData } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
node,
|
||||||
|
bodyData: {
|
||||||
|
action: 'loadPreviousSession',
|
||||||
|
},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
type: NodeConnectionType.AiMemory,
|
||||||
|
typeVersion: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async getInputConnectionData(type) {
|
||||||
|
if (type === NodeConnectionType.AiMemory) {
|
||||||
|
return mockMemory;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the previous session is loaded correctly
|
||||||
|
expect(responseData?.webhookResponse?.data).toHaveLength(4);
|
||||||
|
expect(responseData?.webhookResponse?.data[2]).toEqual({
|
||||||
|
content: 'What is my name?',
|
||||||
|
type: 'human',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when loadPreviousSession is notSupported', async () => {
|
||||||
|
const { responseData } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
node: {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
options: {
|
||||||
|
loadPreviousSession: 'notSupported',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bodyData: {
|
||||||
|
action: 'loadPreviousSession',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(responseData?.webhookResponse?.data).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when loadPreviousSession throws but configured in params', async () => {
|
||||||
|
// Mock memory with an error when getting messages
|
||||||
|
const mockMemory = {
|
||||||
|
chatHistory: {
|
||||||
|
getMessages: jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
toJSON: () => {
|
||||||
|
throw new ApplicationError('Error when getting message');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
parameters: {
|
||||||
|
public: true,
|
||||||
|
mode: 'hostedChat',
|
||||||
|
options: {
|
||||||
|
loadPreviousSession: 'memory',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorLogger = jest.fn();
|
||||||
|
const { responseData } = await testWebhookTriggerNode(ChatTrigger, {
|
||||||
|
node,
|
||||||
|
bodyData: {
|
||||||
|
action: 'loadPreviousSession',
|
||||||
|
},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
type: NodeConnectionType.AiMemory,
|
||||||
|
typeVersion: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async getInputConnectionData(type) {
|
||||||
|
if (type === NodeConnectionType.AiMemory) {
|
||||||
|
return mockMemory;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
error: errorLogger,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the response is empty and contains an error message
|
||||||
|
expect(responseData?.webhookResponse?.data).toHaveLength(0);
|
||||||
|
expect(errorLogger).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Could not retrieve memory for loadPreviousSession'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import set from 'lodash/set';
|
||||||
import { PollContext, returnJsonArray, type InstanceSettings } from 'n8n-core';
|
import { PollContext, returnJsonArray, type InstanceSettings } from 'n8n-core';
|
||||||
import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager';
|
import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager';
|
||||||
import type {
|
import type {
|
||||||
|
FunctionsBase,
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
@ -16,6 +17,7 @@ import type {
|
||||||
ITriggerFunctions,
|
ITriggerFunctions,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
NodeConnectionType,
|
||||||
NodeTypeAndVersion,
|
NodeTypeAndVersion,
|
||||||
VersionedNodeType,
|
VersionedNodeType,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
@ -37,6 +39,8 @@ type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
|
||||||
request?: MockDeepPartial<express.Request>;
|
request?: MockDeepPartial<express.Request>;
|
||||||
bodyData?: IDataObject;
|
bodyData?: IDataObject;
|
||||||
childNodes?: NodeTypeAndVersion[];
|
childNodes?: NodeTypeAndVersion[];
|
||||||
|
getInputConnectionData?: (type: NodeConnectionType) => Promise<unknown>;
|
||||||
|
logger?: Partial<FunctionsBase['logger']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {};
|
type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {};
|
||||||
|
@ -154,7 +158,13 @@ export async function testWebhookTriggerNode(
|
||||||
getInstanceId: () => 'instanceId',
|
getInstanceId: () => 'instanceId',
|
||||||
getBodyData: () => options.bodyData ?? {},
|
getBodyData: () => options.bodyData ?? {},
|
||||||
getHeaderData: () => ({}),
|
getHeaderData: () => ({}),
|
||||||
getInputConnectionData: async () => ({}),
|
getInputConnectionData: options.getInputConnectionData ?? jest.fn(),
|
||||||
|
logger: options.logger ?? {
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
},
|
||||||
getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`,
|
getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`,
|
||||||
getParamsData: () => ({}),
|
getParamsData: () => ({}),
|
||||||
getQueryData: () => ({}),
|
getQueryData: () => ({}),
|
||||||
|
|
Loading…
Reference in a new issue