Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion api/src/auth/apiRequest/AzureResourcesApiRequestContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureResourcesExtensionApi } from "../../extensionApi";
import { AzureResourcesExtensionApi, AzureResourcesExtensionAuthApi } from "../../extensionApi";
import { AzureExtensionApi } from "../../utils/apiUtils";
import { AzExtCredentialManager } from "../credentialManager/AzExtCredentialManager";
import { AzureResourcesApiRequestError } from "./apiRequestErrors";

export interface AzureResourcesApiRequestContext {
Expand All @@ -30,3 +31,19 @@ export interface AzureResourcesApiRequestContext {
*/
onApiRequestError?: (error: AzureResourcesApiRequestError) => void | Promise<void>;
}

// NOTE: Dependency injection options for tests; skip publically exporting this in the index
export interface CustomRequestDependenciesContext extends AzureResourcesApiRequestContext {
/**
* An optional credential manager used for issuing and verifying the client extensions credentials. If none are supplied, a simple UUID credential manager is used.
* @test Use this to more easily mock and inspect the behavior of the underlying credential manager.
*/
credentialManager?: AzExtCredentialManager;

/**
* An optional API provider to be used in lieu of the VS Code extension provider `vscode.extension.getExtension()`.
* This should _NOT_ be used in production environments.
* @test Use this to more easily mock and inject custom host extension API exports.
*/
hostApiProvider?: { getApi(): AzureResourcesExtensionAuthApi };
}
12 changes: 6 additions & 6 deletions api/src/auth/apiRequest/apiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AzureResourcesExtensionAuthApi } from "../../extensionApi";
import { apiUtils, AzureExtensionApi } from "../../utils/apiUtils";
import { AzExtCredentialManager } from "../credentialManager/AzExtCredentialManager";
import { AzExtUUIDCredentialManager } from "../credentialManager/AzExtUUIDCredentialManager";
import { AzureResourcesApiRequestContext } from "./AzureResourcesApiRequestContext";
import { AzureResourcesApiRequestContext, CustomRequestDependenciesContext } from "./AzureResourcesApiRequestContext";
import { AzureResourcesApiRequestErrorCode } from "./apiRequestErrors";

const azureResourcesAuthApiVersion: string = '^4.0.0';
Expand Down Expand Up @@ -38,7 +38,7 @@ export function prepareAzureResourcesApiRequest<T extends AzureExtensionApi>(con
throw new Error('You must specify at least one Azure Resources API version.');
}

const clientCredentialManager: AzExtCredentialManager = new AzExtUUIDCredentialManager();
const clientCredentialManager: AzExtCredentialManager = (context as CustomRequestDependenciesContext).credentialManager ?? new AzExtUUIDCredentialManager();

if (!clientExtensionApi.receiveAzureResourcesApiSession) {
clientExtensionApi.receiveAzureResourcesApiSession = createReceiveAzureResourcesApiSession(context, clientCredentialManager);
Expand All @@ -56,13 +56,13 @@ async function requestAzureResourcesSession(context: AzureResourcesApiRequestCon
clientCredential = await clientCredentialManager.createCredential(context.clientExtensionId);
} catch (err) {
if (err instanceof Error) {
void context.onApiRequestError?.({ code: AzureResourcesApiRequestErrorCode.ClientFailedCreateCredential, message: clientCredentialManager.maskCredentials(err.message) })
void context.onApiRequestError?.({ code: AzureResourcesApiRequestErrorCode.ClientFailedCreateCredential, message: clientCredentialManager.maskCredentials(err.message) });
}
return;
}

try {
const resourcesAuthApi = await getExtensionApi<AzureResourcesExtensionAuthApi>(azureResourcesExtId, azureResourcesAuthApiVersion);
const resourcesAuthApi = (context as CustomRequestDependenciesContext).hostApiProvider?.getApi() ?? await getExtensionApi<AzureResourcesExtensionAuthApi>(azureResourcesExtId, azureResourcesAuthApiVersion);
await resourcesAuthApi.createAzureResourcesApiSession(context.clientExtensionId, clientApiVersion, clientCredential);
} catch (err) {
if (err instanceof Error) {
Expand Down Expand Up @@ -92,7 +92,7 @@ function createReceiveAzureResourcesApiSession(context: AzureResourcesApiRequest
}

try {
const resourcesAuthApi = await getExtensionApi<AzureResourcesExtensionAuthApi>(azureResourcesExtId, azureResourcesAuthApiVersion);
const resourcesAuthApi = (context as CustomRequestDependenciesContext).hostApiProvider?.getApi() ?? await getExtensionApi<AzureResourcesExtensionAuthApi>(azureResourcesExtId, azureResourcesAuthApiVersion);
const resourcesApis = await resourcesAuthApi.getAzureResourcesApis(context.clientExtensionId, azureResourcesCredential, context.azureResourcesApiVersions);
void context.onDidReceiveAzureResourcesApis(resourcesApis);
} catch (err) {
Expand All @@ -101,7 +101,7 @@ function createReceiveAzureResourcesApiSession(context: AzureResourcesApiRequest
}
return;
}
}
};
}

async function getExtensionApi<T extends AzureExtensionApi>(extensionId: string, extensionVersion: string): Promise<T> {
Expand Down
2 changes: 1 addition & 1 deletion api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export * from './auth/apiRequest/apiRequest';
export * from './auth/apiRequest/apiRequestErrors';
export * from './auth/apiRequest/AzureResourcesApiRequestContext';
export { AzureResourcesApiRequestContext } from './auth/apiRequest/AzureResourcesApiRequestContext';
export * from './AzExtResourceType';
export * from './extensionApi';
export * from './getAzExtResourceType';
Expand Down
2 changes: 1 addition & 1 deletion test/api/auth/MockUUIDCredentialManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { maskValue } from "../../../api/src/utils/maskValue";
* but with a public getter to inspect the UUIDs during test.
*/
export class MockUUIDCredentialManager implements AzExtCredentialManager {
#uuidMap: Map<string, string> = new Map();
#uuidMap = new Map<string, string>();

get uuidMap() {
return this.#uuidMap;
Expand Down
92 changes: 92 additions & 0 deletions test/api/auth/v4.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from "assert";
import { AzureExtensionApi, AzureResourcesApiRequestContext, AzureResourcesApiRequestError, AzureResourcesApiRequestErrorCode, AzureResourcesExtensionApi, prepareAzureResourcesApiRequest } from "../../../api/src";
import { CustomRequestDependenciesContext } from "../../../api/src/auth/apiRequest/AzureResourcesApiRequestContext";
import { AzExtUUIDCredentialManager } from "../../../api/src/auth/credentialManager/AzExtUUIDCredentialManager";
import { createMockAuthApi } from "./mockAuthApi";

const clientExtensionId: string = 'ms-azuretools.vscode-azurecontainerapps';

suite('Azure Resources API client-side request tests', async () => {
test('prepareAzureResourcesApiRequest should successfully enable the handshake & return available APIs if on the allow list', async () => {
let receivedResourcesApis: (AzureExtensionApi | AzureResourcesExtensionApi | undefined)[] = [];

await new Promise<void>((resolve) => {
const timeout = setTimeout(resolve, 5000);

const requestContext: AzureResourcesApiRequestContext = {
clientExtensionId,
azureResourcesApiVersions: ['0.0.1', '^2.0.0'],
onDidReceiveAzureResourcesApis: (azureResourcesApis: (AzureExtensionApi | AzureResourcesExtensionApi | undefined)[]) => {
clearTimeout(timeout);
receivedResourcesApis = azureResourcesApis;
resolve();
},
onApiRequestError: () => {
clearTimeout(timeout);
resolve();
},
};

const coreClientExtensionApi: AzureExtensionApi = {
apiVersion: '1.0.0',
};

// Inject an external manager so the two preparation calls that follow will point to the same one
(requestContext as CustomRequestDependenciesContext).credentialManager = new AzExtUUIDCredentialManager();

// For testing, it is necessary to wire up both the client and host api provider to represent the parties on each side of the handshake.
// The prepare call needs to happen twice in order to set this scenario up - once to generate the client API for the host to point to,
// and then once more to pass that client to the host API for the final handshake method to point to.
//
// NOTE: This is not normally necessary since VS Code's API normally manages extension exports; however, this is not something we can rely on
// during tests because we need to be able to point to our own mocked APIs (which requires us to swap out the native VS Code extension provider).

const { clientApi } = prepareAzureResourcesApiRequest(requestContext, coreClientExtensionApi);
const hostApi = createMockAuthApi({ clientApiProvider: { getApi: () => clientApi } });
(requestContext as CustomRequestDependenciesContext).hostApiProvider = { getApi: () => hostApi };

const { requestResourcesApis } = prepareAzureResourcesApiRequest(requestContext, clientApi);
requestResourcesApis();
});

assert.equal(receivedResourcesApis[0]?.apiVersion, '0.0.1');
assert.match(receivedResourcesApis[1]?.apiVersion ?? '', /^2\./);
});

test('prepareAzureResourcesApiRequest should return an error if not on the allow list', async () => {
let receivedError: AzureResourcesApiRequestError | undefined;

await new Promise<void>((resolve) => {
const timeout = setTimeout(resolve, 5000);

const requestContext: AzureResourcesApiRequestContext = {
clientExtensionId: 'extension1',
azureResourcesApiVersions: ['0.0.1', '^2.0.0'],
onDidReceiveAzureResourcesApis: () => {
clearTimeout(timeout);
resolve();
},
onApiRequestError: (error: AzureResourcesApiRequestError) => {
clearTimeout(timeout);
receivedError = error;
resolve();
}
};

const coreClientExtensionApi: AzureExtensionApi = {
apiVersion: '1.0.0',
};

// We don't need to wire up the custom test api providers as we expect the initial call to fail before the host ever tries to reach back out to the client
const { requestResourcesApis } = prepareAzureResourcesApiRequest(requestContext, coreClientExtensionApi);
requestResourcesApis();
});

assert.equal(receivedError?.code, AzureResourcesApiRequestErrorCode.HostCreateSessionFailed);
});
});