@uarlouski/testrail-mcp-server 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +46 -0
- package/.github/workflows/publish.yml +35 -0
- package/CHANGELOG.md +20 -0
- package/LICENSE +201 -0
- package/README.md +41 -0
- package/jest.config.js +24 -0
- package/package.json +50 -0
- package/src/client/testrail.ts +172 -0
- package/src/index.ts +55 -0
- package/src/tools/create_case.ts +36 -0
- package/src/tools/get_case.ts +51 -0
- package/src/tools/get_case_fields.ts +97 -0
- package/src/tools/get_cases.ts +67 -0
- package/src/tools/get_projects.ts +40 -0
- package/src/tools/get_sections.ts +41 -0
- package/src/tools/get_templates.ts +41 -0
- package/src/tools/update_case.ts +32 -0
- package/src/tools/update_cases.ts +31 -0
- package/src/types/custom.ts +34 -0
- package/src/types/testrail.ts +87 -0
- package/src/utils/mapper.ts +90 -0
- package/src/utils/sanitizer.ts +21 -0
- package/test/client/testrail.test.ts +357 -0
- package/test/tools/create_case.test.ts +107 -0
- package/test/tools/get_case.test.ts +200 -0
- package/test/tools/get_case_fields.test.ts +205 -0
- package/test/tools/get_cases.test.ts +193 -0
- package/test/tools/get_projects.test.ts +75 -0
- package/test/tools/get_sections.test.ts +68 -0
- package/test/tools/get_templates.test.ts +67 -0
- package/test/tools/update_case.test.ts +108 -0
- package/test/tools/update_cases.test.ts +88 -0
- package/test/utils/mapper.test.ts +112 -0
- package/test/utils/sanitizer.test.ts +59 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getCaseFieldsTool } from '../../src/tools/get_case_fields.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { CaseField } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_case_fields tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getCaseFieldsMock: jest.Mock<() => Promise<CaseField[]>>;
|
|
9
|
+
|
|
10
|
+
const mockCaseFields: CaseField[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
name: 'is_automated',
|
|
14
|
+
system_name: 'custom_is_automated',
|
|
15
|
+
label: 'Is Automated',
|
|
16
|
+
type_id: 5,
|
|
17
|
+
template_ids: [],
|
|
18
|
+
include_all: true,
|
|
19
|
+
is_active: true,
|
|
20
|
+
display_order: 1,
|
|
21
|
+
description: 'Whether test is automated',
|
|
22
|
+
configs: [{ context: { is_global: true, project_ids: null }, options: { is_required: false } }]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 2,
|
|
26
|
+
name: 'automation_priority',
|
|
27
|
+
system_name: 'custom_automation_priority',
|
|
28
|
+
label: 'Automation Priority',
|
|
29
|
+
type_id: 6,
|
|
30
|
+
template_ids: [],
|
|
31
|
+
include_all: true,
|
|
32
|
+
is_active: true,
|
|
33
|
+
display_order: 2,
|
|
34
|
+
description: 'Priority for automation',
|
|
35
|
+
configs: [{ context: { is_global: true, project_ids: null }, options: { items: '1, P0\n2, P1\n3, P2', is_required: true } }]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 3,
|
|
39
|
+
name: 'inactive_field',
|
|
40
|
+
system_name: 'custom_inactive_field',
|
|
41
|
+
label: 'Inactive Field',
|
|
42
|
+
type_id: 1,
|
|
43
|
+
template_ids: [],
|
|
44
|
+
include_all: true,
|
|
45
|
+
is_active: false,
|
|
46
|
+
display_order: 3,
|
|
47
|
+
description: null,
|
|
48
|
+
configs: []
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 4,
|
|
52
|
+
name: 'components',
|
|
53
|
+
system_name: 'custom_components',
|
|
54
|
+
label: 'Components',
|
|
55
|
+
type_id: 12,
|
|
56
|
+
template_ids: [],
|
|
57
|
+
include_all: true,
|
|
58
|
+
is_active: true,
|
|
59
|
+
display_order: 4,
|
|
60
|
+
description: 'Test components',
|
|
61
|
+
configs: [{ context: { is_global: true, project_ids: null }, options: { items: '1, UI\n2, API\n3, Database' } }]
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
getCaseFieldsMock = jest.fn<() => Promise<CaseField[]>>().mockResolvedValue(mockCaseFields);
|
|
67
|
+
|
|
68
|
+
mockClient = {
|
|
69
|
+
getCaseFields: getCaseFieldsMock
|
|
70
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('exports correct tool definition', () => {
|
|
74
|
+
expect(getCaseFieldsTool.name).toBe('get_case_fields');
|
|
75
|
+
expect(getCaseFieldsTool.description).toContain('field schema');
|
|
76
|
+
expect(getCaseFieldsTool.parameters).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('handler fetches and returns field schema', async () => {
|
|
80
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
81
|
+
|
|
82
|
+
expect(result).toBeDefined();
|
|
83
|
+
expect(result.content[0].type).toBe('text');
|
|
84
|
+
|
|
85
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
86
|
+
expect(parsed.fields).toHaveLength(11); // 8 system fields + 3 active custom fields
|
|
87
|
+
expect(mockClient.getCaseFields).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns only essential schema fields', async () => {
|
|
91
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
92
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
93
|
+
|
|
94
|
+
const field = parsed.fields[0];
|
|
95
|
+
expect(Object.keys(field).sort()).toEqual(['is_required', 'label', 'system_name', 'type']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('returns correct field types', async () => {
|
|
99
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
100
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
101
|
+
|
|
102
|
+
const checkboxField = parsed.fields.find((f: any) => f.system_name === 'custom_is_automated');
|
|
103
|
+
expect(checkboxField.type).toBe('Checkbox');
|
|
104
|
+
|
|
105
|
+
const dropdownField = parsed.fields.find((f: any) => f.system_name === 'custom_automation_priority');
|
|
106
|
+
expect(dropdownField.type).toBe('Dropdown');
|
|
107
|
+
|
|
108
|
+
const multiSelectField = parsed.fields.find((f: any) => f.system_name === 'custom_components');
|
|
109
|
+
expect(multiSelectField.type).toBe('Multi-select');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('parses dropdown options correctly', async () => {
|
|
113
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
114
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
115
|
+
|
|
116
|
+
const dropdownField = parsed.fields.find((f: any) => f.system_name === 'custom_automation_priority');
|
|
117
|
+
expect(dropdownField.options).toEqual(['P0', 'P1', 'P2']);
|
|
118
|
+
|
|
119
|
+
const multiSelectField = parsed.fields.find((f: any) => f.system_name === 'custom_components');
|
|
120
|
+
expect(multiSelectField.options).toEqual(['UI', 'API', 'Database']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('identifies required fields', async () => {
|
|
124
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
125
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
126
|
+
|
|
127
|
+
const optionalField = parsed.fields.find((f: any) => f.system_name === 'custom_is_automated');
|
|
128
|
+
expect(optionalField.is_required).toBe(false);
|
|
129
|
+
|
|
130
|
+
const requiredField = parsed.fields.find((f: any) => f.system_name === 'custom_automation_priority');
|
|
131
|
+
expect(requiredField.is_required).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('filters out inactive fields', async () => {
|
|
135
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
136
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
137
|
+
|
|
138
|
+
const inactiveField = parsed.fields.find((f: any) => f.system_name === 'custom_inactive_field');
|
|
139
|
+
expect(inactiveField).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('handler returns error on failure', async () => {
|
|
143
|
+
getCaseFieldsMock.mockRejectedValue(new Error('API Error'));
|
|
144
|
+
|
|
145
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
146
|
+
|
|
147
|
+
expect(result).toEqual({
|
|
148
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
149
|
+
isError: true
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('includes template_ids for template-specific fields', async () => {
|
|
154
|
+
const templateSpecificFields: CaseField[] = [
|
|
155
|
+
{
|
|
156
|
+
id: 5,
|
|
157
|
+
name: 'steps',
|
|
158
|
+
system_name: 'custom_steps',
|
|
159
|
+
label: 'Steps',
|
|
160
|
+
type_id: 3,
|
|
161
|
+
template_ids: [1, 2],
|
|
162
|
+
include_all: false,
|
|
163
|
+
is_active: true,
|
|
164
|
+
display_order: 5,
|
|
165
|
+
description: 'Test steps',
|
|
166
|
+
configs: []
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
getCaseFieldsMock.mockResolvedValue(templateSpecificFields);
|
|
171
|
+
|
|
172
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
173
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
174
|
+
|
|
175
|
+
const stepsField = parsed.fields.find((f: any) => f.system_name === 'custom_steps');
|
|
176
|
+
expect(stepsField.template_ids).toEqual([1, 2]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('handles unknown field type gracefully', async () => {
|
|
180
|
+
const unknownTypeFields: CaseField[] = [
|
|
181
|
+
{
|
|
182
|
+
id: 6,
|
|
183
|
+
name: 'unknown_type',
|
|
184
|
+
system_name: 'custom_unknown_type',
|
|
185
|
+
label: 'Unknown Type Field',
|
|
186
|
+
type_id: 999,
|
|
187
|
+
template_ids: [],
|
|
188
|
+
include_all: true,
|
|
189
|
+
is_active: true,
|
|
190
|
+
display_order: 6,
|
|
191
|
+
description: null,
|
|
192
|
+
configs: []
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
getCaseFieldsMock.mockResolvedValue(unknownTypeFields);
|
|
197
|
+
|
|
198
|
+
const result = await getCaseFieldsTool.handler({}, mockClient);
|
|
199
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
200
|
+
|
|
201
|
+
const unknownField = parsed.fields.find((f: any) => f.system_name === 'custom_unknown_type');
|
|
202
|
+
expect(unknownField.type).toBe('Unknown (999)');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getCasesTool } from '../../src/tools/get_cases.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Case } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_cases tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getCasesMock: jest.Mock<(projectId: string, sectionId?: string, filter?: Record<string, string>) => Promise<Case[]>>;
|
|
9
|
+
|
|
10
|
+
const mockCases: Case[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 1, title: 'Login test', section_id: 1, template_id: 1, type_id: 1,
|
|
13
|
+
priority_id: 2, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
14
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
15
|
+
suite_id: 1, display_order: 1, is_deleted: 0, labels: [], custom_automation_status: 1
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 2, title: 'Logout test', section_id: 1, template_id: 1, type_id: 1,
|
|
19
|
+
priority_id: 3, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
20
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
21
|
+
suite_id: 1, display_order: 2, is_deleted: 0, labels: [], custom_automation_status: 2
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 3, title: 'Registration test', section_id: 2, template_id: 1, type_id: 1,
|
|
25
|
+
priority_id: 1, milestone_id: null, refs: null, created_by: 1, created_on: 1700000000,
|
|
26
|
+
updated_by: 1, updated_on: 1700000000, estimate: null, estimate_forecast: null,
|
|
27
|
+
suite_id: 1, display_order: 3, is_deleted: 0, labels: [], custom_automation_status: 0
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
getCasesMock = jest.fn<(projectId: string, sectionId?: string, filter?: Record<string, string>) => Promise<Case[]>>()
|
|
33
|
+
.mockResolvedValue(mockCases);
|
|
34
|
+
|
|
35
|
+
mockClient = {
|
|
36
|
+
getCases: getCasesMock
|
|
37
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('exports correct tool definition', () => {
|
|
41
|
+
expect(getCasesTool.name).toBe('get_cases');
|
|
42
|
+
expect(getCasesTool.description).toContain('cases');
|
|
43
|
+
expect(getCasesTool.parameters).toBeDefined();
|
|
44
|
+
expect(getCasesTool.parameters.project_id).toBeDefined();
|
|
45
|
+
expect(getCasesTool.parameters.section_id).toBeDefined();
|
|
46
|
+
expect(getCasesTool.parameters.filter).toBeDefined();
|
|
47
|
+
expect(getCasesTool.parameters.fields).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('handler fetches and returns cases', async () => {
|
|
51
|
+
const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
|
|
52
|
+
|
|
53
|
+
expect(result).toBeDefined();
|
|
54
|
+
expect(result.content[0].type).toBe('text');
|
|
55
|
+
|
|
56
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
57
|
+
expect(parsed.cases).toHaveLength(3);
|
|
58
|
+
expect(mockClient.getCases).toHaveBeenCalledWith('1', undefined, undefined);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('passes section_id when provided', async () => {
|
|
62
|
+
await getCasesTool.handler({ project_id: '1', section_id: '2' }, mockClient);
|
|
63
|
+
|
|
64
|
+
expect(mockClient.getCases).toHaveBeenCalledWith('1', '2', undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('passes filter when provided', async () => {
|
|
68
|
+
const filter = { priority_id: '1,2', type_id: '3' };
|
|
69
|
+
await getCasesTool.handler({ project_id: '1', filter }, mockClient);
|
|
70
|
+
|
|
71
|
+
expect(mockClient.getCases).toHaveBeenCalledWith('1', undefined, filter);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('returns correct case structure', async () => {
|
|
75
|
+
const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
|
|
76
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
77
|
+
|
|
78
|
+
expect(parsed.cases[0]).toEqual({
|
|
79
|
+
id: 1,
|
|
80
|
+
title: 'Login test',
|
|
81
|
+
suite_id: 1,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(parsed.cases[2]).toEqual({
|
|
85
|
+
id: 3,
|
|
86
|
+
title: 'Registration test',
|
|
87
|
+
suite_id: 1,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('includes additional fields when specified', async () => {
|
|
92
|
+
const result = await getCasesTool.handler(
|
|
93
|
+
{ project_id: '1', fields: ['priority_id', 'type_id', 'custom_automation_status'] },
|
|
94
|
+
mockClient
|
|
95
|
+
);
|
|
96
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
97
|
+
|
|
98
|
+
expect(parsed.cases[0]).toEqual({
|
|
99
|
+
id: 1,
|
|
100
|
+
title: 'Login test',
|
|
101
|
+
suite_id: 1,
|
|
102
|
+
priority_id: 2,
|
|
103
|
+
type_id: 1,
|
|
104
|
+
custom_automation_status: 1,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(parsed.cases[1]).toEqual({
|
|
108
|
+
id: 2,
|
|
109
|
+
title: 'Logout test',
|
|
110
|
+
suite_id: 1,
|
|
111
|
+
priority_id: 3,
|
|
112
|
+
type_id: 1,
|
|
113
|
+
custom_automation_status: 2,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('ignores non-existent fields', async () => {
|
|
118
|
+
const result = await getCasesTool.handler(
|
|
119
|
+
{ project_id: '1', fields: ['priority_id', 'nonexistent_field'] },
|
|
120
|
+
mockClient
|
|
121
|
+
);
|
|
122
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
123
|
+
|
|
124
|
+
expect(parsed.cases[0]).toEqual({
|
|
125
|
+
id: 1,
|
|
126
|
+
title: 'Login test',
|
|
127
|
+
suite_id: 1,
|
|
128
|
+
priority_id: 2,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('handler returns error on failure', async () => {
|
|
133
|
+
getCasesMock.mockRejectedValue(new Error('API Error'));
|
|
134
|
+
|
|
135
|
+
const result = await getCasesTool.handler({ project_id: '1' }, mockClient);
|
|
136
|
+
|
|
137
|
+
expect(result).toEqual({
|
|
138
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
139
|
+
isError: true
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('filters cases using where clause with single condition', async () => {
|
|
144
|
+
const result = await getCasesTool.handler(
|
|
145
|
+
{ project_id: '1', where: { custom_automation_status: 1 } },
|
|
146
|
+
mockClient
|
|
147
|
+
);
|
|
148
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
149
|
+
|
|
150
|
+
expect(parsed.cases).toHaveLength(1);
|
|
151
|
+
expect(parsed.cases[0].id).toBe(1);
|
|
152
|
+
expect(parsed.cases[0].title).toBe('Login test');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('filters cases using where clause with multiple conditions', async () => {
|
|
156
|
+
const result = await getCasesTool.handler(
|
|
157
|
+
{ project_id: '1', where: { custom_automation_status: 2, priority_id: 3 } },
|
|
158
|
+
mockClient
|
|
159
|
+
);
|
|
160
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
161
|
+
|
|
162
|
+
expect(parsed.cases).toHaveLength(1);
|
|
163
|
+
expect(parsed.cases[0].id).toBe(2);
|
|
164
|
+
expect(parsed.cases[0].title).toBe('Logout test');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('returns empty array when where clause matches no cases', async () => {
|
|
168
|
+
const result = await getCasesTool.handler(
|
|
169
|
+
{ project_id: '1', where: { custom_automation_status: 999 } },
|
|
170
|
+
mockClient
|
|
171
|
+
);
|
|
172
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
173
|
+
|
|
174
|
+
expect(parsed.cases).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('combines where clause with fields parameter', async () => {
|
|
178
|
+
const result = await getCasesTool.handler(
|
|
179
|
+
{ project_id: '1', where: { priority_id: 2 }, fields: ['priority_id', 'custom_automation_status'] },
|
|
180
|
+
mockClient
|
|
181
|
+
);
|
|
182
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
183
|
+
|
|
184
|
+
expect(parsed.cases).toHaveLength(1);
|
|
185
|
+
expect(parsed.cases[0]).toEqual({
|
|
186
|
+
id: 1,
|
|
187
|
+
title: 'Login test',
|
|
188
|
+
suite_id: 1,
|
|
189
|
+
priority_id: 2,
|
|
190
|
+
custom_automation_status: 1
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getProjectsTool } from '../../src/tools/get_projects.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Project } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_projects tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getProjectsMock: jest.Mock<() => Promise<Project[]>>;
|
|
9
|
+
|
|
10
|
+
const mockProjects: Project[] = [
|
|
11
|
+
{ id: 1, name: 'Project Alpha', announcement: null, is_completed: false, suite_mode: 1 },
|
|
12
|
+
{ id: 2, name: 'Project Beta', announcement: 'Active project', is_completed: false, suite_mode: 3 },
|
|
13
|
+
{ id: 3, name: 'Old Project', announcement: null, is_completed: true, suite_mode: 1 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getProjectsMock = jest.fn<() => Promise<Project[]>>()
|
|
18
|
+
.mockResolvedValue(mockProjects);
|
|
19
|
+
|
|
20
|
+
mockClient = {
|
|
21
|
+
getProjects: getProjectsMock
|
|
22
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('exports correct tool definition', () => {
|
|
26
|
+
expect(getProjectsTool.name).toBe('get_projects');
|
|
27
|
+
expect(getProjectsTool.description).toContain('project');
|
|
28
|
+
expect(getProjectsTool.parameters).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('handler fetches and returns projects', async () => {
|
|
32
|
+
const result = await getProjectsTool.handler({}, mockClient);
|
|
33
|
+
|
|
34
|
+
expect(result).toBeDefined();
|
|
35
|
+
expect(result.content[0].type).toBe('text');
|
|
36
|
+
|
|
37
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
38
|
+
expect(parsed.projects).toHaveLength(2); // Only active projects
|
|
39
|
+
expect(mockClient.getProjects).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('filters out completed projects', async () => {
|
|
43
|
+
const result = await getProjectsTool.handler({}, mockClient);
|
|
44
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
45
|
+
|
|
46
|
+
const completedProject = parsed.projects.find((p: any) => p.name === 'Old Project');
|
|
47
|
+
expect(completedProject).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('returns correct project structure', async () => {
|
|
51
|
+
const result = await getProjectsTool.handler({}, mockClient);
|
|
52
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
53
|
+
|
|
54
|
+
expect(parsed.projects[0]).toEqual({
|
|
55
|
+
id: 1,
|
|
56
|
+
name: 'Project Alpha',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(parsed.projects[1]).toEqual({
|
|
60
|
+
id: 2,
|
|
61
|
+
name: 'Project Beta',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('handler returns error on failure', async () => {
|
|
66
|
+
getProjectsMock.mockRejectedValue(new Error('API Error'));
|
|
67
|
+
|
|
68
|
+
const result = await getProjectsTool.handler({}, mockClient);
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
72
|
+
isError: true
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getSectionsTool } from '../../src/tools/get_sections.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Section } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_sections tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getSectionsMock: jest.Mock<(projectId: string) => Promise<Section[]>>;
|
|
9
|
+
|
|
10
|
+
const mockSections: Section[] = [
|
|
11
|
+
{ id: 1, name: 'Login', description: null, parent_id: null, depth: 0, display_order: 1, suite_id: 1 },
|
|
12
|
+
{ id: 2, name: 'Authentication', description: 'Auth tests', parent_id: 1, depth: 1, display_order: 2, suite_id: 1 },
|
|
13
|
+
{ id: 3, name: 'Registration', description: null, parent_id: null, depth: 0, display_order: 3, suite_id: 1 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getSectionsMock = jest.fn<(projectId: string) => Promise<Section[]>>()
|
|
18
|
+
.mockResolvedValue(mockSections);
|
|
19
|
+
|
|
20
|
+
mockClient = {
|
|
21
|
+
getSections: getSectionsMock
|
|
22
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('exports correct tool definition', () => {
|
|
26
|
+
expect(getSectionsTool.name).toBe('get_sections');
|
|
27
|
+
expect(getSectionsTool.description).toContain('section');
|
|
28
|
+
expect(getSectionsTool.parameters).toBeDefined();
|
|
29
|
+
expect(getSectionsTool.parameters.project_id).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('handler fetches and returns sections', async () => {
|
|
33
|
+
const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
|
|
34
|
+
|
|
35
|
+
expect(result).toBeDefined();
|
|
36
|
+
expect(result.content[0].type).toBe('text');
|
|
37
|
+
|
|
38
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
39
|
+
expect(parsed.sections).toHaveLength(3);
|
|
40
|
+
expect(mockClient.getSections).toHaveBeenCalledWith('1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('returns correct section structure', async () => {
|
|
44
|
+
const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
|
|
45
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
46
|
+
|
|
47
|
+
expect(parsed.sections[0]).toEqual({
|
|
48
|
+
id: 1,
|
|
49
|
+
name: 'Login',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(parsed.sections[1]).toEqual({
|
|
53
|
+
id: 2,
|
|
54
|
+
name: 'Authentication',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('handler returns error on failure', async () => {
|
|
59
|
+
getSectionsMock.mockRejectedValue(new Error('API Error'));
|
|
60
|
+
|
|
61
|
+
const result = await getSectionsTool.handler({ project_id: '1' }, mockClient);
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
65
|
+
isError: true
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getTemplatesTool } from '../../src/tools/get_templates.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Template } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_templates tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getTemplatesMock: jest.Mock<(projectId: string) => Promise<Template[]>>;
|
|
9
|
+
|
|
10
|
+
const mockTemplates: Template[] = [
|
|
11
|
+
{ id: 1, name: 'Test Case (Text)', is_default: false },
|
|
12
|
+
{ id: 2, name: 'Test Case (Steps)', is_default: true },
|
|
13
|
+
{ id: 3, name: 'Exploratory Session', is_default: false },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getTemplatesMock = jest.fn<(projectId: string) => Promise<Template[]>>().mockResolvedValue(mockTemplates);
|
|
18
|
+
|
|
19
|
+
mockClient = {
|
|
20
|
+
getTemplates: getTemplatesMock
|
|
21
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('exports correct tool definition', () => {
|
|
25
|
+
expect(getTemplatesTool.name).toBe('get_templates');
|
|
26
|
+
expect(getTemplatesTool.description).toContain('template');
|
|
27
|
+
expect(getTemplatesTool.parameters).toBeDefined();
|
|
28
|
+
expect(getTemplatesTool.parameters.project_id).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('handler fetches and returns templates', async () => {
|
|
32
|
+
const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
|
|
33
|
+
|
|
34
|
+
expect(result).toBeDefined();
|
|
35
|
+
expect(result.content[0].type).toBe('text');
|
|
36
|
+
|
|
37
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
38
|
+
expect(parsed.templates).toHaveLength(3);
|
|
39
|
+
expect(mockClient.getTemplates).toHaveBeenCalledWith('1');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns correct template structure', async () => {
|
|
43
|
+
const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
|
|
44
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
45
|
+
|
|
46
|
+
expect(parsed.templates[0]).toEqual({
|
|
47
|
+
id: 1,
|
|
48
|
+
name: 'Test Case (Text)',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(parsed.templates[1]).toEqual({
|
|
52
|
+
id: 2,
|
|
53
|
+
name: 'Test Case (Steps)',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('handler returns error on failure', async () => {
|
|
58
|
+
getTemplatesMock.mockRejectedValue(new Error('API Error'));
|
|
59
|
+
|
|
60
|
+
const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
64
|
+
isError: true
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|