n8n-nodes-innotes 1.0.19 → 1.0.21

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,39 +1,80 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Author: marco
4
- * Last updated: 2025-05-30
4
+ * Last updated: 2025-02-14
5
+ * Comprehensive test coverage for InNotes n8n node
5
6
  */
6
7
  Object.defineProperty(exports, "__esModule", { value: true });
7
8
  const InNotes_node_1 = require("../nodes/InNotes/InNotes.node");
9
+ // ============================================================================
10
+ // Test Helpers
11
+ // ============================================================================
12
+ /**
13
+ * Creates a mock IExecuteFunctions object with customizable parameters
14
+ */
15
+ function createMockExecuteFunctions(params = {}, credentials = {}) {
16
+ const defaultCredentials = {
17
+ baseUrl: 'https://test.innotes.com',
18
+ authMethod: 'token',
19
+ token: 'test-token',
20
+ callbackSecret: 'test-callback-secret',
21
+ ...credentials,
22
+ };
23
+ return {
24
+ getInputData: jest.fn().mockReturnValue([{ json: {} }]),
25
+ getNodeParameter: jest.fn().mockImplementation((param, _index, defaultValue) => {
26
+ var _a;
27
+ return (_a = params[param]) !== null && _a !== void 0 ? _a : defaultValue;
28
+ }),
29
+ getCredentials: jest.fn().mockResolvedValue(defaultCredentials),
30
+ continueOnFail: jest.fn().mockReturnValue(false),
31
+ getNode: jest.fn().mockReturnValue({ name: 'InNotes' }),
32
+ helpers: {
33
+ httpRequestWithAuthentication: jest.fn(),
34
+ httpRequest: jest.fn(),
35
+ requestWithAuthenticationPaginated: jest.fn(),
36
+ request: jest.fn(),
37
+ requestWithAuthentication: jest.fn(),
38
+ requestOAuth2: jest.fn(),
39
+ requestOAuth1: jest.fn(),
40
+ returnJsonArray: jest.fn((data) => data.map((item) => ({ json: item }))),
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Creates a mock ILoadOptionsFunctions object
46
+ */
47
+ function createMockLoadOptionsFunctions(credentials = {}) {
48
+ const defaultCredentials = {
49
+ baseUrl: 'https://test.innotes.com',
50
+ authMethod: 'token',
51
+ token: 'test-token',
52
+ };
53
+ return {
54
+ getCredentials: jest.fn().mockResolvedValue({ ...defaultCredentials, ...credentials }),
55
+ getNode: jest.fn().mockReturnValue({ name: 'InNotes' }),
56
+ helpers: {
57
+ httpRequestWithAuthentication: jest.fn(),
58
+ },
59
+ };
60
+ }
61
+ // ============================================================================
62
+ // Tests
63
+ // ============================================================================
8
64
  describe('InNotes Node', () => {
9
65
  let node;
10
- let mockExecuteFunctions;
11
66
  beforeEach(() => {
12
67
  node = new InNotes_node_1.InNotes();
13
- mockExecuteFunctions = {
14
- getInputData: jest.fn().mockReturnValue([{ json: {} }]),
15
- getNodeParameter: jest.fn(),
16
- getCredentials: jest.fn().mockResolvedValue({
17
- username: 'test@example.com',
18
- password: 'testpassword',
19
- baseUrl: 'https://test.innotes.com',
20
- }),
21
- helpers: {
22
- httpRequestWithAuthentication: jest.fn(),
23
- httpRequest: jest.fn(),
24
- requestWithAuthenticationPaginated: jest.fn(),
25
- request: jest.fn(),
26
- requestWithAuthentication: jest.fn(),
27
- requestOAuth2: jest.fn(),
28
- requestOAuth1: jest.fn(),
29
- },
30
- };
68
+ jest.clearAllMocks();
31
69
  });
70
+ // ========================================================================
71
+ // Node Properties Tests
72
+ // ========================================================================
32
73
  describe('Node Properties', () => {
33
74
  it('should have correct description', () => {
34
75
  expect(node.description.displayName).toBe('InNotes');
35
76
  expect(node.description.name).toBe('inNotes');
36
- expect(node.description.group).toEqual(['transform']);
77
+ expect(node.description.group).toEqual(['output']);
37
78
  expect(node.description.version).toBe(1);
38
79
  });
39
80
  it('should have correct credentials', () => {
@@ -45,98 +86,1382 @@ describe('InNotes Node', () => {
45
86
  ]);
46
87
  });
47
88
  it('should have correct inputs and outputs', () => {
48
- expect(node.description.inputs).toEqual(['main']);
49
- expect(node.description.outputs).toEqual(['main']);
89
+ expect(node.description.inputs).toBeDefined();
90
+ expect(node.description.outputs).toBeDefined();
91
+ });
92
+ it('should have all expected resources', () => {
93
+ var _a;
94
+ const resourceProperty = (_a = node.description.properties) === null || _a === void 0 ? void 0 : _a.find((prop) => prop.name === 'resource' && prop.type === 'options');
95
+ expect(resourceProperty).toBeDefined();
96
+ const options = resourceProperty === null || resourceProperty === void 0 ? void 0 : resourceProperty.options;
97
+ const resourceNames = options === null || options === void 0 ? void 0 : options.map((o) => o.value);
98
+ expect(resourceNames).toContain('automation');
99
+ expect(resourceNames).toContain('contact');
100
+ expect(resourceNames).toContain('note');
101
+ expect(resourceNames).toContain('job');
102
+ expect(resourceNames).toContain('status');
103
+ expect(resourceNames).toContain('tag');
104
+ expect(resourceNames).toContain('user');
50
105
  });
51
106
  });
52
- describe('Resource Operations', () => {
53
- it('should have contact resource with correct operations', () => {
54
- var _a, _b;
55
- const contactResource = (_a = node.description.properties) === null || _a === void 0 ? void 0 : _a.find(prop => prop.name === 'resource' && prop.type === 'options');
56
- expect(contactResource).toBeDefined();
57
- const contactOption = (_b = contactResource === null || contactResource === void 0 ? void 0 : contactResource.options) === null || _b === void 0 ? void 0 : _b.find((option) => option.value === 'contact');
58
- expect(contactOption).toBeDefined();
59
- expect(contactOption.name).toBe('Contact');
60
- });
61
- it('should have note resource with correct operations', () => {
62
- var _a, _b;
63
- const noteResource = (_a = node.description.properties) === null || _a === void 0 ? void 0 : _a.find(prop => prop.name === 'resource' && prop.type === 'options');
64
- expect(noteResource).toBeDefined();
65
- const noteOption = (_b = noteResource === null || noteResource === void 0 ? void 0 : noteResource.options) === null || _b === void 0 ? void 0 : _b.find((option) => option.value === 'note');
66
- expect(noteOption).toBeDefined();
67
- expect(noteOption.name).toBe('Note');
107
+ // ========================================================================
108
+ // Load Options Tests
109
+ // ========================================================================
110
+ describe('Load Options', () => {
111
+ describe('jobStatuses', () => {
112
+ it('should load job statuses from API', async () => {
113
+ const mockLoadOptions = createMockLoadOptionsFunctions();
114
+ const mockStatuses = [
115
+ { id: 1, name: 'Applied' },
116
+ { id: 2, name: 'Interviewing' },
117
+ { id: 3, name: 'Offer' },
118
+ ];
119
+ mockLoadOptions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockStatuses);
120
+ const result = await node.methods.loadOptions.jobStatuses.call(mockLoadOptions);
121
+ expect(result).toEqual([
122
+ { name: 'Applied', value: 'Applied' },
123
+ { name: 'Interviewing', value: 'Interviewing' },
124
+ { name: 'Offer', value: 'Offer' },
125
+ ]);
126
+ });
127
+ it('should handle single status response', async () => {
128
+ const mockLoadOptions = createMockLoadOptionsFunctions();
129
+ const mockStatus = { id: 1, name: 'Applied' };
130
+ mockLoadOptions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockStatus);
131
+ const result = await node.methods.loadOptions.jobStatuses.call(mockLoadOptions);
132
+ expect(result).toEqual([{ name: 'Applied', value: 'Applied' }]);
133
+ });
134
+ it('should throw error on API failure', async () => {
135
+ const mockLoadOptions = createMockLoadOptionsFunctions();
136
+ mockLoadOptions.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('API Error'));
137
+ await expect(node.methods.loadOptions.jobStatuses.call(mockLoadOptions)).rejects.toThrow();
138
+ });
68
139
  });
69
140
  });
70
- describe('Execute Method', () => {
71
- it('should handle contact get operation', async () => {
72
- const mockHttpRequest = jest.fn().mockResolvedValue({
73
- data: {
74
- id: 1,
75
- name: 'Test Contact',
76
- email: 'test@example.com',
77
- },
141
+ // ========================================================================
142
+ // Contact Operations Tests
143
+ // ========================================================================
144
+ describe('Contact Operations', () => {
145
+ describe('create', () => {
146
+ it('should create contact with required fields', async () => {
147
+ const mockExecute = createMockExecuteFunctions({
148
+ resource: 'contact',
149
+ operation: 'create',
150
+ options: {},
151
+ name: 'John Doe',
152
+ linkedin_key: 'johndoe123',
153
+ });
154
+ const mockResponse = { id: '1', name: 'John Doe' };
155
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
156
+ const result = await node.execute.call(mockExecute);
157
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
158
+ method: 'POST',
159
+ url: 'https://test.innotes.com/api/contacts',
160
+ body: expect.objectContaining({
161
+ name: 'John Doe',
162
+ linkedin_key: 'johndoe123',
163
+ }),
164
+ }));
165
+ expect(result[0]).toBeDefined();
78
166
  });
79
- mockExecuteFunctions.helpers.httpRequestWithAuthentication = mockHttpRequest;
80
- mockExecuteFunctions.getNodeParameter = jest.fn()
81
- .mockReturnValueOnce('contact') // resource
82
- .mockReturnValueOnce('get') // operation
83
- .mockReturnValueOnce(1); // contactId
84
- const result = await node.execute.call(mockExecuteFunctions);
85
- expect(mockHttpRequest).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
86
- method: 'GET',
87
- url: '/api/contacts/1',
88
- }));
89
- expect(result).toEqual([
90
- [
91
- {
92
- json: {
93
- id: 1,
94
- name: 'Test Contact',
95
- email: 'test@example.com',
96
- },
167
+ it('should include optional fields when provided', async () => {
168
+ const mockExecute = createMockExecuteFunctions({
169
+ resource: 'contact',
170
+ operation: 'create',
171
+ options: {},
172
+ name: 'John Doe',
173
+ linkedin_key: 'johndoe123',
174
+ linkedin_user: 'john.doe',
175
+ location: 'New York',
176
+ current_company: 'Acme Inc',
177
+ picture_url: 'https://example.com/photo.jpg',
178
+ tags: 'vip,prospect',
179
+ status_id: '1',
180
+ });
181
+ const mockResponse = { id: '1', name: 'John Doe' };
182
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
183
+ await node.execute.call(mockExecute);
184
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
185
+ body: expect.objectContaining({
186
+ name: 'John Doe',
187
+ linkedin_key: 'johndoe123',
188
+ linkedin_user: 'john.doe',
189
+ location: 'New York',
190
+ current_company: 'Acme Inc',
191
+ picture_url: 'https://example.com/photo.jpg',
192
+ tags: ['vip', 'prospect'],
193
+ status_id: '1',
194
+ }),
195
+ }));
196
+ });
197
+ });
198
+ describe('get', () => {
199
+ it('should get contact by ID', async () => {
200
+ const mockExecute = createMockExecuteFunctions({
201
+ resource: 'contact',
202
+ operation: 'get',
203
+ options: {},
204
+ contactId: '123',
205
+ });
206
+ const mockResponse = { id: '123', name: 'Test Contact' };
207
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
208
+ const result = await node.execute.call(mockExecute);
209
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
210
+ method: 'GET',
211
+ url: 'https://test.innotes.com/api/contacts/123',
212
+ }));
213
+ expect(result[0]).toBeDefined();
214
+ });
215
+ });
216
+ describe('getAll', () => {
217
+ it('should get all contacts with pagination', async () => {
218
+ const mockExecute = createMockExecuteFunctions({
219
+ resource: 'contact',
220
+ operation: 'getAll',
221
+ options: {},
222
+ returnAll: false,
223
+ limit: 10,
224
+ });
225
+ const mockResponse = [{ id: '1' }, { id: '2' }];
226
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
227
+ const result = await node.execute.call(mockExecute);
228
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
229
+ method: 'GET',
230
+ url: 'https://test.innotes.com/api/contacts',
231
+ qs: expect.objectContaining({
232
+ pageSize: 10,
233
+ page: 1,
234
+ }),
235
+ }));
236
+ expect(result[0]).toBeDefined();
237
+ });
238
+ it('should apply search and tag filters', async () => {
239
+ const mockExecute = createMockExecuteFunctions({
240
+ resource: 'contact',
241
+ operation: 'getAll',
242
+ options: {},
243
+ returnAll: false,
244
+ limit: 24,
245
+ search: 'john',
246
+ tags: 'vip',
247
+ });
248
+ const mockResponse = [{ id: '1' }];
249
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
250
+ await node.execute.call(mockExecute);
251
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
252
+ qs: expect.objectContaining({
253
+ search: 'john',
254
+ tags: 'vip',
255
+ }),
256
+ }));
257
+ });
258
+ });
259
+ describe('update', () => {
260
+ it('should update contact fields', async () => {
261
+ const mockExecute = createMockExecuteFunctions({
262
+ resource: 'contact',
263
+ operation: 'update',
264
+ options: {},
265
+ contactId: '123',
266
+ updateFields: {
267
+ name: 'Updated Name',
268
+ location: 'Boston',
97
269
  },
98
- ],
99
- ]);
270
+ });
271
+ const mockResponse = { id: '123', name: 'Updated Name' };
272
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
273
+ await node.execute.call(mockExecute);
274
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
275
+ method: 'PUT',
276
+ url: 'https://test.innotes.com/api/contacts/123',
277
+ body: {
278
+ name: 'Updated Name',
279
+ location: 'Boston',
280
+ },
281
+ }));
282
+ });
100
283
  });
101
- it('should handle note create operation', async () => {
102
- const mockHttpRequest = jest.fn().mockResolvedValue({
103
- data: {
104
- id: 1,
105
- content: 'Test note content',
106
- contactId: 1,
107
- },
284
+ describe('delete', () => {
285
+ it('should delete contact and return success', async () => {
286
+ const mockExecute = createMockExecuteFunctions({
287
+ resource: 'contact',
288
+ operation: 'delete',
289
+ options: {},
290
+ contactId: '123',
291
+ });
292
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({});
293
+ const result = await node.execute.call(mockExecute);
294
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
295
+ method: 'DELETE',
296
+ url: 'https://test.innotes.com/api/contacts/123',
297
+ }));
298
+ expect(result[0][0].json).toEqual({ success: true });
108
299
  });
109
- mockExecuteFunctions.helpers.httpRequestWithAuthentication = mockHttpRequest;
110
- mockExecuteFunctions.getNodeParameter = jest.fn()
111
- .mockReturnValueOnce('note') // resource
112
- .mockReturnValueOnce('create') // operation
113
- .mockReturnValueOnce('Test note content') // content
114
- .mockReturnValueOnce(1); // contactId
115
- const result = await node.execute.call(mockExecuteFunctions);
116
- expect(mockHttpRequest).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
117
- method: 'POST',
118
- url: '/api/note',
119
- body: {
300
+ });
301
+ });
302
+ // ========================================================================
303
+ // Note Operations Tests
304
+ // ========================================================================
305
+ describe('Note Operations', () => {
306
+ describe('create', () => {
307
+ it('should create note with required fields', async () => {
308
+ const mockExecute = createMockExecuteFunctions({
309
+ resource: 'note',
310
+ operation: 'create',
311
+ options: {},
120
312
  content: 'Test note content',
121
- contactId: 1,
122
- },
123
- }));
124
- expect(result).toEqual([
125
- [
126
- {
127
- json: {
128
- id: 1,
129
- content: 'Test note content',
130
- contactId: 1,
313
+ contactId: '123',
314
+ });
315
+ const mockResponse = { id: '1', text: 'Test note content' };
316
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
317
+ const result = await node.execute.call(mockExecute);
318
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
319
+ method: 'POST',
320
+ url: 'https://test.innotes.com/api/note',
321
+ body: expect.objectContaining({
322
+ content: 'Test note content',
323
+ contact_id: '123',
324
+ }),
325
+ }));
326
+ expect(result[0]).toBeDefined();
327
+ });
328
+ it('should include visibility and ext_table_name when provided', async () => {
329
+ const mockExecute = createMockExecuteFunctions({
330
+ resource: 'note',
331
+ operation: 'create',
332
+ options: {},
333
+ content: 'Test note',
334
+ contactId: '123',
335
+ visibility: 'public',
336
+ ext_table_name: 'jobs',
337
+ });
338
+ const mockResponse = { id: '1' };
339
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
340
+ await node.execute.call(mockExecute);
341
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
342
+ body: expect.objectContaining({
343
+ visibility: 'public',
344
+ ext_table_name: 'jobs',
345
+ }),
346
+ }));
347
+ });
348
+ });
349
+ describe('get', () => {
350
+ it('should get note by ID', async () => {
351
+ const mockExecute = createMockExecuteFunctions({
352
+ resource: 'note',
353
+ operation: 'get',
354
+ options: {},
355
+ noteId: '456',
356
+ });
357
+ const mockResponse = { id: '456', text: 'Test note' };
358
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
359
+ await node.execute.call(mockExecute);
360
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
361
+ method: 'GET',
362
+ url: 'https://test.innotes.com/api/note/456',
363
+ }));
364
+ });
365
+ });
366
+ describe('getAll', () => {
367
+ it('should get all notes for a contact', async () => {
368
+ const mockExecute = createMockExecuteFunctions({
369
+ resource: 'note',
370
+ operation: 'getAll',
371
+ options: {},
372
+ contactId: '123',
373
+ returnAll: false,
374
+ limit: 10,
375
+ });
376
+ const mockResponse = [{ id: '1' }, { id: '2' }];
377
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
378
+ await node.execute.call(mockExecute);
379
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
380
+ method: 'GET',
381
+ url: 'https://test.innotes.com/api/note',
382
+ qs: expect.objectContaining({
383
+ contact_id: '123',
384
+ }),
385
+ }));
386
+ });
387
+ });
388
+ describe('update', () => {
389
+ it('should update note', async () => {
390
+ const mockExecute = createMockExecuteFunctions({
391
+ resource: 'note',
392
+ operation: 'update',
393
+ options: {},
394
+ noteId: '456',
395
+ updateFields: { text: 'Updated content' },
396
+ });
397
+ const mockResponse = { id: '456' };
398
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
399
+ await node.execute.call(mockExecute);
400
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
401
+ method: 'PUT',
402
+ url: 'https://test.innotes.com/api/note/456',
403
+ }));
404
+ });
405
+ });
406
+ describe('delete', () => {
407
+ it('should delete note', async () => {
408
+ const mockExecute = createMockExecuteFunctions({
409
+ resource: 'note',
410
+ operation: 'delete',
411
+ options: {},
412
+ noteId: '456',
413
+ });
414
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({});
415
+ const result = await node.execute.call(mockExecute);
416
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
417
+ method: 'DELETE',
418
+ url: 'https://test.innotes.com/api/note/456',
419
+ }));
420
+ expect(result[0][0].json).toEqual({ success: true });
421
+ });
422
+ });
423
+ });
424
+ // ========================================================================
425
+ // Job Operations Tests (HIGH PRIORITY - Bug Fix Verification)
426
+ // ========================================================================
427
+ describe('Job Operations', () => {
428
+ describe('create', () => {
429
+ it('should create job with status lookup and send status_id as integer', async () => {
430
+ const mockExecute = createMockExecuteFunctions({
431
+ resource: 'job',
432
+ operation: 'create',
433
+ options: {},
434
+ name: 'Software Engineer',
435
+ company_name: 'Tech Corp',
436
+ status: 'Interested',
437
+ });
438
+ // First call returns statuses, second creates job
439
+ mockExecute.helpers.httpRequestWithAuthentication
440
+ .mockResolvedValueOnce([
441
+ { id: 100, name: 'Applied' },
442
+ { id: 200, name: 'Interested' },
443
+ { id: 300, name: 'Rejected' },
444
+ ])
445
+ .mockResolvedValueOnce({ id: '1' });
446
+ await node.execute.call(mockExecute);
447
+ // Verify the job creation call
448
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
449
+ const createJobCall = calls.find((call) => call[1].method === 'POST' && call[1].url.includes('/api/job'));
450
+ expect(createJobCall).toBeDefined();
451
+ expect(createJobCall[1].body.status_id).toBe(200); // Should be integer, not string
452
+ expect(typeof createJobCall[1].body.status_id).toBe('number');
453
+ });
454
+ it('should NOT include status field in request body', async () => {
455
+ const mockExecute = createMockExecuteFunctions({
456
+ resource: 'job',
457
+ operation: 'create',
458
+ options: {},
459
+ name: 'Software Engineer',
460
+ company_name: 'Tech Corp',
461
+ status: 'Interested',
462
+ });
463
+ mockExecute.helpers.httpRequestWithAuthentication
464
+ .mockResolvedValueOnce([{ id: 200, name: 'Interested' }])
465
+ .mockResolvedValueOnce({ id: '1' });
466
+ await node.execute.call(mockExecute);
467
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
468
+ const createJobCall = calls.find((call) => call[1].method === 'POST' && call[1].url.includes('/api/job'));
469
+ expect(createJobCall[1].body).not.toHaveProperty('status');
470
+ });
471
+ it('should exclude picture_url when value is "Not Available"', async () => {
472
+ const mockExecute = createMockExecuteFunctions({
473
+ resource: 'job',
474
+ operation: 'create',
475
+ options: {},
476
+ name: 'Software Engineer',
477
+ company_name: 'Tech Corp',
478
+ picture_url: 'Not Available',
479
+ });
480
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
481
+ await node.execute.call(mockExecute);
482
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
483
+ const createJobCall = calls.find((call) => call[1].method === 'POST');
484
+ expect(createJobCall[1].body).not.toHaveProperty('picture_url');
485
+ });
486
+ it('should include picture_url when value is valid URL', async () => {
487
+ const mockExecute = createMockExecuteFunctions({
488
+ resource: 'job',
489
+ operation: 'create',
490
+ options: {},
491
+ name: 'Software Engineer',
492
+ company_name: 'Tech Corp',
493
+ picture_url: 'https://example.com/logo.png',
494
+ });
495
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
496
+ await node.execute.call(mockExecute);
497
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
498
+ const createJobCall = calls.find((call) => call[1].method === 'POST');
499
+ expect(createJobCall[1].body.picture_url).toBe('https://example.com/logo.png');
500
+ });
501
+ it('should handle tags as comma-separated string converted to array', async () => {
502
+ const mockExecute = createMockExecuteFunctions({
503
+ resource: 'job',
504
+ operation: 'create',
505
+ options: {},
506
+ name: 'Software Engineer',
507
+ company_name: 'Tech Corp',
508
+ tags: 'remote, senior, python',
509
+ });
510
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
511
+ await node.execute.call(mockExecute);
512
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
513
+ const createJobCall = calls.find((call) => call[1].method === 'POST');
514
+ expect(createJobCall[1].body.tags).toEqual(['remote', 'senior', 'python']);
515
+ });
516
+ it('should throw error when status name not found', async () => {
517
+ const mockExecute = createMockExecuteFunctions({
518
+ resource: 'job',
519
+ operation: 'create',
520
+ options: {},
521
+ name: 'Software Engineer',
522
+ company_name: 'Tech Corp',
523
+ status: 'NonExistentStatus',
524
+ });
525
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue([
526
+ { id: 1, name: 'Applied' },
527
+ { id: 2, name: 'Rejected' },
528
+ ]);
529
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/NonExistentStatus.*not found/);
530
+ });
531
+ it('should include all optional fields when provided', async () => {
532
+ const mockExecute = createMockExecuteFunctions({
533
+ resource: 'job',
534
+ operation: 'create',
535
+ options: {},
536
+ name: 'Software Engineer',
537
+ company_name: 'Tech Corp',
538
+ description: 'Great opportunity',
539
+ location: 'Remote',
540
+ remote_setting: 'Remote',
541
+ company_url: 'https://techcorp.com',
542
+ provider: 'linkedin',
543
+ url: 'https://linkedin.com/jobs/123',
544
+ ext_id: 'LI-123',
545
+ });
546
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
547
+ await node.execute.call(mockExecute);
548
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
549
+ const createJobCall = calls.find((call) => call[1].method === 'POST');
550
+ expect(createJobCall[1].body).toEqual(expect.objectContaining({
551
+ name: 'Software Engineer',
552
+ company_name: 'Tech Corp',
553
+ description: 'Great opportunity',
554
+ location: 'Remote',
555
+ remote_setting: 'Remote',
556
+ company_url: 'https://techcorp.com',
557
+ provider: 'linkedin',
558
+ url: 'https://linkedin.com/jobs/123',
559
+ ext_id: 'LI-123',
560
+ }));
561
+ });
562
+ });
563
+ describe('get', () => {
564
+ it('should get job by ID', async () => {
565
+ const mockExecute = createMockExecuteFunctions({
566
+ resource: 'job',
567
+ operation: 'get',
568
+ options: {},
569
+ jobId: '123',
570
+ });
571
+ const mockResponse = { id: '123', name: 'Test Job' };
572
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
573
+ await node.execute.call(mockExecute);
574
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
575
+ method: 'GET',
576
+ url: 'https://test.innotes.com/api/job/123',
577
+ }));
578
+ });
579
+ it('should handle job not found error', async () => {
580
+ const mockExecute = createMockExecuteFunctions({
581
+ resource: 'job',
582
+ operation: 'get',
583
+ options: {},
584
+ jobId: '999',
585
+ });
586
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Job not found'));
587
+ await expect(node.execute.call(mockExecute)).rejects.toThrow();
588
+ });
589
+ });
590
+ describe('getAll', () => {
591
+ it('should get all jobs with pagination', async () => {
592
+ const mockExecute = createMockExecuteFunctions({
593
+ resource: 'job',
594
+ operation: 'getAll',
595
+ options: {},
596
+ returnAll: false,
597
+ limit: 10,
598
+ });
599
+ const mockResponse = [{ id: '1' }, { id: '2' }];
600
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
601
+ await node.execute.call(mockExecute);
602
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
603
+ method: 'GET',
604
+ url: 'https://test.innotes.com/api/job',
605
+ qs: expect.objectContaining({
606
+ pageSize: 10,
607
+ page: 1,
608
+ }),
609
+ }));
610
+ });
611
+ it('should apply search filters', async () => {
612
+ const mockExecute = createMockExecuteFunctions({
613
+ resource: 'job',
614
+ operation: 'getAll',
615
+ options: {},
616
+ returnAll: false,
617
+ limit: 24,
618
+ search: 'engineer',
619
+ tags: 'remote',
620
+ });
621
+ const mockResponse = [{ id: '1' }];
622
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
623
+ await node.execute.call(mockExecute);
624
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
625
+ qs: expect.objectContaining({
626
+ search: 'engineer',
627
+ tags: 'remote',
628
+ }),
629
+ }));
630
+ });
631
+ });
632
+ describe('update', () => {
633
+ it('should update job with status lookup as integer', async () => {
634
+ const mockExecute = createMockExecuteFunctions({
635
+ resource: 'job',
636
+ operation: 'update',
637
+ options: {},
638
+ jobId: '123',
639
+ updateFields: {
640
+ status: 'Interviewing',
641
+ },
642
+ });
643
+ mockExecute.helpers.httpRequestWithAuthentication
644
+ .mockResolvedValueOnce([
645
+ { id: 100, name: 'Applied' },
646
+ { id: 200, name: 'Interviewing' },
647
+ ])
648
+ .mockResolvedValueOnce({ id: '123' });
649
+ await node.execute.call(mockExecute);
650
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
651
+ const updateCall = calls.find((call) => call[1].method === 'PUT');
652
+ expect(updateCall[1].body.status_id).toBe(200);
653
+ expect(typeof updateCall[1].body.status_id).toBe('number');
654
+ expect(updateCall[1].body).not.toHaveProperty('status');
655
+ });
656
+ it('should convert tags string to array', async () => {
657
+ const mockExecute = createMockExecuteFunctions({
658
+ resource: 'job',
659
+ operation: 'update',
660
+ options: {},
661
+ jobId: '123',
662
+ updateFields: {
663
+ tags: 'urgent, high-priority',
664
+ },
665
+ });
666
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '123' });
667
+ await node.execute.call(mockExecute);
668
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
669
+ const updateCall = calls.find((call) => call[1].method === 'PUT');
670
+ expect(updateCall[1].body.tags).toEqual(['urgent', 'high-priority']);
671
+ });
672
+ });
673
+ describe('delete', () => {
674
+ it('should delete job and return success', async () => {
675
+ const mockExecute = createMockExecuteFunctions({
676
+ resource: 'job',
677
+ operation: 'delete',
678
+ options: {},
679
+ jobId: '123',
680
+ });
681
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({});
682
+ const result = await node.execute.call(mockExecute);
683
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
684
+ method: 'DELETE',
685
+ url: 'https://test.innotes.com/api/job/123',
686
+ }));
687
+ expect(result[0][0].json).toEqual({ success: true });
688
+ });
689
+ });
690
+ describe('search', () => {
691
+ it('should search by general term', async () => {
692
+ const mockExecute = createMockExecuteFunctions({
693
+ resource: 'job',
694
+ operation: 'search',
695
+ options: {},
696
+ searchMethod: 'general',
697
+ searchQuery: 'software engineer',
698
+ searchOptions: {},
699
+ });
700
+ const mockResponse = [{ id: '1', name: 'Software Engineer' }];
701
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
702
+ await node.execute.call(mockExecute);
703
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
704
+ qs: expect.objectContaining({
705
+ searchTerm: 'software engineer',
706
+ }),
707
+ }));
708
+ });
709
+ it('should search by ext_id', async () => {
710
+ const mockExecute = createMockExecuteFunctions({
711
+ resource: 'job',
712
+ operation: 'search',
713
+ options: {},
714
+ searchMethod: 'ext_id',
715
+ searchQuery: 'LI-123456',
716
+ searchOptions: {},
717
+ });
718
+ const mockResponse = [{ id: '1', ext_id: 'LI-123456' }];
719
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
720
+ await node.execute.call(mockExecute);
721
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
722
+ qs: expect.objectContaining({
723
+ ext_id: 'LI-123456',
724
+ }),
725
+ }));
726
+ });
727
+ it('should search by specific field (title)', async () => {
728
+ const mockExecute = createMockExecuteFunctions({
729
+ resource: 'job',
730
+ operation: 'search',
731
+ options: {},
732
+ searchMethod: 'title',
733
+ searchQuery: 'manager',
734
+ searchOptions: {},
735
+ });
736
+ const mockResponse = [{ id: '1' }];
737
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
738
+ await node.execute.call(mockExecute);
739
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
740
+ qs: expect.objectContaining({
741
+ searchTerm: 'manager',
742
+ searchField: 'title',
743
+ }),
744
+ }));
745
+ });
746
+ it('should apply additional filters', async () => {
747
+ const mockExecute = createMockExecuteFunctions({
748
+ resource: 'job',
749
+ operation: 'search',
750
+ options: {},
751
+ searchMethod: 'general',
752
+ searchQuery: 'developer',
753
+ searchOptions: {
754
+ remote_setting: 'remote',
755
+ status: 'Applied',
756
+ limit: 50,
757
+ },
758
+ });
759
+ const mockResponse = [];
760
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
761
+ await node.execute.call(mockExecute);
762
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
763
+ qs: expect.objectContaining({
764
+ remote_setting: 'remote',
765
+ status: 'Applied',
766
+ pageSize: 50,
767
+ }),
768
+ }));
769
+ });
770
+ });
771
+ describe('exists', () => {
772
+ it('should return exists:true when found by job_id', async () => {
773
+ const mockExecute = createMockExecuteFunctions({
774
+ resource: 'job',
775
+ operation: 'exists',
776
+ options: {},
777
+ id: '123',
778
+ });
779
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '123' });
780
+ const result = await node.execute.call(mockExecute);
781
+ expect(result[0][0].json).toEqual({
782
+ exists: true,
783
+ found_by: 'job_id',
784
+ id: '123',
785
+ });
786
+ });
787
+ it('should return exists:true when found by ext_id', async () => {
788
+ const mockExecute = createMockExecuteFunctions({
789
+ resource: 'job',
790
+ operation: 'exists',
791
+ options: {},
792
+ id: 'LI-123',
793
+ });
794
+ // First call (get by ID) fails, second call (search by ext_id) succeeds
795
+ mockExecute.helpers.httpRequestWithAuthentication
796
+ .mockRejectedValueOnce(new Error('Not found'))
797
+ .mockResolvedValueOnce([{ id: '456', ext_id: 'LI-123' }]);
798
+ const result = await node.execute.call(mockExecute);
799
+ expect(result[0][0].json).toEqual({
800
+ exists: true,
801
+ found_by: 'ext_id',
802
+ id: 'LI-123',
803
+ });
804
+ });
805
+ it('should return exists:false when not found', async () => {
806
+ const mockExecute = createMockExecuteFunctions({
807
+ resource: 'job',
808
+ operation: 'exists',
809
+ options: {},
810
+ id: 'nonexistent',
811
+ });
812
+ mockExecute.helpers.httpRequestWithAuthentication
813
+ .mockRejectedValueOnce(new Error('Not found'))
814
+ .mockResolvedValueOnce([]);
815
+ const result = await node.execute.call(mockExecute);
816
+ expect(result[0][0].json).toEqual({
817
+ exists: false,
818
+ id: 'nonexistent',
819
+ });
820
+ });
821
+ });
822
+ });
823
+ // ========================================================================
824
+ // Status Operations Tests
825
+ // ========================================================================
826
+ describe('Status Operations', () => {
827
+ describe('getAll', () => {
828
+ it('should get all statuses by type', async () => {
829
+ const mockExecute = createMockExecuteFunctions({
830
+ resource: 'status',
831
+ operation: 'getAll',
832
+ options: {},
833
+ type: 'job',
834
+ });
835
+ const mockResponse = [
836
+ { id: 1, name: 'Applied' },
837
+ { id: 2, name: 'Interviewing' },
838
+ ];
839
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
840
+ await node.execute.call(mockExecute);
841
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
842
+ method: 'GET',
843
+ url: 'https://test.innotes.com/api/statuses',
844
+ qs: { type: 'job' },
845
+ }));
846
+ });
847
+ });
848
+ describe('create', () => {
849
+ it('should create new status', async () => {
850
+ const mockExecute = createMockExecuteFunctions({
851
+ resource: 'status',
852
+ operation: 'create',
853
+ options: {},
854
+ name: 'New Status',
855
+ type: 'job',
856
+ color: '#ff0000',
857
+ });
858
+ const mockResponse = { id: 10, name: 'New Status' };
859
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
860
+ await node.execute.call(mockExecute);
861
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
862
+ method: 'POST',
863
+ url: 'https://test.innotes.com/api/statuses',
864
+ body: expect.objectContaining({
865
+ name: 'New Status',
866
+ type: 'job',
867
+ color: '#ff0000',
868
+ }),
869
+ }));
870
+ });
871
+ });
872
+ describe('update', () => {
873
+ it('should update status', async () => {
874
+ const mockExecute = createMockExecuteFunctions({
875
+ resource: 'status',
876
+ operation: 'update',
877
+ options: {},
878
+ statusId: '5',
879
+ updateFields: { name: 'Updated Status' },
880
+ });
881
+ const mockResponse = { id: 5, name: 'Updated Status' };
882
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
883
+ await node.execute.call(mockExecute);
884
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
885
+ method: 'PUT',
886
+ url: 'https://test.innotes.com/api/statuses/5',
887
+ }));
888
+ });
889
+ });
890
+ describe('delete', () => {
891
+ it('should delete status', async () => {
892
+ const mockExecute = createMockExecuteFunctions({
893
+ resource: 'status',
894
+ operation: 'delete',
895
+ options: {},
896
+ statusId: '5',
897
+ });
898
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({});
899
+ const result = await node.execute.call(mockExecute);
900
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
901
+ method: 'DELETE',
902
+ url: 'https://test.innotes.com/api/statuses/5',
903
+ }));
904
+ expect(result[0][0].json).toEqual({ success: true });
905
+ });
906
+ });
907
+ });
908
+ // ========================================================================
909
+ // Tag Operations Tests
910
+ // ========================================================================
911
+ describe('Tag Operations', () => {
912
+ describe('getAll', () => {
913
+ it('should get all tags', async () => {
914
+ const mockExecute = createMockExecuteFunctions({
915
+ resource: 'tag',
916
+ operation: 'getAll',
917
+ options: {},
918
+ });
919
+ const mockResponse = [
920
+ { value: 'urgent' },
921
+ { value: 'follow-up' },
922
+ ];
923
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
924
+ await node.execute.call(mockExecute);
925
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
926
+ method: 'GET',
927
+ url: 'https://test.innotes.com/api/tags',
928
+ }));
929
+ });
930
+ });
931
+ });
932
+ // ========================================================================
933
+ // User Operations Tests
934
+ // ========================================================================
935
+ describe('User Operations', () => {
936
+ describe('get', () => {
937
+ it('should get current user', async () => {
938
+ const mockExecute = createMockExecuteFunctions({
939
+ resource: 'user',
940
+ operation: 'get',
941
+ options: {},
942
+ });
943
+ const mockResponse = { id: '1', email: 'user@example.com', name: 'Test User' };
944
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
945
+ await node.execute.call(mockExecute);
946
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
947
+ method: 'GET',
948
+ url: 'https://test.innotes.com/api/user',
949
+ }));
950
+ });
951
+ });
952
+ describe('getCv', () => {
953
+ it('should get CV and parse job_preferences JSON', async () => {
954
+ const mockExecute = createMockExecuteFunctions({
955
+ resource: 'user',
956
+ operation: 'getCv',
957
+ options: {},
958
+ });
959
+ const mockResponse = {
960
+ cv: 'My resume content',
961
+ cv_updated_at: '2024-01-01T00:00:00Z',
962
+ job_preferences: JSON.stringify({
963
+ searchJobTitles: ['Software Engineer', 'Developer'],
964
+ locations: ['Remote', 'New York'],
965
+ blacklist: {
966
+ titleBlacklist: ['Sales'],
967
+ companyBlacklist: ['BadCorp'],
968
+ penalize: ['Consultant'],
131
969
  },
970
+ }),
971
+ };
972
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
973
+ const result = await node.execute.call(mockExecute);
974
+ expect(result[0][0].json).toEqual(expect.objectContaining({
975
+ cv: 'My resume content',
976
+ searchJobTitles: 'Software Engineer, Developer',
977
+ locations: 'Remote, New York',
978
+ titleBlacklist: 'Sales',
979
+ companyBlacklist: 'BadCorp',
980
+ penalize: 'Consultant',
981
+ }));
982
+ });
983
+ it('should handle legacy blacklist structure', async () => {
984
+ const mockExecute = createMockExecuteFunctions({
985
+ resource: 'user',
986
+ operation: 'getCv',
987
+ options: {},
988
+ });
989
+ const mockResponse = {
990
+ cv: 'My resume',
991
+ job_preferences: JSON.stringify({
992
+ searchJobTitles: ['Engineer'],
993
+ locations: ['SF'],
994
+ titleBlacklist: ['Manager'],
995
+ companyBlacklist: ['OldCorp'],
996
+ }),
997
+ };
998
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
999
+ const result = await node.execute.call(mockExecute);
1000
+ expect(result[0][0].json).toEqual(expect.objectContaining({
1001
+ titleBlacklist: 'Manager',
1002
+ companyBlacklist: 'OldCorp',
1003
+ }));
1004
+ });
1005
+ it('should handle missing job_preferences', async () => {
1006
+ const mockExecute = createMockExecuteFunctions({
1007
+ resource: 'user',
1008
+ operation: 'getCv',
1009
+ options: {},
1010
+ });
1011
+ const mockResponse = {
1012
+ cv: 'My resume',
1013
+ cv_updated_at: null,
1014
+ job_preferences: null,
1015
+ };
1016
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
1017
+ const result = await node.execute.call(mockExecute);
1018
+ expect(result[0][0].json).toEqual(expect.objectContaining({
1019
+ cv: 'My resume',
1020
+ searchJobTitles: '',
1021
+ locations: '',
1022
+ }));
1023
+ });
1024
+ });
1025
+ describe('update', () => {
1026
+ it('should update user fields', async () => {
1027
+ const mockExecute = createMockExecuteFunctions({
1028
+ resource: 'user',
1029
+ operation: 'update',
1030
+ options: {},
1031
+ updateFields: { home_location: 'Boston, MA' },
1032
+ });
1033
+ const mockResponse = { id: '1', home_location: 'Boston, MA' };
1034
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
1035
+ await node.execute.call(mockExecute);
1036
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1037
+ method: 'PUT',
1038
+ url: 'https://test.innotes.com/api/user',
1039
+ body: { home_location: 'Boston, MA' },
1040
+ }));
1041
+ });
1042
+ });
1043
+ });
1044
+ // ========================================================================
1045
+ // Automation Operations Tests
1046
+ // ========================================================================
1047
+ describe('Automation Operations', () => {
1048
+ describe('getConfig', () => {
1049
+ it('should get automation config successfully', async () => {
1050
+ const mockExecute = createMockExecuteFunctions({
1051
+ resource: 'automation',
1052
+ operation: 'getConfig',
1053
+ options: {},
1054
+ });
1055
+ const mockResponse = {
1056
+ models: {
1057
+ low: 'gpt-4o-mini',
1058
+ medium: 'gpt-5-mini',
1059
+ high: 'gpt-5-mini',
132
1060
  },
133
- ],
1061
+ };
1062
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
1063
+ const result = await node.execute.call(mockExecute);
1064
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1065
+ method: 'GET',
1066
+ url: 'https://test.innotes.com/api/automation/config',
1067
+ }));
1068
+ expect(result[0][0].json).toEqual(mockResponse);
1069
+ });
1070
+ it('should handle API errors in getConfig', async () => {
1071
+ const mockExecute = createMockExecuteFunctions({
1072
+ resource: 'automation',
1073
+ operation: 'getConfig',
1074
+ options: {},
1075
+ });
1076
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Unauthorized'));
1077
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Failed to get automation config/i);
1078
+ });
1079
+ });
1080
+ describe('reportJobCreated', () => {
1081
+ it('should report with success status', async () => {
1082
+ const mockExecute = createMockExecuteFunctions({
1083
+ resource: 'automation',
1084
+ operation: 'reportJobCreated',
1085
+ options: {},
1086
+ jobsAdded: 5,
1087
+ status: 'success',
1088
+ });
1089
+ // First call gets user, second reports callback
1090
+ mockExecute.helpers.httpRequestWithAuthentication
1091
+ .mockResolvedValueOnce({ id: 'user-123' })
1092
+ .mockResolvedValueOnce({ success: true });
1093
+ const result = await node.execute.call(mockExecute);
1094
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
1095
+ const callbackCall = calls.find((call) => call[1].url.includes('/api/automation/callback'));
1096
+ expect(callbackCall[1].body).toEqual(expect.objectContaining({
1097
+ userId: 'user-123',
1098
+ status: 'success',
1099
+ jobsAdded: 5,
1100
+ secret: 'test-callback-secret',
1101
+ }));
1102
+ expect(result[0][0].json).toEqual(expect.objectContaining({
1103
+ success: true,
1104
+ jobsAdded: 5,
1105
+ status: 'success',
1106
+ }));
1107
+ });
1108
+ it('should report with error status and message', async () => {
1109
+ const mockExecute = createMockExecuteFunctions({
1110
+ resource: 'automation',
1111
+ operation: 'reportJobCreated',
1112
+ options: {},
1113
+ jobsAdded: 0,
1114
+ status: 'error',
1115
+ errorMessage: 'Failed to scrape jobs',
1116
+ });
1117
+ mockExecute.helpers.httpRequestWithAuthentication
1118
+ .mockResolvedValueOnce({ id: 'user-123' })
1119
+ .mockResolvedValueOnce({ success: true });
1120
+ await node.execute.call(mockExecute);
1121
+ const calls = mockExecute.helpers.httpRequestWithAuthentication.mock.calls;
1122
+ const callbackCall = calls.find((call) => call[1].url.includes('/api/automation/callback'));
1123
+ expect(callbackCall[1].body).toEqual(expect.objectContaining({
1124
+ status: 'error',
1125
+ error: 'Failed to scrape jobs',
1126
+ }));
1127
+ });
1128
+ it('should throw when callbackSecret not configured', async () => {
1129
+ const mockExecute = createMockExecuteFunctions({
1130
+ resource: 'automation',
1131
+ operation: 'reportJobCreated',
1132
+ options: {},
1133
+ jobsAdded: 1,
1134
+ status: 'success',
1135
+ }, { callbackSecret: '' });
1136
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/callback secret/i);
1137
+ });
1138
+ });
1139
+ });
1140
+ // ========================================================================
1141
+ // Batch Processing Tests
1142
+ // ========================================================================
1143
+ describe('Batch Processing', () => {
1144
+ it('should process items in batches', async () => {
1145
+ const mockExecute = createMockExecuteFunctions({
1146
+ resource: 'contact',
1147
+ operation: 'get',
1148
+ options: { batchSize: 2, timeoutBetweenBatches: 10 },
1149
+ contactId: '1',
1150
+ });
1151
+ // Simulate 3 input items
1152
+ mockExecute.getInputData.mockReturnValue([
1153
+ { json: {} },
1154
+ { json: {} },
1155
+ { json: {} },
134
1156
  ]);
1157
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
1158
+ await node.execute.call(mockExecute);
1159
+ // Should have made 3 API calls (one per item)
1160
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(3);
1161
+ });
1162
+ it('should respect batchSize option', async () => {
1163
+ const mockExecute = createMockExecuteFunctions({
1164
+ resource: 'contact',
1165
+ operation: 'get',
1166
+ options: { batchSize: 5 },
1167
+ contactId: '1',
1168
+ });
1169
+ mockExecute.getInputData.mockReturnValue([{ json: {} }]);
1170
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue({ id: '1' });
1171
+ await node.execute.call(mockExecute);
1172
+ // Verify options were read
1173
+ expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0, {});
1174
+ });
1175
+ });
1176
+ // ========================================================================
1177
+ // Error Handling Tests
1178
+ // ========================================================================
1179
+ describe('Error Handling', () => {
1180
+ it('should handle API errors gracefully', async () => {
1181
+ const mockExecute = createMockExecuteFunctions({
1182
+ resource: 'contact',
1183
+ operation: 'get',
1184
+ options: {},
1185
+ contactId: '999',
1186
+ });
1187
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Not found'));
1188
+ await expect(node.execute.call(mockExecute)).rejects.toThrow();
1189
+ });
1190
+ it('should continue on fail when enabled', async () => {
1191
+ const mockExecute = createMockExecuteFunctions({
1192
+ resource: 'contact',
1193
+ operation: 'get',
1194
+ options: {},
1195
+ contactId: '999',
1196
+ });
1197
+ mockExecute.continueOnFail.mockReturnValue(true);
1198
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('API Error'));
1199
+ const result = await node.execute.call(mockExecute);
1200
+ // Should return error in result instead of throwing
1201
+ expect(result[0][0].json).toEqual({ error: 'API Error' });
135
1202
  });
136
- it('should throw error for unsupported resource', async () => {
137
- mockExecuteFunctions.getNodeParameter = jest.fn()
138
- .mockReturnValueOnce('unsupported'); // resource
139
- await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow('The resource "unsupported" is not known!');
1203
+ it('should include request parameters in job creation error messages', async () => {
1204
+ const mockExecute = createMockExecuteFunctions({
1205
+ resource: 'job',
1206
+ operation: 'create',
1207
+ options: {},
1208
+ name: 'Test Job',
1209
+ company_name: 'Test Corp',
1210
+ });
1211
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Server error'));
1212
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Test Job|Test Corp/);
1213
+ });
1214
+ });
1215
+ // ========================================================================
1216
+ // Unsupported Operations Tests
1217
+ // ========================================================================
1218
+ describe('Unsupported Operations', () => {
1219
+ it('should handle unsupported resource gracefully', async () => {
1220
+ const mockExecute = createMockExecuteFunctions({
1221
+ resource: 'unsupported',
1222
+ operation: 'get',
1223
+ options: {},
1224
+ });
1225
+ const result = await node.execute.call(mockExecute);
1226
+ expect(result).toBeDefined();
1227
+ expect(Array.isArray(result)).toBe(true);
1228
+ });
1229
+ it('should throw error for unsupported contact operation', async () => {
1230
+ const mockExecute = createMockExecuteFunctions({
1231
+ resource: 'contact',
1232
+ operation: 'unsupported_op',
1233
+ options: {},
1234
+ });
1235
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*contact/i);
1236
+ });
1237
+ it('should throw error for unsupported note operation', async () => {
1238
+ const mockExecute = createMockExecuteFunctions({
1239
+ resource: 'note',
1240
+ operation: 'unsupported_op',
1241
+ options: {},
1242
+ });
1243
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*note/i);
1244
+ });
1245
+ it('should throw error for unsupported job operation', async () => {
1246
+ const mockExecute = createMockExecuteFunctions({
1247
+ resource: 'job',
1248
+ operation: 'unsupported_op',
1249
+ options: {},
1250
+ });
1251
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*job/i);
1252
+ });
1253
+ it('should throw error for unsupported status operation', async () => {
1254
+ const mockExecute = createMockExecuteFunctions({
1255
+ resource: 'status',
1256
+ operation: 'unsupported_op',
1257
+ options: {},
1258
+ });
1259
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*status/i);
1260
+ });
1261
+ it('should throw error for unsupported tag operation', async () => {
1262
+ const mockExecute = createMockExecuteFunctions({
1263
+ resource: 'tag',
1264
+ operation: 'unsupported_op',
1265
+ options: {},
1266
+ });
1267
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*tag/i);
1268
+ });
1269
+ it('should throw error for unsupported user operation', async () => {
1270
+ const mockExecute = createMockExecuteFunctions({
1271
+ resource: 'user',
1272
+ operation: 'unsupported_op',
1273
+ options: {},
1274
+ });
1275
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*user/i);
1276
+ });
1277
+ it('should throw error for unsupported automation operation', async () => {
1278
+ const mockExecute = createMockExecuteFunctions({
1279
+ resource: 'automation',
1280
+ operation: 'unsupported_op',
1281
+ options: {},
1282
+ });
1283
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/not supported.*automation/i);
1284
+ });
1285
+ });
1286
+ // ========================================================================
1287
+ // Additional Error Handling Tests for Branch Coverage
1288
+ // ========================================================================
1289
+ describe('Additional Error Handling', () => {
1290
+ describe('Contact Operations', () => {
1291
+ it('should include request params in contact create error', async () => {
1292
+ const mockExecute = createMockExecuteFunctions({
1293
+ resource: 'contact',
1294
+ operation: 'create',
1295
+ options: {},
1296
+ name: 'Test Contact',
1297
+ linkedin_key: 'test-key',
1298
+ linkedin_user: 'test-user',
1299
+ location: 'New York',
1300
+ current_company: 'Test Corp',
1301
+ picture_url: 'http://example.com/pic.jpg',
1302
+ tags: 'tag1,tag2',
1303
+ status_id: '5',
1304
+ });
1305
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Validation error'));
1306
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Test Contact|Validation error/);
1307
+ });
1308
+ });
1309
+ describe('Automation Operations', () => {
1310
+ it('should throw when getting user ID fails', async () => {
1311
+ const mockExecute = createMockExecuteFunctions({
1312
+ resource: 'automation',
1313
+ operation: 'reportJobCreated',
1314
+ options: {},
1315
+ jobsAdded: 5,
1316
+ status: 'success',
1317
+ });
1318
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('User not found'));
1319
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Failed to get user ID/i);
1320
+ });
1321
+ it('should throw when callback request fails', async () => {
1322
+ const mockExecute = createMockExecuteFunctions({
1323
+ resource: 'automation',
1324
+ operation: 'reportJobCreated',
1325
+ options: {},
1326
+ jobsAdded: 5,
1327
+ status: 'success',
1328
+ });
1329
+ mockExecute.helpers.httpRequestWithAuthentication
1330
+ .mockResolvedValueOnce({ id: 'user-123' }) // User request succeeds
1331
+ .mockRejectedValueOnce(new Error('Callback failed')); // Callback fails
1332
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Failed to report job created/i);
1333
+ });
1334
+ });
1335
+ describe('Job Operations', () => {
1336
+ it('should throw when status lookup fails in job create', async () => {
1337
+ const mockExecute = createMockExecuteFunctions({
1338
+ resource: 'job',
1339
+ operation: 'create',
1340
+ options: {},
1341
+ name: 'Test Job',
1342
+ company_name: 'Test Corp',
1343
+ status: 'Applied',
1344
+ });
1345
+ // Status lookup fails with network error
1346
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Network error'));
1347
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Failed to lookup status/i);
1348
+ });
1349
+ it('should throw when status not found in job update', async () => {
1350
+ const mockExecute = createMockExecuteFunctions({
1351
+ resource: 'job',
1352
+ operation: 'update',
1353
+ options: {},
1354
+ jobId: '123',
1355
+ updateFields: { status: 'NonExistentStatus' },
1356
+ });
1357
+ // Return empty statuses array
1358
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValueOnce([
1359
+ { id: '1', name: 'Applied' },
1360
+ { id: '2', name: 'Interviewing' },
1361
+ ]);
1362
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/NonExistentStatus.*not found/i);
1363
+ });
1364
+ it('should throw when status lookup fails in job update', async () => {
1365
+ const mockExecute = createMockExecuteFunctions({
1366
+ resource: 'job',
1367
+ operation: 'update',
1368
+ options: {},
1369
+ jobId: '123',
1370
+ updateFields: { status: 'Applied' },
1371
+ });
1372
+ // Status lookup fails with network error
1373
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Network error'));
1374
+ await expect(node.execute.call(mockExecute)).rejects.toThrow(/Failed to lookup status/i);
1375
+ });
1376
+ it('should return exists:false with error when search throws', async () => {
1377
+ const mockExecute = createMockExecuteFunctions({
1378
+ resource: 'job',
1379
+ operation: 'exists',
1380
+ options: {},
1381
+ existsType: 'byJobId',
1382
+ id: '123',
1383
+ });
1384
+ mockExecute.helpers.httpRequestWithAuthentication.mockRejectedValue(new Error('Search failed'));
1385
+ const result = await node.execute.call(mockExecute);
1386
+ expect(result[0][0].json).toEqual(expect.objectContaining({
1387
+ exists: false,
1388
+ id: '123',
1389
+ error: 'Search failed',
1390
+ }));
1391
+ });
1392
+ });
1393
+ describe('Job Search Types', () => {
1394
+ it('should search by company field', async () => {
1395
+ const mockExecute = createMockExecuteFunctions({
1396
+ resource: 'job',
1397
+ operation: 'search',
1398
+ options: {},
1399
+ searchMethod: 'company',
1400
+ searchQuery: 'Google',
1401
+ searchOptions: { limit: 50 },
1402
+ });
1403
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue([]);
1404
+ await node.execute.call(mockExecute);
1405
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1406
+ qs: expect.objectContaining({
1407
+ searchTerm: 'Google',
1408
+ searchField: 'company',
1409
+ }),
1410
+ }));
1411
+ });
1412
+ it('should search by location field', async () => {
1413
+ const mockExecute = createMockExecuteFunctions({
1414
+ resource: 'job',
1415
+ operation: 'search',
1416
+ options: {},
1417
+ searchMethod: 'location',
1418
+ searchQuery: 'San Francisco',
1419
+ searchOptions: { limit: 50 },
1420
+ });
1421
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue([]);
1422
+ await node.execute.call(mockExecute);
1423
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1424
+ qs: expect.objectContaining({
1425
+ searchTerm: 'San Francisco',
1426
+ searchField: 'location',
1427
+ }),
1428
+ }));
1429
+ });
1430
+ it('should search by description field', async () => {
1431
+ const mockExecute = createMockExecuteFunctions({
1432
+ resource: 'job',
1433
+ operation: 'search',
1434
+ options: {},
1435
+ searchMethod: 'description',
1436
+ searchQuery: 'React',
1437
+ searchOptions: { limit: 50 },
1438
+ });
1439
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue([]);
1440
+ await node.execute.call(mockExecute);
1441
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1442
+ qs: expect.objectContaining({
1443
+ searchTerm: 'React',
1444
+ searchField: 'description',
1445
+ }),
1446
+ }));
1447
+ });
1448
+ it('should use default search when method is unknown', async () => {
1449
+ const mockExecute = createMockExecuteFunctions({
1450
+ resource: 'job',
1451
+ operation: 'search',
1452
+ options: {},
1453
+ searchMethod: 'unknown_method',
1454
+ searchQuery: 'test query',
1455
+ searchOptions: { limit: 50 },
1456
+ });
1457
+ mockExecute.helpers.httpRequestWithAuthentication.mockResolvedValue([]);
1458
+ await node.execute.call(mockExecute);
1459
+ expect(mockExecute.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('inNotesApi', expect.objectContaining({
1460
+ qs: expect.objectContaining({
1461
+ searchTerm: 'test query',
1462
+ }),
1463
+ }));
1464
+ });
140
1465
  });
141
1466
  });
142
1467
  });