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
108 changes: 108 additions & 0 deletions src/client/test/integration/authEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { expect } from "chai";
import * as sinon from "sinon";
import * as vscode from "vscode";
import { AuthEnvironmentService } from "../../uriHandler/utils/authEnvironment";
import { UriParameters } from "../../uriHandler/utils/uriHandlerUtils";
import { PacWrapper } from "../../pac/PacWrapper";

type ProgressReporter = vscode.Progress<{ message?: string; increment?: number }>;
type ProgressTask = (progress: ProgressReporter, token: vscode.CancellationToken) => Thenable<void>;

// Minimal stubbed surface of PacWrapper that the service depends on.
interface PacWrapperStub {
activeOrg: sinon.SinonStub;
orgSelect: sinon.SinonStub;
authCreateNewAuthProfileForOrg: sinon.SinonStub;
resetPacProcess: sinon.SinonStub;
}

describe("AuthEnvironmentService", () => {
let sandbox: sinon.SinonSandbox;
let pacWrapperStub: PacWrapperStub;
let service: AuthEnvironmentService;
let warningStub: sinon.SinonStub;

const uriParams = {
environmentId: "env-1",
orgUrl: "https://org.crm.dynamics.com/"
} as unknown as UriParameters;

beforeEach(() => {
sandbox = sinon.createSandbox();

pacWrapperStub = {
activeOrg: sandbox.stub(),
orgSelect: sandbox.stub().resolves(),
authCreateNewAuthProfileForOrg: sandbox.stub().resolves(),
resetPacProcess: sandbox.stub().resolves()
};

// Run the progress task immediately with a no-op reporter.
sandbox.stub(vscode.window, "withProgress").callsFake(
((_options: vscode.ProgressOptions, task: ProgressTask): Thenable<void> =>
task({ report: () => undefined }, new vscode.CancellationTokenSource().token)
) as unknown as typeof vscode.window.withProgress
);

warningStub = sandbox.stub(vscode.window, "showWarningMessage");
sandbox.stub(vscode.window, "showInformationMessage");

service = new AuthEnvironmentService(pacWrapperStub as unknown as PacWrapper);
});

afterEach(() => {
sandbox.restore();
});

it("does not prompt when already authenticated to the requested environment", async () => {
pacWrapperStub.activeOrg.resolves({
Status: "Success",
Results: { EnvironmentId: "env-1" }
});

await service.prepareAuthenticationAndEnvironment(uriParams, {});

expect(warningStub.called).to.be.false;
expect(pacWrapperStub.orgSelect.called).to.be.false;
expect(pacWrapperStub.authCreateNewAuthProfileForOrg.called).to.be.false;
});

it("switches environment when the active org points at a different environment", async () => {
pacWrapperStub.activeOrg
.onFirstCall().resolves({ Status: "Success", Results: { EnvironmentId: "env-1" } })
.onSecondCall().resolves({ Status: "Success", Results: { EnvironmentId: "other-env" } })
.onThirdCall().resolves({ Status: "Success", Results: { EnvironmentId: "env-1" } });
warningStub.resolves("Yes");

await service.prepareAuthenticationAndEnvironment(uriParams, {});

expect(pacWrapperStub.orgSelect.calledOnceWith("https://org.crm.dynamics.com/")).to.be.true;
});

it("resetPacProcessSafely swallows reset errors", async () => {
pacWrapperStub.resetPacProcess.rejects(new Error("reset boom"));

await service.resetPacProcessSafely({});

expect(pacWrapperStub.resetPacProcess.calledOnce).to.be.true;
});

it("resetPacProcessAndThrow resets and rethrows the original error", async () => {
const original = new Error("boom");
let thrown: unknown;

try {
await service.resetPacProcessAndThrow(original, {}, "message", "error_type");
} catch (error) {
thrown = error;
}

expect(thrown).to.equal(original);
expect(pacWrapperStub.resetPacProcess.calledOnce).to.be.true;
});
});
206 changes: 5 additions & 201 deletions src/client/uriHandler/uriHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UriPath } from "./constants/uriConstants";
import { URI_HANDLER_STRINGS } from "./constants/uriStrings";
import { uriHandlerTelemetryEventNames } from "./telemetry/uriHandlerTelemetryEvents";
import { UriHandlerUtils, UriParameters } from "./utils/uriHandlerUtils";
import { AuthEnvironmentService } from "./utils/authEnvironment";

/**
* Signature for a deep-link route handler. Each registered URI path maps to one handler.
Expand All @@ -24,9 +25,11 @@ export function RegisterUriHandler(pacWrapper: PacWrapper): vscode.Disposable {
export class UriHandler implements vscode.UriHandler {
private readonly pacWrapper: PacWrapper;
private readonly routes: ReadonlyMap<string, UriRouteHandler>;
private readonly authEnvironmentService: AuthEnvironmentService;

constructor(pacWrapper: PacWrapper) {
this.pacWrapper = pacWrapper;
this.authEnvironmentService = new AuthEnvironmentService(pacWrapper);
this.routes = this.buildRoutes();
}

Expand Down Expand Up @@ -124,7 +127,7 @@ export class UriHandler implements vscode.UriHandler {
this.validateRequiredParameters(uriParams, telemetryData);

// Prepare authentication and environment
await this.prepareAuthenticationAndEnvironment(uriParams, telemetryData);
await this.authEnvironmentService.prepareAuthenticationAndEnvironment(uriParams, telemetryData);

// Handle the download process
await this.handleSiteDownload(uriParams, telemetryData, startTime);
Expand Down Expand Up @@ -230,170 +233,6 @@ export class UriHandler implements vscode.UriHandler {
}
}

/**
* Handle authentication and environment setup
*/
private async prepareAuthenticationAndEnvironment(uriParams: UriParameters, telemetryData: Record<string, string>): Promise<void> {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: URI_HANDLER_STRINGS.TITLES.POWER_PAGES,
cancellable: false
},
async (progress) => {
progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.PREPARING,
increment: 10
});

progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.VALIDATING_AUTH,
increment: 20
});

// Check and handle authentication
await this.ensureAuthentication(uriParams, telemetryData, progress);

progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.CHECKING_ENV,
increment: 20
});

// Check and handle environment switching
await this.ensureCorrectEnvironment(uriParams, telemetryData, progress);

progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.READY_TO_SELECT,
increment: 30
});

// Brief delay to let user see the final progress message
await new Promise(resolve => setTimeout(resolve, 500));
}
);
}

/**
* Ensure user is authenticated with PAC CLI
*/
private async ensureAuthentication(uriParams: UriParameters, telemetryData: Record<string, string>, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<void> {
let authInfo;
try {
authInfo = await this.pacWrapper.activeOrg();
} catch (error) {
await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check authentication status', 'auth_check_failed');
}

if (!authInfo || authInfo.Status !== "Success") {
oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_REQUIRED,
{ ...telemetryData, authStatus: authInfo?.Status || 'none' }
);

progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.AUTH_REQUIRED,
increment: 10
});

const authRequired = await vscode.window.showWarningMessage(
URI_HANDLER_STRINGS.PROMPTS.AUTH_REQUIRED,
{ modal: true },
URI_HANDLER_STRINGS.BUTTONS.YES,
URI_HANDLER_STRINGS.BUTTONS.NO
);

if (authRequired === URI_HANDLER_STRINGS.BUTTONS.YES) {
try {
progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.AUTHENTICATING,
increment: 10
});

await this.pacWrapper.authCreateNewAuthProfileForOrg(uriParams.orgUrl!);

const newAuthInfo = await this.pacWrapper.activeOrg();
if (!newAuthInfo || newAuthInfo.Status !== "Success") {
throw new Error(URI_HANDLER_STRINGS.ERRORS.AUTH_FAILED);
}

oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_COMPLETED,
{ ...telemetryData, newAuthStatus: newAuthInfo.Status }
);
} catch (authError) {
await this.resetPacProcessAndThrow(authError, telemetryData, 'Authentication operation failed', 'auth_operation_failed');
}
} else {
vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_AUTH);
oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED,
{ ...telemetryData, reason: 'user_cancelled_auth' }
);
throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_AUTH);
}
}
}

/**
* Ensure we're connected to the correct environment
*/
private async ensureCorrectEnvironment(uriParams: UriParameters, telemetryData: Record<string, string>, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<void> {
let currentAuthInfo;
try {
currentAuthInfo = await this.pacWrapper.activeOrg();
} catch (error) {
await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check current environment', 'env_check_failed');
}

if (currentAuthInfo?.Status === "Success" && currentAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) {
oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_REQUIRED,
{
...telemetryData,
currentEnvId: currentAuthInfo.Results?.EnvironmentId || 'unknown',
requestedEnvId: uriParams.environmentId
}
);

const switchEnv = await vscode.window.showWarningMessage(
URI_HANDLER_STRINGS.PROMPTS.ENV_SWITCH_REQUIRED,
{ modal: true },
URI_HANDLER_STRINGS.BUTTONS.YES,
URI_HANDLER_STRINGS.BUTTONS.NO
);

if (switchEnv === URI_HANDLER_STRINGS.BUTTONS.YES) {
try {
progress.report({
message: URI_HANDLER_STRINGS.PROGRESS.SWITCHING_ENV,
increment: 10
});

await this.pacWrapper.orgSelect(uriParams.orgUrl!);

const verifyAuthInfo = await this.pacWrapper.activeOrg();
if (verifyAuthInfo?.Status !== "Success" || verifyAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) {
throw new Error(URI_HANDLER_STRINGS.ERRORS.ENV_SWITCH_FAILED);
}

oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_COMPLETED,
{ ...telemetryData, switchedToEnvId: verifyAuthInfo.Results?.EnvironmentId }
);
} catch (error) {
await this.resetPacProcessAndThrow(error, telemetryData, 'Error switching environment', 'env_switch_error');
}
} else {
vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_ENV);
oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED,
{ ...telemetryData, reason: 'user_cancelled_env_switch' }
);
throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_ENV_SWITCH);
}
}
}

/**
* Handle the site download process
*/
Expand Down Expand Up @@ -478,44 +317,9 @@ export class UriHandler implements vscode.UriHandler {
{ ...telemetryData, error: 'download_failed' }
);

await this.resetPacProcessSafely(telemetryData);
await this.authEnvironmentService.resetPacProcessSafely(telemetryData);
throw error;
}
}

/**
* Reset PAC process and throw error
*/
private async resetPacProcessAndThrow(error: unknown, telemetryData: Record<string, string>, message: string, errorType: string): Promise<never> {
oneDSLoggerWrapper.getLogger().traceError(
uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED,
message,
error instanceof Error ? error : new Error(String(error)),
{ ...telemetryData, error: errorType }
);

await this.resetPacProcessSafely(telemetryData);
throw error;
}

/**
* Safely reset PAC process without throwing
*/
private async resetPacProcessSafely(telemetryData: Record<string, string>): Promise<void> {
try {
await this.pacWrapper.resetPacProcess();
oneDSLoggerWrapper.getLogger().traceInfo(
uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED,
{ ...telemetryData, message: 'PAC process reset after failure' }
);
} catch (resetError) {
oneDSLoggerWrapper.getLogger().traceError(
uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED,
'Failed to reset PAC process after failure',
resetError instanceof Error ? resetError : new Error(String(resetError)),
{ ...telemetryData, error: 'pac_reset_failed' }
);
}
}

}
Loading
Loading