feat: Refactor codebase, improve types, attempt test fixes
This commit is contained in:
@@ -2,33 +2,40 @@
|
||||
* N8nApiClient unit tests
|
||||
*/
|
||||
|
||||
import '@jest/globals';
|
||||
import axios from 'axios';
|
||||
import { N8nApiClient } from '../../../src/api/client.js';
|
||||
import { EnvConfig } from '../../../src/config/environment.js';
|
||||
import { N8nApiError } from '../../../src/errors/index.js';
|
||||
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js';
|
||||
import { mockApiResponses } from '../../mocks/n8n-fixtures.js';
|
||||
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; // Explicit import
|
||||
import axios, { AxiosInstance } from 'axios'; // Import AxiosInstance type
|
||||
import { N8nApiClient } from '../../../src/api/client.js'; // Add .js
|
||||
import { EnvConfig } from '../../../src/config/environment.js'; // Add .js
|
||||
import { N8nApiError } from '../../../src/errors/index.js'; // Add .js
|
||||
import { createMockAxiosInstance, createMockAxiosResponse } from '../../mocks/axios-mock.js'; // Add .js
|
||||
import { mockApiResponses } from '../../mocks/n8n-fixtures.js'; // Add .js
|
||||
|
||||
// We will spy on axios.create instead of mocking the whole module
|
||||
// jest.mock('axios');
|
||||
// const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('N8nApiClient', () => {
|
||||
// Mock configuration
|
||||
const mockConfig: EnvConfig = {
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
n8nWebhookUsername: 'test-user', // Added missing property
|
||||
n8nWebhookPassword: 'test-password', // Added missing property
|
||||
debug: false,
|
||||
};
|
||||
|
||||
// Define a type for the mock axios instance based on axios-mock.ts
|
||||
type MockAxiosInstance = ReturnType<typeof createMockAxiosInstance>;
|
||||
|
||||
// Mock axios instance
|
||||
let mockAxios;
|
||||
let mockAxios: MockAxiosInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create the mock instance
|
||||
mockAxios = createMockAxiosInstance();
|
||||
(axios.create as jest.Mock).mockReturnValue(mockAxios);
|
||||
// Spy on axios.create and mock its return value
|
||||
jest.spyOn(axios, 'create').mockReturnValue(mockAxios as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -42,7 +49,7 @@ describe('N8nApiClient', () => {
|
||||
new N8nApiClient(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
expect(axios.create).toHaveBeenCalledWith({ // Check the spy
|
||||
baseURL: mockConfig.n8nApiUrl,
|
||||
headers: {
|
||||
'X-N8N-API-KEY': mockConfig.n8nApiKey,
|
||||
@@ -80,6 +87,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: { data: [] },
|
||||
});
|
||||
|
||||
@@ -92,6 +100,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error', // Added statusText
|
||||
data: { message: 'Server error' },
|
||||
});
|
||||
|
||||
@@ -116,6 +125,7 @@ describe('N8nApiClient', () => {
|
||||
const mockWorkflows = mockApiResponses.workflows.list;
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockWorkflows,
|
||||
});
|
||||
|
||||
@@ -132,6 +142,7 @@ describe('N8nApiClient', () => {
|
||||
const client = new N8nApiClient(mockConfig);
|
||||
mockAxios.addMockResponse('get', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: {},
|
||||
});
|
||||
|
||||
@@ -160,6 +171,7 @@ describe('N8nApiClient', () => {
|
||||
const mockWorkflow = mockApiResponses.workflows.single(workflowId);
|
||||
mockAxios.addMockResponse('get', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockWorkflow,
|
||||
});
|
||||
|
||||
@@ -193,6 +205,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/execute`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -214,6 +227,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', '/workflows', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -236,6 +250,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('put', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -257,6 +272,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('delete', `/workflows/${workflowId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -278,6 +294,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/activate`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -299,6 +316,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('post', `/workflows/${workflowId}/deactivate`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -318,6 +336,7 @@ describe('N8nApiClient', () => {
|
||||
const mockExecutions = mockApiResponses.executions.list;
|
||||
mockAxios.addMockResponse('get', '/executions', {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockExecutions,
|
||||
});
|
||||
|
||||
@@ -338,6 +357,7 @@ describe('N8nApiClient', () => {
|
||||
const mockExecution = mockApiResponses.executions.single(executionId);
|
||||
mockAxios.addMockResponse('get', `/executions/${executionId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockExecution,
|
||||
});
|
||||
|
||||
@@ -359,6 +379,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
mockAxios.addMockResponse('delete', `/executions/${executionId}`, {
|
||||
status: 200,
|
||||
statusText: 'OK', // Added statusText
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Simple HTTP client tests without complex dependencies
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Create a simple HTTP client class to test
|
||||
class SimpleHttpClient {
|
||||
constructor(private baseUrl: string, private apiKey: string) {}
|
||||
|
||||
getBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
getApiKey(): string {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
buildAuthHeader(): Record<string, string> {
|
||||
return {
|
||||
'X-N8N-API-KEY': this.apiKey
|
||||
};
|
||||
}
|
||||
|
||||
formatUrl(path: string): string {
|
||||
return `${this.baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SimpleHttpClient', () => {
|
||||
it('should store baseUrl and apiKey properly', () => {
|
||||
const baseUrl = 'https://n8n.example.com/api/v1';
|
||||
const apiKey = 'test-api-key';
|
||||
const client = new SimpleHttpClient(baseUrl, apiKey);
|
||||
|
||||
expect(client.getBaseUrl()).toBe(baseUrl);
|
||||
expect(client.getApiKey()).toBe(apiKey);
|
||||
});
|
||||
|
||||
it('should create proper auth headers', () => {
|
||||
const client = new SimpleHttpClient('https://n8n.example.com/api/v1', 'test-api-key');
|
||||
const headers = client.buildAuthHeader();
|
||||
|
||||
expect(headers).toEqual({ 'X-N8N-API-KEY': 'test-api-key' });
|
||||
});
|
||||
|
||||
it('should format URLs correctly', () => {
|
||||
const baseUrl = 'https://n8n.example.com/api/v1';
|
||||
const client = new SimpleHttpClient(baseUrl, 'test-api-key');
|
||||
|
||||
expect(client.formatUrl('workflows')).toBe(`${baseUrl}/workflows`);
|
||||
expect(client.formatUrl('/workflows')).toBe(`${baseUrl}/workflows`);
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||
import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js';
|
||||
import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js'; // Add .js
|
||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ErrorCode } from '../../../src/errors/error-codes.js';
|
||||
import { mockEnv } from '../../test-setup.js';
|
||||
import { ErrorCode } from '../../../src/errors/error-codes.js'; // Add .js
|
||||
import { mockEnv } from '../../test-setup.js'; // Add .js
|
||||
|
||||
describe('Environment Configuration', () => {
|
||||
const originalEnv = process.env;
|
||||
@@ -36,6 +36,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -44,6 +46,8 @@ describe('Environment Configuration', () => {
|
||||
expect(config).toEqual({
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
n8nWebhookUsername: 'test-user',
|
||||
n8nWebhookPassword: 'test-pass',
|
||||
debug: false,
|
||||
});
|
||||
});
|
||||
@@ -52,6 +56,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'true';
|
||||
|
||||
// Execute
|
||||
@@ -65,6 +71,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'TRUE';
|
||||
|
||||
// Execute
|
||||
@@ -78,6 +86,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
process.env[ENV_VARS.DEBUG] = 'yes';
|
||||
|
||||
// Execute
|
||||
@@ -117,6 +127,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'invalid-url';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute & Assert
|
||||
expect(() => getEnvConfig()).toThrow(
|
||||
@@ -131,6 +143,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'http://localhost:5678/api/v1';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -143,6 +157,8 @@ describe('Environment Configuration', () => {
|
||||
// Setup
|
||||
process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1/';
|
||||
process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key';
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'test-user'; // Add webhook user
|
||||
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'test-pass'; // Add webhook pass
|
||||
|
||||
// Execute
|
||||
const config = getEnvConfig();
|
||||
@@ -155,14 +171,18 @@ describe('Environment Configuration', () => {
|
||||
describe('with mockEnv helper', () => {
|
||||
// Using the mockEnv helper from test-setup
|
||||
mockEnv({
|
||||
[ENV_VARS.N8N_API_URL]: 'https://n8n.example.com/api/v1',
|
||||
[ENV_VARS.N8N_API_KEY]: 'test-api-key',
|
||||
[ENV_VARS.N8N_API_URL]: 'https://mock.n8n.com/api/v1',
|
||||
[ENV_VARS.N8N_API_KEY]: 'mock-api-key',
|
||||
[ENV_VARS.N8N_WEBHOOK_USERNAME]: 'mock-user', // Add webhook user
|
||||
[ENV_VARS.N8N_WEBHOOK_PASSWORD]: 'mock-pass', // Add webhook pass
|
||||
});
|
||||
|
||||
it('should use the mocked environment variables', () => {
|
||||
const config = getEnvConfig();
|
||||
expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1');
|
||||
expect(config.n8nApiKey).toBe('test-api-key');
|
||||
expect(config.n8nApiUrl).toBe('https://mock.n8n.com/api/v1');
|
||||
expect(config.n8nApiKey).toBe('mock-api-key');
|
||||
expect(config.n8nWebhookUsername).toBe('mock-user');
|
||||
expect(config.n8nWebhookPassword).toBe('mock-pass');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Simple environment configuration tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Simple environment validation function to test
|
||||
function validateEnvironment(env: Record<string, string | undefined>): {
|
||||
n8nApiUrl: string;
|
||||
n8nApiKey: string;
|
||||
debug: boolean;
|
||||
} {
|
||||
// Check required variables
|
||||
if (!env.N8N_API_URL) {
|
||||
throw new Error('Missing required environment variable: N8N_API_URL');
|
||||
}
|
||||
|
||||
if (!env.N8N_API_KEY) {
|
||||
throw new Error('Missing required environment variable: N8N_API_KEY');
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(env.N8N_API_URL);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid URL format for N8N_API_URL: ${env.N8N_API_URL}`);
|
||||
}
|
||||
|
||||
// Return parsed config
|
||||
return {
|
||||
n8nApiUrl: env.N8N_API_URL,
|
||||
n8nApiKey: env.N8N_API_KEY,
|
||||
debug: env.DEBUG?.toLowerCase() === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
describe('Environment Configuration', () => {
|
||||
describe('validateEnvironment', () => {
|
||||
it('should return a valid config when all required variables are present', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1',
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
const config = validateEnvironment(env);
|
||||
|
||||
expect(config).toEqual({
|
||||
n8nApiUrl: 'https://n8n.example.com/api/v1',
|
||||
n8nApiKey: 'test-api-key',
|
||||
debug: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should set debug to true when DEBUG=true', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1',
|
||||
N8N_API_KEY: 'test-api-key',
|
||||
DEBUG: 'true'
|
||||
};
|
||||
|
||||
const config = validateEnvironment(env);
|
||||
|
||||
expect(config.debug).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_URL is missing', () => {
|
||||
const env = {
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Missing required environment variable: N8N_API_URL'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_KEY is missing', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'https://n8n.example.com/api/v1'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Missing required environment variable: N8N_API_KEY'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when N8N_API_URL is not a valid URL', () => {
|
||||
const env = {
|
||||
N8N_API_URL: 'invalid-url',
|
||||
N8N_API_KEY: 'test-api-key'
|
||||
};
|
||||
|
||||
expect(() => validateEnvironment(env)).toThrow(
|
||||
'Invalid URL format for N8N_API_URL: invalid-url'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +1,38 @@
|
||||
/**
|
||||
* Simple test for URI Template functionality
|
||||
* Tests for dynamic workflow resource URI functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Simple functions to test without complex imports
|
||||
function getWorkflowResourceTemplateUri() {
|
||||
return 'n8n://workflows/{id}';
|
||||
}
|
||||
|
||||
function extractWorkflowIdFromUri(uri: string): string | null {
|
||||
const regex = /^n8n:\/\/workflows\/([^/]+)$/;
|
||||
const match = uri.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
// Import the actual functions from the source file with .js extension
|
||||
import {
|
||||
getWorkflowResourceTemplateUri,
|
||||
extractWorkflowIdFromUri
|
||||
} from '../../../../src/resources/dynamic/workflow.js';
|
||||
|
||||
describe('Workflow Resource URI Functions', () => {
|
||||
describe('getWorkflowResourceTemplateUri', () => {
|
||||
it('should return the correct URI template', () => {
|
||||
// Test the actual imported function
|
||||
expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractWorkflowIdFromUri', () => {
|
||||
it('should extract workflow ID from valid URI', () => {
|
||||
// Test the actual imported function
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc');
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes');
|
||||
});
|
||||
|
||||
it('should return null for invalid URI formats', () => {
|
||||
// Test the actual imported function
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull(); // Should fail based on regex
|
||||
expect(extractWorkflowIdFromUri('invalid://workflows/123')).toBeNull();
|
||||
expect(extractWorkflowIdFromUri('n8n://workflows/123/extra')).toBeNull(); // Should fail based on regex
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add tests for getWorkflowResource function (requires mocking apiService)
|
||||
});
|
||||
|
||||
154
tests/unit/tools/workflow/list.test.ts
Normal file
154
tests/unit/tools/workflow/list.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* ListWorkflowsHandler unit tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { ListWorkflowsHandler, getListWorkflowsToolDefinition } from '../../../../src/tools/workflow/list.js'; // Add .js back
|
||||
import { createMockWorkflows } from '../../../mocks/n8n-fixtures.js'; // Add .js back
|
||||
// Import BaseWorkflowToolHandler to check constructor call, but don't mock it directly
|
||||
import { BaseWorkflowToolHandler } from '../../../../src/tools/workflow/base-handler.js'; // Add .js back
|
||||
|
||||
// Generate mock data
|
||||
const mockWorkflows = createMockWorkflows();
|
||||
|
||||
// Mock dependencies of the BaseWorkflowToolHandler
|
||||
const mockGetWorkflows = jest.fn(); // Define typed mock function variable
|
||||
const mockApiService = {
|
||||
getWorkflows: mockGetWorkflows,
|
||||
// Add other methods used by BaseWorkflowToolHandler if necessary
|
||||
};
|
||||
const mockEnvConfig = {
|
||||
/* mock necessary env config properties */
|
||||
n8nApiUrl: 'http://mock-n8n.com',
|
||||
n8nApiKey: 'mock-key',
|
||||
n8nWebhookUsername: 'test-user', // Added webhook user
|
||||
n8nWebhookPassword: 'test-pass', // Added webhook pass
|
||||
};
|
||||
|
||||
// Add .js extension back to mock paths
|
||||
jest.mock('../../../../src/config/environment.js', () => ({
|
||||
getEnvConfig: jest.fn(() => mockEnvConfig),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../src/api/n8n-client.js', () => ({
|
||||
createApiService: jest.fn(() => mockApiService),
|
||||
}));
|
||||
|
||||
|
||||
import { Workflow, ToolCallResult } from '../../../../src/types/index.js'; // Add .js back
|
||||
|
||||
|
||||
describe('ListWorkflows Tool', () => {
|
||||
let handler: ListWorkflowsHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
// Instantiate the handler, passing the mocked apiService (cast to any)
|
||||
handler = new ListWorkflowsHandler(mockApiService as any);
|
||||
// Check that dependencies were called by the base constructor
|
||||
// Use the mocked functions directly now, casting the require result
|
||||
const configMock = jest.requireMock('../../../../src/config/environment.js') as any; // Add .js back
|
||||
const apiMock = jest.requireMock('../../../../src/api/n8n-client.js') as any; // Add .js back
|
||||
expect(configMock.getEnvConfig).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.createApiService).toHaveBeenCalledWith(mockEnvConfig);
|
||||
});
|
||||
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
// Execute
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
// Assert
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should fetch all workflows and format them when no filter is provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const expectedFormatted = mockWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({});
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
// Check the actual result, which uses the real base class formatters
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should filter workflows by active=true when provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const activeWorkflows = mockWorkflows.filter(wf => wf.active === true);
|
||||
const expectedFormatted = activeWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({ active: true });
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s) (filtered by active=true)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should filter workflows by active=false when provided', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockResolvedValue(mockWorkflows);
|
||||
const inactiveWorkflows = mockWorkflows.filter(wf => wf.active === false);
|
||||
const expectedFormatted = inactiveWorkflows.map((wf: Workflow) => ({ // Add type annotation
|
||||
id: wf.id,
|
||||
name: wf.name,
|
||||
active: wf.active,
|
||||
updatedAt: wf.updatedAt,
|
||||
}));
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({ active: false });
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
expect(result.isError).toBe(false);
|
||||
expect(result.content[0].text).toContain(`Found ${expectedFormatted.length} workflow(s) (filtered by active=false)`);
|
||||
expect(result.content[0].text).toContain(JSON.stringify(expectedFormatted, null, 2));
|
||||
});
|
||||
|
||||
it('should handle errors during API call', async () => {
|
||||
// Arrange
|
||||
const apiError = new Error('API Failure');
|
||||
// @ts-ignore - Suppress persistent TS2345 error
|
||||
mockGetWorkflows.mockRejectedValue(apiError);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute({});
|
||||
|
||||
// Assert
|
||||
expect(mockApiService.getWorkflows).toHaveBeenCalledTimes(1);
|
||||
// Check the actual error result formatted by the real base class method
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Error executing workflow tool: API Failure');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* ListWorkflowsHandler unit tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { getListWorkflowsToolDefinition } from '../../../../src/tools/workflow/list.js';
|
||||
import { mockApiResponses } from '../../../mocks/n8n-fixtures.js';
|
||||
|
||||
// Since this is an integration test, we'll test the definition directly
|
||||
// rather than mocking the complex handler implementation
|
||||
jest.mock('../../../../src/tools/workflow/base-handler.js');
|
||||
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
// Execute
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
// Assert
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Simple workflow tool tests without complex dependencies
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
// Mock workflow data
|
||||
const mockWorkflows = [
|
||||
{
|
||||
id: '1234abc',
|
||||
name: 'Test Workflow 1',
|
||||
active: true,
|
||||
createdAt: '2025-03-01T12:00:00.000Z',
|
||||
updatedAt: '2025-03-02T14:30:00.000Z',
|
||||
nodes: []
|
||||
},
|
||||
{
|
||||
id: '5678def',
|
||||
name: 'Test Workflow 2',
|
||||
active: false,
|
||||
createdAt: '2025-03-01T12:00:00.000Z',
|
||||
updatedAt: '2025-03-12T10:15:00.000Z',
|
||||
nodes: []
|
||||
}
|
||||
];
|
||||
|
||||
// Simple function to test tool definition
|
||||
function getListWorkflowsToolDefinition() {
|
||||
return {
|
||||
name: 'list_workflows',
|
||||
description: 'List all workflows with optional filtering by status',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Filter workflows by active status'
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Simple function to test workflow filtering
|
||||
function filterWorkflows(workflows, filter) {
|
||||
if (filter && typeof filter.active === 'boolean') {
|
||||
return workflows.filter(workflow => workflow.active === filter.active);
|
||||
}
|
||||
return workflows;
|
||||
}
|
||||
|
||||
describe('Workflow Tools', () => {
|
||||
describe('getListWorkflowsToolDefinition', () => {
|
||||
it('should return the correct tool definition', () => {
|
||||
const definition = getListWorkflowsToolDefinition();
|
||||
|
||||
expect(definition.name).toBe('list_workflows');
|
||||
expect(definition.description).toBeTruthy();
|
||||
expect(definition.inputSchema).toBeDefined();
|
||||
expect(definition.inputSchema.properties).toHaveProperty('active');
|
||||
expect(definition.inputSchema.required).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterWorkflows', () => {
|
||||
it('should return all workflows when no filter is provided', () => {
|
||||
const result = filterWorkflows(mockWorkflows, {});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(mockWorkflows);
|
||||
});
|
||||
|
||||
it('should filter workflows by active status when active is true', () => {
|
||||
const result = filterWorkflows(mockWorkflows, { active: true });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1234abc');
|
||||
expect(result[0].active).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter workflows by active status when active is false', () => {
|
||||
const result = filterWorkflows(mockWorkflows, { active: false });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('5678def');
|
||||
expect(result[0].active).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user