@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.
- package/CHANGELOG.md +18 -0
- package/README.md +62 -16
- package/package.json +3 -2
- package/src/client/testrail.ts +111 -56
- package/src/index.ts +32 -2
- package/src/tools/add_attachment_to_run.ts +62 -0
- package/src/tools/add_results.ts +28 -0
- package/src/tools/add_run.ts +24 -0
- package/src/tools/create_case.ts +10 -17
- package/src/tools/get_case.ts +5 -12
- package/src/tools/get_case_fields.ts +4 -11
- package/src/tools/get_cases.ts +9 -16
- package/src/tools/get_projects.ts +7 -27
- package/src/tools/get_sections.ts +7 -27
- package/src/tools/get_statuses.ts +18 -0
- package/src/tools/get_templates.ts +7 -27
- package/src/tools/get_tests.ts +22 -0
- package/src/tools/update_case.ts +7 -14
- package/src/tools/update_cases.ts +10 -16
- package/src/types/custom.ts +1 -16
- package/src/types/testrail.ts +140 -87
- package/src/utils/sanitizer.ts +29 -0
- package/test/client/testrail.test.ts +329 -141
- package/test/tools/add_results.test.ts +94 -0
- package/test/tools/add_run.test.ts +83 -0
- package/test/tools/create_case.test.ts +10 -16
- package/test/tools/get_case.test.ts +47 -62
- 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_statuses.test.ts +56 -0
- package/test/tools/get_templates.test.ts +1 -30
- package/test/tools/get_tests.test.ts +47 -0
- package/test/tools/update_case.test.ts +10 -16
- package/test/tools/update_cases.test.ts +29 -28
- package/test/utils/mapper.test.ts +13 -13
- package/test/utils/sanitizer.test.ts +85 -1
|
@@ -1,103 +1,99 @@
|
|
|
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
|
|
3
|
+
import * as fs from 'fs';
|
|
4
4
|
|
|
5
5
|
describe('TestRailClient', () => {
|
|
6
6
|
let client: TestRailClient;
|
|
7
|
-
let
|
|
7
|
+
let fetchMock: any;
|
|
8
8
|
|
|
9
9
|
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
|
|
10
|
+
fetchMock = jest.spyOn(global, 'fetch');
|
|
22
11
|
client = new TestRailClient('https://testrail.io/', 'user', 'apikey');
|
|
23
12
|
});
|
|
24
13
|
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
25
18
|
test('constructor cleans trailing slash from baseUrl', async () => {
|
|
26
19
|
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
27
|
-
|
|
20
|
+
fetchMock.mockResolvedValue({
|
|
21
|
+
ok: true,
|
|
22
|
+
json: async () => mockData
|
|
23
|
+
});
|
|
28
24
|
|
|
29
25
|
await client.getCase('1');
|
|
30
26
|
|
|
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);
|
|
27
|
+
expect(fetchMock).toHaveBeenCalledWith('https://testrail.io/index.php?/api/v2/get_case/1', expect.any(Object));
|
|
56
28
|
});
|
|
57
29
|
|
|
58
30
|
test('getCase returns data on success', async () => {
|
|
59
31
|
const mockData = { id: 1, title: 'Test Case', template_id: 1 };
|
|
60
|
-
|
|
32
|
+
fetchMock.mockResolvedValue({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: async () => mockData
|
|
35
|
+
});
|
|
61
36
|
|
|
62
37
|
const result = await client.getCase('1');
|
|
63
38
|
expect(result).toEqual(mockData);
|
|
64
|
-
expect(
|
|
39
|
+
expect(fetchMock).toHaveBeenCalledWith('https://testrail.io/index.php?/api/v2/get_case/1', expect.objectContaining({
|
|
40
|
+
method: 'GET',
|
|
41
|
+
headers: expect.objectContaining({
|
|
42
|
+
'Authorization': expect.stringContaining('Basic'),
|
|
43
|
+
'Content-Type': 'application/json'
|
|
44
|
+
})
|
|
45
|
+
}));
|
|
65
46
|
});
|
|
66
47
|
|
|
67
48
|
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);
|
|
49
|
+
const errorResponse = { error: 'Case not found' };
|
|
50
|
+
fetchMock.mockResolvedValue({
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 404,
|
|
53
|
+
statusText: 'Not Found',
|
|
54
|
+
text: async () => JSON.stringify(errorResponse),
|
|
55
|
+
json: async () => errorResponse
|
|
56
|
+
});
|
|
80
57
|
|
|
81
58
|
await expect(client.getCase('999')).rejects.toThrow(
|
|
82
|
-
'TestRail
|
|
59
|
+
'TestRail: 404 Not Found - {"error":"Case not found"}'
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('getCase throws error containing raw text when response is not JSON', async () => {
|
|
64
|
+
fetchMock.mockResolvedValue({
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 500,
|
|
67
|
+
statusText: 'Internal Server Error',
|
|
68
|
+
text: async () => 'Not JSON Content',
|
|
69
|
+
json: async () => { throw new Error('Not JSON'); }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await expect(client.getCase('1')).rejects.toThrow(
|
|
73
|
+
'TestRail: 500 Internal Server Error - Not JSON Content'
|
|
83
74
|
);
|
|
84
75
|
});
|
|
85
76
|
|
|
86
77
|
test('getCases returns cases without pagination', async () => {
|
|
87
78
|
const mockCases = [{ id: 1, title: 'Case 1' }, { id: 2, title: 'Case 2' }];
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
fetchMock.mockResolvedValue({
|
|
80
|
+
ok: true,
|
|
81
|
+
json: async () => ({ cases: mockCases, _links: { next: null } })
|
|
90
82
|
});
|
|
91
83
|
|
|
92
84
|
const result = await client.getCases('1');
|
|
93
85
|
expect(result).toEqual(mockCases);
|
|
94
|
-
expect(
|
|
86
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
87
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1',
|
|
88
|
+
expect.objectContaining({ method: 'GET' })
|
|
89
|
+
);
|
|
95
90
|
});
|
|
96
91
|
|
|
97
92
|
test('getCases handles response with undefined _links', async () => {
|
|
98
93
|
const mockCases = [{ id: 1, title: 'Case 1' }];
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
fetchMock.mockResolvedValue({
|
|
95
|
+
ok: true,
|
|
96
|
+
json: async () => ({ cases: mockCases })
|
|
101
97
|
});
|
|
102
98
|
|
|
103
99
|
const result = await client.getCases('1');
|
|
@@ -108,36 +104,52 @@ describe('TestRailClient', () => {
|
|
|
108
104
|
const page1Cases = [{ id: 1, title: 'Case 1' }];
|
|
109
105
|
const page2Cases = [{ id: 2, title: 'Case 2' }];
|
|
110
106
|
|
|
111
|
-
|
|
107
|
+
fetchMock
|
|
112
108
|
.mockResolvedValueOnce({
|
|
113
|
-
|
|
109
|
+
ok: true,
|
|
110
|
+
json: async () => ({ cases: page1Cases, _links: { next: '/api/v2/get_cases/1&offset=1' } })
|
|
114
111
|
})
|
|
115
112
|
.mockResolvedValueOnce({
|
|
116
|
-
|
|
113
|
+
ok: true,
|
|
114
|
+
json: async () => ({ cases: page2Cases, _links: { next: null } })
|
|
117
115
|
});
|
|
118
116
|
|
|
119
117
|
const result = await client.getCases('1');
|
|
120
118
|
expect(result).toEqual([...page1Cases, ...page2Cases]);
|
|
121
|
-
expect(
|
|
119
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
120
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
121
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1',
|
|
122
|
+
expect.any(Object)
|
|
123
|
+
);
|
|
124
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
125
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1&offset=1',
|
|
126
|
+
expect.any(Object)
|
|
127
|
+
);
|
|
122
128
|
});
|
|
123
129
|
|
|
124
130
|
test('getCases with sectionId adds section filter', async () => {
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
fetchMock.mockResolvedValue({
|
|
132
|
+
ok: true,
|
|
133
|
+
json: async () => ({ cases: [], _links: { next: null } })
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
await client.getCases('1', '5');
|
|
130
|
-
expect(
|
|
137
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
138
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1§ion_id=5',
|
|
139
|
+
expect.any(Object)
|
|
140
|
+
);
|
|
131
141
|
});
|
|
132
142
|
|
|
133
143
|
test('getCases with filter adds query params', async () => {
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
fetchMock.mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: async () => ({ cases: [], _links: { next: null } })
|
|
136
147
|
});
|
|
137
148
|
|
|
138
149
|
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'
|
|
150
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
151
|
+
'https://testrail.io/index.php?/api/v2/get_cases/1&type_id=1&priority_id=2',
|
|
152
|
+
expect.any(Object)
|
|
141
153
|
);
|
|
142
154
|
});
|
|
143
155
|
|
|
@@ -151,11 +163,17 @@ describe('TestRailClient', () => {
|
|
|
151
163
|
display_order: 1,
|
|
152
164
|
suite_id: 1
|
|
153
165
|
};
|
|
154
|
-
|
|
166
|
+
fetchMock.mockResolvedValue({
|
|
167
|
+
ok: true,
|
|
168
|
+
json: async () => mockData
|
|
169
|
+
});
|
|
155
170
|
|
|
156
171
|
const result = await client.getSection('1');
|
|
157
172
|
expect(result).toEqual(mockData);
|
|
158
|
-
expect(
|
|
173
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
174
|
+
'https://testrail.io/index.php?/api/v2/get_section/1',
|
|
175
|
+
expect.any(Object)
|
|
176
|
+
);
|
|
159
177
|
});
|
|
160
178
|
|
|
161
179
|
test('getPriorities returns data on success', async () => {
|
|
@@ -166,21 +184,30 @@ describe('TestRailClient', () => {
|
|
|
166
184
|
priority: 1,
|
|
167
185
|
short_name: 'Low'
|
|
168
186
|
}];
|
|
169
|
-
|
|
187
|
+
fetchMock.mockResolvedValue({
|
|
188
|
+
ok: true,
|
|
189
|
+
json: async () => mockData
|
|
190
|
+
});
|
|
170
191
|
|
|
171
192
|
const result = await client.getPriorities();
|
|
172
193
|
expect(result).toEqual(mockData);
|
|
173
|
-
expect(
|
|
194
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
195
|
+
'https://testrail.io/index.php?/api/v2/get_priorities',
|
|
196
|
+
expect.any(Object)
|
|
197
|
+
);
|
|
174
198
|
});
|
|
175
199
|
|
|
176
200
|
test('getPriorities caches result', async () => {
|
|
177
201
|
const mockData = [{ id: 1, name: 'Low' }];
|
|
178
|
-
|
|
202
|
+
fetchMock.mockResolvedValue({
|
|
203
|
+
ok: true,
|
|
204
|
+
json: async () => mockData
|
|
205
|
+
});
|
|
179
206
|
|
|
180
207
|
await client.getPriorities();
|
|
181
208
|
await client.getPriorities();
|
|
182
209
|
|
|
183
|
-
expect(
|
|
210
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
184
211
|
});
|
|
185
212
|
|
|
186
213
|
test('getCaseTypes returns data on success', async () => {
|
|
@@ -189,11 +216,17 @@ describe('TestRailClient', () => {
|
|
|
189
216
|
is_default: true,
|
|
190
217
|
name: 'Automated'
|
|
191
218
|
}];
|
|
192
|
-
|
|
219
|
+
fetchMock.mockResolvedValue({
|
|
220
|
+
ok: true,
|
|
221
|
+
json: async () => mockData
|
|
222
|
+
});
|
|
193
223
|
|
|
194
224
|
const result = await client.getCaseTypes();
|
|
195
225
|
expect(result).toEqual(mockData);
|
|
196
|
-
expect(
|
|
226
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
227
|
+
'https://testrail.io/index.php?/api/v2/get_case_types',
|
|
228
|
+
expect.any(Object)
|
|
229
|
+
);
|
|
197
230
|
});
|
|
198
231
|
|
|
199
232
|
test('getCaseFields returns data on success', async () => {
|
|
@@ -204,154 +237,309 @@ describe('TestRailClient', () => {
|
|
|
204
237
|
label: 'Custom Field',
|
|
205
238
|
type_id: 1
|
|
206
239
|
}];
|
|
207
|
-
|
|
240
|
+
fetchMock.mockResolvedValue({
|
|
241
|
+
ok: true,
|
|
242
|
+
json: async () => mockData
|
|
243
|
+
});
|
|
208
244
|
|
|
209
245
|
const result = await client.getCaseFields();
|
|
210
246
|
expect(result).toEqual(mockData);
|
|
211
|
-
expect(
|
|
247
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
248
|
+
'https://testrail.io/index.php?/api/v2/get_case_fields',
|
|
249
|
+
expect.any(Object)
|
|
250
|
+
);
|
|
212
251
|
});
|
|
213
252
|
|
|
214
253
|
test('getCaseFields caches result', async () => {
|
|
215
254
|
const mockData = [{ id: 1, name: 'field' }];
|
|
216
|
-
|
|
255
|
+
fetchMock.mockResolvedValue({
|
|
256
|
+
ok: true,
|
|
257
|
+
json: async () => mockData
|
|
258
|
+
});
|
|
217
259
|
|
|
218
260
|
await client.getCaseFields();
|
|
219
261
|
await client.getCaseFields();
|
|
220
262
|
|
|
221
|
-
expect(
|
|
263
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
222
264
|
});
|
|
223
265
|
|
|
224
266
|
test('getTemplates returns data on success', async () => {
|
|
225
267
|
const mockData = [{ id: 1, name: 'Test Template', is_default: true }];
|
|
226
|
-
|
|
268
|
+
fetchMock.mockResolvedValue({
|
|
269
|
+
ok: true,
|
|
270
|
+
json: async () => mockData
|
|
271
|
+
});
|
|
227
272
|
|
|
228
273
|
const result = await client.getTemplates('1');
|
|
229
274
|
expect(result).toEqual(mockData);
|
|
230
|
-
expect(
|
|
275
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
276
|
+
'https://testrail.io/index.php?/api/v2/get_templates/1',
|
|
277
|
+
expect.any(Object)
|
|
278
|
+
);
|
|
231
279
|
});
|
|
232
280
|
|
|
233
281
|
test('getTemplates caches result per project', async () => {
|
|
234
282
|
const mockData1 = [{ id: 1, name: 'Template 1' }];
|
|
235
283
|
const mockData2 = [{ id: 2, name: 'Template 2' }];
|
|
236
|
-
|
|
237
|
-
.mockResolvedValueOnce({
|
|
238
|
-
|
|
284
|
+
fetchMock
|
|
285
|
+
.mockResolvedValueOnce({
|
|
286
|
+
ok: true,
|
|
287
|
+
json: async () => mockData1
|
|
288
|
+
})
|
|
289
|
+
.mockResolvedValueOnce({
|
|
290
|
+
ok: true,
|
|
291
|
+
json: async () => mockData2
|
|
292
|
+
});
|
|
239
293
|
|
|
240
294
|
await client.getTemplates('1');
|
|
241
295
|
await client.getTemplates('1');
|
|
242
296
|
await client.getTemplates('2');
|
|
243
297
|
|
|
244
|
-
expect(
|
|
298
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
245
299
|
});
|
|
246
300
|
|
|
247
301
|
test('updateCase sends POST request', async () => {
|
|
248
302
|
const mockData = { id: 1, title: 'Updated Case' };
|
|
249
|
-
|
|
303
|
+
fetchMock.mockResolvedValue({
|
|
304
|
+
ok: true,
|
|
305
|
+
json: async () => mockData
|
|
306
|
+
});
|
|
250
307
|
|
|
251
308
|
const result = await client.updateCase('1', { title: 'Updated Case' });
|
|
252
309
|
expect(result).toEqual(mockData);
|
|
253
|
-
expect(
|
|
254
|
-
'/index.php?/api/v2/update_case/1',
|
|
255
|
-
{
|
|
310
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
311
|
+
'https://testrail.io/index.php?/api/v2/update_case/1',
|
|
312
|
+
expect.objectContaining({
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({ title: 'Updated Case' })
|
|
315
|
+
})
|
|
256
316
|
);
|
|
257
317
|
});
|
|
258
318
|
|
|
259
|
-
test('updateCases
|
|
260
|
-
const mockCase = { id: 1, suite_id: 10 };
|
|
319
|
+
test('updateCases uses passed suiteId', async () => {
|
|
261
320
|
const mockUpdated = [{ id: 1, title: 'Updated' }, { id: 2, title: 'Updated' }];
|
|
262
321
|
|
|
263
|
-
|
|
264
|
-
|
|
322
|
+
fetchMock.mockResolvedValue({
|
|
323
|
+
ok: true,
|
|
324
|
+
json: async () => ({ updated_cases: mockUpdated })
|
|
325
|
+
});
|
|
265
326
|
|
|
266
|
-
const result = await client.updateCases([1, 2], { title: 'Updated' });
|
|
327
|
+
const result = await client.updateCases(10, [1, 2], { title: 'Updated' });
|
|
267
328
|
expect(result).toEqual(mockUpdated);
|
|
268
|
-
expect(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
329
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
330
|
+
'https://testrail.io/index.php?/api/v2/update_cases/10',
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
method: 'POST',
|
|
333
|
+
body: JSON.stringify({ case_ids: [1, 2], title: 'Updated' })
|
|
334
|
+
})
|
|
272
335
|
);
|
|
273
336
|
});
|
|
274
337
|
|
|
275
338
|
test('createCase sends POST request', async () => {
|
|
276
339
|
const mockData = { id: 1, title: 'New Case' };
|
|
277
|
-
|
|
340
|
+
fetchMock.mockResolvedValue({
|
|
341
|
+
ok: true,
|
|
342
|
+
json: async () => mockData
|
|
343
|
+
});
|
|
278
344
|
|
|
279
345
|
const result = await client.createCase('5', { title: 'New Case' });
|
|
280
346
|
expect(result).toEqual(mockData);
|
|
281
|
-
expect(
|
|
282
|
-
'/index.php?/api/v2/add_case/5',
|
|
283
|
-
{
|
|
347
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
348
|
+
'https://testrail.io/index.php?/api/v2/add_case/5',
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
method: 'POST',
|
|
351
|
+
body: JSON.stringify({ title: 'New Case' })
|
|
352
|
+
})
|
|
284
353
|
);
|
|
285
354
|
});
|
|
286
355
|
|
|
287
356
|
test('getSections returns sections array', async () => {
|
|
288
357
|
const mockSections = [{ id: 1, name: 'Section 1' }, { id: 2, name: 'Section 2' }];
|
|
289
|
-
|
|
358
|
+
fetchMock.mockResolvedValue({
|
|
359
|
+
ok: true,
|
|
360
|
+
json: async () => ({ sections: mockSections })
|
|
361
|
+
});
|
|
290
362
|
|
|
291
363
|
const result = await client.getSections('1');
|
|
292
364
|
expect(result).toEqual(mockSections);
|
|
293
|
-
expect(
|
|
365
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
366
|
+
'https://testrail.io/index.php?/api/v2/get_sections/1',
|
|
367
|
+
expect.any(Object)
|
|
368
|
+
);
|
|
294
369
|
});
|
|
295
370
|
|
|
296
371
|
test('getProjects returns projects array', async () => {
|
|
297
372
|
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
298
|
-
|
|
373
|
+
fetchMock.mockResolvedValue({
|
|
374
|
+
ok: true,
|
|
375
|
+
json: async () => ({ projects: mockProjects })
|
|
376
|
+
});
|
|
299
377
|
|
|
300
378
|
const result = await client.getProjects();
|
|
301
379
|
expect(result).toEqual(mockProjects);
|
|
302
|
-
expect(
|
|
380
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
381
|
+
'https://testrail.io/index.php?/api/v2/get_projects',
|
|
382
|
+
expect.any(Object)
|
|
383
|
+
);
|
|
303
384
|
});
|
|
304
385
|
|
|
305
386
|
test('getProjects caches result', async () => {
|
|
306
387
|
const mockProjects = [{ id: 1, name: 'Project 1' }];
|
|
307
|
-
|
|
388
|
+
fetchMock.mockResolvedValue({
|
|
389
|
+
ok: true,
|
|
390
|
+
json: async () => ({ projects: mockProjects })
|
|
391
|
+
});
|
|
308
392
|
|
|
309
393
|
await client.getProjects();
|
|
310
394
|
await client.getProjects();
|
|
311
395
|
|
|
312
|
-
expect(
|
|
396
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
313
397
|
});
|
|
314
398
|
|
|
315
399
|
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);
|
|
400
|
+
const errorResponse = { error: 'Invalid field' };
|
|
401
|
+
fetchMock.mockResolvedValue({
|
|
402
|
+
ok: false,
|
|
403
|
+
status: 400,
|
|
404
|
+
statusText: 'Bad Request',
|
|
405
|
+
text: async () => JSON.stringify(errorResponse),
|
|
406
|
+
json: async () => errorResponse
|
|
407
|
+
});
|
|
328
408
|
|
|
329
409
|
await expect(client.updateCase('1', {})).rejects.toThrow(
|
|
330
|
-
'TestRail
|
|
410
|
+
'TestRail: 400 Bad Request - {"error":"Invalid field"}'
|
|
331
411
|
);
|
|
332
412
|
});
|
|
333
413
|
|
|
334
|
-
test('
|
|
335
|
-
const
|
|
336
|
-
message: 'Network Error',
|
|
337
|
-
request: {},
|
|
338
|
-
isAxiosError: true,
|
|
339
|
-
toJSON: () => ({})
|
|
340
|
-
};
|
|
414
|
+
test('addRun posts fields directly', async () => {
|
|
415
|
+
const mockRun = { id: 1, name: 'New Run' };
|
|
341
416
|
|
|
342
|
-
|
|
417
|
+
fetchMock.mockResolvedValue({
|
|
418
|
+
ok: true,
|
|
419
|
+
json: async () => mockRun
|
|
420
|
+
});
|
|
343
421
|
|
|
344
|
-
await
|
|
345
|
-
|
|
422
|
+
const result = await client.addRun('1', { name: 'New Run', suite_id: 20 });
|
|
423
|
+
|
|
424
|
+
expect(result).toEqual(mockRun);
|
|
425
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
426
|
+
'https://testrail.io/index.php?/api/v2/add_run/1',
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
method: 'POST',
|
|
429
|
+
body: JSON.stringify({ name: 'New Run', suite_id: 20 })
|
|
430
|
+
})
|
|
346
431
|
);
|
|
347
432
|
});
|
|
348
433
|
|
|
349
|
-
test('
|
|
350
|
-
|
|
351
|
-
|
|
434
|
+
test('getTests handles pagination', async () => {
|
|
435
|
+
fetchMock
|
|
436
|
+
.mockImplementationOnce(() => Promise.resolve({
|
|
437
|
+
ok: true,
|
|
438
|
+
json: () => Promise.resolve({
|
|
439
|
+
tests: [{ id: 1, case_id: 1, status_id: 1, title: 'Test 1', run_id: 1 }],
|
|
440
|
+
_links: { next: '/api/v2/get_tests/1&offset=1' }
|
|
441
|
+
}),
|
|
442
|
+
} as Response))
|
|
443
|
+
.mockImplementationOnce(() => Promise.resolve({
|
|
444
|
+
ok: true,
|
|
445
|
+
json: () => Promise.resolve({
|
|
446
|
+
tests: [{ id: 2, case_id: 2, status_id: 1, title: 'Test 2', run_id: 1 }],
|
|
447
|
+
_links: { next: null }
|
|
448
|
+
}),
|
|
449
|
+
} as Response));
|
|
450
|
+
|
|
451
|
+
const tests = await client.getTests(1);
|
|
452
|
+
expect(tests).toHaveLength(2);
|
|
453
|
+
expect(tests[0].id).toBe(1);
|
|
454
|
+
expect(tests[1].id).toBe(2);
|
|
455
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
456
|
+
});
|
|
352
457
|
|
|
353
|
-
|
|
354
|
-
|
|
458
|
+
test('getTests with status_id adds filter', async () => {
|
|
459
|
+
fetchMock.mockResolvedValue({
|
|
460
|
+
ok: true,
|
|
461
|
+
json: async () => ({
|
|
462
|
+
tests: [],
|
|
463
|
+
_links: { next: null }
|
|
464
|
+
})
|
|
465
|
+
} as Response);
|
|
466
|
+
|
|
467
|
+
await client.getTests(1, [1, 5]);
|
|
468
|
+
|
|
469
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
470
|
+
expect.stringContaining('/get_tests/1&status_id=1,5'),
|
|
471
|
+
expect.any(Object)
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('getStatuses caches result', async () => {
|
|
476
|
+
const mockStatuses = [{ id: 1, name: 'passed', label: 'Passed' }];
|
|
477
|
+
fetchMock.mockResolvedValue({
|
|
478
|
+
ok: true,
|
|
479
|
+
json: async () => mockStatuses
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await client.getStatuses();
|
|
483
|
+
await client.getStatuses();
|
|
484
|
+
|
|
485
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('addResults posts results array', async () => {
|
|
489
|
+
const mockResults = [
|
|
490
|
+
{ id: 1, test_id: 100, status_id: 1, comment: 'Passed', defects: null },
|
|
491
|
+
{ id: 2, test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
fetchMock.mockResolvedValue({
|
|
495
|
+
ok: true,
|
|
496
|
+
json: async () => mockResults
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const results = await client.addResults(50, [
|
|
500
|
+
{ test_id: 100, status_id: 1, comment: 'Passed' },
|
|
501
|
+
{ test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
expect(results).toEqual(mockResults);
|
|
505
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
506
|
+
'https://testrail.io/index.php?/api/v2/add_results/50',
|
|
507
|
+
expect.objectContaining({
|
|
508
|
+
method: 'POST',
|
|
509
|
+
body: JSON.stringify({
|
|
510
|
+
results: [
|
|
511
|
+
{ test_id: 100, status_id: 1, comment: 'Passed' },
|
|
512
|
+
{ test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
513
|
+
]
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('addAttachmentToRun uploads file using FormData', async () => {
|
|
520
|
+
const mockAttachment = { attachment_id: 443 };
|
|
521
|
+
const mockFileBuffer = Buffer.from('file content');
|
|
522
|
+
|
|
523
|
+
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(mockFileBuffer);
|
|
524
|
+
|
|
525
|
+
fetchMock.mockResolvedValue({
|
|
526
|
+
ok: true,
|
|
527
|
+
json: async () => mockAttachment
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await client.addAttachmentToRun(100, '/path/to/file.txt', 'file.txt');
|
|
531
|
+
|
|
532
|
+
expect(result).toEqual(mockAttachment);
|
|
533
|
+
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/file.txt');
|
|
534
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
535
|
+
'https://testrail.io/index.php?/api/v2/add_attachment_to_run/100',
|
|
536
|
+
expect.objectContaining({
|
|
537
|
+
method: 'POST',
|
|
538
|
+
headers: expect.objectContaining({
|
|
539
|
+
'Authorization': expect.stringContaining('Basic')
|
|
540
|
+
}),
|
|
541
|
+
body: expect.any(FormData)
|
|
542
|
+
})
|
|
355
543
|
);
|
|
356
544
|
});
|
|
357
545
|
});
|