@uarlouski/testrail-mcp-server 1.1.0 → 1.3.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +62 -16
  3. package/package.json +3 -2
  4. package/src/client/testrail.ts +111 -56
  5. package/src/index.ts +32 -2
  6. package/src/tools/add_attachment_to_run.ts +62 -0
  7. package/src/tools/add_results.ts +28 -0
  8. package/src/tools/add_run.ts +24 -0
  9. package/src/tools/create_case.ts +10 -17
  10. package/src/tools/get_case.ts +5 -12
  11. package/src/tools/get_case_fields.ts +4 -11
  12. package/src/tools/get_cases.ts +9 -16
  13. package/src/tools/get_projects.ts +7 -27
  14. package/src/tools/get_sections.ts +7 -27
  15. package/src/tools/get_statuses.ts +18 -0
  16. package/src/tools/get_templates.ts +7 -27
  17. package/src/tools/get_tests.ts +22 -0
  18. package/src/tools/update_case.ts +7 -14
  19. package/src/tools/update_cases.ts +10 -16
  20. package/src/types/custom.ts +1 -16
  21. package/src/types/testrail.ts +140 -87
  22. package/src/utils/sanitizer.ts +29 -0
  23. package/test/client/testrail.test.ts +329 -141
  24. package/test/tools/add_results.test.ts +94 -0
  25. package/test/tools/add_run.test.ts +83 -0
  26. package/test/tools/create_case.test.ts +10 -16
  27. package/test/tools/get_case.test.ts +47 -62
  28. package/test/tools/get_case_fields.test.ts +7 -80
  29. package/test/tools/get_cases.test.ts +19 -108
  30. package/test/tools/get_projects.test.ts +4 -41
  31. package/test/tools/get_sections.test.ts +7 -36
  32. package/test/tools/get_statuses.test.ts +56 -0
  33. package/test/tools/get_templates.test.ts +1 -30
  34. package/test/tools/get_tests.test.ts +47 -0
  35. package/test/tools/update_case.test.ts +10 -16
  36. package/test/tools/update_cases.test.ts +29 -28
  37. package/test/utils/mapper.test.ts +13 -13
  38. package/test/utils/sanitizer.test.ts +85 -1
@@ -10,21 +10,21 @@ describe('get_cases tool', () => {
10
10
  const mockCases: Case[] = [
11
11
  {
12
12
  id: 1, title: 'Login test', section_id: 1, template_id: 1, type_id: 1,
13
- priority_id: 2, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
14
- updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
15
- suite_id: 1, display_order: 1, is_deleted: 0, labels: [], custom_automation_status: 1
13
+ priority_id: 2, milestone_id: null, refs: null, created_on: 1700000000,
14
+ updated_by: 1, updated_on: 1700000000, estimate: null,
15
+ suite_id: 1, labels: [], custom_automation_status: 1
16
16
  },
17
17
  {
18
18
  id: 2, title: 'Logout test', 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: 1700000000, estimate: null, estimate_forecast: null,
21
- suite_id: 1, display_order: 2, is_deleted: 0, labels: [], custom_automation_status: 2
19
+ priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
20
+ updated_by: 1, updated_on: 1700000000, estimate: null,
21
+ suite_id: 1, labels: [], custom_automation_status: 2
22
22
  },
23
23
  {
24
24
  id: 3, title: 'Registration test', section_id: 2, template_id: 1, type_id: 1,
25
- priority_id: 1, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
26
- updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
27
- suite_id: 1, display_order: 3, is_deleted: 0, labels: [], custom_automation_status: 0
25
+ priority_id: 1, milestone_id: null, refs: null, created_on: 1700000000,
26
+ updated_by: 1, updated_on: 1700000000, estimate: null,
27
+ suite_id: 1, labels: [], custom_automation_status: 0
28
28
  },
29
29
  ];
30
30
 
@@ -51,105 +51,19 @@ describe('get_cases tool', () => {
51
51
  const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
52
52
 
53
53
  expect(result).toBeDefined();
54
- expect(result.content[0].type).toBe('text');
55
-
56
- const parsed = JSON.parse(result.content[0].text);
57
- expect(parsed.cases).toHaveLength(3);
54
+ expect(result.cases).toHaveLength(3);
58
55
  expect(mockClient.getCases).toHaveBeenCalledWith('1', undefined, undefined);
59
56
  });
60
57
 
61
- test('passes section_id when provided', async () => {
62
- await getCasesTool.handler({ project_id: '1', section_id: '2' }, mockClient);
63
-
64
- expect(mockClient.getCases).toHaveBeenCalledWith('1', '2', undefined);
65
- });
66
-
67
- test('passes filter when provided', async () => {
68
- const filter = { priority_id: '1,2', type_id: '3' };
69
- await getCasesTool.handler({ project_id: '1', filter }, mockClient);
70
-
71
- expect(mockClient.getCases).toHaveBeenCalledWith('1', undefined, filter);
72
- });
73
-
74
- test('returns correct case structure', async () => {
75
- const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
76
- const parsed = JSON.parse(result.content[0].text);
77
-
78
- expect(parsed.cases[0]).toEqual({
79
- id: 1,
80
- title: 'Login test',
81
- suite_id: 1,
82
- });
83
-
84
- expect(parsed.cases[2]).toEqual({
85
- id: 3,
86
- title: 'Registration test',
87
- suite_id: 1,
88
- });
89
- });
90
-
91
- test('includes additional fields when specified', async () => {
92
- const result = await getCasesTool.handler(
93
- { project_id: '1', fields: ['priority_id', 'type_id', 'custom_automation_status'] },
94
- mockClient
95
- );
96
- const parsed = JSON.parse(result.content[0].text);
97
-
98
- expect(parsed.cases[0]).toEqual({
99
- id: 1,
100
- title: 'Login test',
101
- suite_id: 1,
102
- priority_id: 2,
103
- type_id: 1,
104
- custom_automation_status: 1,
105
- });
106
-
107
- expect(parsed.cases[1]).toEqual({
108
- id: 2,
109
- title: 'Logout test',
110
- suite_id: 1,
111
- priority_id: 3,
112
- type_id: 1,
113
- custom_automation_status: 2,
114
- });
115
- });
116
-
117
- test('ignores non-existent fields', async () => {
118
- const result = await getCasesTool.handler(
119
- { project_id: '1', fields: ['priority_id', 'nonexistent_field'] },
120
- mockClient
121
- );
122
- const parsed = JSON.parse(result.content[0].text);
123
-
124
- expect(parsed.cases[0]).toEqual({
125
- id: 1,
126
- title: 'Login test',
127
- suite_id: 1,
128
- priority_id: 2,
129
- });
130
- });
131
-
132
- test('handler returns error on failure', async () => {
133
- getCasesMock.mockRejectedValue(new Error('API Error'));
134
-
135
- const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
136
-
137
- expect(result).toEqual({
138
- content: [{ type: 'text', text: 'Error: API Error' }],
139
- isError: true
140
- });
141
- });
142
-
143
58
  test('filters cases using where clause with single condition', async () => {
144
59
  const result = await getCasesTool.handler(
145
60
  { project_id: '1', where: { custom_automation_status: 1 } },
146
61
  mockClient
147
62
  );
148
- const parsed = JSON.parse(result.content[0].text);
149
63
 
150
- expect(parsed.cases).toHaveLength(1);
151
- expect(parsed.cases[0].id).toBe(1);
152
- expect(parsed.cases[0].title).toBe('Login test');
64
+ expect(result.cases).toHaveLength(1);
65
+ expect(result.cases[0].id).toBe(1);
66
+ expect(result.cases[0].title).toBe('Login test');
153
67
  });
154
68
 
155
69
  test('filters cases using where clause with multiple conditions', async () => {
@@ -157,11 +71,10 @@ describe('get_cases tool', () => {
157
71
  { project_id: '1', where: { custom_automation_status: 2, priority_id: 3 } },
158
72
  mockClient
159
73
  );
160
- const parsed = JSON.parse(result.content[0].text);
161
74
 
162
- expect(parsed.cases).toHaveLength(1);
163
- expect(parsed.cases[0].id).toBe(2);
164
- expect(parsed.cases[0].title).toBe('Logout test');
75
+ expect(result.cases).toHaveLength(1);
76
+ expect(result.cases[0].id).toBe(2);
77
+ expect(result.cases[0].title).toBe('Logout test');
165
78
  });
166
79
 
167
80
  test('returns empty array when where clause matches no cases', async () => {
@@ -169,9 +82,8 @@ describe('get_cases tool', () => {
169
82
  { project_id: '1', where: { custom_automation_status: 999 } },
170
83
  mockClient
171
84
  );
172
- const parsed = JSON.parse(result.content[0].text);
173
85
 
174
- expect(parsed.cases).toHaveLength(0);
86
+ expect(result.cases).toHaveLength(0);
175
87
  });
176
88
 
177
89
  test('combines where clause with fields parameter', async () => {
@@ -179,10 +91,9 @@ describe('get_cases tool', () => {
179
91
  { project_id: '1', where: { priority_id: 2 }, fields: ['priority_id', 'custom_automation_status'] },
180
92
  mockClient
181
93
  );
182
- const parsed = JSON.parse(result.content[0].text);
183
94
 
184
- expect(parsed.cases).toHaveLength(1);
185
- expect(parsed.cases[0]).toEqual({
95
+ expect(result.cases).toHaveLength(1);
96
+ expect(result.cases[0]).toEqual({
186
97
  id: 1,
187
98
  title: 'Login test',
188
99
  suite_id: 1,
@@ -8,9 +8,9 @@ describe('get_projects tool', () => {
8
8
  let getProjectsMock: jest.Mock<() => Promise<Project[]>>;
9
9
 
10
10
  const mockProjects: Project[] = [
11
- { id: 1, name: 'Project Alpha', announcement: null, is_completed: false, suite_mode: 1 },
12
- { id: 2, name: 'Project Beta', announcement: 'Active project', is_completed: false, suite_mode: 3 },
13
- { id: 3, name: 'Old Project', announcement: null, is_completed: true, suite_mode: 1 },
11
+ { id: 1, name: 'Project Alpha', is_completed: false, suite_mode: 1 },
12
+ { id: 2, name: 'Project Beta', is_completed: false, suite_mode: 3 },
13
+ { id: 3, name: 'Old Project', is_completed: true, suite_mode: 1 },
14
14
  ];
15
15
 
16
16
  beforeEach(() => {
@@ -32,44 +32,7 @@ describe('get_projects tool', () => {
32
32
  const result = await getProjectsTool.handler({}, 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.projects).toHaveLength(2); // Only active projects
35
+ expect(result.projects).toHaveLength(2);
39
36
  expect(mockClient.getProjects).toHaveBeenCalled();
40
37
  });
41
-
42
- test('filters out completed projects', async () => {
43
- const result = await getProjectsTool.handler({}, mockClient);
44
- const parsed = JSON.parse(result.content[0].text);
45
-
46
- const completedProject = parsed.projects.find((p: any) => p.name === 'Old Project');
47
- expect(completedProject).toBeUndefined();
48
- });
49
-
50
- test('returns correct project structure', async () => {
51
- const result = await getProjectsTool.handler({}, mockClient);
52
- const parsed = JSON.parse(result.content[0].text);
53
-
54
- expect(parsed.projects[0]).toEqual({
55
- id: 1,
56
- name: 'Project Alpha',
57
- });
58
-
59
- expect(parsed.projects[1]).toEqual({
60
- id: 2,
61
- name: 'Project Beta',
62
- });
63
- });
64
-
65
- test('handler returns error on failure', async () => {
66
- getProjectsMock.mockRejectedValue(new Error('API Error'));
67
-
68
- const result = await getProjectsTool.handler({}, mockClient);
69
-
70
- expect(result).toEqual({
71
- content: [{ type: 'text', text: 'Error: API Error' }],
72
- isError: true
73
- });
74
- });
75
38
  });
@@ -8,9 +8,9 @@ describe('get_sections tool', () => {
8
8
  let getSectionsMock: jest.Mock<(projectId: string) => Promise<Section[]>>;
9
9
 
10
10
  const mockSections: Section[] = [
11
- { id: 1, name: 'Login', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1 },
12
- { id: 2, name: 'Authentication', description: 'Auth tests', parent_id: 1, depth: 1, display_order: 2, suite_id: 1 },
13
- { id: 3, name: 'Registration', description: null, parent_id: null, depth: 0, display_order: 3, suite_id: 1 },
11
+ { id: 1, name: 'Login', description: null, parent_id: null, suite_id: 1 },
12
+ { id: 2, name: 'Authentication', description: 'Auth tests', parent_id: 1, suite_id: 1 },
13
+ { id: 3, name: 'Registration', description: null, parent_id: null, suite_id: 1 },
14
14
  ];
15
15
 
16
16
  beforeEach(() => {
@@ -29,40 +29,11 @@ describe('get_sections tool', () => {
29
29
  expect(getSectionsTool.parameters.project_id).toBeDefined();
30
30
  });
31
31
 
32
- test('handler fetches and returns sections', async () => {
33
- const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
34
-
35
- expect(result).toBeDefined();
36
- expect(result.content[0].type).toBe('text');
37
-
38
- const parsed = JSON.parse(result.content[0].text);
39
- expect(parsed.sections).toHaveLength(3);
40
- expect(mockClient.getSections).toHaveBeenCalledWith('1');
41
- });
42
-
43
- test('returns correct section structure', async () => {
44
- const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
45
- const parsed = JSON.parse(result.content[0].text);
46
-
47
- expect(parsed.sections[0]).toEqual({
48
- id: 1,
49
- name: 'Login',
50
- });
51
-
52
- expect(parsed.sections[1]).toEqual({
53
- id: 2,
54
- name: 'Authentication',
55
- });
56
- });
57
-
58
- test('handler returns error on failure', async () => {
32
+ test('handler throws error on failure', async () => {
59
33
  getSectionsMock.mockRejectedValue(new Error('API Error'));
60
34
 
61
- const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
62
-
63
- expect(result).toEqual({
64
- content: [{ type: 'text', text: 'Error: API Error' }],
65
- isError: true
66
- });
35
+ await expect(
36
+ getSectionsTool.handler({ project_id: '1' }, mockClient)
37
+ ).rejects.toThrow('API Error');
67
38
  });
68
39
  });
@@ -0,0 +1,56 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { getStatusesTool } from '../../src/tools/get_statuses.js';
3
+ import { TestRailClient } from '../../src/client/testrail.js';
4
+ import { Status } from '../../src/types/testrail.js';
5
+
6
+ describe('get_statuses tool', () => {
7
+ let mockClient: jest.Mocked<TestRailClient>;
8
+ let getStatusesMock: jest.Mock<() => Promise<Status[]>>;
9
+
10
+ const mockStatuses: Status[] = [
11
+ { id: 1, name: 'passed', label: 'Passed' },
12
+ { id: 2, name: 'blocked', label: 'Blocked' },
13
+ { id: 3, name: 'untested', label: 'Untested' },
14
+ { id: 4, name: 'retest', label: 'Retest' },
15
+ { id: 5, name: 'failed', label: 'Failed' },
16
+ ];
17
+
18
+ beforeEach(() => {
19
+ getStatusesMock = jest.fn<() => Promise<Status[]>>().mockResolvedValue(mockStatuses);
20
+
21
+ mockClient = {
22
+ getStatuses: getStatusesMock
23
+ } as unknown as jest.Mocked<TestRailClient>;
24
+ });
25
+
26
+ test('exports correct tool definition', () => {
27
+ expect(getStatusesTool.name).toBe('get_statuses');
28
+ expect(getStatusesTool.description).toContain('statuses');
29
+ expect(getStatusesTool.parameters).toBeDefined();
30
+ });
31
+
32
+ test('handler fetches and returns statuses', async () => {
33
+ const result = await getStatusesTool.handler({}, mockClient);
34
+
35
+ expect(result).toBeDefined();
36
+ expect(result.statuses).toBeDefined();
37
+ expect(result.statuses).toHaveLength(5);
38
+ expect(mockClient.getStatuses).toHaveBeenCalled();
39
+ });
40
+
41
+ test('returns correct status structure', async () => {
42
+ const result = await getStatusesTool.handler({}, mockClient);
43
+
44
+ expect(result.statuses[0]).toEqual({
45
+ id: 1,
46
+ name: 'passed',
47
+ label: 'Passed'
48
+ });
49
+ });
50
+
51
+ test('handler returns error on failure', async () => {
52
+ getStatusesMock.mockRejectedValue(new Error('API Error'));
53
+
54
+ await expect(getStatusesTool.handler({}, mockClient)).rejects.toThrow('API Error');
55
+ });
56
+ });
@@ -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
  });
@@ -0,0 +1,47 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { getTestsTool } from '../../src/tools/get_tests.js';
3
+ import { TestRailClient } from '../../src/client/testrail.js';
4
+ import { Test } from '../../src/types/testrail.js';
5
+
6
+ describe('get_tests tool', () => {
7
+ let mockClient: jest.Mocked<TestRailClient>;
8
+ let getTestsMock: jest.Mock<(runId: number, statusId?: number[]) => Promise<Test[]>>;
9
+
10
+ const mockTests: Test[] = [
11
+ { id: 1, case_id: 101, status_id: 1, title: 'Test 1', run_id: 1 },
12
+ { id: 2, case_id: 102, status_id: 2, title: 'Test 2', run_id: 1 },
13
+ ];
14
+
15
+ beforeEach(() => {
16
+ getTestsMock = jest.fn<(runId: number, statusId?: number[]) => Promise<Test[]>>().mockResolvedValue(mockTests);
17
+
18
+ mockClient = {
19
+ getTests: getTestsMock
20
+ } as unknown as jest.Mocked<TestRailClient>;
21
+ });
22
+
23
+ test('exports correct tool definition', () => {
24
+ expect(getTestsTool.name).toBe('get_tests');
25
+ expect(getTestsTool.description).toBeDefined();
26
+ expect(getTestsTool.parameters).toBeDefined();
27
+ });
28
+
29
+ test('handler fetches and returns tests', async () => {
30
+ const result = await getTestsTool.handler({ run_id: 1 }, mockClient);
31
+
32
+ expect(result).toBeDefined();
33
+ expect(result.tests).toBeDefined();
34
+ expect(result.tests).toHaveLength(2);
35
+ expect(mockClient.getTests).toHaveBeenCalledWith(1, undefined);
36
+ });
37
+
38
+ test('handler passes status_id filter', async () => {
39
+ const result = await getTestsTool.handler({ run_id: 1, status_id: [1, 5] }, mockClient);
40
+ expect(mockClient.getTests).toHaveBeenCalledWith(1, [1, 5]);
41
+ });
42
+
43
+ test('handler returns error on failure', async () => {
44
+ getTestsMock.mockRejectedValue(new Error('API Error'));
45
+ await expect(getTestsTool.handler({ run_id: 1 }, mockClient)).rejects.toThrow('API Error');
46
+ });
47
+ });
@@ -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
 
@@ -33,8 +33,12 @@ describe('update_cases tool', () => {
33
33
  .mockResolvedValue(mockUpdatedCases);
34
34
 
35
35
  mockClient = {
36
- updateCases: updateCasesMock
36
+ updateCases: updateCasesMock,
37
+ getCase: jest.fn(),
37
38
  } as unknown as jest.Mocked<TestRailClient>;
39
+
40
+ // Default mock for getCase to avoid failures in unrelated tests
41
+ (mockClient.getCase as jest.Mock<any>).mockResolvedValue({ suite_id: 1 });
38
42
  });
39
43
 
40
44
  test('exports correct tool definition', () => {
@@ -52,37 +56,34 @@ describe('update_cases tool', () => {
52
56
  );
53
57
 
54
58
  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');
59
+ expect(result.success).toBe(true);
60
+ expect(result.updated_count).toBe(3);
61
+ expect(result.case_ids).toEqual([1, 2, 3]);
62
+ expect(result.message).toContain('3 test cases');
62
63
  });
63
64
 
64
65
  test('passes case_ids and fields correctly', async () => {
65
66
  const fields = { priority_id: 2, type_id: 1 };
67
+ const mockCase = { suite_id: 5 } as Case;
68
+ (mockClient.getCase as jest.Mock<any>).mockResolvedValue(mockCase);
66
69
 
67
70
  await updateCasesTool.handler(
68
71
  { case_ids: [10, 20, 30], fields },
69
72
  mockClient
70
73
  );
71
74
 
72
- expect(mockClient.updateCases).toHaveBeenCalledWith([10, 20, 30], fields);
75
+ expect(mockClient.getCase).toHaveBeenCalledWith('10');
76
+ expect(mockClient.updateCases).toHaveBeenCalledWith(5, [10, 20, 30], fields);
73
77
  });
74
78
 
75
- test('handler returns error on failure', async () => {
79
+ test('handler throws error on failure', async () => {
76
80
  updateCasesMock.mockRejectedValue(new Error('API Error'));
77
81
 
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
- });
82
+ await expect(
83
+ updateCasesTool.handler(
84
+ { case_ids: [1, 2], fields: { priority_id: 2 } },
85
+ mockClient
86
+ )
87
+ ).rejects.toThrow('API Error');
87
88
  });
88
89
  });
@@ -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