diff --git a/extension/task/utils/resolveAzureDevOpsIdentities.ts b/extension/task/utils/resolveAzureDevOpsIdentities.ts index 45fd560e0..c77755db8 100644 --- a/extension/task/utils/resolveAzureDevOpsIdentities.ts +++ b/extension/task/utils/resolveAzureDevOpsIdentities.ts @@ -50,7 +50,7 @@ export async function resolveAzureDevOpsIdentities(organizationUrl: URL, inputs: * In Azure DevOps terms, hosted environment is also known as "Azure DevOps Services" and on-premise environment is known as * "Team Foundation Server" or "Azure DevOps Server". */ -function isHostedAzureDevOps(uri: URL): boolean { +export function isHostedAzureDevOps(uri: URL): boolean { const hostname = uri.hostname.toLowerCase(); return hostname === 'dev.azure.com' || hostname.endsWith('.visualstudio.com'); } @@ -84,10 +84,10 @@ async function querySubject(organizationUrl: URL, email: string): Promise { - var url = `${organizationUrl}_apis/identities?searchFilter=MailAddress&queryMembership=None&filterValue=${email}`; - tl.debug(`GET ${url}`); + const url = `${organizationUrl}_apis/identities?searchFilter=MailAddress&queryMembership=None&filterValue=${email}`; + tl.debug(`GET ${url}`); try { - var response = await axios.get(url, { + const response = await axios.get(url, { headers: { Authorization: `Basic ${encodeBase64("PAT:" + tl.getVariable("System.AccessToken"))}`, Accept: "application/json;api-version=5.0", @@ -101,7 +101,7 @@ async function querySubjectOnPrem(organizationUrl: URL, email: string): Promise< input: email} } } catch (error) { - var responseStatusCode = error?.response?.status; + const responseStatusCode = error?.response?.status; tl.debug(`HTTP Response Status: ${responseStatusCode}`) if (responseStatusCode > 400 && responseStatusCode < 500) { tl.debug(`Access token is ${tl.getVariable("System.AccessToken")?.length > 0 ? "not" : ""} null or empty.`); @@ -116,16 +116,16 @@ async function querySubjectOnPrem(organizationUrl: URL, email: string): Promise< /** * * Make the HTTP Request for a hosted Azure DevOps Service, to resolve an email to an IIdentity - * @param organization - * @param email - * @returns + * @param organization + * @param email + * @returns */ async function querySubjectHosted(organization: string, email: string): Promise { // make HTTP request - var url = `https://vssps.dev.azure.com/${organization}/_apis/graph/subjectquery`; - tl.debug(`GET ${url}`); + const url = `https://vssps.dev.azure.com/${organization}/_apis/graph/subjectquery`; + tl.debug(`GET ${url}`); try { - var response = await axios.post(url, { + const response = await axios.post(url, { headers: { Authorization: `Basic ${encodeBase64("PAT:" + tl.getVariable("System.AccessToken"))}`, Accept: "application/json;api-version=6.0-preview.1", @@ -138,15 +138,15 @@ async function querySubjectHosted(organization: string, email: string): Promise< }); if (isSuccessStatusCode(response.status)) { - var descriptor: string = response.data.value[0]?.descriptor || ""; - var id = decodeBase64(descriptor.substring(descriptor.indexOf(".") + 1)) + const descriptor: string = response.data.value[0]?.descriptor || ""; + const id = decodeBase64(descriptor.substring(descriptor.indexOf(".") + 1)) return { id: id, displayName: response.data.value[0]?.displayName, input: email} } } catch (error) { - var responseStatusCode = error?.response?.status; + const responseStatusCode = error?.response?.status; tl.debug(`HTTP Response Status: ${responseStatusCode}`) if (responseStatusCode > 400 && responseStatusCode < 500) { tl.debug(`Access token is ${tl.getVariable("System.AccessToken")?.length > 0 ? "not" : ""} null or empty.`); diff --git a/extension/tests/utils/resolveAzureDevOpsIdentities.test.ts b/extension/tests/utils/resolveAzureDevOpsIdentities.test.ts new file mode 100644 index 000000000..9b39605bf --- /dev/null +++ b/extension/tests/utils/resolveAzureDevOpsIdentities.test.ts @@ -0,0 +1,93 @@ +import { isHostedAzureDevOps, resolveAzureDevOpsIdentities } from "../../task/utils/resolveAzureDevOpsIdentities"; +import { describe } from "node:test"; +import axios from "axios"; + +describe("isHostedAzureDevOps", () => { + it("Old visualstudio url is hosted.", () => { + const url = new URL("https://example.visualstudio.com/abc") + const result = isHostedAzureDevOps(url); + + expect(result).toBeTruthy(); + }); + it("Dev Azure url is hosted.", () => { + const url = new URL("https://dev.azure.com/example") + const result = isHostedAzureDevOps(url); + + expect(result).toBeTruthy(); + }); + it("private url is not hosted.", () => { + const url = new URL("https://tfs.example.com/tfs/Collection") + const result = isHostedAzureDevOps(url); + + expect(result).toBeFalsy(); + }); +}); + + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const aliceOnPrem = { + id: "any id", + email: "alice@example.com", + providerDisplayName: "Alice" +} + +const aliceHostedId = "any Id" +const aliceHosted = { + descriptor: "aad." + Buffer.from(aliceHostedId, 'utf8').toString('base64'), + email: "alice@example.com", + providerDisplayName: "Alice" +} + +describe("resolveAzureDevOpsIdentities", () => { + it("No email input, is directly returned.", async () => { + const url = new URL("https://example.visualstudio.com/abc") + + const input = ["be9321e2-f404-4ffa-8d6b-44efddb04865"]; + const results = await resolveAzureDevOpsIdentities(url, input); + + const outputs = results.map(identity => identity.id); + expect(outputs).toHaveLength(1); + expect(outputs).toContain(input[0]); + }); + it("successfully resolve id for azure devops server", async () => { + const url = new URL("https://example.onprem.com/abc") + + // Provide the data object to be returned + mockedAxios.get.mockResolvedValue({ + data: { + count: 1, + value: [aliceOnPrem], + }, + status: 200, + }); + + const input = [aliceOnPrem.email]; + const results = await resolveAzureDevOpsIdentities(url, input); + + const outputs = results.map(identity => identity.id); + expect(outputs).toHaveLength(1); + expect(outputs).toContain(aliceOnPrem.id); + }); + it("successfully resolve id for hosted azure devops", async () => { + const url = new URL("https://dev.azure.com/exampleorganization") + + + // Provide the data object to be returned + mockedAxios.post.mockResolvedValue({ + data: { + count: 1, + value: [aliceHosted], + }, + status: 200, + }); + + const input = [aliceHosted.email]; + const results = await resolveAzureDevOpsIdentities(url, input); + + const outputs = results.map(identity => identity.id); + expect(outputs).toHaveLength(1); + expect(outputs).toContain(aliceHostedId); + }); +}); \ No newline at end of file