Skip to content

Commit 042c3fe

Browse files
kashish2508Copilot
andauthored
fix(loadtesting): correct tenant name in url (Azure#37277)
### Packages impacted by this PR azure/playwright ### Issues associated with this PR ### Describe the problem that is addressed by this PR ### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? ### Are there test cases added in this PR? _(If not, why?)_ ### Provide a list of related PRs _(if any)_ ### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ ### Checklists - [ ] Added impacted package name to the issue description - [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ - [ ] Added a changelog (if necessary) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e93d676 commit 042c3fe

File tree

8 files changed

+243
-12
lines changed

8 files changed

+243
-12
lines changed

sdk/loadtesting/playwright/src/common/constants.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ export const Constants = {
6868
HTTP_CALL_TIMEOUT: 10000,
6969
};
7070

71+
export const ArmConstants = {
72+
TenantsApiUrl: "https://management.azure.com/tenants",
73+
TenantsApiVersion: "2025-04-01",
74+
};
75+
76+
export const UrlConstants = {
77+
AzurePortalBaseUrl: "https://ms.portal.azure.com",
78+
ReportingApiSubdomain: "reporting.api",
79+
ApiSubdomain: "api",
80+
PlaywrightWorkspacesPath: "playwrightworkspaces",
81+
TestRunsPath: "test-runs",
82+
LoadTestServiceProvider: "Microsoft.LoadTestService",
83+
PlaywrightWorkspacesResourceType: "playwrightWorkspaces",
84+
TestRunsRoute: "TestRuns",
85+
ResourceGroupsPath: "resourcegroups",
86+
ResourcePath: "/resource",
87+
SubscriptionsPath: "/subscriptions",
88+
ResourceGroupsUrlPath: "/resourceGroups",
89+
ProvidersPath: "/providers",
90+
};
91+
7192
export const InternalEnvironmentVariables = {
7293
MPT_PLAYWRIGHT_VERSION: "_MPT_PLAYWRIGHT_VERSION",
7394
MPT_SETUP_FATAL_ERROR: "_MPT_SETUP_FATAL_ERROR",

sdk/loadtesting/playwright/src/common/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ export type WorkspaceMetaData = {
212212
reporting?: string;
213213
};
214214

215+
export type TenantInfo = {
216+
tenantId?: string;
217+
defaultDomain?: string;
218+
};
219+
215220
export interface UploadResult {
216221
success: boolean;
217222
errorMessage?: string;

sdk/loadtesting/playwright/src/reporter/playwrightReporter.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getHtmlReporterOutputFolder,
77
getPortalTestRunUrl,
88
getVersionInfo,
9+
resolveTenantDomain,
910
} from "../utils/utils.js";
1011
import { coreLogger } from "../common/logger.js";
1112
import { PlaywrightServiceConfig } from "../common/playwrightServiceConfig.js";
@@ -22,6 +23,7 @@ import type { WorkspaceMetaData, UploadResult } from "../common/types.js";
2223
export default class PlaywrightReporter implements Reporter {
2324
private config: FullConfig | undefined;
2425
private workspaceMetadata: WorkspaceMetaData | null = null;
26+
private tenantDomain: string | undefined;
2527
private isReportingEnabled = false;
2628

2729
/**
@@ -107,6 +109,20 @@ export default class PlaywrightReporter implements Reporter {
107109
return;
108110
}
109111

112+
// Resolve tenant domain for portal URL (if tenantId is available)
113+
if (this.workspaceMetadata.tenantId) {
114+
try {
115+
const tenants = await playwrightServiceApiClient.getTenants();
116+
this.tenantDomain = resolveTenantDomain(this.workspaceMetadata.tenantId, tenants);
117+
} catch (error) {
118+
coreLogger.error(`Failed to resolve tenant domain: ${error}`);
119+
}
120+
} else {
121+
coreLogger.info(
122+
"Workspace metadata does not contain tenantId; skipping tenant domain resolution.",
123+
);
124+
}
125+
110126
this.isReportingEnabled = true;
111127
console.log(ServiceErrorMessageConstants.REPORTING_ENABLED.message);
112128
} catch (error) {
@@ -142,7 +158,7 @@ export default class PlaywrightReporter implements Reporter {
142158
}
143159
// Display portal URL for both full and partial success
144160
if (this.workspaceMetadata) {
145-
const portalUrl = getPortalTestRunUrl(this.workspaceMetadata);
161+
const portalUrl = getPortalTestRunUrl(this.workspaceMetadata, this.tenantDomain);
146162
console.log(ServiceErrorMessageConstants.TEST_REPORT_VIEW_URL.formatWithUrl(portalUrl));
147163
}
148164
} else {

sdk/loadtesting/playwright/src/utils/PlaywrightServiceClient.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
} from "./utils.js";
99
import { ServiceErrorMessageConstants } from "../common/messages.js";
1010
import { HttpService } from "../common/httpService.js";
11-
import { TestRunCreatePayload, WorkspaceMetaData } from "../common/types.js";
12-
import { Constants, InternalEnvironmentVariables } from "../common/constants.js";
11+
import { TestRunCreatePayload, WorkspaceMetaData, TenantInfo } from "../common/types.js";
12+
import { Constants, InternalEnvironmentVariables, ArmConstants } from "../common/constants.js";
1313

1414
export class PlaywrightServiceClient {
1515
private httpService: HttpService;
@@ -102,4 +102,39 @@ export class PlaywrightServiceClient {
102102
throw new Error(ServiceErrorMessageConstants.FAILED_TO_GET_WORKSPACE_METADATA.message);
103103
}
104104
}
105+
106+
async getTenants(): Promise<TenantInfo[]> {
107+
try {
108+
const token = getAccessToken();
109+
if (!token) {
110+
throw new Error("PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set.");
111+
}
112+
const url = new URL(ArmConstants.TenantsApiUrl);
113+
url.searchParams.set("api-version", ArmConstants.TenantsApiVersion);
114+
const method = "GET";
115+
const correlationId = crypto.randomUUID();
116+
117+
const response = await this.httpService.callAPI(
118+
method,
119+
url.toString(),
120+
null,
121+
token,
122+
"",
123+
correlationId,
124+
);
125+
if (response.status !== 200) {
126+
const errorMessage = extractErrorMessage(response?.bodyAsText ?? "");
127+
throw new Error(
128+
errorMessage || `HTTP ${response.status}: Failed to retrieve tenant information`,
129+
);
130+
}
131+
const responseBody = response.bodyAsText ? JSON.parse(response.bodyAsText) : {};
132+
return responseBody.value ?? [];
133+
} catch (error) {
134+
if (error instanceof Error) {
135+
throw error;
136+
}
137+
throw new Error("Failed to retrieve tenant information.");
138+
}
139+
}
105140
}

sdk/loadtesting/playwright/src/utils/utils.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import type {
77
JwtPayload,
88
RunConfig,
99
WorkspaceMetaData,
10+
TenantInfo,
1011
} from "../common/types.js";
1112
import {
1213
Constants,
1314
InternalEnvironmentVariables,
1415
ServiceEnvironmentVariable,
1516
RunConfigConstants,
1617
GitHubActionsConstants,
18+
UrlConstants,
1719
} from "../common/constants.js";
1820
import { ServiceErrorMessageConstants } from "../common/messages.js";
1921
import { coreLogger } from "../common/logger.js";
@@ -262,7 +264,7 @@ export function getTestRunApiUrl(): string {
262264
if (!result?.region || !result?.domain || !result?.accountId) {
263265
exitWithFailureMessage(ServiceErrorMessageConstants.NO_SERVICE_URL_ERROR);
264266
}
265-
const baseUrl = `https://${result?.region}.reporting.api.${result?.domain}/playwrightworkspaces/${result?.accountId}/test-runs`;
267+
const baseUrl = `https://${result?.region}.${UrlConstants.ReportingApiSubdomain}.${result?.domain}/${UrlConstants.PlaywrightWorkspacesPath}/${result?.accountId}/${UrlConstants.TestRunsPath}`;
266268
const url = runId ? `${baseUrl}/${runId}` : baseUrl;
267269

268270
return `${url}?api-version=${Constants.LatestAPIVersion}`;
@@ -337,7 +339,7 @@ export function getWorkspaceMetaDataApiUrl(): string {
337339
if (!result?.region || !result?.domain || !result?.accountId) {
338340
exitWithFailureMessage(ServiceErrorMessageConstants.NO_SERVICE_URL_ERROR);
339341
}
340-
const baseUrl = `https://${result?.region}.api.${result?.domain}/playwrightworkspaces/${result?.accountId}`;
342+
const baseUrl = `https://${result?.region}.${UrlConstants.ApiSubdomain}.${result?.domain}/${UrlConstants.PlaywrightWorkspacesPath}/${result?.accountId}`;
341343

342344
return `${baseUrl}?api-version=${Constants.LatestAPIVersion}`;
343345
}
@@ -473,7 +475,24 @@ export function collectAllFiles(
473475
return files;
474476
}
475477

476-
export function getPortalTestRunUrl(workspaceMetadata: WorkspaceMetaData | null): string {
478+
export function resolveTenantDomain(
479+
tenantId: string | undefined,
480+
tenants: TenantInfo[],
481+
): string | undefined {
482+
if (!tenantId || tenants.length === 0) {
483+
return undefined;
484+
}
485+
const matchingTenant = tenants.find((t) => t.tenantId === tenantId);
486+
coreLogger.info(
487+
`Resolved tenant domain: ${JSON.stringify(matchingTenant?.defaultDomain)} for tenant ID: ${tenantId}`,
488+
);
489+
return matchingTenant?.defaultDomain;
490+
}
491+
492+
export function getPortalTestRunUrl(
493+
workspaceMetadata: WorkspaceMetaData | null,
494+
tenantDomain?: string,
495+
): string {
477496
const { subscriptionId, resourceId, name } = workspaceMetadata ?? {};
478497
if (!subscriptionId || !resourceId || !name) {
479498
throw new Error(
@@ -484,15 +503,16 @@ export function getPortalTestRunUrl(workspaceMetadata: WorkspaceMetaData | null)
484503
// Extract resource group from resourceId
485504
const resourceIdParts = resourceId.split("/");
486505
const resourceGroupIndex = resourceIdParts.findIndex(
487-
(part) => part.toLowerCase() === "resourcegroups",
506+
(part) => part.toLowerCase() === UrlConstants.ResourceGroupsPath,
488507
);
489508

490509
if (resourceGroupIndex === -1 || resourceGroupIndex + 1 >= resourceIdParts.length) {
491510
throw new Error("Invalid resourceId format: could not extract resource group name");
492511
}
493512

494513
const resourceGroupName = resourceIdParts[resourceGroupIndex + 1];
495-
return `https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${encodeURIComponent(subscriptionId)}/resourceGroups/${encodeURIComponent(resourceGroupName)}/providers/Microsoft.LoadTestService/playwrightWorkspaces/${encodeURIComponent(name)}/TestRuns`;
514+
const tenantFragment = tenantDomain ? `#@${tenantDomain}` : "#";
515+
return `${UrlConstants.AzurePortalBaseUrl}/${tenantFragment}${UrlConstants.ResourcePath}${UrlConstants.SubscriptionsPath}/${encodeURIComponent(subscriptionId)}${UrlConstants.ResourceGroupsUrlPath}/${encodeURIComponent(resourceGroupName!)}${UrlConstants.ProvidersPath}/${UrlConstants.LoadTestServiceProvider}/${UrlConstants.PlaywrightWorkspacesResourceType}/${encodeURIComponent(name)}/${UrlConstants.TestRunsRoute}`;
496516
}
497517

498518
export const getStorageAccountNameFromUri = (storageUri: string): string | null => {

sdk/loadtesting/playwright/test/reporter/playwrightReporter.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ vi.mock("../../src/utils/playwrightReporterStorageManager.js", () => {
2222

2323
vi.mock("../../src/utils/PlaywrightServiceClient.js", () => {
2424
const getWorkspaceMetadataMock = vi.fn();
25+
const getTenantsMock = vi.fn();
2526
(globalThis as any).__getWorkspaceMetadataMock = getWorkspaceMetadataMock;
27+
(globalThis as any).__getTenantsMock = getTenantsMock;
2628
class MockApiCall {
2729
getWorkspaceMetadata = getWorkspaceMetadataMock;
30+
getTenants = getTenantsMock;
2831
}
2932
return {
3033
PlaywrightServiceClient: MockApiCall,
@@ -35,12 +38,15 @@ vi.mock("../../src/utils/utils.js", async (importActual) => {
3538
const actual = await importActual<typeof import("../../src/utils/utils.js")>();
3639
const getHtmlReporterOutputFolderMock = vi.fn().mockReturnValue("playwright-report");
3740
const getPortalTestRunUrlMock = vi.fn().mockReturnValue("https://portal/link");
41+
const resolveTenantDomainMock = vi.fn().mockReturnValue(undefined);
3842
(globalThis as any).__getHtmlReporterOutputFolderMock = getHtmlReporterOutputFolderMock;
3943
(globalThis as any).__getPortalTestRunUrlMock = getPortalTestRunUrlMock;
44+
(globalThis as any).__resolveTenantDomainMock = resolveTenantDomainMock;
4045
return {
4146
...actual,
4247
getHtmlReporterOutputFolder: getHtmlReporterOutputFolderMock,
4348
getPortalTestRunUrl: getPortalTestRunUrlMock,
49+
resolveTenantDomain: resolveTenantDomainMock,
4450
};
4551
});
4652

@@ -68,6 +74,8 @@ describe("PlaywrightReporter", () => {
6874
process.env[InternalEnvironmentVariables.TEST_RUN_CREATION_SUCCESS] = "true";
6975
(globalThis as any).__getHtmlReporterOutputFolderMock.mockReturnValue("playwright-report");
7076
(globalThis as any).__getPortalTestRunUrlMock.mockReturnValue("https://portal/link");
77+
(globalThis as any).__resolveTenantDomainMock.mockReturnValue(undefined);
78+
(globalThis as any).__getTenantsMock.mockResolvedValue([]);
7179
// Set default Playwright version to supported version
7280
(globalThis as any).__getPlaywrightVersionMock.mockReturnValue("1.57.0");
7381
});
@@ -128,8 +136,12 @@ describe("PlaywrightReporter", () => {
128136
resourceId:
129137
"/subscriptions/sub-id/resourceGroups/my-rg/providers/Microsoft.LoadTestService/playwrightWorkspaces/workspace-name",
130138
name: "workspace-name",
139+
tenantId: "tenant-1",
131140
};
141+
const tenantList = [{ tenantId: "tenant-1", defaultDomain: "contoso.onmicrosoft.com" }];
132142
(globalThis as any).__getWorkspaceMetadataMock.mockResolvedValue(workspaceMetadata);
143+
(globalThis as any).__getTenantsMock.mockResolvedValue(tenantList);
144+
(globalThis as any).__resolveTenantDomainMock.mockReturnValue("contoso.onmicrosoft.com");
133145
(globalThis as any).__getHtmlReporterOutputFolderMock.mockReturnValue("custom-report");
134146
(globalThis as any).__uploadHtmlReportAfterTestsMock.mockResolvedValue({ success: true });
135147
(globalThis as any).__getPortalTestRunUrlMock.mockReturnValue("https://portal/link/test");
@@ -142,11 +154,19 @@ describe("PlaywrightReporter", () => {
142154
await reporter.onEnd();
143155

144156
expect((globalThis as any).__getWorkspaceMetadataMock).toHaveBeenCalled();
157+
expect((globalThis as any).__getTenantsMock).toHaveBeenCalled();
158+
expect((globalThis as any).__resolveTenantDomainMock).toHaveBeenCalledWith(
159+
"tenant-1",
160+
tenantList,
161+
);
145162
expect((globalThis as any).__uploadHtmlReportAfterTestsMock).toHaveBeenCalledWith(
146163
"custom-report",
147164
workspaceMetadata,
148165
);
149-
expect((globalThis as any).__getPortalTestRunUrlMock).toHaveBeenCalledWith(workspaceMetadata);
166+
expect((globalThis as any).__getPortalTestRunUrlMock).toHaveBeenCalledWith(
167+
workspaceMetadata,
168+
"contoso.onmicrosoft.com",
169+
);
150170
expect(consoleLogSpy).toHaveBeenCalledWith(
151171
ServiceErrorMessageConstants.COLLECTING_ARTIFACTS.message,
152172
);

sdk/loadtesting/playwright/test/utils/PlaywrightServiceClient.spec.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
55
import { PlaywrightServiceClient } from "../../src/utils/PlaywrightServiceClient.js";
6-
import { Constants, InternalEnvironmentVariables } from "../../src/common/constants.js";
6+
import {
7+
Constants,
8+
InternalEnvironmentVariables,
9+
ArmConstants,
10+
} from "../../src/common/constants.js";
711
import { ServiceErrorMessageConstants } from "../../src/common/messages.js";
812
import { TestRunCreatePayload } from "../../src/common/types.js";
913

@@ -213,4 +217,75 @@ describe("PlaywrightServiceClient", () => {
213217
expect(mockState.extractErrorMessage).toHaveBeenCalledWith(mockResponse.bodyAsText);
214218
});
215219
});
220+
221+
describe("getTenants", () => {
222+
it("should call the ARM tenants API and return parsed tenant list", async () => {
223+
const tenantList = [
224+
{ tenantId: "tenant-1", defaultDomain: "contoso.onmicrosoft.com" },
225+
{ tenantId: "tenant-2", defaultDomain: "fabrikam.onmicrosoft.com" },
226+
];
227+
const mockResponse = {
228+
status: 200,
229+
bodyAsText: JSON.stringify({ value: tenantList }),
230+
};
231+
mockState.callAPI.mockResolvedValue(mockResponse);
232+
233+
const result = await apiCall.getTenants();
234+
235+
expect(mockState.getAccessToken).toHaveBeenCalledTimes(1);
236+
expect(mockState.callAPI).toHaveBeenCalledWith(
237+
"GET",
238+
`${ArmConstants.TenantsApiUrl}?api-version=${ArmConstants.TenantsApiVersion}`,
239+
null,
240+
"mock-token",
241+
"",
242+
"mock-uuid",
243+
);
244+
expect(result).toEqual(tenantList);
245+
});
246+
247+
it("should throw when access token is missing", async () => {
248+
mockState.getAccessToken.mockReturnValue(undefined);
249+
250+
await expect(apiCall.getTenants()).rejects.toThrow(
251+
"PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set.",
252+
);
253+
254+
expect(mockState.callAPI).not.toHaveBeenCalled();
255+
});
256+
257+
it("should throw with error message when API returns non-200 status", async () => {
258+
const mockResponse = {
259+
status: 403,
260+
bodyAsText: JSON.stringify({ error: { message: "Forbidden" } }),
261+
};
262+
mockState.callAPI.mockResolvedValue(mockResponse);
263+
mockState.extractErrorMessage.mockReturnValue("Forbidden");
264+
265+
await expect(apiCall.getTenants()).rejects.toThrow("Forbidden");
266+
expect(mockState.extractErrorMessage).toHaveBeenCalledWith(mockResponse.bodyAsText);
267+
});
268+
269+
it("should return empty array when response body has no value", async () => {
270+
const mockResponse = {
271+
status: 200,
272+
bodyAsText: JSON.stringify({}),
273+
};
274+
mockState.callAPI.mockResolvedValue(mockResponse);
275+
276+
const result = await apiCall.getTenants();
277+
expect(result).toEqual([]);
278+
});
279+
280+
it("should return empty array when response body is empty", async () => {
281+
const mockResponse = {
282+
status: 200,
283+
bodyAsText: "",
284+
};
285+
mockState.callAPI.mockResolvedValue(mockResponse);
286+
287+
const result = await apiCall.getTenants();
288+
expect(result).toEqual([]);
289+
});
290+
});
216291
});

0 commit comments

Comments
 (0)