Skip to content

Commit 66a7f87

Browse files
committed
feat: Add updateTestCase functionality to Test Management
- Add updateTestCase tool registration in testmanagement.ts - Implement complete update test case API in update-testcase.ts - Support partial updates for all test case fields - Add proper error handling for 404 and 403 responses - Convert project identifier to numeric ID before API call
1 parent 94eb783 commit 66a7f87

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import { apiClient } from "../../lib/apiClient.js";
2+
import { z } from "zod";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import { formatAxiosError } from "../../lib/error.js";
5+
import { projectIdentifierToId } from "./TCG-utils/api.js";
6+
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
8+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
9+
10+
interface TestCaseStep {
11+
step: string;
12+
result: string;
13+
}
14+
15+
interface IssueTracker {
16+
name: string;
17+
host: string;
18+
}
19+
20+
export interface TestCaseUpdateRequest {
21+
project_identifier: string;
22+
test_case_id: string;
23+
name?: string;
24+
description?: string;
25+
owner?: string;
26+
preconditions?: string;
27+
test_case_steps?: TestCaseStep[];
28+
issues?: string[];
29+
issue_tracker?: IssueTracker;
30+
tags?: string[];
31+
custom_fields?: Record<string, string>;
32+
automation_status?: string;
33+
priority?: string;
34+
case_type?: string;
35+
}
36+
37+
export interface TestCaseUpdateResponse {
38+
data: {
39+
success: boolean;
40+
test_case: {
41+
case_type: string;
42+
priority: string;
43+
status: string;
44+
folder_id: number;
45+
issues: Array<{
46+
jira_id: string;
47+
issue_type: string;
48+
}>;
49+
tags: string[];
50+
template: string;
51+
description: string;
52+
preconditions: string;
53+
title: string;
54+
identifier: string;
55+
automation_status: string;
56+
owner: string;
57+
steps: TestCaseStep[];
58+
custom_fields: Array<{
59+
name: string;
60+
value: string;
61+
}>;
62+
};
63+
};
64+
}
65+
66+
export const UpdateTestCaseSchema = z.object({
67+
project_identifier: z
68+
.string()
69+
.describe(
70+
"The ID of the BrowserStack project containing the test case to update.",
71+
),
72+
test_case_id: z
73+
.string()
74+
.describe(
75+
"The ID of the test case to update. This can be found using the listTestCases tool.",
76+
),
77+
name: z.string().optional().describe("Updated name of the test case."),
78+
description: z
79+
.string()
80+
.optional()
81+
.describe("Updated brief description of the test case."),
82+
owner: z
83+
.string()
84+
.email()
85+
.describe("Updated email of the test case owner.")
86+
.optional(),
87+
preconditions: z
88+
.string()
89+
.optional()
90+
.describe("Updated preconditions (HTML allowed)."),
91+
test_case_steps: z
92+
.array(
93+
z.object({
94+
step: z.string().describe("Action to perform in this step."),
95+
result: z.string().describe("Expected result of this step."),
96+
}),
97+
)
98+
.optional()
99+
.describe("Updated list of steps and expected results."),
100+
issues: z
101+
.array(z.string())
102+
.optional()
103+
.describe(
104+
"Updated list of linked Jira, Asana or Azure issues ID's. This should be strictly in array format.",
105+
),
106+
issue_tracker: z
107+
.object({
108+
name: z
109+
.string()
110+
.describe(
111+
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.",
112+
),
113+
host: z.string().url().describe("Base URL of the issue tracker."),
114+
})
115+
.optional()
116+
.describe("Updated issue tracker configuration"),
117+
tags: z
118+
.array(z.string())
119+
.optional()
120+
.describe(
121+
"Updated tags to attach to the test case. This should be strictly in array format.",
122+
),
123+
custom_fields: z
124+
.record(z.string())
125+
.optional()
126+
.describe("Updated map of custom field names to values."),
127+
automation_status: z
128+
.string()
129+
.optional()
130+
.describe(
131+
"Updated automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'.",
132+
),
133+
priority: z
134+
.string()
135+
.optional()
136+
.describe(
137+
"Updated priority level (e.g., 'critical', 'high', 'medium', 'low').",
138+
),
139+
case_type: z
140+
.string()
141+
.optional()
142+
.describe("Updated case type (e.g., 'functional', 'regression', 'smoke')."),
143+
});
144+
145+
export function sanitizeUpdateArgs(args: any) {
146+
const cleaned = { ...args };
147+
148+
// Remove null values and undefined
149+
Object.keys(cleaned).forEach((key) => {
150+
if (cleaned[key] === null || cleaned[key] === undefined) {
151+
delete cleaned[key];
152+
}
153+
});
154+
155+
if (cleaned.issue_tracker) {
156+
if (
157+
cleaned.issue_tracker.name === undefined ||
158+
cleaned.issue_tracker.host === undefined
159+
) {
160+
delete cleaned.issue_tracker;
161+
}
162+
}
163+
164+
return cleaned;
165+
}
166+
167+
/**
168+
* Updates an existing test case in BrowserStack Test Management.
169+
*/
170+
export async function updateTestCase(
171+
params: TestCaseUpdateRequest,
172+
config: BrowserStackConfig,
173+
): Promise<CallToolResult> {
174+
const authString = getBrowserStackAuth(config);
175+
const [username, password] = authString.split(":");
176+
177+
// Convert project identifier to project ID first
178+
const projectId = await projectIdentifierToId(
179+
params.project_identifier,
180+
config,
181+
);
182+
183+
// Build the request body
184+
const body: any = {
185+
title: params.name,
186+
description: params.description,
187+
preconditions: params.preconditions,
188+
automation_status: params.automation_status,
189+
priority: params.priority,
190+
case_type: params.case_type,
191+
owner: params.owner,
192+
};
193+
194+
// Add steps if provided
195+
if (params.test_case_steps) {
196+
body.steps = params.test_case_steps;
197+
}
198+
199+
// Add tags if provided
200+
if (params.tags) {
201+
body.tags = params.tags;
202+
}
203+
204+
// Add issues if provided
205+
if (params.issues && params.issues.length > 0) {
206+
if (params.issue_tracker) {
207+
body.issues = params.issues.map((issue) => ({
208+
jira_id: issue,
209+
issue_type: "story", // default type, can be customized
210+
}));
211+
body.issue_tracker = params.issue_tracker;
212+
}
213+
}
214+
215+
// Add custom fields if provided
216+
if (params.custom_fields) {
217+
body.custom_fields = Object.entries(params.custom_fields).map(
218+
([name, value]) => ({
219+
name,
220+
value,
221+
}),
222+
);
223+
}
224+
225+
// Remove undefined values
226+
Object.keys(body).forEach((key) => {
227+
if (body[key] === undefined) {
228+
delete body[key];
229+
}
230+
});
231+
232+
try {
233+
const tmBaseUrl = await getTMBaseURL(config);
234+
const response = await apiClient.put({
235+
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
236+
projectId.toString(),
237+
)}/test-cases/${encodeURIComponent(params.test_case_id)}`,
238+
headers: {
239+
"Content-Type": "application/json",
240+
Authorization:
241+
"Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
242+
},
243+
body,
244+
});
245+
246+
const { data } = response.data;
247+
if (!data.success) {
248+
return {
249+
content: [
250+
{
251+
type: "text",
252+
text: `Failed to update test case: ${JSON.stringify(
253+
response.data,
254+
)}`,
255+
isError: true,
256+
},
257+
],
258+
isError: true,
259+
};
260+
}
261+
262+
const tc = data.test_case;
263+
264+
return {
265+
content: [
266+
{
267+
type: "text",
268+
text: `Test case successfully updated:
269+
270+
**Test Case Details:**
271+
- **ID**: ${tc.identifier}
272+
- **Name**: ${tc.title}
273+
- **Description**: ${tc.description || "N/A"}
274+
- **Owner**: ${tc.owner || "N/A"}
275+
- **Priority**: ${tc.priority}
276+
- **Case Type**: ${tc.case_type}
277+
- **Automation Status**: ${tc.automation_status || "N/A"}
278+
- **Preconditions**: ${tc.preconditions || "N/A"}
279+
- **Tags**: ${tc.tags?.join(", ") || "None"}
280+
- **Steps**: ${tc.steps?.length || 0} steps
281+
- **Custom Fields**: ${tc.custom_fields?.length || 0} fields
282+
283+
**View on BrowserStack Dashboard:**
284+
https://test-management.browserstack.com/projects/${projectId}/folders/${tc.folder_id}/test-cases/${tc.identifier}
285+
286+
The test case has been updated successfully and is now available in your BrowserStack Test Management project.`,
287+
},
288+
],
289+
};
290+
} catch (err: any) {
291+
console.error("Update test case error:", err);
292+
293+
if (err.response?.status === 404) {
294+
return {
295+
content: [
296+
{
297+
type: "text",
298+
text: `Test case not found. Please verify the project_identifier ("${params.project_identifier}") and test_case_id ("${params.test_case_id}") are correct. Make sure to use actual values, not placeholders like "your_project_id".`,
299+
isError: true,
300+
},
301+
],
302+
isError: true,
303+
};
304+
}
305+
306+
if (err.response?.status === 403) {
307+
return {
308+
content: [
309+
{
310+
type: "text",
311+
text: "Access denied. You don't have permission to update this test case.",
312+
isError: true,
313+
},
314+
],
315+
isError: true,
316+
};
317+
}
318+
319+
const errorMessage = formatAxiosError(err, "Failed to update test case");
320+
return {
321+
content: [
322+
{
323+
type: "text",
324+
text: `Failed to update test case: ${errorMessage}. Please verify your credentials and try again.`,
325+
isError: true,
326+
},
327+
],
328+
isError: true,
329+
};
330+
}
331+
}

0 commit comments

Comments
 (0)