@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.
- package/README.md +12 -2
- package/dist/client/testrail.js +167 -0
- package/{src/index.ts → dist/index.js} +35 -35
- package/dist/tools/add_attachment_to_run.js +52 -0
- package/dist/tools/add_results.js +24 -0
- package/dist/tools/add_run.js +19 -0
- package/dist/tools/create_case.js +23 -0
- package/{src/tools/get_case.ts → dist/tools/get_case.js} +3 -12
- package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
- package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +10 -24
- package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +3 -10
- package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +4 -11
- package/dist/tools/get_statuses.js +13 -0
- package/dist/tools/get_templates.js +16 -0
- package/dist/tools/get_tests.js +17 -0
- package/{src/tools/update_case.ts → dist/tools/update_case.js} +2 -8
- package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +4 -8
- package/dist/types/custom.js +1 -0
- package/{src/types/testrail.ts → dist/types/testrail.js} +37 -24
- package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
- package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
- package/package.json +10 -2
- package/.env.example +0 -3
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -35
- package/CHANGELOG.md +0 -27
- package/jest.config.js +0 -24
- package/src/client/testrail.ts +0 -161
- package/src/tools/create_case.ts +0 -29
- package/src/tools/get_templates.ts +0 -23
- package/src/types/custom.ts +0 -19
- package/test/client/testrail.test.ts +0 -430
- package/test/tools/create_case.test.ts +0 -101
- package/test/tools/get_case.test.ts +0 -185
- package/test/tools/get_case_fields.test.ts +0 -132
- package/test/tools/get_cases.test.ts +0 -104
- package/test/tools/get_projects.test.ts +0 -38
- package/test/tools/get_sections.test.ts +0 -39
- package/test/tools/get_templates.test.ts +0 -38
- package/test/tools/update_case.test.ts +0 -102
- package/test/tools/update_cases.test.ts +0 -61
- package/test/utils/mapper.test.ts +0 -112
- package/test/utils/sanitizer.test.ts +0 -143
- package/tsconfig.json +0 -24
package/src/client/testrail.ts
DELETED
|
@@ -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 += `§ion_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
|
-
}
|
package/src/tools/create_case.ts
DELETED
|
@@ -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
|
-
};
|
package/src/types/custom.ts
DELETED
|
@@ -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§ion_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
|
-
});
|