@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.
- package/CHANGELOG.md +7 -0
- package/README.md +52 -16
- package/package.json +1 -2
- package/src/client/testrail.ts +42 -53
- package/src/index.ts +22 -2
- package/src/tools/create_case.ts +6 -13
- package/src/tools/get_case.ts +4 -11
- package/src/tools/get_case_fields.ts +4 -11
- package/src/tools/get_cases.ts +4 -11
- package/src/tools/get_projects.ts +7 -25
- package/src/tools/get_sections.ts +7 -25
- package/src/tools/get_templates.ts +7 -25
- package/src/tools/update_case.ts +6 -13
- package/src/tools/update_cases.ts +7 -14
- package/src/types/custom.ts +1 -16
- package/src/types/testrail.ts +87 -87
- package/src/utils/sanitizer.ts +29 -0
- package/test/client/testrail.test.ts +215 -142
- package/test/tools/create_case.test.ts +10 -16
- package/test/tools/get_case.test.ts +46 -61
- package/test/tools/get_case_fields.test.ts +7 -80
- package/test/tools/get_cases.test.ts +19 -108
- package/test/tools/get_projects.test.ts +4 -41
- package/test/tools/get_sections.test.ts +7 -36
- package/test/tools/get_templates.test.ts +1 -30
- package/test/tools/update_case.test.ts +10 -16
- package/test/tools/update_cases.test.ts +14 -41
- package/test/utils/mapper.test.ts +13 -13
- package/test/utils/sanitizer.test.ts +85 -1
|
@@ -1,103 +1,98 @@
|
|
|
1
|
-
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
1
|
+
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
2
|
import { TestRailClient } from '../../src/client/testrail.js';
|
|
3
|
-
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
4
3
|
|
|
5
4
|
describe('TestRailClient', () => {
|
|
6
5
|
let client: TestRailClient;
|
|
7
|
-
let
|
|
6
|
+
let fetchMock: any;
|
|
8
7
|
|
|
9
8
|
beforeEach(() => {
|
|
10
|
-
|
|
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
|
|
9
|
+
fetchMock = jest.spyOn(global, 'fetch');
|
|
22
10
|
client = new TestRailClient('https://testrail.io/', 'user', 'apikey');
|
|
23
11
|
});
|
|
24
12
|
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
jest.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
25
17
|
test('constructor cleans trailing slash from baseUrl', async () => {
|
|
26
18
|
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
27
|
-
|
|
19
|
+
fetchMock.mockResolvedValue({
|
|
20
|
+
ok: true,
|
|
21
|
+
json: async () => mockData
|
|
22
|
+
});
|
|
28
23
|
|
|
29
24
|
await client.getCase('1');
|
|
30
25
|
|
|
31
|
-
|
|
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);
|
|
26
|
+
expect(fetchMock).toHaveBeenCalledWith('https://testrail.io/index.php?/api/v2/get_case/1', expect.any(Object));
|
|
56
27
|
});
|
|
57
28
|
|
|
58
29
|
test('getCase returns data on success', async () => {
|
|
59
30
|
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
60
|
-
|
|
31
|
+
fetchMock.mockResolvedValue({
|
|
32
|
+
ok: true,
|
|
33
|
+
json: async () => mockData
|
|
34
|
+
});
|
|
61
35
|
|
|
62
36
|
const result = await client.getCase('1');
|
|
63
37
|
expect(result).toEqual(mockData);
|
|
64
|
-
expect(
|
|
38
|
+
expect(fetchMock).toHaveBeenCalledWith('https://testrail.io/index.php?/api/v2/get_case/1', expect.objectContaining({
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: expect.objectContaining({
|
|
41
|
+
'Authorization': expect.stringContaining('Basic'),
|
|
42
|
+
'Content-Type': 'application/json'
|
|
43
|
+
})
|
|
44
|
+
}));
|
|
65
45
|
});
|
|
66
46
|
|
|
67
47
|
test('getCase throws formatted error on API error', async () => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
toJSON: () => ({})
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
mockAxiosInstance.get.mockRejectedValue(mockError);
|
|
48
|
+
const errorResponse = { error: 'Case not found' };
|
|
49
|
+
fetchMock.mockResolvedValue({
|
|
50
|
+
ok: false,
|
|
51
|
+
status: 404,
|
|
52
|
+
statusText: 'Not Found',
|
|
53
|
+
text: async () => JSON.stringify(errorResponse),
|
|
54
|
+
json: async () => errorResponse
|
|
55
|
+
});
|
|
80
56
|
|
|
81
57
|
await expect(client.getCase('999')).rejects.toThrow(
|
|
82
|
-
'TestRail
|
|
58
|
+
'TestRail: 404 Not Found - {"error":"Case not found"}'
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('getCase throws error containing raw text when response is not JSON', async () => {
|
|
63
|
+
fetchMock.mockResolvedValue({
|
|
64
|
+
ok: false,
|
|
65
|
+
status: 500,
|
|
66
|
+
statusText: 'Internal Server Error',
|
|
67
|
+
text: async () => 'Not JSON Content',
|
|
68
|
+
json: async () => { throw new Error('Not JSON'); }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await expect(client.getCase('1')).rejects.toThrow(
|
|
72
|
+
'TestRail: 500 Internal Server Error - Not JSON Content'
|
|
83
73
|
);
|
|
84
74
|
});
|
|
85
75
|
|
|
86
76
|
test('getCases returns cases without pagination', async () => {
|
|
87
77
|
const mockCases = [{ id: 1, title: 'Case 1' }, { id: 2, title: 'Case 2' }];
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
fetchMock.mockResolvedValue({
|
|
79
|
+
ok: true,
|
|
80
|
+
json: async () => ({ cases: mockCases, _links: { next: null } })
|
|
90
81
|
});
|
|
91
82
|
|
|
92
83
|
const result = await client.getCases('1');
|
|
93
84
|
expect(result).toEqual(mockCases);
|
|
94
|
-
expect(
|
|
85
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
86
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1',
|
|
87
|
+
expect.objectContaining({ method: 'GET' })
|
|
88
|
+
);
|
|
95
89
|
});
|
|
96
90
|
|
|
97
91
|
test('getCases handles response with undefined _links', async () => {
|
|
98
92
|
const mockCases = [{ id: 1, title: 'Case 1' }];
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
fetchMock.mockResolvedValue({
|
|
94
|
+
ok: true,
|
|
95
|
+
json: async () => ({ cases: mockCases })
|
|
101
96
|
});
|
|
102
97
|
|
|
103
98
|
const result = await client.getCases('1');
|
|
@@ -108,36 +103,52 @@ describe('TestRailClient', () => {
|
|
|
108
103
|
const page1Cases = [{ id: 1, title: 'Case 1' }];
|
|
109
104
|
const page2Cases = [{ id: 2, title: 'Case 2' }];
|
|
110
105
|
|
|
111
|
-
|
|
106
|
+
fetchMock
|
|
112
107
|
.mockResolvedValueOnce({
|
|
113
|
-
|
|
108
|
+
ok: true,
|
|
109
|
+
json: async () => ({ cases: page1Cases, _links: { next: '/api/v2/get_cases/1&offset=1' } })
|
|
114
110
|
})
|
|
115
111
|
.mockResolvedValueOnce({
|
|
116
|
-
|
|
112
|
+
ok: true,
|
|
113
|
+
json: async () => ({ cases: page2Cases, _links: { next: null } })
|
|
117
114
|
});
|
|
118
115
|
|
|
119
116
|
const result = await client.getCases('1');
|
|
120
117
|
expect(result).toEqual([...page1Cases, ...page2Cases]);
|
|
121
|
-
expect(
|
|
118
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
119
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
120
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1',
|
|
121
|
+
expect.any(Object)
|
|
122
|
+
);
|
|
123
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
124
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1&offset=1',
|
|
125
|
+
expect.any(Object)
|
|
126
|
+
);
|
|
122
127
|
});
|
|
123
128
|
|
|
124
129
|
test('getCases with sectionId adds section filter', async () => {
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
fetchMock.mockResolvedValue({
|
|
131
|
+
ok: true,
|
|
132
|
+
json: async () => ({ cases: [], _links: { next: null } })
|
|
127
133
|
});
|
|
128
134
|
|
|
129
135
|
await client.getCases('1', '5');
|
|
130
|
-
expect(
|
|
136
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
137
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1§ion_id=5',
|
|
138
|
+
expect.any(Object)
|
|
139
|
+
);
|
|
131
140
|
});
|
|
132
141
|
|
|
133
142
|
test('getCases with filter adds query params', async () => {
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
fetchMock.mockResolvedValue({
|
|
144
|
+
ok: true,
|
|
145
|
+
json: async () => ({ cases: [], _links: { next: null } })
|
|
136
146
|
});
|
|
137
147
|
|
|
138
148
|
await client.getCases('1', undefined, { type_id: '1', priority_id: '2' });
|
|
139
|
-
expect(
|
|
140
|
-
'/index.php?/api/v2/get_cases/1&type_id=1&priority_id=2'
|
|
149
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
150
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1&type_id=1&priority_id=2',
|
|
151
|
+
expect.any(Object)
|
|
141
152
|
);
|
|
142
153
|
});
|
|
143
154
|
|
|
@@ -151,11 +162,17 @@ describe('TestRailClient', () => {
|
|
|
151
162
|
display_order: 1,
|
|
152
163
|
suite_id: 1
|
|
153
164
|
};
|
|
154
|
-
|
|
165
|
+
fetchMock.mockResolvedValue({
|
|
166
|
+
ok: true,
|
|
167
|
+
json: async () => mockData
|
|
168
|
+
});
|
|
155
169
|
|
|
156
170
|
const result = await client.getSection('1');
|
|
157
171
|
expect(result).toEqual(mockData);
|
|
158
|
-
expect(
|
|
172
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
173
|
+
'https://testrail.io/index.php?/api/v2/get_section/1',
|
|
174
|
+
expect.any(Object)
|
|
175
|
+
);
|
|
159
176
|
});
|
|
160
177
|
|
|
161
178
|
test('getPriorities returns data on success', async () => {
|
|
@@ -166,21 +183,30 @@ describe('TestRailClient', () => {
|
|
|
166
183
|
priority: 1,
|
|
167
184
|
short_name: 'Low'
|
|
168
185
|
}];
|
|
169
|
-
|
|
186
|
+
fetchMock.mockResolvedValue({
|
|
187
|
+
ok: true,
|
|
188
|
+
json: async () => mockData
|
|
189
|
+
});
|
|
170
190
|
|
|
171
191
|
const result = await client.getPriorities();
|
|
172
192
|
expect(result).toEqual(mockData);
|
|
173
|
-
expect(
|
|
193
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
194
|
+
'https://testrail.io/index.php?/api/v2/get_priorities',
|
|
195
|
+
expect.any(Object)
|
|
196
|
+
);
|
|
174
197
|
});
|
|
175
198
|
|
|
176
199
|
test('getPriorities caches result', async () => {
|
|
177
200
|
const mockData = [{ id: 1, name: 'Low' }];
|
|
178
|
-
|
|
201
|
+
fetchMock.mockResolvedValue({
|
|
202
|
+
ok: true,
|
|
203
|
+
json: async () => mockData
|
|
204
|
+
});
|
|
179
205
|
|
|
180
206
|
await client.getPriorities();
|
|
181
207
|
await client.getPriorities();
|
|
182
208
|
|
|
183
|
-
expect(
|
|
209
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
184
210
|
});
|
|
185
211
|
|
|
186
212
|
test('getCaseTypes returns data on success', async () => {
|
|
@@ -189,11 +215,17 @@ describe('TestRailClient', () => {
|
|
|
189
215
|
is_default: true,
|
|
190
216
|
name: 'Automated'
|
|
191
217
|
}];
|
|
192
|
-
|
|
218
|
+
fetchMock.mockResolvedValue({
|
|
219
|
+
ok: true,
|
|
220
|
+
json: async () => mockData
|
|
221
|
+
});
|
|
193
222
|
|
|
194
223
|
const result = await client.getCaseTypes();
|
|
195
224
|
expect(result).toEqual(mockData);
|
|
196
|
-
expect(
|
|
225
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
226
|
+
'https://testrail.io/index.php?/api/v2/get_case_types',
|
|
227
|
+
expect.any(Object)
|
|
228
|
+
);
|
|
197
229
|
});
|
|
198
230
|
|
|
199
231
|
test('getCaseFields returns data on success', async () => {
|
|
@@ -204,55 +236,82 @@ describe('TestRailClient', () => {
|
|
|
204
236
|
label: 'Custom Field',
|
|
205
237
|
type_id: 1
|
|
206
238
|
}];
|
|
207
|
-
|
|
239
|
+
fetchMock.mockResolvedValue({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: async () => mockData
|
|
242
|
+
});
|
|
208
243
|
|
|
209
244
|
const result = await client.getCaseFields();
|
|
210
245
|
expect(result).toEqual(mockData);
|
|
211
|
-
expect(
|
|
246
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
247
|
+
'https://testrail.io/index.php?/api/v2/get_case_fields',
|
|
248
|
+
expect.any(Object)
|
|
249
|
+
);
|
|
212
250
|
});
|
|
213
251
|
|
|
214
252
|
test('getCaseFields caches result', async () => {
|
|
215
253
|
const mockData = [{ id: 1, name: 'field' }];
|
|
216
|
-
|
|
254
|
+
fetchMock.mockResolvedValue({
|
|
255
|
+
ok: true,
|
|
256
|
+
json: async () => mockData
|
|
257
|
+
});
|
|
217
258
|
|
|
218
259
|
await client.getCaseFields();
|
|
219
260
|
await client.getCaseFields();
|
|
220
261
|
|
|
221
|
-
expect(
|
|
262
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
222
263
|
});
|
|
223
264
|
|
|
224
265
|
test('getTemplates returns data on success', async () => {
|
|
225
266
|
const mockData = [{ id: 1, name: 'Test Template', is_default: true }];
|
|
226
|
-
|
|
267
|
+
fetchMock.mockResolvedValue({
|
|
268
|
+
ok: true,
|
|
269
|
+
json: async () => mockData
|
|
270
|
+
});
|
|
227
271
|
|
|
228
272
|
const result = await client.getTemplates('1');
|
|
229
273
|
expect(result).toEqual(mockData);
|
|
230
|
-
expect(
|
|
274
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
275
|
+
'https://testrail.io/index.php?/api/v2/get_templates/1',
|
|
276
|
+
expect.any(Object)
|
|
277
|
+
);
|
|
231
278
|
});
|
|
232
279
|
|
|
233
280
|
test('getTemplates caches result per project', async () => {
|
|
234
281
|
const mockData1 = [{ id: 1, name: 'Template 1' }];
|
|
235
282
|
const mockData2 = [{ id: 2, name: 'Template 2' }];
|
|
236
|
-
|
|
237
|
-
.mockResolvedValueOnce({
|
|
238
|
-
|
|
283
|
+
fetchMock
|
|
284
|
+
.mockResolvedValueOnce({
|
|
285
|
+
ok: true,
|
|
286
|
+
json: async () => mockData1
|
|
287
|
+
})
|
|
288
|
+
.mockResolvedValueOnce({
|
|
289
|
+
ok: true,
|
|
290
|
+
json: async () => mockData2
|
|
291
|
+
});
|
|
239
292
|
|
|
240
293
|
await client.getTemplates('1');
|
|
241
294
|
await client.getTemplates('1');
|
|
242
295
|
await client.getTemplates('2');
|
|
243
296
|
|
|
244
|
-
expect(
|
|
297
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
245
298
|
});
|
|
246
299
|
|
|
247
300
|
test('updateCase sends POST request', async () => {
|
|
248
301
|
const mockData = { id: 1, title: 'Updated Case' };
|
|
249
|
-
|
|
302
|
+
fetchMock.mockResolvedValue({
|
|
303
|
+
ok: true,
|
|
304
|
+
json: async () => mockData
|
|
305
|
+
});
|
|
250
306
|
|
|
251
307
|
const result = await client.updateCase('1', { title: 'Updated Case' });
|
|
252
308
|
expect(result).toEqual(mockData);
|
|
253
|
-
expect(
|
|
254
|
-
'/index.php?/api/v2/update_case/1',
|
|
255
|
-
{
|
|
309
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
310
|
+
'https://testrail.io/index.php?/api/v2/update_case/1',
|
|
311
|
+
expect.objectContaining({
|
|
312
|
+
method: 'POST',
|
|
313
|
+
body: JSON.stringify({ title: 'Updated Case' })
|
|
314
|
+
})
|
|
256
315
|
);
|
|
257
316
|
});
|
|
258
317
|
|
|
@@ -260,98 +319,112 @@ describe('TestRailClient', () => {
|
|
|
260
319
|
const mockCase = { id: 1, suite_id: 10 };
|
|
261
320
|
const mockUpdated = [{ id: 1, title: 'Updated' }, { id: 2, title: 'Updated' }];
|
|
262
321
|
|
|
263
|
-
|
|
264
|
-
|
|
322
|
+
fetchMock
|
|
323
|
+
.mockResolvedValueOnce({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => mockCase
|
|
326
|
+
})
|
|
327
|
+
.mockResolvedValueOnce({
|
|
328
|
+
ok: true,
|
|
329
|
+
json: async () => ({ updated_cases: mockUpdated })
|
|
330
|
+
});
|
|
265
331
|
|
|
266
332
|
const result = await client.updateCases([1, 2], { title: 'Updated' });
|
|
267
333
|
expect(result).toEqual(mockUpdated);
|
|
268
|
-
expect(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
334
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
335
|
+
'https://testrail.io/index.php?/api/v2/get_case/1',
|
|
336
|
+
expect.any(Object)
|
|
337
|
+
);
|
|
338
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
339
|
+
'https://testrail.io/index.php?/api/v2/update_cases/10',
|
|
340
|
+
expect.objectContaining({
|
|
341
|
+
method: 'POST',
|
|
342
|
+
body: JSON.stringify({ case_ids: [1, 2], title: 'Updated' })
|
|
343
|
+
})
|
|
272
344
|
);
|
|
273
345
|
});
|
|
274
346
|
|
|
275
347
|
test('createCase sends POST request', async () => {
|
|
276
348
|
const mockData = { id: 1, title: 'New Case' };
|
|
277
|
-
|
|
349
|
+
fetchMock.mockResolvedValue({
|
|
350
|
+
ok: true,
|
|
351
|
+
json: async () => mockData
|
|
352
|
+
});
|
|
278
353
|
|
|
279
354
|
const result = await client.createCase('5', { title: 'New Case' });
|
|
280
355
|
expect(result).toEqual(mockData);
|
|
281
|
-
expect(
|
|
282
|
-
'/index.php?/api/v2/add_case/5',
|
|
283
|
-
{
|
|
356
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
357
|
+
'https://testrail.io/index.php?/api/v2/add_case/5',
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
method: 'POST',
|
|
360
|
+
body: JSON.stringify({ title: 'New Case' })
|
|
361
|
+
})
|
|
284
362
|
);
|
|
285
363
|
});
|
|
286
364
|
|
|
287
365
|
test('getSections returns sections array', async () => {
|
|
288
366
|
const mockSections = [{ id: 1, name: 'Section 1' }, { id: 2, name: 'Section 2' }];
|
|
289
|
-
|
|
367
|
+
fetchMock.mockResolvedValue({
|
|
368
|
+
ok: true,
|
|
369
|
+
json: async () => ({ sections: mockSections })
|
|
370
|
+
});
|
|
290
371
|
|
|
291
372
|
const result = await client.getSections('1');
|
|
292
373
|
expect(result).toEqual(mockSections);
|
|
293
|
-
expect(
|
|
374
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
375
|
+
'https://testrail.io/index.php?/api/v2/get_sections/1',
|
|
376
|
+
expect.any(Object)
|
|
377
|
+
);
|
|
294
378
|
});
|
|
295
379
|
|
|
296
380
|
test('getProjects returns projects array', async () => {
|
|
297
381
|
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
298
|
-
|
|
382
|
+
fetchMock.mockResolvedValue({
|
|
383
|
+
ok: true,
|
|
384
|
+
json: async () => ({ projects: mockProjects })
|
|
385
|
+
});
|
|
299
386
|
|
|
300
387
|
const result = await client.getProjects();
|
|
301
388
|
expect(result).toEqual(mockProjects);
|
|
302
|
-
expect(
|
|
389
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
390
|
+
'https://testrail.io/index.php?/api/v2/get_projects',
|
|
391
|
+
expect.any(Object)
|
|
392
|
+
);
|
|
303
393
|
});
|
|
304
394
|
|
|
305
395
|
test('getProjects caches result', async () => {
|
|
306
396
|
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
307
|
-
|
|
397
|
+
fetchMock.mockResolvedValue({
|
|
398
|
+
ok: true,
|
|
399
|
+
json: async () => ({ projects: mockProjects })
|
|
400
|
+
});
|
|
308
401
|
|
|
309
402
|
await client.getProjects();
|
|
310
403
|
await client.getProjects();
|
|
311
404
|
|
|
312
|
-
expect(
|
|
405
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
313
406
|
});
|
|
314
407
|
|
|
315
408
|
test('postRequest throws formatted error on API error', async () => {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
toJSON: () => ({})
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
mockAxiosInstance.post.mockRejectedValue(mockError);
|
|
409
|
+
const errorResponse = { error: 'Invalid field' };
|
|
410
|
+
fetchMock.mockResolvedValue({
|
|
411
|
+
ok: false,
|
|
412
|
+
status: 400,
|
|
413
|
+
statusText: 'Bad Request',
|
|
414
|
+
text: async () => JSON.stringify(errorResponse),
|
|
415
|
+
json: async () => errorResponse
|
|
416
|
+
});
|
|
328
417
|
|
|
329
418
|
await expect(client.updateCase('1', {})).rejects.toThrow(
|
|
330
|
-
'TestRail
|
|
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'
|
|
419
|
+
'TestRail: 400 Bad Request - {"error":"Invalid field"}'
|
|
346
420
|
);
|
|
347
421
|
});
|
|
348
422
|
|
|
349
|
-
test('handleError
|
|
350
|
-
|
|
351
|
-
mockAxiosInstance.get.mockRejectedValue(mockError);
|
|
423
|
+
test('handleError formats error when network fails', async () => {
|
|
424
|
+
fetchMock.mockRejectedValue(new Error('Network Error'));
|
|
352
425
|
|
|
353
426
|
await expect(client.getCase('1')).rejects.toThrow(
|
|
354
|
-
'
|
|
427
|
+
'Network Error'
|
|
355
428
|
);
|
|
356
429
|
});
|
|
357
430
|
});
|
|
@@ -53,12 +53,9 @@ describe('create_case tool', () => {
|
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
expect(result).toBeDefined();
|
|
56
|
-
expect(result.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(parsed.success).toBe(true);
|
|
60
|
-
expect(parsed.case_id).toBe(456);
|
|
61
|
-
expect(parsed.message).toContain('C456');
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
expect(result.case_id).toBe(456);
|
|
58
|
+
expect(result.message).toContain('C456');
|
|
62
59
|
expect(mockClient.createCase).toHaveBeenCalledWith('1', { title: 'New Test Case' });
|
|
63
60
|
});
|
|
64
61
|
|
|
@@ -91,17 +88,14 @@ describe('create_case tool', () => {
|
|
|
91
88
|
expect(mockClient.createCase).toHaveBeenCalledWith('10', { title: 'Simple Test' });
|
|
92
89
|
});
|
|
93
90
|
|
|
94
|
-
test('handler
|
|
91
|
+
test('handler throws error on failure', async () => {
|
|
95
92
|
createCaseMock.mockRejectedValue(new Error('API Error'));
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
104
|
-
isError: true
|
|
105
|
-
});
|
|
94
|
+
await expect(
|
|
95
|
+
createCaseTool.handler(
|
|
96
|
+
{ section_id: '1', title: 'Test' },
|
|
97
|
+
mockClient
|
|
98
|
+
)
|
|
99
|
+
).rejects.toThrow('API Error');
|
|
106
100
|
});
|
|
107
101
|
});
|