From 66a7f87c243b96a07bfeb70659db2e78a8f28c78 Mon Sep 17 00:00:00 2001 From: manoj-k04 Date: Fri, 21 Nov 2025 11:41:29 +0530 Subject: [PATCH 1/4] 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 --- .../testmanagement-utils/update-testcase.ts | 331 ++++++++++++++++++ src/tools/testmanagement.ts | 50 +++ 2 files changed, 381 insertions(+) create mode 100644 src/tools/testmanagement-utils/update-testcase.ts diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts new file mode 100644 index 00000000..f81e5338 --- /dev/null +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -0,0 +1,331 @@ +import { apiClient } from "../../lib/apiClient.js"; +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { formatAxiosError } from "../../lib/error.js"; +import { projectIdentifierToId } from "./TCG-utils/api.js"; +import { BrowserStackConfig } from "../../lib/types.js"; +import { getTMBaseURL } from "../../lib/tm-base-url.js"; +import { getBrowserStackAuth } from "../../lib/get-auth.js"; + +interface TestCaseStep { + step: string; + result: string; +} + +interface IssueTracker { + name: string; + host: string; +} + +export interface TestCaseUpdateRequest { + project_identifier: string; + test_case_id: string; + name?: string; + description?: string; + owner?: string; + preconditions?: string; + test_case_steps?: TestCaseStep[]; + issues?: string[]; + issue_tracker?: IssueTracker; + tags?: string[]; + custom_fields?: Record; + automation_status?: string; + priority?: string; + case_type?: string; +} + +export interface TestCaseUpdateResponse { + data: { + success: boolean; + test_case: { + case_type: string; + priority: string; + status: string; + folder_id: number; + issues: Array<{ + jira_id: string; + issue_type: string; + }>; + tags: string[]; + template: string; + description: string; + preconditions: string; + title: string; + identifier: string; + automation_status: string; + owner: string; + steps: TestCaseStep[]; + custom_fields: Array<{ + name: string; + value: string; + }>; + }; + }; +} + +export const UpdateTestCaseSchema = z.object({ + project_identifier: z + .string() + .describe( + "The ID of the BrowserStack project containing the test case to update.", + ), + test_case_id: z + .string() + .describe( + "The ID of the test case to update. This can be found using the listTestCases tool.", + ), + name: z.string().optional().describe("Updated name of the test case."), + description: z + .string() + .optional() + .describe("Updated brief description of the test case."), + owner: z + .string() + .email() + .describe("Updated email of the test case owner.") + .optional(), + preconditions: z + .string() + .optional() + .describe("Updated preconditions (HTML allowed)."), + test_case_steps: z + .array( + z.object({ + step: z.string().describe("Action to perform in this step."), + result: z.string().describe("Expected result of this step."), + }), + ) + .optional() + .describe("Updated list of steps and expected results."), + issues: z + .array(z.string()) + .optional() + .describe( + "Updated list of linked Jira, Asana or Azure issues ID's. This should be strictly in array format.", + ), + issue_tracker: z + .object({ + name: z + .string() + .describe( + "Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.", + ), + host: z.string().url().describe("Base URL of the issue tracker."), + }) + .optional() + .describe("Updated issue tracker configuration"), + tags: z + .array(z.string()) + .optional() + .describe( + "Updated tags to attach to the test case. This should be strictly in array format.", + ), + custom_fields: z + .record(z.string()) + .optional() + .describe("Updated map of custom field names to values."), + automation_status: z + .string() + .optional() + .describe( + "Updated automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'.", + ), + priority: z + .string() + .optional() + .describe( + "Updated priority level (e.g., 'critical', 'high', 'medium', 'low').", + ), + case_type: z + .string() + .optional() + .describe("Updated case type (e.g., 'functional', 'regression', 'smoke')."), +}); + +export function sanitizeUpdateArgs(args: any) { + const cleaned = { ...args }; + + // Remove null values and undefined + Object.keys(cleaned).forEach((key) => { + if (cleaned[key] === null || cleaned[key] === undefined) { + delete cleaned[key]; + } + }); + + if (cleaned.issue_tracker) { + if ( + cleaned.issue_tracker.name === undefined || + cleaned.issue_tracker.host === undefined + ) { + delete cleaned.issue_tracker; + } + } + + return cleaned; +} + +/** + * Updates an existing test case in BrowserStack Test Management. + */ +export async function updateTestCase( + params: TestCaseUpdateRequest, + config: BrowserStackConfig, +): Promise { + const authString = getBrowserStackAuth(config); + const [username, password] = authString.split(":"); + + // Convert project identifier to project ID first + const projectId = await projectIdentifierToId( + params.project_identifier, + config, + ); + + // Build the request body + const body: any = { + title: params.name, + description: params.description, + preconditions: params.preconditions, + automation_status: params.automation_status, + priority: params.priority, + case_type: params.case_type, + owner: params.owner, + }; + + // Add steps if provided + if (params.test_case_steps) { + body.steps = params.test_case_steps; + } + + // Add tags if provided + if (params.tags) { + body.tags = params.tags; + } + + // Add issues if provided + if (params.issues && params.issues.length > 0) { + if (params.issue_tracker) { + body.issues = params.issues.map((issue) => ({ + jira_id: issue, + issue_type: "story", // default type, can be customized + })); + body.issue_tracker = params.issue_tracker; + } + } + + // Add custom fields if provided + if (params.custom_fields) { + body.custom_fields = Object.entries(params.custom_fields).map( + ([name, value]) => ({ + name, + value, + }), + ); + } + + // Remove undefined values + Object.keys(body).forEach((key) => { + if (body[key] === undefined) { + delete body[key]; + } + }); + + try { + const tmBaseUrl = await getTMBaseURL(config); + const response = await apiClient.put({ + url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent( + projectId.toString(), + )}/test-cases/${encodeURIComponent(params.test_case_id)}`, + headers: { + "Content-Type": "application/json", + Authorization: + "Basic " + Buffer.from(`${username}:${password}`).toString("base64"), + }, + body, + }); + + const { data } = response.data; + if (!data.success) { + return { + content: [ + { + type: "text", + text: `Failed to update test case: ${JSON.stringify( + response.data, + )}`, + isError: true, + }, + ], + isError: true, + }; + } + + const tc = data.test_case; + + return { + content: [ + { + type: "text", + text: `Test case successfully updated: + +**Test Case Details:** +- **ID**: ${tc.identifier} +- **Name**: ${tc.title} +- **Description**: ${tc.description || "N/A"} +- **Owner**: ${tc.owner || "N/A"} +- **Priority**: ${tc.priority} +- **Case Type**: ${tc.case_type} +- **Automation Status**: ${tc.automation_status || "N/A"} +- **Preconditions**: ${tc.preconditions || "N/A"} +- **Tags**: ${tc.tags?.join(", ") || "None"} +- **Steps**: ${tc.steps?.length || 0} steps +- **Custom Fields**: ${tc.custom_fields?.length || 0} fields + +**View on BrowserStack Dashboard:** +https://test-management.browserstack.com/projects/${projectId}/folders/${tc.folder_id}/test-cases/${tc.identifier} + +The test case has been updated successfully and is now available in your BrowserStack Test Management project.`, + }, + ], + }; + } catch (err: any) { + console.error("Update test case error:", err); + + if (err.response?.status === 404) { + return { + content: [ + { + type: "text", + 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".`, + isError: true, + }, + ], + isError: true, + }; + } + + if (err.response?.status === 403) { + return { + content: [ + { + type: "text", + text: "Access denied. You don't have permission to update this test case.", + isError: true, + }, + ], + isError: true, + }; + } + + const errorMessage = formatAxiosError(err, "Failed to update test case"); + return { + content: [ + { + type: "text", + text: `Failed to update test case: ${errorMessage}. Please verify your credentials and try again.`, + isError: true, + }, + ], + isError: true, + }; + } +} diff --git a/src/tools/testmanagement.ts b/src/tools/testmanagement.ts index 830639cd..bfbedebe 100644 --- a/src/tools/testmanagement.ts +++ b/src/tools/testmanagement.ts @@ -14,6 +14,13 @@ import { CreateTestCaseSchema, } from "./testmanagement-utils/create-testcase.js"; +import { + updateTestCase as updateTestCaseAPI, + TestCaseUpdateRequest, + sanitizeUpdateArgs, + UpdateTestCaseSchema, +} from "./testmanagement-utils/update-testcase.js"; + import { listTestCases, ListTestCasesSchema, @@ -130,6 +137,42 @@ export async function createTestCaseTool( } } +/** + * Updates an existing test case in BrowserStack Test Management. + */ +export async function updateTestCaseTool( + args: TestCaseUpdateRequest, + config: BrowserStackConfig, + server: McpServer, +): Promise { + // Sanitize input arguments + const cleanedArgs = sanitizeUpdateArgs(args); + try { + trackMCP( + "updateTestCase", + server.server.getClientVersion()!, + undefined, + config, + ); + return await updateTestCaseAPI(cleanedArgs, config); + } catch (err) { + logger.error("Failed to update test case: %s", err); + trackMCP("updateTestCase", server.server.getClientVersion()!, err, config); + return { + content: [ + { + type: "text", + text: `Failed to update test case: ${ + err instanceof Error ? err.message : "Unknown error" + }. Please open an issue on GitHub if the problem persists`, + isError: true, + }, + ], + isError: true, + }; + } +} + /** * Lists test cases in a project with optional filters (status, priority, custom fields, etc.) */ @@ -426,6 +469,13 @@ export default function addTestManagementTools( (args) => createTestCaseTool(args, config, server), ); + tools.updateTestCase = server.tool( + "updateTestCase", + "Use this tool to update an existing test case in BrowserStack Test Management. Allows editing test case details like name, description, steps, owner, priority, and more.", + UpdateTestCaseSchema.shape, + (args) => updateTestCaseTool(args, config, server), + ); + tools.listTestCases = server.tool( "listTestCases", "List test cases in a project with optional filters (status, priority, custom fields, etc.)", From 6b26693e8d282ed81bdd5ef62a300ba5aded5a63 Mon Sep 17 00:00:00 2001 From: manoj-k04 Date: Mon, 1 Dec 2025 13:25:01 +0530 Subject: [PATCH 2/4] feat: Update test case implementation with API fixes --- .../testmanagement-utils/update-testcase.ts | 178 ++++-------------- 1 file changed, 33 insertions(+), 145 deletions(-) diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index f81e5338..e6e6ac6b 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -6,32 +6,13 @@ import { projectIdentifierToId } from "./TCG-utils/api.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { getTMBaseURL } from "../../lib/tm-base-url.js"; import { getBrowserStackAuth } from "../../lib/get-auth.js"; - -interface TestCaseStep { - step: string; - result: string; -} - -interface IssueTracker { - name: string; - host: string; -} +import logger from "../../logger.js"; export interface TestCaseUpdateRequest { project_identifier: string; - test_case_id: string; + test_case_identifier: string; name?: string; description?: string; - owner?: string; - preconditions?: string; - test_case_steps?: TestCaseStep[]; - issues?: string[]; - issue_tracker?: IssueTracker; - tags?: string[]; - custom_fields?: Record; - automation_status?: string; - priority?: string; - case_type?: string; } export interface TestCaseUpdateResponse { @@ -54,7 +35,10 @@ export interface TestCaseUpdateResponse { identifier: string; automation_status: string; owner: string; - steps: TestCaseStep[]; + steps: Array<{ + step: string; + result: string; + }>; custom_fields: Array<{ name: string; value: string; @@ -69,7 +53,7 @@ export const UpdateTestCaseSchema = z.object({ .describe( "The ID of the BrowserStack project containing the test case to update.", ), - test_case_id: z + test_case_identifier: z .string() .describe( "The ID of the test case to update. This can be found using the listTestCases tool.", @@ -79,67 +63,6 @@ export const UpdateTestCaseSchema = z.object({ .string() .optional() .describe("Updated brief description of the test case."), - owner: z - .string() - .email() - .describe("Updated email of the test case owner.") - .optional(), - preconditions: z - .string() - .optional() - .describe("Updated preconditions (HTML allowed)."), - test_case_steps: z - .array( - z.object({ - step: z.string().describe("Action to perform in this step."), - result: z.string().describe("Expected result of this step."), - }), - ) - .optional() - .describe("Updated list of steps and expected results."), - issues: z - .array(z.string()) - .optional() - .describe( - "Updated list of linked Jira, Asana or Azure issues ID's. This should be strictly in array format.", - ), - issue_tracker: z - .object({ - name: z - .string() - .describe( - "Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.", - ), - host: z.string().url().describe("Base URL of the issue tracker."), - }) - .optional() - .describe("Updated issue tracker configuration"), - tags: z - .array(z.string()) - .optional() - .describe( - "Updated tags to attach to the test case. This should be strictly in array format.", - ), - custom_fields: z - .record(z.string()) - .optional() - .describe("Updated map of custom field names to values."), - automation_status: z - .string() - .optional() - .describe( - "Updated automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'.", - ), - priority: z - .string() - .optional() - .describe( - "Updated priority level (e.g., 'critical', 'high', 'medium', 'low').", - ), - case_type: z - .string() - .optional() - .describe("Updated case type (e.g., 'functional', 'regression', 'smoke')."), }); export function sanitizeUpdateArgs(args: any) { @@ -174,67 +97,25 @@ export async function updateTestCase( const authString = getBrowserStackAuth(config); const [username, password] = authString.split(":"); - // Convert project identifier to project ID first - const projectId = await projectIdentifierToId( - params.project_identifier, - config, - ); + // Build the request body with only the fields to update + const testCaseBody: any = {}; - // Build the request body - const body: any = { - title: params.name, - description: params.description, - preconditions: params.preconditions, - automation_status: params.automation_status, - priority: params.priority, - case_type: params.case_type, - owner: params.owner, - }; - - // Add steps if provided - if (params.test_case_steps) { - body.steps = params.test_case_steps; + if (params.name !== undefined) { + testCaseBody.name = params.name; } - // Add tags if provided - if (params.tags) { - body.tags = params.tags; + if (params.description !== undefined) { + testCaseBody.description = params.description; } - // Add issues if provided - if (params.issues && params.issues.length > 0) { - if (params.issue_tracker) { - body.issues = params.issues.map((issue) => ({ - jira_id: issue, - issue_type: "story", // default type, can be customized - })); - body.issue_tracker = params.issue_tracker; - } - } - - // Add custom fields if provided - if (params.custom_fields) { - body.custom_fields = Object.entries(params.custom_fields).map( - ([name, value]) => ({ - name, - value, - }), - ); - } - - // Remove undefined values - Object.keys(body).forEach((key) => { - if (body[key] === undefined) { - delete body[key]; - } - }); + const body = { test_case: testCaseBody }; try { const tmBaseUrl = await getTMBaseURL(config); - const response = await apiClient.put({ + const response = await apiClient.patch({ url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent( - projectId.toString(), - )}/test-cases/${encodeURIComponent(params.test_case_id)}`, + params.project_identifier, + )}/test-cases/${encodeURIComponent(params.test_case_identifier)}`, headers: { "Content-Type": "application/json", Authorization: @@ -261,6 +142,12 @@ export async function updateTestCase( const tc = data.test_case; + // Convert project identifier to project ID for dashboard URL + const projectId = await projectIdentifierToId( + params.project_identifier, + config, + ); + return { content: [ { @@ -271,14 +158,9 @@ export async function updateTestCase( - **ID**: ${tc.identifier} - **Name**: ${tc.title} - **Description**: ${tc.description || "N/A"} -- **Owner**: ${tc.owner || "N/A"} -- **Priority**: ${tc.priority} - **Case Type**: ${tc.case_type} -- **Automation Status**: ${tc.automation_status || "N/A"} -- **Preconditions**: ${tc.preconditions || "N/A"} -- **Tags**: ${tc.tags?.join(", ") || "None"} -- **Steps**: ${tc.steps?.length || 0} steps -- **Custom Fields**: ${tc.custom_fields?.length || 0} fields +- **Priority**: ${tc.priority} +- **Status**: ${tc.status} **View on BrowserStack Dashboard:** https://test-management.browserstack.com/projects/${projectId}/folders/${tc.folder_id}/test-cases/${tc.identifier} @@ -288,14 +170,20 @@ The test case has been updated successfully and is now available in your Browser ], }; } catch (err: any) { - console.error("Update test case error:", err); + logger.error("Failed to update test case: %s", err); + logger.error( + "Error details:", + JSON.stringify(err.response?.data || err.message), + ); if (err.response?.status === 404) { return { content: [ { type: "text", - 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".`, + text: `Test case not found. Please verify the project_identifier ("${params.project_identifier}") and test_case_identifier ("${params.test_case_identifier}") are correct. Make sure to use actual values, not placeholders like "your_project_id". + +Error details: ${JSON.stringify(err.response?.data || err.message)}`, isError: true, }, ], From 56b19546ef33a0bef273ad2beee602f552420404 Mon Sep 17 00:00:00 2001 From: manoj-k04 Date: Tue, 2 Dec 2025 18:48:08 +0530 Subject: [PATCH 3/4] Removed redundant functions --- .../testmanagement-utils/update-testcase.ts | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index e6e6ac6b..bfab691a 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -15,37 +15,6 @@ export interface TestCaseUpdateRequest { description?: string; } -export interface TestCaseUpdateResponse { - data: { - success: boolean; - test_case: { - case_type: string; - priority: string; - status: string; - folder_id: number; - issues: Array<{ - jira_id: string; - issue_type: string; - }>; - tags: string[]; - template: string; - description: string; - preconditions: string; - title: string; - identifier: string; - automation_status: string; - owner: string; - steps: Array<{ - step: string; - result: string; - }>; - custom_fields: Array<{ - name: string; - value: string; - }>; - }; - }; -} export const UpdateTestCaseSchema = z.object({ project_identifier: z @@ -65,27 +34,6 @@ export const UpdateTestCaseSchema = z.object({ .describe("Updated brief description of the test case."), }); -export function sanitizeUpdateArgs(args: any) { - const cleaned = { ...args }; - - // Remove null values and undefined - Object.keys(cleaned).forEach((key) => { - if (cleaned[key] === null || cleaned[key] === undefined) { - delete cleaned[key]; - } - }); - - if (cleaned.issue_tracker) { - if ( - cleaned.issue_tracker.name === undefined || - cleaned.issue_tracker.host === undefined - ) { - delete cleaned.issue_tracker; - } - } - - return cleaned; -} /** * Updates an existing test case in BrowserStack Test Management. From 00ef595d95192e3f6c69dd9a55bd58459e45a5f0 Mon Sep 17 00:00:00 2001 From: manoj-k04 Date: Tue, 2 Dec 2025 19:52:13 +0530 Subject: [PATCH 4/4] Added Precondition and steps in schema --- .../testmanagement-utils/update-testcase.ts | 28 +++++++++++++++++-- src/tools/testmanagement.ts | 5 +--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index bfab691a..2bb71ea9 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -13,9 +13,13 @@ export interface TestCaseUpdateRequest { test_case_identifier: string; name?: string; description?: string; + preconditions?: string; + test_case_steps?: Array<{ + step: string; + result: string; + }>; } - export const UpdateTestCaseSchema = z.object({ project_identifier: z .string() @@ -32,9 +36,21 @@ export const UpdateTestCaseSchema = z.object({ .string() .optional() .describe("Updated brief description of the test case."), + preconditions: z + .string() + .optional() + .describe("Updated preconditions for the test case."), + test_case_steps: z + .array( + z.object({ + step: z.string().describe("The action to perform in this step."), + result: z.string().describe("The expected result of this step."), + }), + ) + .optional() + .describe("Updated list of test case steps with expected results."), }); - /** * Updates an existing test case in BrowserStack Test Management. */ @@ -56,6 +72,14 @@ export async function updateTestCase( testCaseBody.description = params.description; } + if (params.preconditions !== undefined) { + testCaseBody.preconditions = params.preconditions; + } + + if (params.test_case_steps !== undefined) { + testCaseBody.steps = params.test_case_steps; + } + const body = { test_case: testCaseBody }; try { diff --git a/src/tools/testmanagement.ts b/src/tools/testmanagement.ts index bfbedebe..dc0aee59 100644 --- a/src/tools/testmanagement.ts +++ b/src/tools/testmanagement.ts @@ -17,7 +17,6 @@ import { import { updateTestCase as updateTestCaseAPI, TestCaseUpdateRequest, - sanitizeUpdateArgs, UpdateTestCaseSchema, } from "./testmanagement-utils/update-testcase.js"; @@ -145,8 +144,6 @@ export async function updateTestCaseTool( config: BrowserStackConfig, server: McpServer, ): Promise { - // Sanitize input arguments - const cleanedArgs = sanitizeUpdateArgs(args); try { trackMCP( "updateTestCase", @@ -154,7 +151,7 @@ export async function updateTestCaseTool( undefined, config, ); - return await updateTestCaseAPI(cleanedArgs, config); + return await updateTestCaseAPI(args, config); } catch (err) { logger.error("Failed to update test case: %s", err); trackMCP("updateTestCase", server.server.getClientVersion()!, err, config);