@uarlouski/testrail-mcp-server 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +52 -16
- package/package.json +1 -2
- package/src/client/testrail.ts +42 -53
- package/src/index.ts +22 -2
- package/src/tools/create_case.ts +6 -13
- package/src/tools/get_case.ts +4 -11
- package/src/tools/get_case_fields.ts +4 -11
- package/src/tools/get_cases.ts +4 -11
- package/src/tools/get_projects.ts +7 -25
- package/src/tools/get_sections.ts +7 -25
- package/src/tools/get_templates.ts +7 -25
- package/src/tools/update_case.ts +6 -13
- package/src/tools/update_cases.ts +7 -14
- package/src/types/custom.ts +1 -16
- package/src/types/testrail.ts +87 -87
- package/src/utils/sanitizer.ts +29 -0
- package/test/client/testrail.test.ts +215 -142
- package/test/tools/create_case.test.ts +10 -16
- package/test/tools/get_case.test.ts +46 -61
- package/test/tools/get_case_fields.test.ts +7 -80
- package/test/tools/get_cases.test.ts +19 -108
- package/test/tools/get_projects.test.ts +4 -41
- package/test/tools/get_sections.test.ts +7 -36
- package/test/tools/get_templates.test.ts +1 -30
- package/test/tools/update_case.test.ts +10 -16
- package/test/tools/update_cases.test.ts +14 -41
- package/test/utils/mapper.test.ts +13 -13
- package/test/utils/sanitizer.test.ts +85 -1
|
@@ -32,36 +32,7 @@ describe('get_templates tool', () => {
|
|
|
32
32
|
const result = await getTemplatesTool.handler({ project_id: '1' }, mockClient);
|
|
33
33
|
|
|
34
34
|
expect(result).toBeDefined();
|
|
35
|
-
expect(result.
|
|
36
|
-
|
|
37
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
38
|
-
expect(parsed.templates).toHaveLength(3);
|
|
35
|
+
expect(result.templates).toHaveLength(3);
|
|
39
36
|
expect(mockClient.getTemplates).toHaveBeenCalledWith('1');
|
|
40
37
|
});
|
|
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
38
|
});
|
|
@@ -52,12 +52,9 @@ describe('update_case tool', () => {
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
expect(result).toBeDefined();
|
|
55
|
-
expect(result.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(parsed.success).toBe(true);
|
|
59
|
-
expect(parsed.case_id).toBe(123);
|
|
60
|
-
expect(parsed.message).toContain('C123');
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.case_id).toBe(123);
|
|
57
|
+
expect(result.message).toContain('C123');
|
|
61
58
|
expect(mockClient.updateCase).toHaveBeenCalledWith('123', { title: 'Updated Title' });
|
|
62
59
|
});
|
|
63
60
|
|
|
@@ -92,17 +89,14 @@ describe('update_case tool', () => {
|
|
|
92
89
|
expect(mockClient.updateCase).toHaveBeenCalledWith('123', fields);
|
|
93
90
|
});
|
|
94
91
|
|
|
95
|
-
test('handler
|
|
92
|
+
test('handler throws error on failure', async () => {
|
|
96
93
|
updateCaseMock.mockRejectedValue(new Error('API Error'));
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
105
|
-
isError: true
|
|
106
|
-
});
|
|
95
|
+
await expect(
|
|
96
|
+
updateCaseTool.handler(
|
|
97
|
+
{ case_id: '123', fields: { title: 'Test' } },
|
|
98
|
+
mockClient
|
|
99
|
+
)
|
|
100
|
+
).rejects.toThrow('API Error');
|
|
107
101
|
});
|
|
108
102
|
});
|
|
@@ -10,21 +10,21 @@ describe('update_cases tool', () => {
|
|
|
10
10
|
const mockUpdatedCases: Case[] = [
|
|
11
11
|
{
|
|
12
12
|
id: 1, title: 'Test Case 1', section_id: 1, template_id: 1, type_id: 1,
|
|
13
|
-
priority_id: 3, milestone_id: null, refs: null,
|
|
14
|
-
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
15
|
-
suite_id: 1,
|
|
13
|
+
priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
|
|
14
|
+
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
15
|
+
suite_id: 1, labels: []
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
id: 2, title: 'Test Case 2', section_id: 1, template_id: 1, type_id: 1,
|
|
19
|
-
priority_id: 3, milestone_id: null, refs: null,
|
|
20
|
-
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
21
|
-
suite_id: 1,
|
|
19
|
+
priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
|
|
20
|
+
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
21
|
+
suite_id: 1, labels: []
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
id: 3, title: 'Test Case 3', section_id: 1, template_id: 1, type_id: 1,
|
|
25
|
-
priority_id: 3, milestone_id: null, refs: null,
|
|
26
|
-
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
27
|
-
suite_id: 1,
|
|
25
|
+
priority_id: 3, milestone_id: null, refs: null, created_on: 1700000000,
|
|
26
|
+
updated_by: 1, updated_on: 1700000001, estimate: null,
|
|
27
|
+
suite_id: 1, labels: []
|
|
28
28
|
},
|
|
29
29
|
];
|
|
30
30
|
|
|
@@ -52,37 +52,10 @@ describe('update_cases tool', () => {
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
expect(result).toBeDefined();
|
|
55
|
-
expect(result.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
60
|
-
expect(parsed.case_ids).toEqual([1, 2, 3]);
|
|
61
|
-
expect(parsed.message).toContain('3 test cases');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('passes case_ids and fields correctly', async () => {
|
|
65
|
-
const fields = { priority_id: 2, type_id: 1 };
|
|
66
|
-
|
|
67
|
-
await updateCasesTool.handler(
|
|
68
|
-
{ case_ids: [10, 20, 30], fields },
|
|
69
|
-
mockClient
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
expect(mockClient.updateCases).toHaveBeenCalledWith([10, 20, 30], fields);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('handler returns error on failure', async () => {
|
|
76
|
-
updateCasesMock.mockRejectedValue(new Error('API Error'));
|
|
77
|
-
|
|
78
|
-
const result = await updateCasesTool.handler(
|
|
79
|
-
{ case_ids: [1, 2], fields: { priority_id: 2 } },
|
|
80
|
-
mockClient
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
expect(result).toEqual({
|
|
84
|
-
content: [{ type: 'text', text: 'Error: API Error' }],
|
|
85
|
-
isError: true
|
|
86
|
-
});
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.updated_count).toBe(3);
|
|
57
|
+
expect(result.case_ids).toEqual([1, 2, 3]);
|
|
58
|
+
expect(result.message).toContain('3 test cases');
|
|
59
|
+
expect(mockClient.updateCases).toHaveBeenCalledWith([1, 2, 3], { priority_id: 3 });
|
|
87
60
|
});
|
|
88
61
|
});
|
|
@@ -6,13 +6,13 @@ describe('processCustomFields', () => {
|
|
|
6
6
|
|
|
7
7
|
// Mock CaseField array simulating TestRail API response
|
|
8
8
|
const mockCaseFields: CaseField[] = [
|
|
9
|
-
{ id: 1, name: 'is_automated', system_name: 'custom_is_automated', label: 'Is Automated', type_id: 1, template_ids: [], include_all: true, is_active: true,
|
|
10
|
-
{ id: 2, name: 'refs', system_name: 'custom_refs', label: 'Defects', type_id: 1, template_ids: [], include_all: true, is_active: true,
|
|
11
|
-
{ id: 3, name: 'case_automation_priority', system_name: 'custom_case_automation_priority', label: 'Automation Priority', type_id: 6, template_ids: [], include_all: true, is_active: true,
|
|
12
|
-
{ id: 4, name: 'preconds', system_name: 'custom_preconds', label: 'Preconditions', type_id: 1, template_ids: [], include_all: true, is_active: true,
|
|
13
|
-
{ id: 5, name: 'steps', system_name: 'custom_steps', label: 'Steps', type_id: 1, template_ids: [1], include_all: false, is_active: true,
|
|
14
|
-
{ id: 6, name: 'expected', system_name: 'custom_expected', label: 'Expected', type_id: 1, template_ids: [1], include_all: false, is_active: true,
|
|
15
|
-
{ id: 7, name: 'steps_separated', system_name: 'custom_steps_separated', label: 'Steps Separated', type_id: 1, template_ids: [2], include_all: false, is_active: true,
|
|
9
|
+
{ id: 1, name: 'is_automated', system_name: 'custom_is_automated', label: 'Is Automated', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
|
|
10
|
+
{ id: 2, name: 'refs', system_name: 'custom_refs', label: 'Defects', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
|
|
11
|
+
{ id: 3, name: 'case_automation_priority', system_name: 'custom_case_automation_priority', label: 'Automation Priority', type_id: 6, template_ids: [], include_all: true, is_active: true, description: null, configs: [{ options: { items: '2, P0\n3, P1\n4, P2\n5, P3\n6, P4' } }] },
|
|
12
|
+
{ id: 4, name: 'preconds', system_name: 'custom_preconds', label: 'Preconditions', type_id: 1, template_ids: [], include_all: true, is_active: true, description: null, configs: [] },
|
|
13
|
+
{ id: 5, name: 'steps', system_name: 'custom_steps', label: 'Steps', type_id: 1, template_ids: [1], include_all: false, is_active: true, description: null, configs: [] },
|
|
14
|
+
{ id: 6, name: 'expected', system_name: 'custom_expected', label: 'Expected', type_id: 1, template_ids: [1], include_all: false, is_active: true, description: null, configs: [] },
|
|
15
|
+
{ id: 7, name: 'steps_separated', system_name: 'custom_steps_separated', label: 'Steps Separated', type_id: 1, template_ids: [2], include_all: false, is_active: true, description: null, configs: [] },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
test('maps custom fields using field metadata', () => {
|
|
@@ -21,7 +21,7 @@ describe('processCustomFields', () => {
|
|
|
21
21
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
22
22
|
custom_is_automated: true,
|
|
23
23
|
custom_refs: 'DEF-123',
|
|
24
|
-
display_order: 1, suite_id: 1,
|
|
24
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
25
25
|
is_deleted: 0, refs: null, labels: [],
|
|
26
26
|
} as unknown as Case;
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ describe('processCustomFields', () => {
|
|
|
35
35
|
id: 1, title: 'Foo', template_id: 1,
|
|
36
36
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
37
37
|
custom_case_automation_priority: 2,
|
|
38
|
-
display_order: 1, suite_id: 1,
|
|
38
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
39
39
|
is_deleted: 0, refs: null, labels: [],
|
|
40
40
|
} as unknown as Case;
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ describe('processCustomFields', () => {
|
|
|
50
50
|
custom_expected: 'Expectation...',
|
|
51
51
|
custom_steps_separated: [{ content: 'Should not appear' }], // Template 2 field
|
|
52
52
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
53
|
-
display_order: 1, suite_id: 1,
|
|
53
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
54
54
|
is_deleted: 0, refs: null, labels: [],
|
|
55
55
|
} as unknown as Case;
|
|
56
56
|
|
|
@@ -66,7 +66,7 @@ describe('processCustomFields', () => {
|
|
|
66
66
|
custom_steps: 'Should not appear', // Template 1 field
|
|
67
67
|
custom_steps_separated: [{ content: 'Step 1' }],
|
|
68
68
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
69
|
-
display_order: 1, suite_id: 1,
|
|
69
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
70
70
|
is_deleted: 0, refs: null, labels: [],
|
|
71
71
|
} as unknown as Case;
|
|
72
72
|
|
|
@@ -80,7 +80,7 @@ describe('processCustomFields', () => {
|
|
|
80
80
|
id: 1, title: 'Foo', template_id: 999,
|
|
81
81
|
custom_unknown_field: 'some value',
|
|
82
82
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
83
|
-
display_order: 1, suite_id: 1,
|
|
83
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
84
84
|
is_deleted: 0, refs: null, labels: [],
|
|
85
85
|
} as unknown as Case;
|
|
86
86
|
|
|
@@ -101,7 +101,7 @@ describe('processCustomFields', () => {
|
|
|
101
101
|
id: 1, title: 'Foo', template_id: 1,
|
|
102
102
|
section_id: 0, type_id: 0, priority_id: 0,
|
|
103
103
|
custom_case_automation_priority: 999, // Unknown option ID
|
|
104
|
-
display_order: 1, suite_id: 1,
|
|
104
|
+
display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
|
|
105
105
|
is_deleted: 0, refs: null, labels: [],
|
|
106
106
|
} as unknown as Case;
|
|
107
107
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from '@jest/globals';
|
|
2
|
-
import { sanitizeValue } from '../../src/utils/sanitizer.js';
|
|
2
|
+
import { sanitizeValue, removeNullish } from '../../src/utils/sanitizer.js';
|
|
3
3
|
|
|
4
4
|
describe('sanitizeValue', () => {
|
|
5
5
|
test('strips style attributes from string values', () => {
|
|
@@ -57,3 +57,87 @@ describe('sanitizeValue', () => {
|
|
|
57
57
|
expect(sanitizeValue(null)).toBe(null);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('removeNullish', () => {
|
|
62
|
+
test('removes null values from object', () => {
|
|
63
|
+
const input = { a: 1, b: null, c: 3 };
|
|
64
|
+
expect(removeNullish(input)).toEqual({ a: 1, c: 3 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('removes undefined values from object', () => {
|
|
68
|
+
const input = { a: 1, b: undefined, c: 3 };
|
|
69
|
+
expect(removeNullish(input)).toEqual({ a: 1, c: 3 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('handles nested objects', () => {
|
|
73
|
+
const input = {
|
|
74
|
+
name: 'test',
|
|
75
|
+
metadata: { id: 1, value: null, active: true },
|
|
76
|
+
count: null
|
|
77
|
+
};
|
|
78
|
+
expect(removeNullish(input)).toEqual({
|
|
79
|
+
name: 'test',
|
|
80
|
+
metadata: { id: 1, active: true }
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('filters null items from arrays', () => {
|
|
85
|
+
const input = [1, null, 3, undefined, 5];
|
|
86
|
+
expect(removeNullish(input)).toEqual([1, 3, 5]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('cleans nested objects in arrays', () => {
|
|
90
|
+
const input = [
|
|
91
|
+
{ id: 1, name: 'item1', value: null },
|
|
92
|
+
{ id: 2, name: null, value: 'test' }
|
|
93
|
+
];
|
|
94
|
+
expect(removeNullish(input)).toEqual([
|
|
95
|
+
{ id: 1, name: 'item1' },
|
|
96
|
+
{ id: 2, value: 'test' }
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('handles deeply nested structures', () => {
|
|
101
|
+
const input = {
|
|
102
|
+
projects: [
|
|
103
|
+
{
|
|
104
|
+
id: 1,
|
|
105
|
+
name: 'Project A',
|
|
106
|
+
metadata: { created: null, updated: '2024-01-01' },
|
|
107
|
+
tags: [null, 'important', undefined, 'urgent']
|
|
108
|
+
},
|
|
109
|
+
null
|
|
110
|
+
],
|
|
111
|
+
count: undefined
|
|
112
|
+
};
|
|
113
|
+
expect(removeNullish(input)).toEqual({
|
|
114
|
+
projects: [
|
|
115
|
+
{
|
|
116
|
+
id: 1,
|
|
117
|
+
name: 'Project A',
|
|
118
|
+
metadata: { updated: '2024-01-01' },
|
|
119
|
+
tags: ['important', 'urgent']
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('preserves primitive values', () => {
|
|
126
|
+
expect(removeNullish(42)).toBe(42);
|
|
127
|
+
expect(removeNullish('test')).toBe('test');
|
|
128
|
+
expect(removeNullish(true)).toBe(true);
|
|
129
|
+
expect(removeNullish(0)).toBe(0);
|
|
130
|
+
expect(removeNullish('')).toBe('');
|
|
131
|
+
expect(removeNullish(false)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('returns null/undefined as-is for root values', () => {
|
|
135
|
+
expect(removeNullish(null)).toBe(null);
|
|
136
|
+
expect(removeNullish(undefined)).toBe(undefined);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('handles empty objects and arrays', () => {
|
|
140
|
+
expect(removeNullish({})).toEqual({});
|
|
141
|
+
expect(removeNullish([])).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
});
|