mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-22 10:23:08 +00:00
feat: replace n8n_create_data_table with n8n_manage_datatable (10 actions)
Replaces the single-purpose n8n_create_data_table tool with a comprehensive n8n_manage_datatable tool covering all 10 n8n data table API endpoints: Table operations: createTable, listTables, getTable, updateTable, deleteTable Row operations: getRows, insertRows, updateRows, upsertRows, deleteRows - Filter system with and/or logic and 8 condition operators - Dry-run support for updateRows, upsertRows, deleteRows - Pagination, sorting, and full-text search for row listing - 9 new N8nApiClient methods for all data table endpoints - Shared error handler and consolidated Zod schemas - Comprehensive tool documentation with examples per action - 36 handler tests + 18 API client tests BREAKING: n8n_create_data_table removed. Use n8n_manage_datatable with action="createTable" instead. Based on work by @djakielski in PR #646. Co-Authored-By: Dominik Jakielski <dominik.jakielski@urlaubsguru.de> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,251 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||
import { N8nApiError } from '@/utils/n8n-errors';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/services/n8n-api-client');
|
||||
vi.mock('@/config/n8n-api', () => ({
|
||||
getN8nApiConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/services/n8n-validation', () => ({
|
||||
validateWorkflowStructure: vi.fn(),
|
||||
hasWebhookTrigger: vi.fn(),
|
||||
getWebhookUrl: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
Logger: vi.fn().mockImplementation(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
})),
|
||||
LogLevel: {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('handleCreateDataTable', () => {
|
||||
let mockApiClient: any;
|
||||
let handlers: any;
|
||||
let getN8nApiConfig: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup mock API client
|
||||
mockApiClient = {
|
||||
createWorkflow: vi.fn(),
|
||||
getWorkflow: vi.fn(),
|
||||
updateWorkflow: vi.fn(),
|
||||
deleteWorkflow: vi.fn(),
|
||||
listWorkflows: vi.fn(),
|
||||
triggerWebhook: vi.fn(),
|
||||
getExecution: vi.fn(),
|
||||
listExecutions: vi.fn(),
|
||||
deleteExecution: vi.fn(),
|
||||
healthCheck: vi.fn(),
|
||||
createDataTable: vi.fn(),
|
||||
};
|
||||
|
||||
// Import mocked modules
|
||||
getN8nApiConfig = (await import('@/config/n8n-api')).getN8nApiConfig;
|
||||
|
||||
// Mock the API config
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue({
|
||||
baseUrl: 'https://n8n.test.com',
|
||||
apiKey: 'test-key',
|
||||
timeout: 30000,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
// Mock the N8nApiClient constructor
|
||||
vi.mocked(N8nApiClient).mockImplementation(() => mockApiClient);
|
||||
|
||||
// Import handlers module after setting up mocks
|
||||
handlers = await import('@/mcp/handlers-n8n-manager');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (handlers) {
|
||||
const clientGetter = handlers.getN8nApiClient;
|
||||
if (clientGetter) {
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||
clientGetter();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should create data table with name and columns successfully', async () => {
|
||||
const createdTable = {
|
||||
id: 'dt-123',
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ id: 'col-1', name: 'email', type: 'string', index: 0 },
|
||||
{ id: 'col-2', name: 'age', type: 'number', index: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ name: 'email', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: { id: 'dt-123', name: 'My Data Table' },
|
||||
message: 'Data table "My Data Table" created with ID: dt-123',
|
||||
});
|
||||
|
||||
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ name: 'email', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create data table with name only (no columns)', async () => {
|
||||
const createdTable = {
|
||||
id: 'dt-456',
|
||||
name: 'Empty Table',
|
||||
};
|
||||
|
||||
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Empty Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: { id: 'dt-456', name: 'Empty Table' },
|
||||
message: 'Data table "Empty Table" created with ID: dt-456',
|
||||
});
|
||||
|
||||
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||
name: 'Empty Table',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when n8n API is not configured', async () => {
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Test Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Zod validation error when name is missing', async () => {
|
||||
const result = await handlers.handleCreateDataTable({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
|
||||
it('should return Zod validation error when name is empty string', async () => {
|
||||
const result = await handlers.handleCreateDataTable({ name: '' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
|
||||
it('should return Zod validation error when column name is empty string', async () => {
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Valid Table',
|
||||
columns: [{ name: '' }],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
|
||||
it('should return error when API call fails', async () => {
|
||||
const apiError = new Error('Data table creation failed on the server');
|
||||
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Duplicate Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Data table creation failed on the server',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return structured error for N8nApiError', async () => {
|
||||
const apiError = new N8nApiError('Feature not available', 402, 'PAYMENT_REQUIRED', { plan: 'enterprise' });
|
||||
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Enterprise Table',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.code).toBe('PAYMENT_REQUIRED');
|
||||
expect(result.details).toEqual({ plan: 'enterprise' });
|
||||
});
|
||||
|
||||
it('should return error when API returns empty response (null)', async () => {
|
||||
mockApiClient.createDataTable.mockResolvedValue(null);
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Ghost Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Data table creation failed: n8n API returned an empty or invalid response',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when API returns response without id', async () => {
|
||||
mockApiClient.createDataTable.mockResolvedValue({ name: 'No ID Table' });
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'No ID Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Data table creation failed: n8n API returned an empty or invalid response',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Unknown error when a non-Error value is thrown', async () => {
|
||||
mockApiClient.createDataTable.mockRejectedValue('string-error');
|
||||
|
||||
const result = await handlers.handleCreateDataTable({
|
||||
name: 'Error Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Unknown error occurred',
|
||||
});
|
||||
});
|
||||
});
|
||||
727
tests/unit/mcp/handlers-manage-datatable.test.ts
Normal file
727
tests/unit/mcp/handlers-manage-datatable.test.ts
Normal file
@@ -0,0 +1,727 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||
import { N8nApiError } from '@/utils/n8n-errors';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/services/n8n-api-client');
|
||||
vi.mock('@/config/n8n-api', () => ({
|
||||
getN8nApiConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/services/n8n-validation', () => ({
|
||||
validateWorkflowStructure: vi.fn(),
|
||||
hasWebhookTrigger: vi.fn(),
|
||||
getWebhookUrl: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
Logger: vi.fn().mockImplementation(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
})),
|
||||
LogLevel: {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Data Table Handlers (n8n_manage_datatable)', () => {
|
||||
let mockApiClient: any;
|
||||
let handlers: any;
|
||||
let getN8nApiConfig: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup mock API client with all data table methods
|
||||
mockApiClient = {
|
||||
createWorkflow: vi.fn(),
|
||||
getWorkflow: vi.fn(),
|
||||
updateWorkflow: vi.fn(),
|
||||
deleteWorkflow: vi.fn(),
|
||||
listWorkflows: vi.fn(),
|
||||
triggerWebhook: vi.fn(),
|
||||
getExecution: vi.fn(),
|
||||
listExecutions: vi.fn(),
|
||||
deleteExecution: vi.fn(),
|
||||
healthCheck: vi.fn(),
|
||||
createDataTable: vi.fn(),
|
||||
listDataTables: vi.fn(),
|
||||
getDataTable: vi.fn(),
|
||||
updateDataTable: vi.fn(),
|
||||
deleteDataTable: vi.fn(),
|
||||
getDataTableRows: vi.fn(),
|
||||
insertDataTableRows: vi.fn(),
|
||||
updateDataTableRows: vi.fn(),
|
||||
upsertDataTableRow: vi.fn(),
|
||||
deleteDataTableRows: vi.fn(),
|
||||
};
|
||||
|
||||
// Import mocked modules
|
||||
getN8nApiConfig = (await import('@/config/n8n-api')).getN8nApiConfig;
|
||||
|
||||
// Mock the API config
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue({
|
||||
baseUrl: 'https://n8n.test.com',
|
||||
apiKey: 'test-key',
|
||||
timeout: 30000,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
// Mock the N8nApiClient constructor
|
||||
vi.mocked(N8nApiClient).mockImplementation(() => mockApiClient);
|
||||
|
||||
// Import handlers module after setting up mocks
|
||||
handlers = await import('@/mcp/handlers-n8n-manager');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (handlers) {
|
||||
const clientGetter = handlers.getN8nApiClient;
|
||||
if (clientGetter) {
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||
clientGetter();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleCreateTable
|
||||
// ========================================================================
|
||||
describe('handleCreateTable', () => {
|
||||
it('should create data table with name and columns successfully', async () => {
|
||||
const createdTable = {
|
||||
id: 'dt-123',
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ id: 'col-1', name: 'email', type: 'string', index: 0 },
|
||||
{ id: 'col-2', name: 'age', type: 'number', index: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ name: 'email', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: { id: 'dt-123', name: 'My Data Table' },
|
||||
message: 'Data table "My Data Table" created with ID: dt-123',
|
||||
});
|
||||
|
||||
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||
name: 'My Data Table',
|
||||
columns: [
|
||||
{ name: 'email', type: 'string' },
|
||||
{ name: 'age', type: 'number' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create data table with name only (no columns)', async () => {
|
||||
const createdTable = {
|
||||
id: 'dt-456',
|
||||
name: 'Empty Table',
|
||||
};
|
||||
|
||||
mockApiClient.createDataTable.mockResolvedValue(createdTable);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Empty Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: { id: 'dt-456', name: 'Empty Table' },
|
||||
message: 'Data table "Empty Table" created with ID: dt-456',
|
||||
});
|
||||
|
||||
expect(mockApiClient.createDataTable).toHaveBeenCalledWith({
|
||||
name: 'Empty Table',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when API returns empty response (null)', async () => {
|
||||
mockApiClient.createDataTable.mockResolvedValue(null);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Ghost Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Data table creation failed: n8n API returned an empty or invalid response',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when API call fails', async () => {
|
||||
const apiError = new Error('Data table creation failed on the server');
|
||||
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Broken Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Data table creation failed on the server',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Zod validation error when name is missing', async () => {
|
||||
const result = await handlers.handleCreateTable({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
|
||||
it('should return error when n8n API is not configured', async () => {
|
||||
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Test Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return structured error for N8nApiError', async () => {
|
||||
const apiError = new N8nApiError('Feature not available', 402, 'PAYMENT_REQUIRED', { plan: 'enterprise' });
|
||||
mockApiClient.createDataTable.mockRejectedValue(apiError);
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Enterprise Table',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.code).toBe('PAYMENT_REQUIRED');
|
||||
expect(result.details).toEqual({ plan: 'enterprise' });
|
||||
});
|
||||
|
||||
it('should return Unknown error when a non-Error value is thrown', async () => {
|
||||
mockApiClient.createDataTable.mockRejectedValue('string-error');
|
||||
|
||||
const result = await handlers.handleCreateTable({
|
||||
name: 'Error Table',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'Unknown error occurred',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleListTables
|
||||
// ========================================================================
|
||||
describe('handleListTables', () => {
|
||||
it('should list tables successfully', async () => {
|
||||
const tables = [
|
||||
{ id: 'dt-1', name: 'Table One' },
|
||||
{ id: 'dt-2', name: 'Table Two' },
|
||||
];
|
||||
mockApiClient.listDataTables.mockResolvedValue({ data: tables, nextCursor: null });
|
||||
|
||||
const result = await handlers.handleListTables({});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
tables,
|
||||
count: 2,
|
||||
nextCursor: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty list when no tables exist', async () => {
|
||||
mockApiClient.listDataTables.mockResolvedValue({ data: [], nextCursor: null });
|
||||
|
||||
const result = await handlers.handleListTables({});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
tables: [],
|
||||
count: 0,
|
||||
nextCursor: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass pagination params (limit, cursor)', async () => {
|
||||
mockApiClient.listDataTables.mockResolvedValue({
|
||||
data: [{ id: 'dt-3', name: 'Page Two' }],
|
||||
nextCursor: 'cursor-next',
|
||||
});
|
||||
|
||||
const result = await handlers.handleListTables({ limit: 10, cursor: 'cursor-abc' });
|
||||
|
||||
expect(mockApiClient.listDataTables).toHaveBeenCalledWith({ limit: 10, cursor: 'cursor-abc' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.nextCursor).toBe('cursor-next');
|
||||
});
|
||||
|
||||
it('should handle API error', async () => {
|
||||
mockApiClient.listDataTables.mockRejectedValue(new Error('Server down'));
|
||||
|
||||
const result = await handlers.handleListTables({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Server down');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleGetTable
|
||||
// ========================================================================
|
||||
describe('handleGetTable', () => {
|
||||
it('should get table successfully', async () => {
|
||||
const table = { id: 'dt-1', name: 'My Table', columns: [] };
|
||||
mockApiClient.getDataTable.mockResolvedValue(table);
|
||||
|
||||
const result = await handlers.handleGetTable({ tableId: 'dt-1' });
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: table,
|
||||
});
|
||||
expect(mockApiClient.getDataTable).toHaveBeenCalledWith('dt-1');
|
||||
});
|
||||
|
||||
it('should return error on 404', async () => {
|
||||
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
|
||||
mockApiClient.getDataTable.mockRejectedValue(notFoundError);
|
||||
|
||||
const result = await handlers.handleGetTable({ tableId: 'dt-nonexistent' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.code).toBe('NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should return Zod validation error when tableId is missing', async () => {
|
||||
const result = await handlers.handleGetTable({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleUpdateTable
|
||||
// ========================================================================
|
||||
describe('handleUpdateTable', () => {
|
||||
it('should rename table successfully', async () => {
|
||||
const updatedTable = { id: 'dt-1', name: 'Renamed Table' };
|
||||
mockApiClient.updateDataTable.mockResolvedValue(updatedTable);
|
||||
|
||||
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'Renamed Table' });
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: updatedTable,
|
||||
message: 'Data table renamed to "Renamed Table"',
|
||||
});
|
||||
expect(mockApiClient.updateDataTable).toHaveBeenCalledWith('dt-1', { name: 'Renamed Table' });
|
||||
});
|
||||
|
||||
it('should return Zod validation error when tableId is missing', async () => {
|
||||
const result = await handlers.handleUpdateTable({ name: 'New Name' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
|
||||
it('should return error when API call fails', async () => {
|
||||
mockApiClient.updateDataTable.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
const result = await handlers.handleUpdateTable({ tableId: 'dt-1', name: 'New Name' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Update failed');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleDeleteTable
|
||||
// ========================================================================
|
||||
describe('handleDeleteTable', () => {
|
||||
it('should delete table successfully', async () => {
|
||||
mockApiClient.deleteDataTable.mockResolvedValue(undefined);
|
||||
|
||||
const result = await handlers.handleDeleteTable({ tableId: 'dt-1' });
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Data table dt-1 deleted successfully',
|
||||
});
|
||||
expect(mockApiClient.deleteDataTable).toHaveBeenCalledWith('dt-1');
|
||||
});
|
||||
|
||||
it('should return error on 404', async () => {
|
||||
const notFoundError = new N8nApiError('Data table not found', 404, 'NOT_FOUND');
|
||||
mockApiClient.deleteDataTable.mockRejectedValue(notFoundError);
|
||||
|
||||
const result = await handlers.handleDeleteTable({ tableId: 'dt-nonexistent' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.code).toBe('NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleGetRows
|
||||
// ========================================================================
|
||||
describe('handleGetRows', () => {
|
||||
it('should get rows with default params', async () => {
|
||||
const rows = [
|
||||
{ id: 1, email: 'a@b.com', score: 10 },
|
||||
{ id: 2, email: 'c@d.com', score: 20 },
|
||||
];
|
||||
mockApiClient.getDataTableRows.mockResolvedValue({ data: rows, nextCursor: null });
|
||||
|
||||
const result = await handlers.handleGetRows({ tableId: 'dt-1' });
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
rows,
|
||||
count: 2,
|
||||
nextCursor: undefined,
|
||||
},
|
||||
});
|
||||
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {});
|
||||
});
|
||||
|
||||
it('should pass filter, sort, and search params', async () => {
|
||||
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||
|
||||
await handlers.handleGetRows({
|
||||
tableId: 'dt-1',
|
||||
limit: 50,
|
||||
sortBy: 'name:asc',
|
||||
search: 'john',
|
||||
});
|
||||
|
||||
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
limit: 50,
|
||||
sortBy: 'name:asc',
|
||||
search: 'john',
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize object filter to JSON string', async () => {
|
||||
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||
|
||||
const objectFilter = {
|
||||
type: 'and' as const,
|
||||
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'active' }],
|
||||
};
|
||||
|
||||
await handlers.handleGetRows({
|
||||
tableId: 'dt-1',
|
||||
filter: objectFilter,
|
||||
});
|
||||
|
||||
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: JSON.stringify(objectFilter),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass through string filter as-is', async () => {
|
||||
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
|
||||
|
||||
await handlers.handleGetRows({
|
||||
tableId: 'dt-1',
|
||||
filter: '{"type":"and","filters":[]}',
|
||||
});
|
||||
|
||||
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: '{"type":"and","filters":[]}',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleInsertRows
|
||||
// ========================================================================
|
||||
describe('handleInsertRows', () => {
|
||||
it('should insert rows successfully', async () => {
|
||||
const insertResult = { insertedCount: 2, ids: [1, 2] };
|
||||
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
|
||||
|
||||
const result = await handlers.handleInsertRows({
|
||||
tableId: 'dt-1',
|
||||
data: [
|
||||
{ email: 'a@b.com', score: 10 },
|
||||
{ email: 'c@d.com', score: 20 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: insertResult,
|
||||
message: 'Rows inserted into data table dt-1',
|
||||
});
|
||||
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
data: [
|
||||
{ email: 'a@b.com', score: 10 },
|
||||
{ email: 'c@d.com', score: 20 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass returnType to the API client', async () => {
|
||||
const insertResult = [{ id: 1, email: 'a@b.com', score: 10 }];
|
||||
mockApiClient.insertDataTableRows.mockResolvedValue(insertResult);
|
||||
|
||||
const result = await handlers.handleInsertRows({
|
||||
tableId: 'dt-1',
|
||||
data: [{ email: 'a@b.com', score: 10 }],
|
||||
returnType: 'all',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(insertResult);
|
||||
expect(mockApiClient.insertDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
data: [{ email: 'a@b.com', score: 10 }],
|
||||
returnType: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Zod validation error when data is empty array', async () => {
|
||||
const result = await handlers.handleInsertRows({
|
||||
tableId: 'dt-1',
|
||||
data: [],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid input');
|
||||
expect(result.details).toHaveProperty('errors');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleUpdateRows
|
||||
// ========================================================================
|
||||
describe('handleUpdateRows', () => {
|
||||
it('should update rows successfully', async () => {
|
||||
const updateResult = { updatedCount: 3 };
|
||||
mockApiClient.updateDataTableRows.mockResolvedValue(updateResult);
|
||||
|
||||
const filter = {
|
||||
type: 'and' as const,
|
||||
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'inactive' }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleUpdateRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
data: { status: 'active' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: updateResult,
|
||||
message: 'Rows updated successfully',
|
||||
});
|
||||
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter,
|
||||
data: { status: 'active' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should support dryRun mode', async () => {
|
||||
const dryRunResult = { matchedCount: 5 };
|
||||
mockApiClient.updateDataTableRows.mockResolvedValue(dryRunResult);
|
||||
|
||||
const filter = {
|
||||
filters: [{ columnName: 'score', condition: 'lt' as const, value: 5 }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleUpdateRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
data: { status: 'low' },
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Dry run: rows matched (no changes applied)');
|
||||
expect(mockApiClient.updateDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: { type: 'and', ...filter },
|
||||
data: { status: 'low' },
|
||||
dryRun: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error on API failure', async () => {
|
||||
mockApiClient.updateDataTableRows.mockRejectedValue(new Error('Conflict'));
|
||||
|
||||
const result = await handlers.handleUpdateRows({
|
||||
tableId: 'dt-1',
|
||||
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
|
||||
data: { name: 'test' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Conflict');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleUpsertRows
|
||||
// ========================================================================
|
||||
describe('handleUpsertRows', () => {
|
||||
it('should upsert row successfully', async () => {
|
||||
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com', score: 15 } };
|
||||
mockApiClient.upsertDataTableRow.mockResolvedValue(upsertResult);
|
||||
|
||||
const filter = {
|
||||
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleUpsertRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
data: { score: 15 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: upsertResult,
|
||||
message: 'Row upserted successfully',
|
||||
});
|
||||
expect(mockApiClient.upsertDataTableRow).toHaveBeenCalledWith('dt-1', {
|
||||
filter: { type: 'and', ...filter },
|
||||
data: { score: 15 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should support dryRun mode', async () => {
|
||||
const dryRunResult = { action: 'would_update', matchedRows: 1 };
|
||||
mockApiClient.upsertDataTableRow.mockResolvedValue(dryRunResult);
|
||||
|
||||
const filter = {
|
||||
filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleUpsertRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
data: { score: 20 },
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Dry run: upsert previewed (no changes applied)');
|
||||
});
|
||||
|
||||
it('should return error on API failure', async () => {
|
||||
const apiError = new N8nApiError('Server error', 500, 'INTERNAL_ERROR');
|
||||
mockApiClient.upsertDataTableRow.mockRejectedValue(apiError);
|
||||
|
||||
const result = await handlers.handleUpsertRows({
|
||||
tableId: 'dt-1',
|
||||
filter: { filters: [{ columnName: 'id', condition: 'eq' as const, value: 1 }] },
|
||||
data: { name: 'test' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.code).toBe('INTERNAL_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// handleDeleteRows
|
||||
// ========================================================================
|
||||
describe('handleDeleteRows', () => {
|
||||
it('should delete rows successfully', async () => {
|
||||
const deleteResult = { deletedCount: 2 };
|
||||
mockApiClient.deleteDataTableRows.mockResolvedValue(deleteResult);
|
||||
|
||||
const filter = {
|
||||
filters: [{ columnName: 'status', condition: 'eq' as const, value: 'deleted' }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleDeleteRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: deleteResult,
|
||||
message: 'Rows deleted successfully',
|
||||
});
|
||||
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: JSON.stringify({ type: 'and', ...filter }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize filter object to JSON string for API call', async () => {
|
||||
mockApiClient.deleteDataTableRows.mockResolvedValue({ deletedCount: 1 });
|
||||
|
||||
const filter = {
|
||||
type: 'or' as const,
|
||||
filters: [
|
||||
{ columnName: 'score', condition: 'lt' as const, value: 0 },
|
||||
{ columnName: 'status', condition: 'eq' as const, value: 'spam' },
|
||||
],
|
||||
};
|
||||
|
||||
await handlers.handleDeleteRows({ tableId: 'dt-1', filter });
|
||||
|
||||
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: JSON.stringify(filter),
|
||||
});
|
||||
});
|
||||
|
||||
it('should support dryRun mode', async () => {
|
||||
const dryRunResult = { matchedCount: 4 };
|
||||
mockApiClient.deleteDataTableRows.mockResolvedValue(dryRunResult);
|
||||
|
||||
const filter = {
|
||||
filters: [{ columnName: 'active', condition: 'eq' as const, value: false }],
|
||||
};
|
||||
|
||||
const result = await handlers.handleDeleteRows({
|
||||
tableId: 'dt-1',
|
||||
filter,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Dry run: rows matched for deletion (no changes applied)');
|
||||
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
|
||||
filter: JSON.stringify({ type: 'and', ...filter }),
|
||||
dryRun: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1102,10 +1102,10 @@ describe('handlers-n8n-manager', () => {
|
||||
enabled: true,
|
||||
},
|
||||
managementTools: {
|
||||
count: 13,
|
||||
count: 14,
|
||||
enabled: true,
|
||||
},
|
||||
totalAvailable: 20,
|
||||
totalAvailable: 21,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -542,8 +542,8 @@ describe('Parameter Validation', () => {
|
||||
await expect(server.testExecuteTool('n8n_test_workflow', {}))
|
||||
.rejects.toThrow('Missing required parameters for n8n_test_workflow: workflowId');
|
||||
|
||||
await expect(server.testExecuteTool('n8n_create_data_table', {}))
|
||||
.rejects.toThrow('Missing required parameters for n8n_create_data_table: name');
|
||||
await expect(server.testExecuteTool('n8n_manage_datatable', {}))
|
||||
.rejects.toThrow('n8n_manage_datatable: Validation failed:\n • action: action is required');
|
||||
|
||||
for (const tool of n8nToolsWithRequiredParams) {
|
||||
await expect(server.testExecuteTool(tool.name, tool.args))
|
||||
|
||||
@@ -1353,6 +1353,310 @@ describe('N8nApiClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDataTables', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should list data tables successfully', async () => {
|
||||
const response = { data: [{ id: 'dt-1', name: 'Table One' }], nextCursor: 'abc' };
|
||||
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
||||
|
||||
const result = await client.listDataTables({ limit: 10, cursor: 'xyz' });
|
||||
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables', { params: { limit: 10, cursor: 'xyz' } });
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 500, data: { message: 'Internal server error' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('get', error);
|
||||
|
||||
try {
|
||||
await client.listDataTables();
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nServerError);
|
||||
expect((err as N8nServerError).statusCode).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataTable', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should get data table successfully', async () => {
|
||||
const table = { id: 'dt-1', name: 'My Table', columns: [] };
|
||||
mockAxiosInstance.get.mockResolvedValue({ data: table });
|
||||
|
||||
const result = await client.getDataTable('dt-1');
|
||||
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1');
|
||||
expect(result).toEqual(table);
|
||||
});
|
||||
|
||||
it('should handle 404 error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 404, data: { message: 'Data table not found' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('get', error);
|
||||
|
||||
try {
|
||||
await client.getDataTable('dt-nonexistent');
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDataTable', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should update data table successfully', async () => {
|
||||
const updated = { id: 'dt-1', name: 'Renamed' };
|
||||
mockAxiosInstance.patch.mockResolvedValue({ data: updated });
|
||||
|
||||
const result = await client.updateDataTable('dt-1', { name: 'Renamed' });
|
||||
|
||||
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1', { name: 'Renamed' });
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 400, data: { message: 'Invalid name' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('patch', error);
|
||||
|
||||
try {
|
||||
await client.updateDataTable('dt-1', { name: '' });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nValidationError);
|
||||
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDataTable', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should delete data table successfully', async () => {
|
||||
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
|
||||
|
||||
await client.deleteDataTable('dt-1');
|
||||
|
||||
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1');
|
||||
});
|
||||
|
||||
it('should handle 404 error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 404, data: { message: 'Data table not found' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('delete', error);
|
||||
|
||||
try {
|
||||
await client.deleteDataTable('dt-nonexistent');
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nNotFoundError);
|
||||
expect((err as N8nNotFoundError).statusCode).toBe(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataTableRows', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should get data table rows with params', async () => {
|
||||
const response = { data: [{ id: 1, email: 'a@b.com' }], nextCursor: null };
|
||||
mockAxiosInstance.get.mockResolvedValue({ data: response });
|
||||
|
||||
const params = { limit: 50, sortBy: 'email:asc', search: 'john' };
|
||||
const result = await client.getDataTableRows('dt-1', params);
|
||||
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1/rows', { params });
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 500, data: { message: 'Internal server error' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('get', error);
|
||||
|
||||
try {
|
||||
await client.getDataTableRows('dt-1');
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nServerError);
|
||||
expect((err as N8nServerError).statusCode).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertDataTableRows', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should insert data table rows successfully', async () => {
|
||||
const insertResult = { insertedCount: 2 };
|
||||
mockAxiosInstance.post.mockResolvedValue({ data: insertResult });
|
||||
|
||||
const params = { data: [{ email: 'a@b.com' }, { email: 'c@d.com' }], returnType: 'count' as const };
|
||||
const result = await client.insertDataTableRows('dt-1', params);
|
||||
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows', params);
|
||||
expect(result).toEqual(insertResult);
|
||||
});
|
||||
|
||||
it('should handle 400 error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 400, data: { message: 'Invalid row data' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('post', error);
|
||||
|
||||
try {
|
||||
await client.insertDataTableRows('dt-1', { data: [{}] });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nValidationError);
|
||||
expect((err as N8nValidationError).message).toBe('Invalid row data');
|
||||
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDataTableRows', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should update data table rows successfully', async () => {
|
||||
const updateResult = { updatedCount: 3 };
|
||||
mockAxiosInstance.patch.mockResolvedValue({ data: updateResult });
|
||||
|
||||
const params = {
|
||||
filter: { type: 'and' as const, filters: [{ columnName: 'status', condition: 'eq' as const, value: 'old' }] },
|
||||
data: { status: 'new' },
|
||||
};
|
||||
const result = await client.updateDataTableRows('dt-1', params);
|
||||
|
||||
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1/rows/update', params);
|
||||
expect(result).toEqual(updateResult);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 500, data: { message: 'Internal server error' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('patch', error);
|
||||
|
||||
try {
|
||||
await client.updateDataTableRows('dt-1', {
|
||||
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
|
||||
data: { name: 'test' },
|
||||
});
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nServerError);
|
||||
expect((err as N8nServerError).statusCode).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertDataTableRow', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should upsert data table row successfully', async () => {
|
||||
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com' } };
|
||||
mockAxiosInstance.post.mockResolvedValue({ data: upsertResult });
|
||||
|
||||
const params = {
|
||||
filter: { type: 'and' as const, filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }] },
|
||||
data: { score: 15 },
|
||||
};
|
||||
const result = await client.upsertDataTableRow('dt-1', params);
|
||||
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows/upsert', params);
|
||||
expect(result).toEqual(upsertResult);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 400, data: { message: 'Invalid upsert params' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('post', error);
|
||||
|
||||
try {
|
||||
await client.upsertDataTableRow('dt-1', {
|
||||
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
|
||||
data: { name: 'test' },
|
||||
});
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nValidationError);
|
||||
expect((err as N8nValidationError).statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDataTableRows', () => {
|
||||
beforeEach(() => {
|
||||
client = new N8nApiClient(defaultConfig);
|
||||
});
|
||||
|
||||
it('should delete data table rows successfully', async () => {
|
||||
const deleteResult = { deletedCount: 2 };
|
||||
mockAxiosInstance.delete.mockResolvedValue({ data: deleteResult });
|
||||
|
||||
const params = { filter: '{"type":"and","filters":[]}', dryRun: false };
|
||||
const result = await client.deleteDataTableRows('dt-1', params);
|
||||
|
||||
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1/rows/delete', { params });
|
||||
expect(result).toEqual(deleteResult);
|
||||
});
|
||||
|
||||
it('should handle error', async () => {
|
||||
const error = {
|
||||
message: 'Request failed',
|
||||
response: { status: 500, data: { message: 'Internal server error' } },
|
||||
};
|
||||
await mockAxiosInstance.simulateError('delete', error);
|
||||
|
||||
try {
|
||||
await client.deleteDataTableRows('dt-1', { filter: '{}' });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(N8nServerError);
|
||||
expect((err as N8nServerError).statusCode).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('interceptors', () => {
|
||||
let requestInterceptor: any;
|
||||
let responseInterceptor: any;
|
||||
|
||||
Reference in New Issue
Block a user