@uarlouski/testrail-mcp-server 1.2.0 → 1.3.1

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 (44) hide show
  1. package/README.md +12 -2
  2. package/dist/client/testrail.js +167 -0
  3. package/{src/index.ts → dist/index.js} +35 -35
  4. package/dist/tools/add_attachment_to_run.js +52 -0
  5. package/dist/tools/add_results.js +24 -0
  6. package/dist/tools/add_run.js +19 -0
  7. package/dist/tools/create_case.js +23 -0
  8. package/{src/tools/get_case.ts → dist/tools/get_case.js} +3 -12
  9. package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
  10. package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +10 -24
  11. package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +3 -10
  12. package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +4 -11
  13. package/dist/tools/get_statuses.js +13 -0
  14. package/dist/tools/get_templates.js +16 -0
  15. package/dist/tools/get_tests.js +17 -0
  16. package/{src/tools/update_case.ts → dist/tools/update_case.js} +2 -8
  17. package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +4 -8
  18. package/dist/types/custom.js +1 -0
  19. package/{src/types/testrail.ts → dist/types/testrail.js} +37 -24
  20. package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
  21. package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
  22. package/package.json +10 -2
  23. package/.env.example +0 -3
  24. package/.github/workflows/ci.yml +0 -46
  25. package/.github/workflows/publish.yml +0 -35
  26. package/CHANGELOG.md +0 -27
  27. package/jest.config.js +0 -24
  28. package/src/client/testrail.ts +0 -161
  29. package/src/tools/create_case.ts +0 -29
  30. package/src/tools/get_templates.ts +0 -23
  31. package/src/types/custom.ts +0 -19
  32. package/test/client/testrail.test.ts +0 -430
  33. package/test/tools/create_case.test.ts +0 -101
  34. package/test/tools/get_case.test.ts +0 -185
  35. package/test/tools/get_case_fields.test.ts +0 -132
  36. package/test/tools/get_cases.test.ts +0 -104
  37. package/test/tools/get_projects.test.ts +0 -38
  38. package/test/tools/get_sections.test.ts +0 -39
  39. package/test/tools/get_templates.test.ts +0 -38
  40. package/test/tools/update_case.test.ts +0 -102
  41. package/test/tools/update_cases.test.ts +0 -61
  42. package/test/utils/mapper.test.ts +0 -112
  43. package/test/utils/sanitizer.test.ts +0 -143
  44. package/tsconfig.json +0 -24
@@ -1,161 +0,0 @@
1
- import { Case, Section, Priority, CaseType, CaseField, Template, Project } from "../types/testrail.js";
2
-
3
- interface PaginatedCasesResponse {
4
- cases: Case[];
5
- _links: { next: string | null };
6
- }
7
-
8
- const API_INDEX = '/index.php?';
9
- const API_BASE_V2 = `${API_INDEX}/api/v2`;
10
-
11
- export class TestRailClient {
12
- private baseUrl: string;
13
- private headers: HeadersInit;
14
- private prioritiesPromise: Promise<Priority[]> | null = null;
15
- private caseTypesPromise: Promise<CaseType[]> | null = null;
16
- private caseFieldsPromise: Promise<CaseField[]> | null = null;
17
- private projectsPromise: Promise<Project[]> | null = null;
18
- private templatesPromiseMap: Map<string, Promise<Template[]>> = new Map();
19
-
20
- constructor(baseUrl: string, email: string, apiKey: string) {
21
- this.baseUrl = baseUrl.replace(/\/$/, "");
22
- const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
23
- this.headers = {
24
- "Content-Type": "application/json",
25
- "Authorization": `Basic ${auth}`,
26
- };
27
- }
28
-
29
- async getCase(caseId: string): Promise<Case> {
30
- return this.get<Case>(`${API_BASE_V2}/get_case/${caseId}`);
31
- }
32
-
33
- async getCases(projectId: string, sectionId?: string, filter?: Record<string, string>): Promise<Case[]> {
34
- let url = `${API_BASE_V2}/get_cases/${projectId}`;
35
-
36
- if (sectionId) {
37
- url += `&section_id=${sectionId}`;
38
- }
39
-
40
- if (filter) {
41
- for (const [key, value] of Object.entries(filter)) {
42
- url += `&${key}=${encodeURIComponent(value)}`;
43
- }
44
- }
45
-
46
- const allCases: Case[] = [];
47
- let nextUrl: string | null = url;
48
-
49
- while (nextUrl) {
50
- const response: PaginatedCasesResponse = await this.get<PaginatedCasesResponse>(nextUrl);
51
-
52
- allCases.push(...response.cases);
53
-
54
- nextUrl = response._links?.next;
55
- if (nextUrl) {
56
- nextUrl = `${API_INDEX}${nextUrl}`;
57
- }
58
- }
59
-
60
- return allCases;
61
- }
62
-
63
- async getSection(sectionId: string): Promise<Section> {
64
- return this.get<Section>(`${API_BASE_V2}/get_section/${sectionId}`);
65
- }
66
-
67
- async getPriorities(): Promise<Priority[]> {
68
- if (!this.prioritiesPromise) {
69
- this.prioritiesPromise = this.get<Priority[]>(`${API_BASE_V2}/get_priorities`);
70
- }
71
- return this.prioritiesPromise;
72
- }
73
-
74
- async getCaseTypes(): Promise<CaseType[]> {
75
- if (!this.caseTypesPromise) {
76
- this.caseTypesPromise = this.get<CaseType[]>(`${API_BASE_V2}/get_case_types`);
77
- }
78
- return this.caseTypesPromise;
79
- }
80
-
81
- async getCaseFields(): Promise<CaseField[]> {
82
- if (!this.caseFieldsPromise) {
83
- this.caseFieldsPromise = this.get<CaseField[]>(`${API_BASE_V2}/get_case_fields`);
84
- }
85
- return this.caseFieldsPromise;
86
- }
87
-
88
- async getTemplates(projectId: string): Promise<Template[]> {
89
- if (!this.templatesPromiseMap.has(projectId)) {
90
- this.templatesPromiseMap.set(
91
- projectId,
92
- this.get<Template[]>(`${API_BASE_V2}/get_templates/${projectId}`)
93
- );
94
- }
95
- return this.templatesPromiseMap.get(projectId)!;
96
- }
97
-
98
- async updateCase(caseId: string, fields: Record<string, any>): Promise<Case> {
99
- return this.post<Case>(`${API_BASE_V2}/update_case/${caseId}`, fields);
100
- }
101
-
102
- async updateCases(caseIds: number[], fields: Record<string, any>): Promise<Case[]> {
103
- // Assume the project is operating in single suite mode, get suite_id from the first case
104
- const caseData = await this.getCase(String(caseIds[0]));
105
-
106
- const response = await this.post<{ updated_cases: Case[] }>(`${API_BASE_V2}/update_cases/${caseData.suite_id}`, {
107
- case_ids: caseIds,
108
- ...fields,
109
- });
110
-
111
- return response.updated_cases;
112
- }
113
-
114
- async createCase(sectionId: string, fields: Record<string, any>): Promise<Case> {
115
- return this.post<Case>(`${API_BASE_V2}/add_case/${sectionId}`, fields);
116
- }
117
-
118
- async getSections(projectId: string): Promise<Section[]> {
119
- const response = await this.get<{ sections: Section[] }>(`${API_BASE_V2}/get_sections/${projectId}`);
120
- return response.sections;
121
- }
122
-
123
- async getProjects(): Promise<Project[]> {
124
- if (!this.projectsPromise) {
125
- this.projectsPromise = this.get<{ projects: Project[] }>(`${API_BASE_V2}/get_projects`)
126
- .then(response => response.projects);
127
- }
128
- return this.projectsPromise;
129
- }
130
-
131
- private async get<T>(endpoint: string): Promise<T> {
132
- return this.executeRequest<T>('GET', endpoint);
133
- }
134
-
135
- private async post<T>(endpoint: string, data: Record<string, any>): Promise<T> {
136
- return this.executeRequest<T>('POST', endpoint, data);
137
- }
138
-
139
- private async executeRequest<T>(method: 'GET' | 'POST', endpoint: string, data?: Record<string, any>): Promise<T> {
140
- const url = `${this.baseUrl}${endpoint}`;
141
- const params: RequestInit = {
142
- method,
143
- headers: this.headers,
144
- };
145
-
146
- if (data) {
147
- params.body = JSON.stringify(data);
148
- }
149
-
150
- const response = await fetch(url, params);
151
-
152
- if (!response.ok) {
153
- let errorMessage = `TestRail: ${response.status} ${response.statusText}`;
154
- const errorText = await response.text();
155
- errorMessage += ` - ${errorText}`;
156
- throw new Error(errorMessage);
157
- }
158
-
159
- return await response.json() as T;
160
- }
161
- }
@@ -1,29 +0,0 @@
1
- import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
-
5
- const parameters = {
6
- section_id: z.string().describe("The ID of the section where the case should be created"),
7
- title: z.string().describe("The title of the test case (required)"),
8
- fields: z.record(z.string(), z.any()).optional().describe("Optional object containing additional fields. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
9
- };
10
-
11
- export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
12
- name: "create_case",
13
- description: "Create a new test case in TestRail. IMPORTANT: Before creating a case, first call get_templates to discover available templates for the project, then use get_case_fields to see which fields are available for the chosen template. Requires section_id and title.",
14
- parameters,
15
- handler: async ({ section_id, title, fields }, client) => {
16
- const caseData = {
17
- title,
18
- ...fields,
19
- };
20
-
21
- const createdCase = await client.createCase(section_id, caseData);
22
-
23
- return {
24
- success: true,
25
- case_id: createdCase.id,
26
- message: `Test case C${createdCase.id} created successfully`,
27
- };
28
- }
29
- };
@@ -1,23 +0,0 @@
1
- import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
- import { TemplateSchema } from "../types/testrail.js";
5
-
6
- const parameters = {
7
- project_id: z.string().describe("The ID of the project"),
8
- };
9
-
10
- export const getTemplatesTool: ToolDefinition<typeof parameters, TestRailClient> = {
11
- name: "get_templates",
12
- description: "Get available test case templates for a project. Returns template IDs and names. Use template IDs with get_case_fields to understand which fields apply to each template.",
13
- parameters,
14
- handler: async ({ project_id }, client) => {
15
- const templates = await client.getTemplates(project_id);
16
-
17
- const response = {
18
- templates: templates.map(t => TemplateSchema.parse(t)),
19
- };
20
-
21
- return response;
22
- }
23
- };
@@ -1,19 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export interface TestCaseResponse {
4
- id: number;
5
- title: string;
6
- section: string;
7
- type: string;
8
- priority: string;
9
- labels: string[];
10
- references: string | null;
11
- [key: string]: any;
12
- }
13
-
14
- export interface ToolDefinition<T extends z.ZodRawShape, Context = any> {
15
- name: string;
16
- description: string;
17
- parameters: T;
18
- handler: (args: z.infer<z.ZodObject<T>>, context: Context) => Promise<Record<string, any>>;
19
- }
@@ -1,430 +0,0 @@
1
- import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
- import { TestRailClient } from '../../src/client/testrail.js';
3
-
4
- describe('TestRailClient', () => {
5
- let client: TestRailClient;
6
- let fetchMock: any;
7
-
8
- beforeEach(() => {
9
- fetchMock = jest.spyOn(global, 'fetch');
10
- client = new TestRailClient('https://testrail.io/', 'user', 'apikey');
11
- });
12
-
13
- afterEach(() => {
14
- jest.restoreAllMocks();
15
- });
16
-
17
- test('constructor cleans trailing slash from baseUrl', async () => {
18
- const mockData = { id: 1, title: 'Test Case', template_id: 1 };
19
- fetchMock.mockResolvedValue({
20
- ok: true,
21
- json: async () => mockData
22
- });
23
-
24
- await client.getCase('1');
25
-
26
- expect(fetchMock).toHaveBeenCalledWith('https://testrail.io/index.php?/api/v2/get_case/1', expect.any(Object));
27
- });
28
-
29
- test('getCase returns data on success', async () => {
30
- const mockData = { id: 1, title: 'Test Case', template_id: 1 };
31
- fetchMock.mockResolvedValue({
32
- ok: true,
33
- json: async () => mockData
34
- });
35
-
36
- const result = await client.getCase('1');
37
- expect(result).toEqual(mockData);
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
- }));
45
- });
46
-
47
- test('getCase throws formatted error on API error', async () => {
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
- });
56
-
57
- await expect(client.getCase('999')).rejects.toThrow(
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'
73
- );
74
- });
75
-
76
- test('getCases returns cases without pagination', async () => {
77
- const mockCases = [{ id: 1, title: 'Case 1' }, { id: 2, title: 'Case 2' }];
78
- fetchMock.mockResolvedValue({
79
- ok: true,
80
- json: async () => ({ cases: mockCases, _links: { next: null } })
81
- });
82
-
83
- const result = await client.getCases('1');
84
- expect(result).toEqual(mockCases);
85
- expect(fetchMock).toHaveBeenCalledWith(
86
- 'https://testrail.io/index.php?/api/v2/get_cases/1',
87
- expect.objectContaining({ method: 'GET' })
88
- );
89
- });
90
-
91
- test('getCases handles response with undefined _links', async () => {
92
- const mockCases = [{ id: 1, title: 'Case 1' }];
93
- fetchMock.mockResolvedValue({
94
- ok: true,
95
- json: async () => ({ cases: mockCases })
96
- });
97
-
98
- const result = await client.getCases('1');
99
- expect(result).toEqual(mockCases);
100
- });
101
-
102
- test('getCases handles pagination', async () => {
103
- const page1Cases = [{ id: 1, title: 'Case 1' }];
104
- const page2Cases = [{ id: 2, title: 'Case 2' }];
105
-
106
- fetchMock
107
- .mockResolvedValueOnce({
108
- ok: true,
109
- json: async () => ({ cases: page1Cases, _links: { next: '/api/v2/get_cases/1&offset=1' } })
110
- })
111
- .mockResolvedValueOnce({
112
- ok: true,
113
- json: async () => ({ cases: page2Cases, _links: { next: null } })
114
- });
115
-
116
- const result = await client.getCases('1');
117
- expect(result).toEqual([...page1Cases, ...page2Cases]);
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
- );
127
- });
128
-
129
- test('getCases with sectionId adds section filter', async () => {
130
- fetchMock.mockResolvedValue({
131
- ok: true,
132
- json: async () => ({ cases: [], _links: { next: null } })
133
- });
134
-
135
- await client.getCases('1', '5');
136
- expect(fetchMock).toHaveBeenCalledWith(
137
- 'https://testrail.io/index.php?/api/v2/get_cases/1&section_id=5',
138
- expect.any(Object)
139
- );
140
- });
141
-
142
- test('getCases with filter adds query params', async () => {
143
- fetchMock.mockResolvedValue({
144
- ok: true,
145
- json: async () => ({ cases: [], _links: { next: null } })
146
- });
147
-
148
- await client.getCases('1', undefined, { 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)
152
- );
153
- });
154
-
155
- test('getSection returns data on success', async () => {
156
- const mockData = {
157
- id: 1,
158
- name: 'Section 1',
159
- description: null,
160
- parent_id: null,
161
- depth: 0,
162
- display_order: 1,
163
- suite_id: 1
164
- };
165
- fetchMock.mockResolvedValue({
166
- ok: true,
167
- json: async () => mockData
168
- });
169
-
170
- const result = await client.getSection('1');
171
- expect(result).toEqual(mockData);
172
- expect(fetchMock).toHaveBeenCalledWith(
173
- 'https://testrail.io/index.php?/api/v2/get_section/1',
174
- expect.any(Object)
175
- );
176
- });
177
-
178
- test('getPriorities returns data on success', async () => {
179
- const mockData = [{
180
- id: 1,
181
- is_default: true,
182
- name: 'Low',
183
- priority: 1,
184
- short_name: 'Low'
185
- }];
186
- fetchMock.mockResolvedValue({
187
- ok: true,
188
- json: async () => mockData
189
- });
190
-
191
- const result = await client.getPriorities();
192
- expect(result).toEqual(mockData);
193
- expect(fetchMock).toHaveBeenCalledWith(
194
- 'https://testrail.io/index.php?/api/v2/get_priorities',
195
- expect.any(Object)
196
- );
197
- });
198
-
199
- test('getPriorities caches result', async () => {
200
- const mockData = [{ id: 1, name: 'Low' }];
201
- fetchMock.mockResolvedValue({
202
- ok: true,
203
- json: async () => mockData
204
- });
205
-
206
- await client.getPriorities();
207
- await client.getPriorities();
208
-
209
- expect(fetchMock).toHaveBeenCalledTimes(1);
210
- });
211
-
212
- test('getCaseTypes returns data on success', async () => {
213
- const mockData = [{
214
- id: 1,
215
- is_default: true,
216
- name: 'Automated'
217
- }];
218
- fetchMock.mockResolvedValue({
219
- ok: true,
220
- json: async () => mockData
221
- });
222
-
223
- const result = await client.getCaseTypes();
224
- expect(result).toEqual(mockData);
225
- expect(fetchMock).toHaveBeenCalledWith(
226
- 'https://testrail.io/index.php?/api/v2/get_case_types',
227
- expect.any(Object)
228
- );
229
- });
230
-
231
- test('getCaseFields returns data on success', async () => {
232
- const mockData = [{
233
- id: 1,
234
- name: 'custom_field',
235
- system_name: 'custom_field',
236
- label: 'Custom Field',
237
- type_id: 1
238
- }];
239
- fetchMock.mockResolvedValue({
240
- ok: true,
241
- json: async () => mockData
242
- });
243
-
244
- const result = await client.getCaseFields();
245
- expect(result).toEqual(mockData);
246
- expect(fetchMock).toHaveBeenCalledWith(
247
- 'https://testrail.io/index.php?/api/v2/get_case_fields',
248
- expect.any(Object)
249
- );
250
- });
251
-
252
- test('getCaseFields caches result', async () => {
253
- const mockData = [{ id: 1, name: 'field' }];
254
- fetchMock.mockResolvedValue({
255
- ok: true,
256
- json: async () => mockData
257
- });
258
-
259
- await client.getCaseFields();
260
- await client.getCaseFields();
261
-
262
- expect(fetchMock).toHaveBeenCalledTimes(1);
263
- });
264
-
265
- test('getTemplates returns data on success', async () => {
266
- const mockData = [{ id: 1, name: 'Test Template', is_default: true }];
267
- fetchMock.mockResolvedValue({
268
- ok: true,
269
- json: async () => mockData
270
- });
271
-
272
- const result = await client.getTemplates('1');
273
- expect(result).toEqual(mockData);
274
- expect(fetchMock).toHaveBeenCalledWith(
275
- 'https://testrail.io/index.php?/api/v2/get_templates/1',
276
- expect.any(Object)
277
- );
278
- });
279
-
280
- test('getTemplates caches result per project', async () => {
281
- const mockData1 = [{ id: 1, name: 'Template 1' }];
282
- const mockData2 = [{ id: 2, name: 'Template 2' }];
283
- fetchMock
284
- .mockResolvedValueOnce({
285
- ok: true,
286
- json: async () => mockData1
287
- })
288
- .mockResolvedValueOnce({
289
- ok: true,
290
- json: async () => mockData2
291
- });
292
-
293
- await client.getTemplates('1');
294
- await client.getTemplates('1');
295
- await client.getTemplates('2');
296
-
297
- expect(fetchMock).toHaveBeenCalledTimes(2);
298
- });
299
-
300
- test('updateCase sends POST request', async () => {
301
- const mockData = { id: 1, title: 'Updated Case' };
302
- fetchMock.mockResolvedValue({
303
- ok: true,
304
- json: async () => mockData
305
- });
306
-
307
- const result = await client.updateCase('1', { title: 'Updated Case' });
308
- expect(result).toEqual(mockData);
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
- })
315
- );
316
- });
317
-
318
- test('updateCases gets suite_id from first case and updates all', async () => {
319
- const mockCase = { id: 1, suite_id: 10 };
320
- const mockUpdated = [{ id: 1, title: 'Updated' }, { id: 2, title: 'Updated' }];
321
-
322
- fetchMock
323
- .mockResolvedValueOnce({
324
- ok: true,
325
- json: async () => mockCase
326
- })
327
- .mockResolvedValueOnce({
328
- ok: true,
329
- json: async () => ({ updated_cases: mockUpdated })
330
- });
331
-
332
- const result = await client.updateCases([1, 2], { title: 'Updated' });
333
- expect(result).toEqual(mockUpdated);
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
- })
344
- );
345
- });
346
-
347
- test('createCase sends POST request', async () => {
348
- const mockData = { id: 1, title: 'New Case' };
349
- fetchMock.mockResolvedValue({
350
- ok: true,
351
- json: async () => mockData
352
- });
353
-
354
- const result = await client.createCase('5', { title: 'New Case' });
355
- expect(result).toEqual(mockData);
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
- })
362
- );
363
- });
364
-
365
- test('getSections returns sections array', async () => {
366
- const mockSections = [{ id: 1, name: 'Section 1' }, { id: 2, name: 'Section 2' }];
367
- fetchMock.mockResolvedValue({
368
- ok: true,
369
- json: async () => ({ sections: mockSections })
370
- });
371
-
372
- const result = await client.getSections('1');
373
- expect(result).toEqual(mockSections);
374
- expect(fetchMock).toHaveBeenCalledWith(
375
- 'https://testrail.io/index.php?/api/v2/get_sections/1',
376
- expect.any(Object)
377
- );
378
- });
379
-
380
- test('getProjects returns projects array', async () => {
381
- const mockProjects = [{ id: 1, name: 'Project 1' }];
382
- fetchMock.mockResolvedValue({
383
- ok: true,
384
- json: async () => ({ projects: mockProjects })
385
- });
386
-
387
- const result = await client.getProjects();
388
- expect(result).toEqual(mockProjects);
389
- expect(fetchMock).toHaveBeenCalledWith(
390
- 'https://testrail.io/index.php?/api/v2/get_projects',
391
- expect.any(Object)
392
- );
393
- });
394
-
395
- test('getProjects caches result', async () => {
396
- const mockProjects = [{ id: 1, name: 'Project 1' }];
397
- fetchMock.mockResolvedValue({
398
- ok: true,
399
- json: async () => ({ projects: mockProjects })
400
- });
401
-
402
- await client.getProjects();
403
- await client.getProjects();
404
-
405
- expect(fetchMock).toHaveBeenCalledTimes(1);
406
- });
407
-
408
- test('postRequest throws formatted error on API error', async () => {
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
- });
417
-
418
- await expect(client.updateCase('1', {})).rejects.toThrow(
419
- 'TestRail: 400 Bad Request - {"error":"Invalid field"}'
420
- );
421
- });
422
-
423
- test('handleError formats error when network fails', async () => {
424
- fetchMock.mockRejectedValue(new Error('Network Error'));
425
-
426
- await expect(client.getCase('1')).rejects.toThrow(
427
- 'Network Error'
428
- );
429
- });
430
- });