Skip to content

Commit a9b28be

Browse files
committed
feat: refactor TM API URLs to use dynamic base URL retrieval
1 parent 8f346f9 commit a9b28be

File tree

13 files changed

+133
-38
lines changed

13 files changed

+133
-38
lines changed

src/lib/tm-base-url.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { apiClient } from "./apiClient.js";
2+
import logger from "../logger.js";
3+
4+
const TM_BASE_URLS = [
5+
"https://test-management.browserstack.com",
6+
"https://test-management-eu.browserstack.com",
7+
"https://test-management-in.browserstack.com",
8+
] as const;
9+
10+
let cachedBaseUrl: string | null = null;
11+
12+
export async function getTMBaseURL(): Promise<string> {
13+
if (cachedBaseUrl) {
14+
logger.debug(`Using cached TM base URL: ${cachedBaseUrl}`);
15+
return cachedBaseUrl;
16+
}
17+
18+
logger.info("No cached TM base URL found, testing available URLs");
19+
20+
for (const baseUrl of TM_BASE_URLS) {
21+
try {
22+
const res = await apiClient.get({
23+
url: `${baseUrl}/api/v2/projects/`,
24+
raise_error: false,
25+
});
26+
27+
if (res.ok) {
28+
cachedBaseUrl = baseUrl;
29+
logger.info(`Selected TM base URL: ${baseUrl}`);
30+
return baseUrl;
31+
}
32+
} catch (err) {
33+
logger.debug(`Failed TM base URL: ${baseUrl} (${err})`);
34+
}
35+
}
36+
37+
const fallback = TM_BASE_URLS[0];
38+
cachedBaseUrl = fallback;
39+
logger.warn(`All TM URLs failed. Using fallback: ${fallback}`);
40+
return fallback;
41+
}
42+
43+
export function clearTMBaseURLCache(): void {
44+
cachedBaseUrl = null;
45+
logger.debug("Cleared TM base URL cache");
46+
}

src/tools/testmanagement-utils/TCG-utils/api.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { apiClient } from "../../../lib/apiClient.js";
22
import {
3-
TCG_TRIGGER_URL,
4-
TCG_POLL_URL,
5-
FETCH_DETAILS_URL,
6-
FORM_FIELDS_URL,
7-
BULK_CREATE_URL,
3+
getTCGTriggerURL,
4+
getTCGPollURL,
5+
getFetchDetailsURL,
6+
getFormFieldsURL,
7+
getBulkCreateURL,
88
} from "./config.js";
99
import {
1010
DefaultFieldMaps,
@@ -14,6 +14,7 @@ import {
1414
import { createTestCasePayload } from "./helpers.js";
1515
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
1616
import { BrowserStackConfig } from "../../../lib/types.js";
17+
import { getTMBaseURL } from "../../../lib/tm-base-url.js";
1718

1819
/**
1920
* Fetch default and custom form fields for a project.
@@ -22,8 +23,9 @@ export async function fetchFormFields(
2223
projectId: string,
2324
config: BrowserStackConfig,
2425
): Promise<{ default_fields: any; custom_fields: any }> {
26+
const url = await getFormFieldsURL(projectId);
2527
const res = await apiClient.get({
26-
url: FORM_FIELDS_URL(projectId),
28+
url,
2729
headers: {
2830
"API-TOKEN": getBrowserStackAuth(config),
2931
},
@@ -42,8 +44,10 @@ export async function triggerTestCaseGeneration(
4244
source: string,
4345
config: BrowserStackConfig,
4446
): Promise<string> {
47+
const url = await getTCGTriggerURL();
48+
const tmBaseUrl = await getTMBaseURL();
4549
const res = await apiClient.post({
46-
url: TCG_TRIGGER_URL,
50+
url,
4751
headers: {
4852
"API-TOKEN": getBrowserStackAuth(config),
4953
"Content-Type": "application/json",
@@ -55,7 +59,7 @@ export async function triggerTestCaseGeneration(
5559
folderId,
5660
projectId,
5761
source,
58-
webhookUrl: `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
62+
webhookUrl: `${tmBaseUrl}/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
5963
},
6064
});
6165
if (res.status !== 200) {
@@ -78,8 +82,9 @@ export async function fetchTestCaseDetails(
7882
if (testCaseIds.length === 0) {
7983
throw new Error("No testCaseIds provided to fetchTestCaseDetails");
8084
}
85+
const url = await getFetchDetailsURL();
8186
const res = await apiClient.post({
82-
url: FETCH_DETAILS_URL,
87+
url,
8388
headers: {
8489
"API-TOKEN": getBrowserStackAuth(config),
8590
"request-source": source,
@@ -107,13 +112,14 @@ export async function pollTestCaseDetails(
107112
): Promise<Record<string, any>> {
108113
const detailMap: Record<string, any> = {};
109114
let done = false;
115+
const pollUrl = await getTCGPollURL();
110116

111117
while (!done) {
112118
// add a bit of jitter to avoid synchronized polling storms
113119
await new Promise((r) => setTimeout(r, 10000 + Math.random() * 5000));
114120

115121
const poll = await apiClient.post({
116-
url: `${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
122+
url: `${pollUrl}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
117123
headers: {
118124
"API-TOKEN": getBrowserStackAuth(config),
119125
},
@@ -157,6 +163,7 @@ export async function pollScenariosTestDetails(
157163
const scenariosMap: Record<string, Scenario> = {};
158164
const detailPromises: Promise<Record<string, any>>[] = [];
159165
let iteratorCount = 0;
166+
const TCG_POLL_URL = await getTCGPollURL();
160167

161168
// Promisify interval-style polling using a wrapper
162169
await new Promise<void>((resolve, reject) => {
@@ -279,6 +286,7 @@ export async function bulkCreateTestCases(
279286
const total = Object.keys(scenariosMap).length;
280287
let doneCount = 0;
281288
let testCaseCount = 0;
289+
const BULK_CREATE_URL = await getBulkCreateURL(projectId, folderId);
282290

283291
for (const { id, testcases } of Object.values(scenariosMap)) {
284292
const testCaseLength = testcases.length;
@@ -300,7 +308,7 @@ export async function bulkCreateTestCases(
300308

301309
try {
302310
const resp = await apiClient.post({
303-
url: BULK_CREATE_URL(projectId, folderId),
311+
url: BULK_CREATE_URL,
304312
headers: {
305313
"API-TOKEN": getBrowserStackAuth(config),
306314
"Content-Type": "application/json",
@@ -341,7 +349,8 @@ export async function projectIdentifierToId(
341349
projectId: string,
342350
config: BrowserStackConfig,
343351
): Promise<string> {
344-
const url = `https://test-management.browserstack.com/api/v1/projects/?q=${projectId}`;
352+
const tmBaseUrl = await getTMBaseURL();
353+
const url = `${tmBaseUrl}/api/v1/projects/?q=${projectId}`;
345354

346355
const response = await apiClient.get({
347356
url,
@@ -368,7 +377,8 @@ export async function testCaseIdentifierToDetails(
368377
testCaseIdentifier: string,
369378
config: BrowserStackConfig,
370379
): Promise<{ testCaseId: string; folderId: string }> {
371-
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
380+
const tmBaseUrl = await getTMBaseURL();
381+
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
372382

373383
const response = await apiClient.get({
374384
url,
Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1-
export const TCG_TRIGGER_URL =
2-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/suggest-test-cases";
3-
export const TCG_POLL_URL =
4-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/test-cases-polling";
5-
export const FETCH_DETAILS_URL =
6-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/fetch-test-case-details";
7-
export const FORM_FIELDS_URL = (projectId: string): string =>
8-
`https://test-management.browserstack.com/api/v1/projects/${projectId}/form-fields-v2`;
9-
export const BULK_CREATE_URL = (projectId: string, folderId: string): string =>
10-
`https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
1+
import { getTMBaseURL } from "../../../lib/tm-base-url.js";
2+
3+
export const getTCGTriggerURL = async (): Promise<string> => {
4+
const baseUrl = await getTMBaseURL();
5+
return `${baseUrl}/api/v1/integration/tcg/test-generation/suggest-test-cases`;
6+
};
7+
8+
export const getTCGPollURL = async (): Promise<string> => {
9+
const baseUrl = await getTMBaseURL();
10+
return `${baseUrl}/api/v1/integration/tcg/test-generation/test-cases-polling`;
11+
};
12+
13+
export const getFetchDetailsURL = async (): Promise<string> => {
14+
const baseUrl = await getTMBaseURL();
15+
return `${baseUrl}/api/v1/integration/tcg/test-generation/fetch-test-case-details`;
16+
};
17+
18+
export const getFormFieldsURL = async (projectId: string): Promise<string> => {
19+
const baseUrl = await getTMBaseURL();
20+
return `${baseUrl}/api/v1/projects/${projectId}/form-fields-v2`;
21+
};
22+
23+
export const getBulkCreateURL = async (
24+
projectId: string,
25+
folderId: string,
26+
): Promise<string> => {
27+
const baseUrl = await getTMBaseURL();
28+
return `${baseUrl}/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
29+
};

src/tools/testmanagement-utils/add-test-result.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getBrowserStackAuth } from "../../lib/get-auth.js";
33
import { z } from "zod";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55
import { BrowserStackConfig } from "../../lib/types.js";
6+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
67

78
/**
89
* Schema for adding a test result to a test run.
@@ -37,7 +38,8 @@ export async function addTestResult(
3738
): Promise<CallToolResult> {
3839
try {
3940
const args = AddTestResultSchema.parse(rawArgs);
40-
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
41+
const tmBaseUrl = await getTMBaseURL();
42+
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
4143
args.project_identifier,
4244
)}/test-runs/${encodeURIComponent(args.test_run_id)}/results`;
4345

src/tools/testmanagement-utils/create-lca-steps.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { pollLCAStatus } from "./poll-lca-status.js";
99
import { getBrowserStackAuth } from "../../lib/get-auth.js";
1010
import { BrowserStackConfig } from "../../lib/types.js";
11+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
1112

1213
/**
1314
* Schema for creating LCA steps for a test case
@@ -81,7 +82,8 @@ export async function createLCASteps(
8182
config,
8283
);
8384

84-
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
85+
const tmBaseUrl = await getTMBaseURL();
86+
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
8587

8688
const payload = {
8789
base_url: args.base_url,
@@ -90,7 +92,7 @@ export async function createLCASteps(
9092
test_name: args.test_name,
9193
test_case_details: args.test_case_details,
9294
version: "v2",
93-
webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
95+
webhook_path: `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
9496
};
9597

9698
await apiClient.post({

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { formatAxiosError } from "../../lib/error.js";
55
import { projectIdentifierToId } from "../testmanagement-utils/TCG-utils/api.js";
66
import { getBrowserStackAuth } from "../../lib/get-auth.js";
77
import { BrowserStackConfig } from "../../lib/types.js";
8+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
89

910
// Schema for combined project/folder creation
1011
export const CreateProjFoldSchema = z.object({
@@ -55,6 +56,7 @@ export async function createProjectOrFolder(
5556
);
5657
}
5758

59+
const tmBaseUrl = await getTMBaseURL();
5860
const authString = getBrowserStackAuth(config);
5961
const [username, password] = authString.split(":");
6062

@@ -66,7 +68,7 @@ export async function createProjectOrFolder(
6668
const authString = getBrowserStackAuth(config);
6769
const [username, password] = authString.split(":");
6870
const res = await apiClient.post({
69-
url: "https://test-management.browserstack.com/api/v2/projects",
71+
url: `${tmBaseUrl}/api/v2/projects`,
7072
headers: {
7173
"Content-Type": "application/json",
7274
Authorization:
@@ -96,7 +98,7 @@ export async function createProjectOrFolder(
9698
throw new Error("Cannot create folder without project_identifier.");
9799
try {
98100
const res = await apiClient.post({
99-
url: `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
101+
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
100102
projId,
101103
)}/folders`,
102104
headers: {
@@ -130,7 +132,7 @@ export async function createProjectOrFolder(
130132
- ID: ${folder.id}
131133
- Name: ${folder.name}
132134
- Project Identifier: ${projId}
133-
Access it here: https://test-management.browserstack.com/projects/${projectId}/folder/${folder.id}/`,
135+
Access it here: ${tmBaseUrl}/projects/${projectId}/folder/${folder.id}/`,
134136
},
135137
],
136138
};

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import { getBrowserStackAuth } from "../../lib/get-auth.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
78

89
/**
910
* Schema for creating a test run.
@@ -66,7 +67,8 @@ export async function createTestRun(
6667
};
6768
const args = CreateTestRunSchema.parse(inputArgs);
6869

69-
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
70+
const tmBaseUrl = await getTMBaseURL();
71+
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
7072
args.project_identifier,
7173
)}/test-runs`;
7274

src/tools/testmanagement-utils/list-testcases.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import { getBrowserStackAuth } from "../../lib/get-auth.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
78

89
/**
910
* Schema for listing test cases with optional filters.
@@ -49,7 +50,8 @@ export async function listTestCases(
4950
if (args.priority) params.append("priority", args.priority);
5051
if (args.p !== undefined) params.append("p", args.p.toString());
5152

52-
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
53+
const tmBaseUrl = await getTMBaseURL();
54+
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
5355
args.project_identifier,
5456
)}/test-cases?${params.toString()}`;
5557

src/tools/testmanagement-utils/list-testruns.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import { getBrowserStackAuth } from "../../lib/get-auth.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
78

89
/**
910
* Schema for listing test runs with optional filters.
@@ -35,10 +36,11 @@ export async function listTestRuns(
3536
params.set("run_state", args.run_state);
3637
}
3738

39+
const tmBaseUrl = await getTMBaseURL();
3840
const url =
39-
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
41+
`${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
4042
args.project_identifier,
41-
)}/test-runs?` + params.toString();
43+
)}/test-runs` + (params.toString() ? `?${params.toString()}` : "");
4244

4345
const authString = getBrowserStackAuth(config);
4446
const [username, password] = authString.split(":");

src/tools/testmanagement-utils/poll-lca-status.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { apiClient } from "../../lib/apiClient.js";
22
import { getBrowserStackAuth } from "../../lib/get-auth.js";
33
import { BrowserStackConfig } from "../../lib/types.js";
4+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
45

56
/**
67
* Interface for the test case response structure
@@ -39,7 +40,8 @@ export async function pollLCAStatus(
3940
pollIntervalMs: number = 10 * 1000, // 10 seconds interval
4041
config: BrowserStackConfig,
4142
): Promise<{ resource_path: string; status: string } | null> {
42-
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/test-cases/${testCaseId}`;
43+
const tmBaseUrl = await getTMBaseURL();
44+
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/folder/${folderId}/test-cases/${testCaseId}`;
4345

4446
const startTime = Date.now();
4547

0 commit comments

Comments
 (0)