Skip to content

Commit 944ba44

Browse files
Fix: Centralize Axios error formatting and improve error handling in project and test case creation
1 parent b924e0e commit 944ba44

File tree

5 files changed

+90
-123
lines changed

5 files changed

+90
-123
lines changed

src/lib/error.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AxiosError } from "axios";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
4+
/**
5+
* Formats an AxiosError into a CallToolResult with an appropriate message.
6+
* @param err - The error object to format
7+
* @param defaultText - The fallback error message
8+
*/
9+
export function formatAxiosError(
10+
err: unknown,
11+
defaultText: string,
12+
): CallToolResult {
13+
let text = defaultText;
14+
15+
if (err instanceof AxiosError && err.response?.data) {
16+
const message =
17+
err.response.data.message ||
18+
err.response.data.error ||
19+
err.message ||
20+
defaultText;
21+
text = message;
22+
} else if (err instanceof Error) {
23+
text = err.message;
24+
}
25+
26+
return {
27+
content: [{ type: "text", text }],
28+
isError: true,
29+
};
30+
}

src/tools/testmanagement-utils/create-project-folder.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import axios, { AxiosError } from "axios";
1+
import axios from "axios";
22
import config from "../../config";
33
import { z } from "zod";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error"; // or correct path
56

67
// Schema for combined project/folder creation
78
export const CreateProjFoldSchema = z.object({
@@ -30,29 +31,6 @@ export const CreateProjFoldSchema = z.object({
3031

3132
type CreateProjFoldArgs = z.infer<typeof CreateProjFoldSchema>;
3233

33-
function formatAxiosError(
34-
err: unknown,
35-
defaultText: string,
36-
): { content: { type: "text"; text: string }[]; isError: true } {
37-
let text = defaultText;
38-
39-
if (err instanceof AxiosError && err.response?.data) {
40-
const { message: apiMessage } = err.response.data;
41-
const status = err.response.status;
42-
43-
if (status >= 400 && status < 500 && apiMessage) {
44-
text = apiMessage;
45-
}
46-
} else if (err instanceof Error) {
47-
text = err.message;
48-
}
49-
50-
return {
51-
content: [{ type: "text" as const, text }],
52-
isError: true,
53-
};
54-
}
55-
5634
/**
5735
* Creates a project and/or folder in BrowserStack Test Management.
5836
*/
@@ -138,7 +116,7 @@ export async function createProjectOrFolder(
138116
content: [
139117
{
140118
type: "text",
141-
text: `Folder created: ID=${folder.id}, name="${folder.name}" in project with identifier ${projId}`,
119+
text: `Folder created: ID=${folder.id}, name=${folder.name} in project with identifier ${projId}`,
142120
},
143121
],
144122
};

src/tools/testmanagement-utils/create-testcase.ts

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import axios, { AxiosError } from "axios";
1+
import axios from "axios";
22
import config from "../../config";
33
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error"; // or correct
46

57
interface TestCaseStep {
68
step: string;
@@ -70,18 +72,15 @@ export const CreateTestCaseSchema = z.object({
7072
description: z
7173
.string()
7274
.optional()
73-
.nullish()
7475
.describe("Brief description of the test case."),
7576
owner: z
7677
.string()
7778
.email()
7879
.describe("Email of the test case owner.")
79-
.optional()
80-
.nullish(),
80+
.optional(),
8181
preconditions: z
8282
.string()
8383
.optional()
84-
.nullish()
8584
.describe("Any preconditions (HTML allowed)."),
8685
test_case_steps: z
8786
.array(
@@ -101,15 +100,10 @@ export const CreateTestCaseSchema = z.object({
101100
.object({
102101
name: z
103102
.string()
104-
.nullish()
105103
.describe(
106-
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana",
104+
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.",
107105
),
108-
host: z
109-
.string()
110-
.url()
111-
.describe("Base URL of the issue tracker.")
112-
.nullish(),
106+
host: z.string().url().describe("Base URL of the issue tracker."),
113107
})
114108
.optional(),
115109
tags: z
@@ -145,68 +139,52 @@ export function sanitizeArgs(args: any) {
145139

146140
export async function createTestCase(
147141
params: TestCaseCreateRequest,
148-
): Promise<TestCaseResponse> {
149-
const {
150-
project_identifier,
151-
folder_id,
152-
name,
153-
description,
154-
owner,
155-
preconditions,
156-
test_case_steps,
157-
issues,
158-
issue_tracker,
159-
tags,
160-
custom_fields,
161-
} = params;
162-
163-
const body = {
164-
test_case: {
165-
name,
166-
description,
167-
owner,
168-
preconditions,
169-
test_case_steps,
170-
issues,
171-
issue_tracker,
172-
tags,
173-
custom_fields,
174-
},
175-
};
142+
): Promise<CallToolResult> {
143+
const body = { test_case: params };
176144

177145
try {
178146
const response = await axios.post<TestCaseResponse>(
179-
`https://test-management.browserstack.com/api/v2/projects/${project_identifier}/folders/${folder_id}/test-cases`,
147+
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
148+
params.project_identifier,
149+
)}/folders/${encodeURIComponent(params.folder_id)}/test-cases`,
180150
body,
181151
{
182152
auth: {
183153
username: config.browserstackUsername,
184154
password: config.browserstackAccessKey,
185155
},
186-
headers: {
187-
"Content-Type": "application/json",
188-
},
156+
headers: { "Content-Type": "application/json" },
189157
},
190158
);
191159

192-
// Check if the response indicates success
193-
if (!response.data.data.success) {
194-
throw new Error(
195-
`Failed to create test case: ${JSON.stringify(response.data)}`,
196-
);
160+
const { data } = response.data;
161+
if (!data.success) {
162+
return {
163+
content: [
164+
{
165+
type: "text",
166+
text: `Failed to create test case: ${JSON.stringify(
167+
response.data,
168+
)}`,
169+
isError: true,
170+
},
171+
],
172+
isError: true,
173+
};
197174
}
198175

199-
return response.data;
200-
} catch (error) {
201-
if (error instanceof AxiosError) {
202-
if (error.response?.data?.message) {
203-
throw new Error(
204-
`Failed to create test case: ${error.response.data.message}`,
205-
);
206-
} else {
207-
throw new Error(`Failed to create test case: ${error.message}`);
208-
}
209-
}
210-
throw error;
176+
const tc = data.test_case;
177+
return {
178+
content: [
179+
{
180+
type: "text",
181+
text: `Successfully created test case ${tc.identifier}: ${tc.title}`,
182+
},
183+
{ type: "text", text: JSON.stringify(tc, null, 2) },
184+
],
185+
};
186+
} catch (err) {
187+
// Delegate to our centralized Axios error formatter
188+
return formatAxiosError(err, "Failed to create test case");
211189
}
212190
}

src/tools/testmanagement.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ export async function createProjectOrFolderTool(
2020
): Promise<CallToolResult> {
2121
try {
2222
return await createProjectOrFolder(args);
23-
} catch (error) {
24-
const msg = error instanceof Error ? error.message : "Unknown error";
23+
} catch (err) {
2524
return {
2625
content: [
2726
{
2827
type: "text",
29-
text: `Failed to create project/folder: ${msg}`,
28+
text: `Failed to create project/folder: ${
29+
err instanceof Error ? err.message : "Unknown error"
30+
}. Please open an issue on GitHub if the problem persists`,
3031
isError: true,
3132
},
3233
],
@@ -44,28 +45,14 @@ export async function createTestCaseTool(
4445
// Sanitize input arguments
4546
const cleanedArgs = sanitizeArgs(args);
4647
try {
47-
const response = await createTestCaseAPI(cleanedArgs);
48-
const testCase = response.data.test_case;
49-
50-
return {
51-
content: [
52-
{
53-
type: "text",
54-
text: `Successfully created test case ${testCase.identifier}: "${testCase.title}"`,
55-
},
56-
{
57-
type: "text",
58-
text: JSON.stringify(testCase, null, 2),
59-
},
60-
],
61-
};
62-
} catch (error) {
48+
return await createTestCaseAPI(cleanedArgs);
49+
} catch (err) {
6350
return {
6451
content: [
6552
{
6653
type: "text",
6754
text: `Failed to create test case: ${
68-
error instanceof Error ? error.message : "Unknown error"
55+
err instanceof Error ? err.message : "Unknown error"
6956
}. Please open an issue on GitHub if the problem persists`,
7057
isError: true,
7158
},
@@ -90,8 +77,6 @@ export default function addTestManagementTools(server: McpServer) {
9077
"createTestCase",
9178
"Use this tool to create a test case in BrowserStack Test Management.",
9279
CreateTestCaseSchema.shape,
93-
async (args) => {
94-
return createTestCaseTool(args as TestCaseCreateRequest);
95-
},
80+
createTestCaseTool,
9681
);
9782
}

tests/tools/testmanagement.test.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,22 @@ describe('createTestCaseTool', () => {
4545
custom_fields: { priority: 'high' },
4646
};
4747

48-
const mockTestCaseResponse = {
49-
data: {
50-
success: true,
51-
test_case: {
52-
identifier: 'TC-001',
53-
title: 'Sample Test Case',
54-
// additional fields omitted for brevity
55-
},
56-
},
48+
const mockCallToolResult = {
49+
content: [
50+
{ type: 'text', text: 'Successfully created test case TC-001: Sample Test Case' },
51+
{ type: 'text', text: JSON.stringify({ identifier: 'TC-001', title: 'Sample Test Case' }, null, 2) },
52+
],
53+
isError: false,
5754
};
5855

5956
it('should successfully create a test case', async () => {
60-
(createTestCase as jest.Mock).mockResolvedValue(mockTestCaseResponse);
57+
(createTestCase as jest.Mock).mockResolvedValue(mockCallToolResult);
6158

6259
const result = await createTestCaseTool(validArgs);
6360

6461
expect(sanitizeArgs).toHaveBeenCalledWith(validArgs);
6562
expect(createTestCase).toHaveBeenCalledWith(validArgs);
66-
expect(result.content[0].text).toContain('Successfully created test case TC-001');
67-
expect(result.content[1].text).toContain('"title": "Sample Test Case"');
63+
expect(result).toBe(mockCallToolResult);
6864
});
6965

7066
it('should handle API errors while creating test case', async () => {
@@ -134,7 +130,7 @@ describe('createProjectOrFolderTool', () => {
134130
const result = await createProjectOrFolderTool(validProjectArgs);
135131

136132
expect(result.isError).toBe(true);
137-
expect(result.content[0].text).toContain('Failed to create project/folder: Failed to create project/folder');
133+
expect(result.content[0].text).toContain('Failed to create project/folder: Failed to create project/folder. Please open an issue on GitHub if the problem persists');
138134
});
139135

140136
it('should handle unknown error while creating project or folder', async () => {
@@ -143,6 +139,6 @@ describe('createProjectOrFolderTool', () => {
143139
const result = await createProjectOrFolderTool(validProjectArgs);
144140

145141
expect(result.isError).toBe(true);
146-
expect(result.content[0].text).toContain('Failed to create project/folder: Unknown error');
142+
expect(result.content[0].text).toContain('Failed to create project/folder: Unknown error. Please open an issue on GitHub if the problem persists');
147143
});
148144
});

0 commit comments

Comments
 (0)