@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,102 +0,0 @@
1
- import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
- import { updateCaseTool } from '../../src/tools/update_case.js';
3
- import { TestRailClient } from '../../src/client/testrail.js';
4
- import { Case } from '../../src/types/testrail.js';
5
-
6
- describe('update_case tool', () => {
7
- let mockClient: jest.Mocked<TestRailClient>;
8
- let updateCaseMock: jest.Mock<(caseId: string, fields: Record<string, any>) => Promise<Case>>;
9
-
10
- const mockUpdatedCase: Case = {
11
- id: 123,
12
- title: 'Updated Title',
13
- section_id: 1,
14
- template_id: 1,
15
- type_id: 1,
16
- priority_id: 2,
17
- milestone_id: null,
18
- refs: null,
19
- created_by: 1,
20
- created_on: 1700000000,
21
- updated_by: 1,
22
- updated_on: 1700000001,
23
- estimate: null,
24
- estimate_forecast: null,
25
- suite_id: 1,
26
- display_order: 1,
27
- is_deleted: 0,
28
- labels: [],
29
- };
30
-
31
- beforeEach(() => {
32
- updateCaseMock = jest.fn<(caseId: string, fields: Record<string, any>) => Promise<Case>>()
33
- .mockResolvedValue(mockUpdatedCase);
34
-
35
- mockClient = {
36
- updateCase: updateCaseMock
37
- } as unknown as jest.Mocked<TestRailClient>;
38
- });
39
-
40
- test('exports correct tool definition', () => {
41
- expect(updateCaseTool.name).toBe('update_case');
42
- expect(updateCaseTool.description).toContain('Update');
43
- expect(updateCaseTool.parameters).toBeDefined();
44
- expect(updateCaseTool.parameters.case_id).toBeDefined();
45
- expect(updateCaseTool.parameters.fields).toBeDefined();
46
- });
47
-
48
- test('handler updates case and returns success response', async () => {
49
- const result = await updateCaseTool.handler(
50
- { case_id: '123', fields: { title: 'Updated Title' } },
51
- mockClient
52
- );
53
-
54
- expect(result).toBeDefined();
55
- expect(result.success).toBe(true);
56
- expect(result.case_id).toBe(123);
57
- expect(result.message).toContain('C123');
58
- expect(mockClient.updateCase).toHaveBeenCalledWith('123', { title: 'Updated Title' });
59
- });
60
-
61
- test('strips C prefix from case_id', async () => {
62
- await updateCaseTool.handler(
63
- { case_id: 'C456', fields: { title: 'New Title' } },
64
- mockClient
65
- );
66
-
67
- expect(mockClient.updateCase).toHaveBeenCalledWith('456', { title: 'New Title' });
68
- });
69
-
70
- test('handles lowercase c prefix', async () => {
71
- await updateCaseTool.handler(
72
- { case_id: 'c789', fields: { priority_id: 3 } },
73
- mockClient
74
- );
75
-
76
- expect(mockClient.updateCase).toHaveBeenCalledWith('789', { priority_id: 3 });
77
- });
78
-
79
- test('passes multiple fields correctly', async () => {
80
- const fields = {
81
- title: 'New Title',
82
- priority_id: 2,
83
- custom_automation_priority: 1,
84
- refs: 'JIRA-123',
85
- };
86
-
87
- await updateCaseTool.handler({ case_id: '123', fields }, mockClient);
88
-
89
- expect(mockClient.updateCase).toHaveBeenCalledWith('123', fields);
90
- });
91
-
92
- test('handler throws error on failure', async () => {
93
- updateCaseMock.mockRejectedValue(new Error('API Error'));
94
-
95
- await expect(
96
- updateCaseTool.handler(
97
- { case_id: '123', fields: { title: 'Test' } },
98
- mockClient
99
- )
100
- ).rejects.toThrow('API Error');
101
- });
102
- });
@@ -1,61 +0,0 @@
1
- import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
- import { updateCasesTool } from '../../src/tools/update_cases.js';
3
- import { TestRailClient } from '../../src/client/testrail.js';
4
- import { Case } from '../../src/types/testrail.js';
5
-
6
- describe('update_cases tool', () => {
7
- let mockClient: jest.Mocked<TestRailClient>;
8
- let updateCasesMock: jest.Mock<(caseIds: number[], fields: Record<string, any>) => Promise<Case[]>>;
9
-
10
- const mockUpdatedCases: Case[] = [
11
- {
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, created_on: 1700000000,
14
- updated_by: 1, updated_on: 1700000001, estimate: null,
15
- suite_id: 1, labels: []
16
- },
17
- {
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, created_on: 1700000000,
20
- updated_by: 1, updated_on: 1700000001, estimate: null,
21
- suite_id: 1, labels: []
22
- },
23
- {
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, created_on: 1700000000,
26
- updated_by: 1, updated_on: 1700000001, estimate: null,
27
- suite_id: 1, labels: []
28
- },
29
- ];
30
-
31
- beforeEach(() => {
32
- updateCasesMock = jest.fn<(caseIds: number[], fields: Record<string, any>) => Promise<Case[]>>()
33
- .mockResolvedValue(mockUpdatedCases);
34
-
35
- mockClient = {
36
- updateCases: updateCasesMock
37
- } as unknown as jest.Mocked<TestRailClient>;
38
- });
39
-
40
- test('exports correct tool definition', () => {
41
- expect(updateCasesTool.name).toBe('update_cases');
42
- expect(updateCasesTool.description).toContain('Bulk');
43
- expect(updateCasesTool.parameters).toBeDefined();
44
- expect(updateCasesTool.parameters.case_ids).toBeDefined();
45
- expect(updateCasesTool.parameters.fields).toBeDefined();
46
- });
47
-
48
- test('handler updates cases and returns success response', async () => {
49
- const result = await updateCasesTool.handler(
50
- { case_ids: [1, 2, 3], fields: { priority_id: 3 } },
51
- mockClient
52
- );
53
-
54
- expect(result).toBeDefined();
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 });
60
- });
61
- });
@@ -1,112 +0,0 @@
1
- import { jest, describe, test, expect } from '@jest/globals';
2
- import { Case, CaseField } from '../../src/types/testrail.js';
3
- import { processCustomFields } from '../../src/utils/mapper.js';
4
-
5
- describe('processCustomFields', () => {
6
-
7
- // Mock CaseField array simulating TestRail API response
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, 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
- ];
17
-
18
- test('maps custom fields using field metadata', () => {
19
- const input: Case = {
20
- id: 1, title: 'Foo', template_id: 1,
21
- section_id: 0, type_id: 0, priority_id: 0,
22
- custom_is_automated: true,
23
- custom_refs: 'DEF-123',
24
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
25
- is_deleted: 0, refs: null, labels: [],
26
- } as unknown as Case;
27
-
28
- const result = processCustomFields(input, mockCaseFields);
29
- expect(result.is_automated).toBe(true);
30
- expect(result.defects).toBe('DEF-123');
31
- });
32
-
33
- test('maps fields using value mapping from config', () => {
34
- const input: Case = {
35
- id: 1, title: 'Foo', template_id: 1,
36
- section_id: 0, type_id: 0, priority_id: 0,
37
- custom_case_automation_priority: 2,
38
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
39
- is_deleted: 0, refs: null, labels: [],
40
- } as unknown as Case;
41
-
42
- const result = processCustomFields(input, mockCaseFields);
43
- expect(result.automation_priority).toBe('P0');
44
- });
45
-
46
- test('includes only template-specific fields for template 1', () => {
47
- const input: Case = {
48
- id: 1, title: 'Foo', template_id: 1,
49
- custom_steps: 'Step 1...',
50
- custom_expected: 'Expectation...',
51
- custom_steps_separated: [{ content: 'Should not appear' }], // Template 2 field
52
- section_id: 0, type_id: 0, priority_id: 0,
53
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
54
- is_deleted: 0, refs: null, labels: [],
55
- } as unknown as Case;
56
-
57
- const result = processCustomFields(input, mockCaseFields);
58
- expect(result.steps).toBe('Step 1...');
59
- expect(result.expected).toBe('Expectation...');
60
- expect(result.steps_separated).toBeUndefined(); // Not included for template 1
61
- });
62
-
63
- test('includes only template-specific fields for template 2', () => {
64
- const input: Case = {
65
- id: 1, title: 'Foo', template_id: 2,
66
- custom_steps: 'Should not appear', // Template 1 field
67
- custom_steps_separated: [{ content: 'Step 1' }],
68
- section_id: 0, type_id: 0, priority_id: 0,
69
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
70
- is_deleted: 0, refs: null, labels: [],
71
- } as unknown as Case;
72
-
73
- const result = processCustomFields(input, mockCaseFields);
74
- expect(result.steps_separated).toEqual([{ content: 'Step 1' }]);
75
- expect(result.steps).toBeUndefined(); // Not included for template 2
76
- });
77
-
78
- test('keeps unknown fields as-is when no mapping found', () => {
79
- const input: Case = {
80
- id: 1, title: 'Foo', template_id: 999,
81
- custom_unknown_field: 'some value',
82
- section_id: 0, type_id: 0, priority_id: 0,
83
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
84
- is_deleted: 0, refs: null, labels: [],
85
- } as unknown as Case;
86
-
87
- const result = processCustomFields(input, mockCaseFields);
88
- expect(result.custom_unknown_field).toBe('some value'); // Kept as-is with original key
89
- });
90
-
91
- test('throws error when testCase is null', () => {
92
- expect(() => processCustomFields(null as any, mockCaseFields)).toThrow('Test case is undefined or null');
93
- });
94
-
95
- test('throws error when testCase is undefined', () => {
96
- expect(() => processCustomFields(undefined as any, mockCaseFields)).toThrow('Test case is undefined or null');
97
- });
98
-
99
- test('returns original value when dropdown option is not found', () => {
100
- const input: Case = {
101
- id: 1, title: 'Foo', template_id: 1,
102
- section_id: 0, type_id: 0, priority_id: 0,
103
- custom_case_automation_priority: 999, // Unknown option ID
104
- display_order: 1, suite_id: 1, created_on: 1, updated_on: 1,
105
- is_deleted: 0, refs: null, labels: [],
106
- } as unknown as Case;
107
-
108
- const result = processCustomFields(input, mockCaseFields);
109
- expect(result.automation_priority).toBe(999); // Falls back to original value
110
- });
111
- });
112
-
@@ -1,143 +0,0 @@
1
- import { describe, test, expect } from '@jest/globals';
2
- import { sanitizeValue, removeNullish } from '../../src/utils/sanitizer.js';
3
-
4
- describe('sanitizeValue', () => {
5
- test('strips style attributes from string values', () => {
6
- const input = '<p style="color: red; font-size: 14px;">Hello</p>';
7
- expect(sanitizeValue(input)).toBe('<p>Hello</p>');
8
- });
9
-
10
- test('strips multiple style attributes', () => {
11
- const input = '<div style="margin: 10px;"><span style="font-weight: bold;">Text</span></div>';
12
- expect(sanitizeValue(input)).toBe('<div><span>Text</span></div>');
13
- });
14
-
15
- test('handles single quotes in style', () => {
16
- const input = "<p style='color: blue;'>Text</p>";
17
- expect(sanitizeValue(input)).toBe('<p>Text</p>');
18
- });
19
-
20
- test('preserves other attributes', () => {
21
- const input = '<a href="http://example.com" style="color: blue;" class="link">Link</a>';
22
- expect(sanitizeValue(input)).toBe('<a href="http://example.com" class="link">Link</a>');
23
- });
24
-
25
- test('returns unchanged text without styles', () => {
26
- const input = '<p>No styles here</p>';
27
- expect(sanitizeValue(input)).toBe('<p>No styles here</p>');
28
- });
29
- test('sanitizes string values', () => {
30
- const input = '<p style="color: red;">Hello</p>';
31
- expect(sanitizeValue(input)).toBe('<p>Hello</p>');
32
- });
33
-
34
- test('sanitizes nested objects', () => {
35
- const input = {
36
- content: '<div style="margin: 10px;">Step 1</div>',
37
- expected: '<p style="color: green;">Result</p>'
38
- };
39
- const result = sanitizeValue(input);
40
- expect(result.content).toBe('<div>Step 1</div>');
41
- expect(result.expected).toBe('<p>Result</p>');
42
- });
43
-
44
- test('sanitizes arrays', () => {
45
- const input = [
46
- { content: '<span style="font-size: 12px;">Item 1</span>' },
47
- { content: '<span style="font-size: 14px;">Item 2</span>' }
48
- ];
49
- const result = sanitizeValue(input);
50
- expect(result[0].content).toBe('<span>Item 1</span>');
51
- expect(result[1].content).toBe('<span>Item 2</span>');
52
- });
53
-
54
- test('preserves non-string values', () => {
55
- expect(sanitizeValue(42)).toBe(42);
56
- expect(sanitizeValue(true)).toBe(true);
57
- expect(sanitizeValue(null)).toBe(null);
58
- });
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
- });
package/tsconfig.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "types": [
13
- "node",
14
- "jest"
15
- ]
16
- },
17
- "include": [
18
- "src/**/*"
19
- ],
20
- "exclude": [
21
- "node_modules",
22
- "**/*.test.ts"
23
- ]
24
- }