@uarlouski/testrail-mcp-server 1.1.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.
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +46 -0
- package/.github/workflows/publish.yml +35 -0
- package/CHANGELOG.md +20 -0
- package/LICENSE +201 -0
- package/README.md +41 -0
- package/jest.config.js +24 -0
- package/package.json +50 -0
- package/src/client/testrail.ts +172 -0
- package/src/index.ts +55 -0
- package/src/tools/create_case.ts +36 -0
- package/src/tools/get_case.ts +51 -0
- package/src/tools/get_case_fields.ts +97 -0
- package/src/tools/get_cases.ts +67 -0
- package/src/tools/get_projects.ts +40 -0
- package/src/tools/get_sections.ts +41 -0
- package/src/tools/get_templates.ts +41 -0
- package/src/tools/update_case.ts +32 -0
- package/src/tools/update_cases.ts +31 -0
- package/src/types/custom.ts +34 -0
- package/src/types/testrail.ts +87 -0
- package/src/utils/mapper.ts +90 -0
- package/src/utils/sanitizer.ts +21 -0
- package/test/client/testrail.test.ts +357 -0
- package/test/tools/create_case.test.ts +107 -0
- package/test/tools/get_case.test.ts +200 -0
- package/test/tools/get_case_fields.test.ts +205 -0
- package/test/tools/get_cases.test.ts +193 -0
- package/test/tools/get_projects.test.ts +75 -0
- package/test/tools/get_sections.test.ts +68 -0
- package/test/tools/get_templates.test.ts +67 -0
- package/test/tools/update_case.test.ts +108 -0
- package/test/tools/update_cases.test.ts +88 -0
- package/test/utils/mapper.test.ts +112 -0
- package/test/utils/sanitizer.test.ts +59 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
3
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
4
|
+
|
|
5
|
+
describe('TestRailClient', () => {
|
|
6
|
+
let client: TestRailClient;
|
|
7
|
+
let mockAxiosInstance: any;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Create a simple object mock for AxiosInstance to avoid complex mocking types
|
|
11
|
+
mockAxiosInstance = {
|
|
12
|
+
get: jest.fn(),
|
|
13
|
+
post: jest.fn(),
|
|
14
|
+
defaults: { headers: { common: {} } },
|
|
15
|
+
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mock axios.create to return our mock instance - using type casting to bypass TS warnings
|
|
19
|
+
jest.spyOn(axios, 'create').mockReturnValue(mockAxiosInstance);
|
|
20
|
+
|
|
21
|
+
// Pass the mock instance directly to constructor
|
|
22
|
+
client = new TestRailClient('https://testrail.io/', 'user', 'apikey');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('constructor cleans trailing slash from baseUrl', async () => {
|
|
26
|
+
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
27
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
28
|
+
|
|
29
|
+
await client.getCase('1');
|
|
30
|
+
|
|
31
|
+
// Verify axios.create was called with cleaned URL (no trailing slash)
|
|
32
|
+
expect(axios.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
33
|
+
baseURL: 'https://testrail.io'
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('validateStatus returns true for 2xx status codes', () => {
|
|
38
|
+
// Get the config passed to axios.create
|
|
39
|
+
const createCall = (axios.create as jest.Mock).mock.calls[0][0] as { validateStatus: (status: number) => boolean };
|
|
40
|
+
const validateStatus = createCall.validateStatus;
|
|
41
|
+
|
|
42
|
+
expect(validateStatus(200)).toBe(true);
|
|
43
|
+
expect(validateStatus(201)).toBe(true);
|
|
44
|
+
expect(validateStatus(299)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('validateStatus returns false for non-2xx status codes', () => {
|
|
48
|
+
// Get the config passed to axios.create
|
|
49
|
+
const createCall = (axios.create as jest.Mock).mock.calls[0][0] as { validateStatus: (status: number) => boolean };
|
|
50
|
+
const validateStatus = createCall.validateStatus;
|
|
51
|
+
|
|
52
|
+
expect(validateStatus(199)).toBe(false);
|
|
53
|
+
expect(validateStatus(300)).toBe(false);
|
|
54
|
+
expect(validateStatus(400)).toBe(false);
|
|
55
|
+
expect(validateStatus(500)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('getCase returns data on success', async () => {
|
|
59
|
+
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
60
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
61
|
+
|
|
62
|
+
const result = await client.getCase('1');
|
|
63
|
+
expect(result).toEqual(mockData);
|
|
64
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_case/1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('getCase throws formatted error on API error', async () => {
|
|
68
|
+
const mockError = {
|
|
69
|
+
message: 'Request failed',
|
|
70
|
+
response: {
|
|
71
|
+
status: 404,
|
|
72
|
+
statusText: 'Not Found',
|
|
73
|
+
data: { error: 'Case not found' }
|
|
74
|
+
},
|
|
75
|
+
isAxiosError: true,
|
|
76
|
+
toJSON: () => ({})
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
mockAxiosInstance.get.mockRejectedValue(mockError);
|
|
80
|
+
|
|
81
|
+
await expect(client.getCase('999')).rejects.toThrow(
|
|
82
|
+
'TestRail API error: 404 Not Found - {"error":"Case not found"}'
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('getCases returns cases without pagination', async () => {
|
|
87
|
+
const mockCases = [{ id: 1, title: 'Case 1' }, { id: 2, title: 'Case 2' }];
|
|
88
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
89
|
+
data: { cases: mockCases, _links: { next: null } }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await client.getCases('1');
|
|
93
|
+
expect(result).toEqual(mockCases);
|
|
94
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_cases/1');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('getCases handles response with undefined _links', async () => {
|
|
98
|
+
const mockCases = [{ id: 1, title: 'Case 1' }];
|
|
99
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
100
|
+
data: { cases: mockCases }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await client.getCases('1');
|
|
104
|
+
expect(result).toEqual(mockCases);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('getCases handles pagination', async () => {
|
|
108
|
+
const page1Cases = [{ id: 1, title: 'Case 1' }];
|
|
109
|
+
const page2Cases = [{ id: 2, title: 'Case 2' }];
|
|
110
|
+
|
|
111
|
+
mockAxiosInstance.get
|
|
112
|
+
.mockResolvedValueOnce({
|
|
113
|
+
data: { cases: page1Cases, _links: { next: '/api/v2/get_cases/1&offset=1' } }
|
|
114
|
+
})
|
|
115
|
+
.mockResolvedValueOnce({
|
|
116
|
+
data: { cases: page2Cases, _links: { next: null } }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await client.getCases('1');
|
|
120
|
+
expect(result).toEqual([...page1Cases, ...page2Cases]);
|
|
121
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('getCases with sectionId adds section filter', async () => {
|
|
125
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
126
|
+
data: { cases: [], _links: { next: null } }
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await client.getCases('1', '5');
|
|
130
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_cases/1§ion_id=5');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('getCases with filter adds query params', async () => {
|
|
134
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
135
|
+
data: { cases: [], _links: { next: null } }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await client.getCases('1', undefined, { type_id: '1', priority_id: '2' });
|
|
139
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith(
|
|
140
|
+
'/index.php?/api/v2/get_cases/1&type_id=1&priority_id=2'
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('getSection returns data on success', async () => {
|
|
145
|
+
const mockData = {
|
|
146
|
+
id: 1,
|
|
147
|
+
name: 'Section 1',
|
|
148
|
+
description: null,
|
|
149
|
+
parent_id: null,
|
|
150
|
+
depth: 0,
|
|
151
|
+
display_order: 1,
|
|
152
|
+
suite_id: 1
|
|
153
|
+
};
|
|
154
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
155
|
+
|
|
156
|
+
const result = await client.getSection('1');
|
|
157
|
+
expect(result).toEqual(mockData);
|
|
158
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_section/1');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('getPriorities returns data on success', async () => {
|
|
162
|
+
const mockData = [{
|
|
163
|
+
id: 1,
|
|
164
|
+
is_default: true,
|
|
165
|
+
name: 'Low',
|
|
166
|
+
priority: 1,
|
|
167
|
+
short_name: 'Low'
|
|
168
|
+
}];
|
|
169
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
170
|
+
|
|
171
|
+
const result = await client.getPriorities();
|
|
172
|
+
expect(result).toEqual(mockData);
|
|
173
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_priorities');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('getPriorities caches result', async () => {
|
|
177
|
+
const mockData = [{ id: 1, name: 'Low' }];
|
|
178
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
179
|
+
|
|
180
|
+
await client.getPriorities();
|
|
181
|
+
await client.getPriorities();
|
|
182
|
+
|
|
183
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('getCaseTypes returns data on success', async () => {
|
|
187
|
+
const mockData = [{
|
|
188
|
+
id: 1,
|
|
189
|
+
is_default: true,
|
|
190
|
+
name: 'Automated'
|
|
191
|
+
}];
|
|
192
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
193
|
+
|
|
194
|
+
const result = await client.getCaseTypes();
|
|
195
|
+
expect(result).toEqual(mockData);
|
|
196
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_case_types');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('getCaseFields returns data on success', async () => {
|
|
200
|
+
const mockData = [{
|
|
201
|
+
id: 1,
|
|
202
|
+
name: 'custom_field',
|
|
203
|
+
system_name: 'custom_field',
|
|
204
|
+
label: 'Custom Field',
|
|
205
|
+
type_id: 1
|
|
206
|
+
}];
|
|
207
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
208
|
+
|
|
209
|
+
const result = await client.getCaseFields();
|
|
210
|
+
expect(result).toEqual(mockData);
|
|
211
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_case_fields');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('getCaseFields caches result', async () => {
|
|
215
|
+
const mockData = [{ id: 1, name: 'field' }];
|
|
216
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
217
|
+
|
|
218
|
+
await client.getCaseFields();
|
|
219
|
+
await client.getCaseFields();
|
|
220
|
+
|
|
221
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('getTemplates returns data on success', async () => {
|
|
225
|
+
const mockData = [{ id: 1, name: 'Test Template', is_default: true }];
|
|
226
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockData });
|
|
227
|
+
|
|
228
|
+
const result = await client.getTemplates('1');
|
|
229
|
+
expect(result).toEqual(mockData);
|
|
230
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_templates/1');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('getTemplates caches result per project', async () => {
|
|
234
|
+
const mockData1 = [{ id: 1, name: 'Template 1' }];
|
|
235
|
+
const mockData2 = [{ id: 2, name: 'Template 2' }];
|
|
236
|
+
mockAxiosInstance.get
|
|
237
|
+
.mockResolvedValueOnce({ data: mockData1 })
|
|
238
|
+
.mockResolvedValueOnce({ data: mockData2 });
|
|
239
|
+
|
|
240
|
+
await client.getTemplates('1');
|
|
241
|
+
await client.getTemplates('1');
|
|
242
|
+
await client.getTemplates('2');
|
|
243
|
+
|
|
244
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('updateCase sends POST request', async () => {
|
|
248
|
+
const mockData = { id: 1, title: 'Updated Case' };
|
|
249
|
+
mockAxiosInstance.post.mockResolvedValue({ data: mockData });
|
|
250
|
+
|
|
251
|
+
const result = await client.updateCase('1', { title: 'Updated Case' });
|
|
252
|
+
expect(result).toEqual(mockData);
|
|
253
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
|
254
|
+
'/index.php?/api/v2/update_case/1',
|
|
255
|
+
{ title: 'Updated Case' }
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('updateCases gets suite_id from first case and updates all', async () => {
|
|
260
|
+
const mockCase = { id: 1, suite_id: 10 };
|
|
261
|
+
const mockUpdated = [{ id: 1, title: 'Updated' }, { id: 2, title: 'Updated' }];
|
|
262
|
+
|
|
263
|
+
mockAxiosInstance.get.mockResolvedValue({ data: mockCase });
|
|
264
|
+
mockAxiosInstance.post.mockResolvedValue({ data: { updated_cases: mockUpdated } });
|
|
265
|
+
|
|
266
|
+
const result = await client.updateCases([1, 2], { title: 'Updated' });
|
|
267
|
+
expect(result).toEqual(mockUpdated);
|
|
268
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_case/1');
|
|
269
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
|
270
|
+
'/index.php?/api/v2/update_cases/10',
|
|
271
|
+
{ case_ids: [1, 2], title: 'Updated' }
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('createCase sends POST request', async () => {
|
|
276
|
+
const mockData = { id: 1, title: 'New Case' };
|
|
277
|
+
mockAxiosInstance.post.mockResolvedValue({ data: mockData });
|
|
278
|
+
|
|
279
|
+
const result = await client.createCase('5', { title: 'New Case' });
|
|
280
|
+
expect(result).toEqual(mockData);
|
|
281
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
|
282
|
+
'/index.php?/api/v2/add_case/5',
|
|
283
|
+
{ title: 'New Case' }
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('getSections returns sections array', async () => {
|
|
288
|
+
const mockSections = [{ id: 1, name: 'Section 1' }, { id: 2, name: 'Section 2' }];
|
|
289
|
+
mockAxiosInstance.get.mockResolvedValue({ data: { sections: mockSections } });
|
|
290
|
+
|
|
291
|
+
const result = await client.getSections('1');
|
|
292
|
+
expect(result).toEqual(mockSections);
|
|
293
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_sections/1');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('getProjects returns projects array', async () => {
|
|
297
|
+
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
298
|
+
mockAxiosInstance.get.mockResolvedValue({ data: { projects: mockProjects } });
|
|
299
|
+
|
|
300
|
+
const result = await client.getProjects();
|
|
301
|
+
expect(result).toEqual(mockProjects);
|
|
302
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/index.php?/api/v2/get_projects');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('getProjects caches result', async () => {
|
|
306
|
+
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
307
|
+
mockAxiosInstance.get.mockResolvedValue({ data: { projects: mockProjects } });
|
|
308
|
+
|
|
309
|
+
await client.getProjects();
|
|
310
|
+
await client.getProjects();
|
|
311
|
+
|
|
312
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('postRequest throws formatted error on API error', async () => {
|
|
316
|
+
const mockError = {
|
|
317
|
+
message: 'Request failed',
|
|
318
|
+
response: {
|
|
319
|
+
status: 400,
|
|
320
|
+
statusText: 'Bad Request',
|
|
321
|
+
data: { error: 'Invalid field' }
|
|
322
|
+
},
|
|
323
|
+
isAxiosError: true,
|
|
324
|
+
toJSON: () => ({})
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
mockAxiosInstance.post.mockRejectedValue(mockError);
|
|
328
|
+
|
|
329
|
+
await expect(client.updateCase('1', {})).rejects.toThrow(
|
|
330
|
+
'TestRail API error: 400 Bad Request - {"error":"Invalid field"}'
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('handleError formats error when no response received', async () => {
|
|
335
|
+
const mockError = {
|
|
336
|
+
message: 'Network Error',
|
|
337
|
+
request: {},
|
|
338
|
+
isAxiosError: true,
|
|
339
|
+
toJSON: () => ({})
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
mockAxiosInstance.get.mockRejectedValue(mockError);
|
|
343
|
+
|
|
344
|
+
await expect(client.getCase('1')).rejects.toThrow(
|
|
345
|
+
'TestRail API error: No response received. Network Error'
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('handleError returns generic message for non-axios errors', async () => {
|
|
350
|
+
const mockError = new Error('Unknown error');
|
|
351
|
+
mockAxiosInstance.get.mockRejectedValue(mockError);
|
|
352
|
+
|
|
353
|
+
await expect(client.getCase('1')).rejects.toThrow(
|
|
354
|
+
'TestRail API error: Unknown error'
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createCaseTool } from '../../src/tools/create_case.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Case } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('create_case tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let createCaseMock: jest.Mock<(sectionId: string, fields: Record<string, any>) => Promise<Case>>;
|
|
9
|
+
|
|
10
|
+
const mockCreatedCase: Case = {
|
|
11
|
+
id: 456,
|
|
12
|
+
title: 'New Test Case',
|
|
13
|
+
section_id: 1,
|
|
14
|
+
template_id: 1,
|
|
15
|
+
type_id: 1,
|
|
16
|
+
priority_id: 2,
|
|
17
|
+
milestone_id: null,
|
|
18
|
+
refs: null,
|
|
19
|
+
created_by: 1,
|
|
20
|
+
created_on: 1700000000,
|
|
21
|
+
updated_by: 1,
|
|
22
|
+
updated_on: 1700000000,
|
|
23
|
+
estimate: null,
|
|
24
|
+
estimate_forecast: null,
|
|
25
|
+
suite_id: 1,
|
|
26
|
+
display_order: 1,
|
|
27
|
+
is_deleted: 0,
|
|
28
|
+
labels: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
createCaseMock = jest.fn<(sectionId: string, fields: Record<string, any>) => Promise<Case>>()
|
|
33
|
+
.mockResolvedValue(mockCreatedCase);
|
|
34
|
+
|
|
35
|
+
mockClient = {
|
|
36
|
+
createCase: createCaseMock
|
|
37
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('exports correct tool definition', () => {
|
|
41
|
+
expect(createCaseTool.name).toBe('create_case');
|
|
42
|
+
expect(createCaseTool.description).toContain('Create');
|
|
43
|
+
expect(createCaseTool.parameters).toBeDefined();
|
|
44
|
+
expect(createCaseTool.parameters.section_id).toBeDefined();
|
|
45
|
+
expect(createCaseTool.parameters.title).toBeDefined();
|
|
46
|
+
expect(createCaseTool.parameters.fields).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('handler creates case and returns success response', async () => {
|
|
50
|
+
const result = await createCaseTool.handler(
|
|
51
|
+
{ section_id: '1', title: 'New Test Case' },
|
|
52
|
+
mockClient
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result).toBeDefined();
|
|
56
|
+
expect(result.content[0].type).toBe('text');
|
|
57
|
+
|
|
58
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
59
|
+
expect(parsed.success).toBe(true);
|
|
60
|
+
expect(parsed.case_id).toBe(456);
|
|
61
|
+
expect(parsed.message).toContain('C456');
|
|
62
|
+
expect(mockClient.createCase).toHaveBeenCalledWith('1', { title: 'New Test Case' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('passes title and optional fields correctly', async () => {
|
|
66
|
+
const fields = {
|
|
67
|
+
priority_id: 2,
|
|
68
|
+
template_id: 1,
|
|
69
|
+
custom_automation_priority: 1,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await createCaseTool.handler(
|
|
73
|
+
{ section_id: '5', title: 'My Test', fields },
|
|
74
|
+
mockClient
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(mockClient.createCase).toHaveBeenCalledWith('5', {
|
|
78
|
+
title: 'My Test',
|
|
79
|
+
priority_id: 2,
|
|
80
|
+
template_id: 1,
|
|
81
|
+
custom_automation_priority: 1,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('works without optional fields', async () => {
|
|
86
|
+
await createCaseTool.handler(
|
|
87
|
+
{ section_id: '10', title: 'Simple Test' },
|
|
88
|
+
mockClient
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(mockClient.createCase).toHaveBeenCalledWith('10', { title: 'Simple Test' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('handler returns error on failure', async () => {
|
|
95
|
+
createCaseMock.mockRejectedValue(new Error('API Error'));
|
|
96
|
+
|
|
97
|
+
const result = await createCaseTool.handler(
|
|
98
|
+
{ section_id: '1', title: 'Test' },
|
|
99
|
+
mockClient
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual({
|
|
103
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
104
|
+
isError: true
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getCaseTool } from '../../src/tools/get_case.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Case, Section, CaseType, Priority, CaseField } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_case tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getCaseMock: jest.Mock<(id: string) => Promise<Case>>;
|
|
9
|
+
let getSectionMock: jest.Mock<(id: string) => Promise<Section>>;
|
|
10
|
+
let getCaseTypesMock: jest.Mock<() => Promise<CaseType[]>>;
|
|
11
|
+
let getPrioritiesMock: jest.Mock<() => Promise<Priority[]>>;
|
|
12
|
+
let getCaseFieldsMock: jest.Mock<() => Promise<CaseField[]>>;
|
|
13
|
+
|
|
14
|
+
const mockCaseFields: CaseField[] = [
|
|
15
|
+
{ 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: [] },
|
|
16
|
+
{ 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: [] },
|
|
17
|
+
{ 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' } }] },
|
|
18
|
+
{ 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: [] },
|
|
19
|
+
{ 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: [] },
|
|
20
|
+
{ 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: [] },
|
|
21
|
+
{ 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: [] },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
getCaseMock = jest.fn<(id: string) => Promise<Case>>();
|
|
26
|
+
getSectionMock = jest.fn<(id: string) => Promise<Section>>();
|
|
27
|
+
getCaseTypesMock = jest.fn<() => Promise<CaseType[]>>();
|
|
28
|
+
getPrioritiesMock = jest.fn<() => Promise<Priority[]>>();
|
|
29
|
+
getCaseFieldsMock = jest.fn<() => Promise<CaseField[]>>().mockResolvedValue(mockCaseFields);
|
|
30
|
+
|
|
31
|
+
mockClient = {
|
|
32
|
+
getCase: getCaseMock,
|
|
33
|
+
getSection: getSectionMock,
|
|
34
|
+
getCaseTypes: getCaseTypesMock,
|
|
35
|
+
getPriorities: getPrioritiesMock,
|
|
36
|
+
getCaseFields: getCaseFieldsMock
|
|
37
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('exports correct tool definition', () => {
|
|
41
|
+
expect(getCaseTool.name).toBe('get_case');
|
|
42
|
+
expect(getCaseTool.description).toBe('Get a test case from TestRail by ID');
|
|
43
|
+
expect(getCaseTool.parameters).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handler fetches data and returns transformed response', async () => {
|
|
47
|
+
const mockCase: Case = {
|
|
48
|
+
id: 123, title: 'My Case', template_id: 1,
|
|
49
|
+
section_id: 456,
|
|
50
|
+
type_id: 2,
|
|
51
|
+
priority_id: 3,
|
|
52
|
+
milestone_id: null,
|
|
53
|
+
refs: 'JIRA-123',
|
|
54
|
+
created_by: 1,
|
|
55
|
+
created_on: 1700000000,
|
|
56
|
+
updated_by: 1,
|
|
57
|
+
updated_on: 1700000000,
|
|
58
|
+
estimate: null,
|
|
59
|
+
estimate_forecast: null,
|
|
60
|
+
suite_id: 1,
|
|
61
|
+
display_order: 1,
|
|
62
|
+
is_deleted: 0,
|
|
63
|
+
custom_refs: null,
|
|
64
|
+
custom_case_automation_priority: null,
|
|
65
|
+
custom_is_automated: null,
|
|
66
|
+
custom_reviewer: null,
|
|
67
|
+
custom_review_status: null,
|
|
68
|
+
custom_testrail_bdd_scenario: null,
|
|
69
|
+
custom_override: null,
|
|
70
|
+
custom_preconds: null,
|
|
71
|
+
custom_steps: null,
|
|
72
|
+
custom_expected: null,
|
|
73
|
+
custom_steps_separated: null,
|
|
74
|
+
custom_mission: null,
|
|
75
|
+
custom_goals: null,
|
|
76
|
+
custom_postcondition: null,
|
|
77
|
+
custom_comments: null,
|
|
78
|
+
labels: ['tag1']
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const mockSection: Section = {
|
|
82
|
+
id: 456, name: 'My Section', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const mockCaseTypes: CaseType[] = [
|
|
86
|
+
{ id: 1, name: 'Automated', is_default: false },
|
|
87
|
+
{ id: 2, name: 'Functional', is_default: true }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const mockPriorities: Priority[] = [
|
|
91
|
+
{ id: 3, name: 'High', priority: 3, short_name: 'High', is_default: false }
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
getCaseMock.mockResolvedValue(mockCase);
|
|
95
|
+
getSectionMock.mockResolvedValue(mockSection);
|
|
96
|
+
getCaseTypesMock.mockResolvedValue(mockCaseTypes);
|
|
97
|
+
getPrioritiesMock.mockResolvedValue(mockPriorities);
|
|
98
|
+
|
|
99
|
+
const result = await getCaseTool.handler({ case_id: 'C123' }, mockClient);
|
|
100
|
+
|
|
101
|
+
expect(result).toBeDefined();
|
|
102
|
+
if ('content' in result) {
|
|
103
|
+
expect(result.content[0].type).toBe('text');
|
|
104
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
105
|
+
|
|
106
|
+
expect(parsed).toEqual({
|
|
107
|
+
id: 123,
|
|
108
|
+
title: 'My Case',
|
|
109
|
+
section: 'My Section',
|
|
110
|
+
type: 'Functional',
|
|
111
|
+
priority: 'High',
|
|
112
|
+
labels: ['tag1'],
|
|
113
|
+
references: 'JIRA-123',
|
|
114
|
+
updated_on: 1700000000
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
throw new Error('Result should have content');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expect(mockClient.getCase).toHaveBeenCalledWith('123');
|
|
121
|
+
expect(mockClient.getSection).toHaveBeenCalledWith('456');
|
|
122
|
+
expect(mockClient.getCaseTypes).toHaveBeenCalled();
|
|
123
|
+
expect(mockClient.getPriorities).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
test('handler returns error on failure', async () => {
|
|
129
|
+
getCaseMock.mockRejectedValue(new Error('API Error'));
|
|
130
|
+
|
|
131
|
+
const result = await getCaseTool.handler({ case_id: '123' }, mockClient);
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
135
|
+
isError: true
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('handler returns "Unknown" when caseType is not found', async () => {
|
|
140
|
+
const mockCase: Case = {
|
|
141
|
+
id: 123, title: 'My Case', template_id: 1,
|
|
142
|
+
section_id: 456, type_id: 999, priority_id: 3,
|
|
143
|
+
milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
144
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
145
|
+
suite_id: 1, display_order: 1, is_deleted: 0, labels: []
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
getCaseMock.mockResolvedValue(mockCase);
|
|
149
|
+
getSectionMock.mockResolvedValue({ id: 456, name: 'Section', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1 });
|
|
150
|
+
getCaseTypesMock.mockResolvedValue([{ id: 1, name: 'Automated', is_default: false }]);
|
|
151
|
+
getPrioritiesMock.mockResolvedValue([{ id: 3, name: 'High', priority: 3, short_name: 'High', is_default: false }]);
|
|
152
|
+
|
|
153
|
+
const result = await getCaseTool.handler({ case_id: '123' }, mockClient);
|
|
154
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
155
|
+
|
|
156
|
+
expect(parsed.type).toBe('Unknown');
|
|
157
|
+
expect(parsed.priority).toBe('High');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('handler returns "Unknown" when priority is not found', async () => {
|
|
161
|
+
const mockCase: Case = {
|
|
162
|
+
id: 123, title: 'My Case', template_id: 1,
|
|
163
|
+
section_id: 456, type_id: 1, priority_id: 999,
|
|
164
|
+
milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
165
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
166
|
+
suite_id: 1, display_order: 1, is_deleted: 0, labels: []
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
getCaseMock.mockResolvedValue(mockCase);
|
|
170
|
+
getSectionMock.mockResolvedValue({ id: 456, name: 'Section', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1 });
|
|
171
|
+
getCaseTypesMock.mockResolvedValue([{ id: 1, name: 'Automated', is_default: false }]);
|
|
172
|
+
getPrioritiesMock.mockResolvedValue([{ id: 3, name: 'High', priority: 3, short_name: 'High', is_default: false }]);
|
|
173
|
+
|
|
174
|
+
const result = await getCaseTool.handler({ case_id: '123' }, mockClient);
|
|
175
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
176
|
+
|
|
177
|
+
expect(parsed.type).toBe('Automated');
|
|
178
|
+
expect(parsed.priority).toBe('Unknown');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('handler uses empty array when labels is undefined', async () => {
|
|
182
|
+
const mockCase: Case = {
|
|
183
|
+
id: 123, title: 'My Case', template_id: 1,
|
|
184
|
+
section_id: 456, type_id: 1, priority_id: 3,
|
|
185
|
+
milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
186
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
187
|
+
suite_id: 1, display_order: 1, is_deleted: 0, labels: undefined as any
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
getCaseMock.mockResolvedValue(mockCase);
|
|
191
|
+
getSectionMock.mockResolvedValue({ id: 456, name: 'Section', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1 });
|
|
192
|
+
getCaseTypesMock.mockResolvedValue([{ id: 1, name: 'Automated', is_default: false }]);
|
|
193
|
+
getPrioritiesMock.mockResolvedValue([{ id: 3, name: 'High', priority: 3, short_name: 'High', is_default: false }]);
|
|
194
|
+
|
|
195
|
+
const result = await getCaseTool.handler({ case_id: '123' }, mockClient);
|
|
196
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
197
|
+
|
|
198
|
+
expect(parsed.labels).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
});
|