Skip to content
Draft
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;
});
});
111 changes: 111 additions & 0 deletions src/client/test/integration/createHandlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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 { AgenticCreateHandler } from "../../uriHandler/handlers/agenticCreateHandler";
import { PacCreateHandler } from "../../uriHandler/handlers/pacCreateHandler";
import { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants";
import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient";
import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper";
import { uriHandlerTelemetryEventNames } from "../../uriHandler/telemetry/uriHandlerTelemetryEvents";
import { PacWrapper } from "../../pac/PacWrapper";

describe("Create deep-link handlers (gated)", () => {
let sandbox: sinon.SinonSandbox;
let getConfigStub: sinon.SinonStub;
let traceInfoStub: sinon.SinonStub;
let traceErrorStub: sinon.SinonStub;

const pacCreateUri = vscode.Uri.parse(
`vscode://${URI_CONSTANTS.EXTENSION_ID}${URI_CONSTANTS.PATHS.PAC_CREATE}` +
`?${URI_CONSTANTS.PARAMETERS.SOURCE}=${URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME}` +
`&${URI_CONSTANTS.PARAMETERS.ENV_ID}=env-1&${URI_CONSTANTS.PARAMETERS.VERSION}=${URI_CONSTANTS.CONTRACT_VERSION.CURRENT}`
);
const agenticCreateUri = vscode.Uri.parse(
`vscode://${URI_CONSTANTS.EXTENSION_ID}${URI_CONSTANTS.PATHS.AGENTIC_CREATE}` +
`?${URI_CONSTANTS.PARAMETERS.SOURCE}=${URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME}` +
`&${URI_CONSTANTS.PARAMETERS.AGENT_HOST}=${URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT}`
);

const setFlags = (enabled: boolean): void => {
getConfigStub.returns({
enablePacCreateFromHome: enabled,
enableAgenticCreateFromHome: enabled
});
};

beforeEach(() => {
sandbox = sinon.createSandbox();
getConfigStub = sandbox.stub(ECSFeaturesClient, "getConfig") as unknown as sinon.SinonStub;
traceInfoStub = sandbox.stub();
traceErrorStub = sandbox.stub();
sandbox.stub(oneDSLoggerWrapper, "getLogger").returns(
{ traceInfo: traceInfoStub, traceError: traceErrorStub } as unknown as ReturnType<typeof oneDSLoggerWrapper.getLogger>
);
});

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

it("PacCreateHandler is a no-op that only emits disabled telemetry when the flag is off", async () => {
setFlags(false);
const handler = new PacCreateHandler({} as PacWrapper);

await handler.handle(pacCreateUri);

expect(PacCreateHandler.isEnabled()).to.be.false;
expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_DISABLED)).to.be.true;
expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_TRIGGERED)).to.be.false;
});

it("PacCreateHandler parses params and emits triggered telemetry when the flag is on", async () => {
setFlags(true);
const handler = new PacCreateHandler({} as PacWrapper);

await handler.handle(pacCreateUri);

expect(PacCreateHandler.isEnabled()).to.be.true;
const triggered = traceInfoStub.getCalls().find(
(call) => call.args[0] === uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_TRIGGERED
);
expect(triggered, "expected a triggered telemetry event").to.not.be.undefined;
expect(triggered?.args[1]).to.include({
source: URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME,
version: URI_CONSTANTS.CONTRACT_VERSION.CURRENT,
hasEnvironmentId: "true"
});
});

it("AgenticCreateHandler is a no-op that only emits disabled telemetry when the flag is off", async () => {
setFlags(false);
const handler = new AgenticCreateHandler({} as PacWrapper);

await handler.handle(agenticCreateUri);

expect(AgenticCreateHandler.isEnabled()).to.be.false;
expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_DISABLED)).to.be.true;
expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_TRIGGERED)).to.be.false;
});

it("AgenticCreateHandler parses params and emits triggered telemetry when the flag is on", async () => {
setFlags(true);
const handler = new AgenticCreateHandler({} as PacWrapper);

await handler.handle(agenticCreateUri);

expect(AgenticCreateHandler.isEnabled()).to.be.true;
const triggered = traceInfoStub.getCalls().find(
(call) => call.args[0] === uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_TRIGGERED
);
expect(triggered, "expected a triggered telemetry event").to.not.be.undefined;
expect(triggered?.args[1]).to.include({
source: URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME,
agentHost: URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT
});
});
});
90 changes: 90 additions & 0 deletions src/client/test/integration/uriHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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 { UriHandler } from "../../uriHandler/uriHandler";
import { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants";
import { PacWrapper } from "../../pac/PacWrapper";
import { AgenticCreateHandler } from "../../uriHandler/handlers/agenticCreateHandler";
import { PacCreateHandler } from "../../uriHandler/handlers/pacCreateHandler";

// Shape used to stub the private route targets on the prototype without resorting to `any`.
type UriHandlerRoutes = {
pcfInit: () => Promise<void>;
handleOpenPowerPages: (uri: vscode.Uri) => Promise<void>;
};

describe("UriHandler routing", () => {
let sandbox: sinon.SinonSandbox;
let pcfInitStub: sinon.SinonStub;
let openStub: sinon.SinonStub;
let agenticCreateStub: sinon.SinonStub;
let pacCreateStub: sinon.SinonStub;
let handler: UriHandler;

const makeUri = (path: string): vscode.Uri =>
vscode.Uri.parse(`vscode://${URI_CONSTANTS.EXTENSION_ID}${path}`);

beforeEach(() => {
sandbox = sinon.createSandbox();
const prototype = UriHandler.prototype as unknown as UriHandlerRoutes;
pcfInitStub = sandbox.stub(prototype, "pcfInit").resolves();
openStub = sandbox.stub(prototype, "handleOpenPowerPages").resolves();
agenticCreateStub = sandbox.stub(AgenticCreateHandler.prototype, "handle").resolves();
pacCreateStub = sandbox.stub(PacCreateHandler.prototype, "handle").resolves();
handler = new UriHandler({} as PacWrapper);
});

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

it("dispatches /pcfInit to the PCF init handler", async () => {
await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.PCF_INIT));

expect(pcfInitStub.calledOnce).to.be.true;
expect(openStub.called).to.be.false;
expect(agenticCreateStub.called).to.be.false;
expect(pacCreateStub.called).to.be.false;
});

it("dispatches /open to the open Power Pages handler", async () => {
await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.OPEN));

expect(openStub.calledOnce).to.be.true;
expect(pcfInitStub.called).to.be.false;
expect(agenticCreateStub.called).to.be.false;
expect(pacCreateStub.called).to.be.false;
});

it("dispatches /agenticCreate to the agentic create handler", async () => {
await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.AGENTIC_CREATE));

expect(agenticCreateStub.calledOnce).to.be.true;
expect(pacCreateStub.called).to.be.false;
expect(pcfInitStub.called).to.be.false;
expect(openStub.called).to.be.false;
});

it("dispatches /pacCreate to the PAC create handler", async () => {
await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.PAC_CREATE));

expect(pacCreateStub.calledOnce).to.be.true;
expect(agenticCreateStub.called).to.be.false;
expect(pcfInitStub.called).to.be.false;
expect(openStub.called).to.be.false;
});

it("ignores unknown paths without throwing", async () => {
await handler.handleUri(makeUri("/someUnknownPath"));

expect(pcfInitStub.called).to.be.false;
expect(openStub.called).to.be.false;
expect(agenticCreateStub.called).to.be.false;
expect(pacCreateStub.called).to.be.false;
});
});
40 changes: 40 additions & 0 deletions src/client/test/unit/uriConstants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants";

// These constants form the versioned deep-link contract between the Power Pages
// home page and the VS Code extension. They are asserted here so that any accidental
// rename or removal is caught before it can break an already-shipped deep link.
describe("URI_CONSTANTS deep-link contract", () => {
it("keeps the existing open and pcfInit paths unchanged", () => {
expect(URI_CONSTANTS.PATHS.PCF_INIT).to.equal("/pcfInit");
expect(URI_CONSTANTS.PATHS.OPEN).to.equal("/open");
});

it("registers the agentic and PAC create paths", () => {
expect(URI_CONSTANTS.PATHS.AGENTIC_CREATE).to.equal("/agenticCreate");
expect(URI_CONSTANTS.PATHS.PAC_CREATE).to.equal("/pacCreate");
});

it("exposes the shared context parameter names", () => {
expect(URI_CONSTANTS.PARAMETERS.ENV_ID).to.equal("envid");
expect(URI_CONSTANTS.PARAMETERS.ORG_URL).to.equal("orgurl");
expect(URI_CONSTANTS.PARAMETERS.REGION).to.equal("region");
expect(URI_CONSTANTS.PARAMETERS.TENANT_ID).to.equal("tenantid");
expect(URI_CONSTANTS.PARAMETERS.SOURCE).to.equal("source");
expect(URI_CONSTANTS.PARAMETERS.AGENT_HOST).to.equal("agenthost");
expect(URI_CONSTANTS.PARAMETERS.VERSION).to.equal("v");
});

it("defines the versioned contract and known enumerated values", () => {
expect(URI_CONSTANTS.CONTRACT_VERSION.CURRENT).to.equal("1");
expect(URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME).to.equal("powerPagesHome");
expect(URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT).to.equal("copilot");
expect(URI_CONSTANTS.AGENT_HOST_VALUES.CLAUDE).to.equal("claude");
expect(URI_CONSTANTS.AGENT_HOST_VALUES.AUTO).to.equal("auto");
});
});
24 changes: 22 additions & 2 deletions src/client/uriHandler/constants/uriConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const URI_CONSTANTS = {
EXTENSION_ID: 'microsoft-IsvExpTools.powerplatform-vscode',
PATHS: {
PCF_INIT: '/pcfInit',
OPEN: '/open'
OPEN: '/open',
AGENTIC_CREATE: '/agenticCreate',
PAC_CREATE: '/pacCreate'
},
PARAMETERS: {
WEBSITE_ID: 'websiteid',
Expand All @@ -20,11 +22,27 @@ export const URI_CONSTANTS = {
SITE_NAME: 'sitename',
WEBSITE_NAME: 'websitename',
SITE_URL: 'siteurl',
WEBSITE_PREVIEW_URL: 'websitepreviewurl'
WEBSITE_PREVIEW_URL: 'websitepreviewurl',
REGION: 'region',
TENANT_ID: 'tenantid',
SOURCE: 'source',
AGENT_HOST: 'agenthost',
VERSION: 'v'
},
SCHEMA_VALUES: {
PORTAL_SCHEMA_V2: 'portalschemav2'
},
SOURCE_VALUES: {
POWER_PAGES_HOME: 'powerPagesHome'
},
AGENT_HOST_VALUES: {
COPILOT: 'copilot',
CLAUDE: 'claude',
AUTO: 'auto'
},
CONTRACT_VERSION: {
CURRENT: '1'
},
MODEL_VERSIONS: {
VERSION_1: 1,
VERSION_2: 2
Expand All @@ -40,4 +58,6 @@ export const URI_CONSTANTS = {
export const enum UriPath {
PcfInit = '/pcfInit',
Open = '/open',
AgenticCreate = '/agenticCreate',
PacCreate = '/pacCreate',
}
Loading
Loading