@pagelines/n8n-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,362 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { N8nClient } from './n8n-client.js';
3
-
4
- // Mock fetch globally
5
- const mockFetch = vi.fn();
6
- global.fetch = mockFetch;
7
-
8
- describe('N8nClient', () => {
9
- let client: N8nClient;
10
-
11
- beforeEach(() => {
12
- client = new N8nClient({
13
- apiUrl: 'https://n8n.example.com',
14
- apiKey: 'test-api-key',
15
- });
16
- mockFetch.mockReset();
17
- });
18
-
19
- describe('constructor', () => {
20
- it('normalizes URL by removing trailing slash', () => {
21
- const clientWithSlash = new N8nClient({
22
- apiUrl: 'https://n8n.example.com/',
23
- apiKey: 'key',
24
- });
25
- // Access private property for testing
26
- expect((clientWithSlash as any).baseUrl).toBe('https://n8n.example.com');
27
- });
28
- });
29
-
30
- describe('listWorkflows', () => {
31
- it('calls correct endpoint', async () => {
32
- mockFetch.mockResolvedValueOnce({
33
- ok: true,
34
- text: async () => JSON.stringify({ data: [] }),
35
- });
36
-
37
- await client.listWorkflows();
38
-
39
- expect(mockFetch).toHaveBeenCalledWith(
40
- 'https://n8n.example.com/api/v1/workflows',
41
- expect.objectContaining({
42
- method: 'GET',
43
- headers: expect.objectContaining({
44
- 'X-N8N-API-KEY': 'test-api-key',
45
- }),
46
- })
47
- );
48
- });
49
-
50
- it('includes query params when provided', async () => {
51
- mockFetch.mockResolvedValueOnce({
52
- ok: true,
53
- text: async () => JSON.stringify({ data: [] }),
54
- });
55
-
56
- await client.listWorkflows({ active: true, limit: 10 });
57
-
58
- expect(mockFetch).toHaveBeenCalledWith(
59
- expect.stringContaining('active=true'),
60
- expect.any(Object)
61
- );
62
- expect(mockFetch).toHaveBeenCalledWith(
63
- expect.stringContaining('limit=10'),
64
- expect.any(Object)
65
- );
66
- });
67
- });
68
-
69
- describe('patchWorkflow', () => {
70
- const mockWorkflow = {
71
- id: '1',
72
- name: 'test_workflow',
73
- active: false,
74
- nodes: [
75
- {
76
- id: 'node1',
77
- name: 'existing_node',
78
- type: 'n8n-nodes-base.set',
79
- typeVersion: 1,
80
- position: [0, 0] as [number, number],
81
- parameters: { param1: 'value1', param2: 'value2' },
82
- },
83
- ],
84
- connections: {},
85
- createdAt: '2024-01-01',
86
- updatedAt: '2024-01-01',
87
- };
88
-
89
- beforeEach(() => {
90
- // Mock GET workflow
91
- mockFetch.mockResolvedValueOnce({
92
- ok: true,
93
- text: async () => JSON.stringify(mockWorkflow),
94
- });
95
- // Mock PUT workflow
96
- mockFetch.mockResolvedValueOnce({
97
- ok: true,
98
- text: async () => JSON.stringify(mockWorkflow),
99
- });
100
- });
101
-
102
- it('warns when updateNode would remove parameters', async () => {
103
- const { warnings } = await client.patchWorkflow('1', [
104
- {
105
- type: 'updateNode',
106
- nodeName: 'existing_node',
107
- properties: {
108
- parameters: { newParam: 'newValue' }, // Missing param1, param2
109
- },
110
- },
111
- ]);
112
-
113
- expect(warnings).toContainEqual(
114
- expect.stringContaining('remove parameters')
115
- );
116
- expect(warnings).toContainEqual(
117
- expect.stringContaining('param1')
118
- );
119
- });
120
-
121
- it('warns when removing non-existent node', async () => {
122
- const { warnings } = await client.patchWorkflow('1', [
123
- {
124
- type: 'removeNode',
125
- nodeName: 'nonexistent_node',
126
- },
127
- ]);
128
-
129
- expect(warnings).toContainEqual(
130
- expect.stringContaining('not found')
131
- );
132
- });
133
-
134
- it('adds node correctly', async () => {
135
- mockFetch.mockReset();
136
- mockFetch.mockResolvedValueOnce({
137
- ok: true,
138
- text: async () => JSON.stringify(mockWorkflow),
139
- });
140
-
141
- const updatedWorkflow = {
142
- ...mockWorkflow,
143
- nodes: [
144
- ...mockWorkflow.nodes,
145
- {
146
- id: 'new-id',
147
- name: 'new_node',
148
- type: 'n8n-nodes-base.code',
149
- typeVersion: 1,
150
- position: [100, 100],
151
- parameters: {},
152
- },
153
- ],
154
- };
155
-
156
- mockFetch.mockResolvedValueOnce({
157
- ok: true,
158
- text: async () => JSON.stringify(updatedWorkflow),
159
- });
160
-
161
- const { workflow } = await client.patchWorkflow('1', [
162
- {
163
- type: 'addNode',
164
- node: {
165
- name: 'new_node',
166
- type: 'n8n-nodes-base.code',
167
- typeVersion: 1,
168
- position: [100, 100],
169
- parameters: {},
170
- },
171
- },
172
- ]);
173
-
174
- // Verify PUT was called with the new node
175
- const putCall = mockFetch.mock.calls[1];
176
- const putBody = JSON.parse(putCall[1].body);
177
- expect(putBody.nodes).toHaveLength(2);
178
- expect(putBody.nodes[1].name).toBe('new_node');
179
- });
180
-
181
- it('adds connection correctly', async () => {
182
- mockFetch.mockReset();
183
- mockFetch.mockResolvedValueOnce({
184
- ok: true,
185
- text: async () => JSON.stringify(mockWorkflow),
186
- });
187
-
188
- const updatedWorkflow = {
189
- ...mockWorkflow,
190
- connections: {
191
- existing_node: {
192
- main: [[{ node: 'target_node', type: 'main', index: 0 }]],
193
- },
194
- },
195
- };
196
-
197
- mockFetch.mockResolvedValueOnce({
198
- ok: true,
199
- text: async () => JSON.stringify(updatedWorkflow),
200
- });
201
-
202
- await client.patchWorkflow('1', [
203
- {
204
- type: 'addConnection',
205
- from: 'existing_node',
206
- to: 'target_node',
207
- },
208
- ]);
209
-
210
- const putCall = mockFetch.mock.calls[1];
211
- const putBody = JSON.parse(putCall[1].body);
212
- expect(putBody.connections.existing_node.main[0][0].node).toBe('target_node');
213
- });
214
- });
215
-
216
- describe('error handling', () => {
217
- it('throws on non-ok response', async () => {
218
- mockFetch.mockResolvedValueOnce({
219
- ok: false,
220
- status: 404,
221
- text: async () => 'Not found',
222
- });
223
-
224
- await expect(client.getWorkflow('999')).rejects.toThrow('n8n API error (404)');
225
- });
226
- });
227
-
228
- describe('listNodeTypes', () => {
229
- it('calls correct endpoint', async () => {
230
- const mockNodeTypes = [
231
- {
232
- name: 'n8n-nodes-base.webhook',
233
- displayName: 'Webhook',
234
- description: 'Starts workflow on webhook call',
235
- group: ['trigger'],
236
- version: 2,
237
- },
238
- {
239
- name: 'n8n-nodes-base.set',
240
- displayName: 'Set',
241
- description: 'Set values',
242
- group: ['transform'],
243
- version: 3,
244
- },
245
- ];
246
-
247
- mockFetch.mockResolvedValueOnce({
248
- ok: true,
249
- text: async () => JSON.stringify(mockNodeTypes),
250
- });
251
-
252
- const result = await client.listNodeTypes();
253
-
254
- expect(mockFetch).toHaveBeenCalledWith(
255
- 'https://n8n.example.com/api/v1/nodes',
256
- expect.objectContaining({
257
- method: 'GET',
258
- headers: expect.objectContaining({
259
- 'X-N8N-API-KEY': 'test-api-key',
260
- }),
261
- })
262
- );
263
-
264
- expect(result).toHaveLength(2);
265
- expect(result[0].name).toBe('n8n-nodes-base.webhook');
266
- expect(result[1].name).toBe('n8n-nodes-base.set');
267
- });
268
- });
269
-
270
- describe('updateWorkflow', () => {
271
- it('strips disallowed properties before sending to API', async () => {
272
- const fullWorkflow = {
273
- id: '123',
274
- name: 'test_workflow',
275
- active: true,
276
- nodes: [{ id: 'n1', name: 'node1', type: 'test', typeVersion: 1, position: [0, 0] as [number, number], parameters: {} }],
277
- connections: {},
278
- settings: { timezone: 'UTC' },
279
- createdAt: '2024-01-01T00:00:00.000Z',
280
- updatedAt: '2024-01-02T00:00:00.000Z',
281
- versionId: 'v1',
282
- staticData: undefined,
283
- tags: [{ id: 't1', name: 'tag1' }],
284
- };
285
-
286
- mockFetch.mockResolvedValueOnce({
287
- ok: true,
288
- text: async () => JSON.stringify(fullWorkflow),
289
- });
290
-
291
- await client.updateWorkflow('123', fullWorkflow);
292
-
293
- // Verify the request body does NOT contain disallowed properties
294
- const putCall = mockFetch.mock.calls[0];
295
- const putBody = JSON.parse(putCall[1].body);
296
-
297
- // These should be stripped
298
- expect(putBody.id).toBeUndefined();
299
- expect(putBody.createdAt).toBeUndefined();
300
- expect(putBody.updatedAt).toBeUndefined();
301
- expect(putBody.active).toBeUndefined();
302
- expect(putBody.versionId).toBeUndefined();
303
-
304
- // These should be preserved
305
- expect(putBody.name).toBe('test_workflow');
306
- expect(putBody.nodes).toHaveLength(1);
307
- expect(putBody.connections).toEqual({});
308
- expect(putBody.settings).toEqual({ timezone: 'UTC' });
309
- expect(putBody.staticData).toBeUndefined();
310
- expect(putBody.tags).toEqual([{ id: 't1', name: 'tag1' }]);
311
- });
312
-
313
- it('works with partial workflow (only some fields)', async () => {
314
- mockFetch.mockResolvedValueOnce({
315
- ok: true,
316
- text: async () => JSON.stringify({ id: '123', name: 'updated' }),
317
- });
318
-
319
- await client.updateWorkflow('123', { name: 'updated', nodes: [] });
320
-
321
- const putCall = mockFetch.mock.calls[0];
322
- const putBody = JSON.parse(putCall[1].body);
323
-
324
- expect(putBody.name).toBe('updated');
325
- expect(putBody.nodes).toEqual([]);
326
- });
327
-
328
- it('handles workflow from formatWorkflow (simulating workflow_format apply)', async () => {
329
- // This simulates the exact scenario that caused the bug:
330
- // workflow_format returns a full N8nWorkflow object with id, createdAt, etc.
331
- const formattedWorkflow = {
332
- id: 'zbB1fCxWgZXgpjB1',
333
- name: 'my_workflow',
334
- active: false,
335
- nodes: [],
336
- connections: {},
337
- createdAt: '2024-01-01T00:00:00.000Z',
338
- updatedAt: '2024-01-02T00:00:00.000Z',
339
- };
340
-
341
- mockFetch.mockResolvedValueOnce({
342
- ok: true,
343
- text: async () => JSON.stringify(formattedWorkflow),
344
- });
345
-
346
- // This should NOT throw "must NOT have additional properties"
347
- await client.updateWorkflow('zbB1fCxWgZXgpjB1', formattedWorkflow);
348
-
349
- const putCall = mockFetch.mock.calls[0];
350
- const putBody = JSON.parse(putCall[1].body);
351
-
352
- // Critical: these must NOT be in the request body
353
- expect(putBody.id).toBeUndefined();
354
- expect(putBody.createdAt).toBeUndefined();
355
- expect(putBody.updatedAt).toBeUndefined();
356
- expect(putBody.active).toBeUndefined();
357
-
358
- // Only allowed properties should be sent
359
- expect(Object.keys(putBody).sort()).toEqual(['connections', 'name', 'nodes']);
360
- });
361
- });
362
- });