@uarlouski/testrail-mcp-server 1.1.0 → 1.2.0

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.
@@ -32,36 +32,7 @@ describe('get_templates tool', () => {
32
32
  const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
33
33
 
34
34
  expect(result).toBeDefined();
35
- expect(result.content[0].type).toBe('text');
36
-
37
- const parsed = JSON.parse(result.content[0].text);
38
- expect(parsed.templates).toHaveLength(3);
35
+ expect(result.templates).toHaveLength(3);
39
36
  expect(mockClient.getTemplates).toHaveBeenCalledWith('1');
40
37
  });
41
-
42
- test('returns correct template structure', async () => {
43
- const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
44
- const parsed = JSON.parse(result.content[0].text);
45
-
46
- expect(parsed.templates[0]).toEqual({
47
- id: 1,
48
- name: 'Test Case (Text)',
49
- });
50
-
51
- expect(parsed.templates[1]).toEqual({
52
- id: 2,
53
- name: 'Test Case (Steps)',
54
- });
55
- });
56
-
57
- test('handler returns error on failure', async () => {
58
- getTemplatesMock.mockRejectedValue(new Error('API Error'));
59
-
60
- const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
61
-
62
- expect(result).toEqual({
63
- content: [{ type: 'text', text: 'Error: API Error' }],
64
- isError: true
65
- });
66
- });
67
38
  });
@@ -52,12 +52,9 @@ describe('update_case tool', () => {
52
52
  );
53
53
 
54
54
  expect(result).toBeDefined();
55
- expect(result.content[0].type).toBe('text');
56
-
57
- const parsed = JSON.parse(result.content[0].text);
58
- expect(parsed.success).toBe(true);
59
- expect(parsed.case_id).toBe(123);
60
- expect(parsed.message).toContain('C123');
55
+ expect(result.success).toBe(true);
56
+ expect(result.case_id).toBe(123);
57
+ expect(result.message).toContain('C123');
61
58
  expect(mockClient.updateCase).toHaveBeenCalledWith('123', { title: 'Updated Title' });
62
59
  });
63
60
 
@@ -92,17 +89,14 @@ describe('update_case tool', () => {
92
89
  expect(mockClient.updateCase).toHaveBeenCalledWith('123', fields);
93
90
  });
94
91
 
95
- test('handler returns error on failure', async () => {
92
+ test('handler throws error on failure', async () => {
96
93
  updateCaseMock.mockRejectedValue(new Error('API Error'));
97
94
 
98
- const result = await updateCaseTool.handler(
99
- { case_id: '123', fields: { title: 'Test' } },
100
- mockClient
101
- );
102
-
103
- expect(result).toEqual({
104
- content: [{ type: 'text', text: 'Error: API Error' }],
105
- isError: true
106
- });
95
+ await expect(
96
+ updateCaseTool.handler(
97
+ { case_id: '123', fields: { title: 'Test' } },
98
+ mockClient
99
+ )
100
+ ).rejects.toThrow('API Error');
107
101
  });
108
102
  });
@@ -10,21 +10,21 @@ describe('update_cases tool', () => {
10
10
  const mockUpdatedCases: Case[] = [
11
11
  {
12
12
  id: 1, title: 'Test Case 1', section_id: 1, template_id: 1, type_id: 1,
13
- priority_id: 3, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
14
- updated_by: 1, updated_on: 1700000001, estimate: null, estimate_forecast: null,
15
- suite_id: 1, display_order: 1, is_deleted: 0, labels: []
13
+ priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
14
+ updated_by: 1, updated_on: 1700000001, estimate: null,
15
+ suite_id: 1, labels: []
16
16
  },
17
17
  {
18
18
  id: 2, title: 'Test Case 2', section_id: 1, template_id: 1, type_id: 1,
19
- priority_id: 3, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
20
- updated_by: 1, updated_on: 1700000001, estimate: null, estimate_forecast: null,
21
- suite_id: 1, display_order: 2, is_deleted: 0, labels: []
19
+ priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
20
+ updated_by: 1, updated_on: 1700000001, estimate: null,
21
+ suite_id: 1, labels: []
22
22
  },
23
23
  {
24
24
  id: 3, title: 'Test Case 3', section_id: 1, template_id: 1, type_id: 1,
25
- priority_id: 3, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
26
- updated_by: 1, updated_on: 1700000001, estimate: null, estimate_forecast: null,
27
- suite_id: 1, display_order: 3, is_deleted: 0, labels: []
25
+ priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
26
+ updated_by: 1, updated_on: 1700000001, estimate: null,
27
+ suite_id: 1, labels: []
28
28
  },
29
29
  ];
30
30
 
@@ -52,37 +52,10 @@ describe('update_cases tool', () => {
52
52
  );
53
53
 
54
54
  expect(result).toBeDefined();
55
- expect(result.content[0].type).toBe('text');
56
-
57
- const parsed = JSON.parse(result.content[0].text);
58
- expect(parsed.success).toBe(true);
59
- expect(parsed.updated_count).toBe(3);
60
- expect(parsed.case_ids).toEqual([1, 2, 3]);
61
- expect(parsed.message).toContain('3 test cases');
62
- });
63
-
64
- test('passes case_ids and fields correctly', async () => {
65
- const fields = { priority_id: 2, type_id: 1 };
66
-
67
- await updateCasesTool.handler(
68
- { case_ids: [10, 20, 30], fields },
69
- mockClient
70
- );
71
-
72
- expect(mockClient.updateCases).toHaveBeenCalledWith([10, 20, 30], fields);
73
- });
74
-
75
- test('handler returns error on failure', async () => {
76
- updateCasesMock.mockRejectedValue(new Error('API Error'));
77
-
78
- const result = await updateCasesTool.handler(
79
- { case_ids: [1, 2], fields: { priority_id: 2 } },
80
- mockClient
81
- );
82
-
83
- expect(result).toEqual({
84
- content: [{ type: 'text', text: 'Error: API Error' }],
85
- isError: true
86
- });
55
+ expect(result.success).toBe(true);
56
+ expect(result.updated_count).toBe(3);
57
+ expect(result.case_ids).toEqual([1, 2, 3]);
58
+ expect(result.message).toContain('3 test cases');
59
+ expect(mockClient.updateCases).toHaveBeenCalledWith([1, 2, 3], { priority_id: 3 });
87
60
  });
88
61
  });
@@ -6,13 +6,13 @@ describe('processCustomFields', () => {
6
6
 
7
7
  // Mock CaseField array simulating TestRail API response
8
8
  const mockCaseFields: CaseField[] = [
9
- { id: 1, name: 'is_automated', system_name: 'custom_is_automated', label: 'Is Automated', type_id: 1, template_ids: [], include_all: true, is_active: true, display_order: 1, description: null, configs: [] },
10
- { id: 2, name: 'refs', system_name: 'custom_refs', label: 'Defects', type_id: 1, template_ids: [], include_all: true, is_active: true, display_order: 2, description: null, configs: [] },
11
- { id: 3, name: 'case_automation_priority', system_name: 'custom_case_automation_priority', label: 'Automation Priority', type_id: 6, template_ids: [], include_all: true, is_active: true, display_order: 3, description: null, configs: [{ context: { is_global: true, project_ids: null }, options: { items: '2, P0\n3, P1\n4, P2\n5, P3\n6, P4' } }] },
12
- { id: 4, name: 'preconds', system_name: 'custom_preconds', label: 'Preconditions', type_id: 1, template_ids: [], include_all: true, is_active: true, display_order: 4, description: null, configs: [] },
13
- { id: 5, name: 'steps', system_name: 'custom_steps', label: 'Steps', type_id: 1, template_ids: [1], include_all: false, is_active: true, display_order: 5, description: null, configs: [] },
14
- { id: 6, name: 'expected', system_name: 'custom_expected', label: 'Expected', type_id: 1, template_ids: [1], include_all: false, is_active: true, display_order: 6, description: null, configs: [] },
15
- { id: 7, name: 'steps_separated', system_name: 'custom_steps_separated', label: 'Steps Separated', type_id: 1, template_ids: [2], include_all: false, is_active: true, display_order: 7, description: null, configs: [] },
9
+ { id: 1, name: 'is_automated', system_name: 'custom_is_automated', label: 'Is Automated', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
10
+ { id: 2, name: 'refs', system_name: 'custom_refs', label: 'Defects', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
11
+ { id: 3, name: 'case_automation_priority', system_name: 'custom_case_automation_priority', label: 'Automation Priority', type_id: 6, template_ids: [], include_all: true, is_active: true, description: null, configs: [{ options: { items: '2, P0\n3, P1\n4, P2\n5, P3\n6, P4' } }] },
12
+ { id: 4, name: 'preconds', system_name: 'custom_preconds', label: 'Preconditions', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
13
+ { id: 5, name: 'steps', system_name: 'custom_steps', label: 'Steps', type_id: 1, template_ids: [1], include_all: false, is_active: true, description: null, configs: [] },
14
+ { id: 6, name: 'expected', system_name: 'custom_expected', label: 'Expected', type_id: 1, template_ids: [1], include_all: false, is_active: true, description: null, configs: [] },
15
+ { id: 7, name: 'steps_separated', system_name: 'custom_steps_separated', label: 'Steps Separated', type_id: 1, template_ids: [2], include_all: false, is_active: true, description: null, configs: [] },
16
16
  ];
17
17
 
18
18
  test('maps custom fields using field metadata', () => {
@@ -21,7 +21,7 @@ describe('processCustomFields', () => {
21
21
  section_id: 0, type_id: 0, priority_id: 0,
22
22
  custom_is_automated: true,
23
23
  custom_refs: 'DEF-123',
24
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
24
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
25
25
  is_deleted: 0, refs: null, labels: [],
26
26
  } as unknown as Case;
27
27
 
@@ -35,7 +35,7 @@ describe('processCustomFields', () => {
35
35
  id: 1, title: 'Foo', template_id: 1,
36
36
  section_id: 0, type_id: 0, priority_id: 0,
37
37
  custom_case_automation_priority: 2,
38
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
38
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
39
39
  is_deleted: 0, refs: null, labels: [],
40
40
  } as unknown as Case;
41
41
 
@@ -50,7 +50,7 @@ describe('processCustomFields', () => {
50
50
  custom_expected: 'Expectation...',
51
51
  custom_steps_separated: [{ content: 'Should not appear' }], // Template 2 field
52
52
  section_id: 0, type_id: 0, priority_id: 0,
53
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
53
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
54
54
  is_deleted: 0, refs: null, labels: [],
55
55
  } as unknown as Case;
56
56
 
@@ -66,7 +66,7 @@ describe('processCustomFields', () => {
66
66
  custom_steps: 'Should not appear', // Template 1 field
67
67
  custom_steps_separated: [{ content: 'Step 1' }],
68
68
  section_id: 0, type_id: 0, priority_id: 0,
69
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
69
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
70
70
  is_deleted: 0, refs: null, labels: [],
71
71
  } as unknown as Case;
72
72
 
@@ -80,7 +80,7 @@ describe('processCustomFields', () => {
80
80
  id: 1, title: 'Foo', template_id: 999,
81
81
  custom_unknown_field: 'some value',
82
82
  section_id: 0, type_id: 0, priority_id: 0,
83
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
83
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
84
84
  is_deleted: 0, refs: null, labels: [],
85
85
  } as unknown as Case;
86
86
 
@@ -101,7 +101,7 @@ describe('processCustomFields', () => {
101
101
  id: 1, title: 'Foo', template_id: 1,
102
102
  section_id: 0, type_id: 0, priority_id: 0,
103
103
  custom_case_automation_priority: 999, // Unknown option ID
104
- display_order: 1, suite_id: 1, created_by: 1, created_on: 1, updated_by: 1, updated_on: 1,
104
+ display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
105
105
  is_deleted: 0, refs: null, labels: [],
106
106
  } as unknown as Case;
107
107
 
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from '@jest/globals';
2
- import { sanitizeValue } from '../../src/utils/sanitizer.js';
2
+ import { sanitizeValue, removeNullish } from '../../src/utils/sanitizer.js';
3
3
 
4
4
  describe('sanitizeValue', () => {
5
5
  test('strips style attributes from string values', () => {
@@ -57,3 +57,87 @@ describe('sanitizeValue', () => {
57
57
  expect(sanitizeValue(null)).toBe(null);
58
58
  });
59
59
  });
60
+
61
+ describe('removeNullish', () => {
62
+ test('removes null values from object', () => {
63
+ const input = { a: 1, b: null, c: 3 };
64
+ expect(removeNullish(input)).toEqual({ a: 1, c: 3 });
65
+ });
66
+
67
+ test('removes undefined values from object', () => {
68
+ const input = { a: 1, b: undefined, c: 3 };
69
+ expect(removeNullish(input)).toEqual({ a: 1, c: 3 });
70
+ });
71
+
72
+ test('handles nested objects', () => {
73
+ const input = {
74
+ name: 'test',
75
+ metadata: { id: 1, value: null, active: true },
76
+ count: null
77
+ };
78
+ expect(removeNullish(input)).toEqual({
79
+ name: 'test',
80
+ metadata: { id: 1, active: true }
81
+ });
82
+ });
83
+
84
+ test('filters null items from arrays', () => {
85
+ const input = [1, null, 3, undefined, 5];
86
+ expect(removeNullish(input)).toEqual([1, 3, 5]);
87
+ });
88
+
89
+ test('cleans nested objects in arrays', () => {
90
+ const input = [
91
+ { id: 1, name: 'item1', value: null },
92
+ { id: 2, name: null, value: 'test' }
93
+ ];
94
+ expect(removeNullish(input)).toEqual([
95
+ { id: 1, name: 'item1' },
96
+ { id: 2, value: 'test' }
97
+ ]);
98
+ });
99
+
100
+ test('handles deeply nested structures', () => {
101
+ const input = {
102
+ projects: [
103
+ {
104
+ id: 1,
105
+ name: 'Project A',
106
+ metadata: { created: null, updated: '2024-01-01' },
107
+ tags: [null, 'important', undefined, 'urgent']
108
+ },
109
+ null
110
+ ],
111
+ count: undefined
112
+ };
113
+ expect(removeNullish(input)).toEqual({
114
+ projects: [
115
+ {
116
+ id: 1,
117
+ name: 'Project A',
118
+ metadata: { updated: '2024-01-01' },
119
+ tags: ['important', 'urgent']
120
+ }
121
+ ]
122
+ });
123
+ });
124
+
125
+ test('preserves primitive values', () => {
126
+ expect(removeNullish(42)).toBe(42);
127
+ expect(removeNullish('test')).toBe('test');
128
+ expect(removeNullish(true)).toBe(true);
129
+ expect(removeNullish(0)).toBe(0);
130
+ expect(removeNullish('')).toBe('');
131
+ expect(removeNullish(false)).toBe(false);
132
+ });
133
+
134
+ test('returns null/undefined as-is for root values', () => {
135
+ expect(removeNullish(null)).toBe(null);
136
+ expect(removeNullish(undefined)).toBe(undefined);
137
+ });
138
+
139
+ test('handles empty objects and arrays', () => {
140
+ expect(removeNullish({})).toEqual({});
141
+ expect(removeNullish([])).toEqual([]);
142
+ });
143
+ });