diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index eb777ecc65..983741a57c 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -129,6 +129,10 @@ import { checkProjectTypeAndSendTelemetry, isM365Project } from "./utils/project import { ReleaseNote } from "./utils/releaseNote"; import { ExtensionSurvey } from "./utils/survey"; import { getSettingsVersion, projectVersionCheck } from "./utils/telemetryUtils"; +import { showError } from "./error/common"; +import { TreeViewCommand } from "./treeview/treeViewCommand"; +import { signOutM365, signOutAzure } from "./utils/accountUtils"; +import { cmpAccountsHandler, createAccountHandler } from "./handlers/accountHandlers"; export async function activate(context: vscode.ExtensionContext) { process.env[FeatureFlags.ChatParticipant] = ( @@ -214,7 +218,7 @@ export async function activate(context: vscode.ExtensionContext) { export async function deactivate() { await ExtTelemetry.cacheTelemetryEventAsync(TelemetryEvent.Deactivate); await ExtTelemetry.dispose(); - handlers.cmdHdlDisposeTreeView(); + TreeViewManagerInstance.dispose(); await disableRunIcon(); } @@ -224,7 +228,7 @@ function activateTeamsFxRegistration(context: vscode.ExtensionContext) { registerTreeViewCommandsInHelper(context); registerTeamsFxCommands(context); registerMenuCommands(context); - handlers.registerAccountMenuCommands(context); + registerAccountMenuCommands(context); TreeViewManagerInstance.registerTreeViews(context); accountTreeViewProviderInstance.subscribeToStatusChanges({ @@ -292,7 +296,7 @@ function registerActivateCommands(context: vscode.ExtensionContext) { // user can manage account in non-teamsfx project const cmpAccountsCmd = vscode.commands.registerCommand("fx-extension.cmpAccounts", (...args) => - Correlator.run(handlers.cmpAccountsHandler, args) + Correlator.run(cmpAccountsHandler, args) ); context.subscriptions.push(cmpAccountsCmd); @@ -662,8 +666,7 @@ function registerMenuCommands(context: vscode.ExtensionContext) { const createAccountCmd = vscode.commands.registerCommand( "fx-extension.createAccount", - (...args) => - Correlator.run(handlers.createAccountHandler, [TelemetryTriggerFrom.ViewTitleNavigation]) + (...args) => Correlator.run(createAccountHandler, [TelemetryTriggerFrom.ViewTitleNavigation]) ); context.subscriptions.push(createAccountCmd); @@ -900,6 +903,32 @@ function registerOfficeDevMenuCommands(context: vscode.ExtensionContext) { context.subscriptions.push(reportIssueCmd); } +function registerAccountMenuCommands(context: vscode.ExtensionContext) { + // Register SignOut tree view command + context.subscriptions.push( + vscode.commands.registerCommand("fx-extension.signOut", async (node: TreeViewCommand) => { + try { + switch (node.contextValue) { + case "signedinM365": { + await Correlator.run(async () => { + await signOutM365(true); + }); + break; + } + case "signedinAzure": { + await Correlator.run(async () => { + await signOutAzure(true); + }); + break; + } + } + } catch (e) { + void showError(e as FxError); + } + }) + ); +} + async function initializeContextKey(context: vscode.ExtensionContext, isTeamsFxProject: boolean) { await vscode.commands.executeCommand("setContext", "fx-extension.isSPFx", isSPFxProject); diff --git a/packages/vscode-extension/src/handlers.ts b/packages/vscode-extension/src/handlers.ts index 19c42f79c0..e0457b6806 100644 --- a/packages/vscode-extension/src/handlers.ts +++ b/packages/vscode-extension/src/handlers.ts @@ -61,7 +61,7 @@ import * as fs from "fs-extra"; import * as path from "path"; import * as util from "util"; import * as vscode from "vscode"; -import { ExtensionContext, QuickPickItem, Uri, commands, env, window, workspace } from "vscode"; +import { Uri, commands, env, window, workspace } from "vscode"; import azureAccountManager from "./commonlib/azureLogin"; import VsCodeLogInstance from "./commonlib/log"; import M365TokenInstance from "./commonlib/m365Login"; @@ -104,8 +104,6 @@ import { AzureAccountNode } from "./treeview/account/azureNode"; import { AccountItemStatus } from "./treeview/account/common"; import { M365AccountNode } from "./treeview/account/m365Node"; import envTreeProviderInstance from "./treeview/environmentTreeViewProvider"; -import { TreeViewCommand } from "./treeview/treeViewCommand"; -import TreeViewManagerInstance from "./treeview/treeViewManager"; import { getAppName } from "./utils/appDefinitionUtils"; import { checkCoreNotEmpty, @@ -453,46 +451,6 @@ export async function preDebugCheckHandler(): Promise { } } -export async function createAccountHandler(args: any[]): Promise { - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccountStart, getTriggerFromProperty(args)); - const m365Option: OptionItem = { - id: "createAccountM365", - label: `$(add) ${localize("teamstoolkit.commands.createAccount.m365")}`, - description: localize("teamstoolkit.commands.createAccount.requireSubscription"), - }; - const azureOption: OptionItem = { - id: "createAccountAzure", - label: `$(add) ${localize("teamstoolkit.commands.createAccount.azure")}`, - description: localize("teamstoolkit.commands.createAccount.free"), - }; - const option: SingleSelectConfig = { - name: "CreateAccounts", - title: localize("teamstoolkit.commands.createAccount.title"), - options: [m365Option, azureOption], - }; - const result = await VS_CODE_UI.selectOption(option); - if (result.isOk()) { - if (result.value.result === m365Option.id) { - await VS_CODE_UI.openUrl("https://developer.microsoft.com/microsoft-365/dev-program"); - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccount, { - [TelemetryProperty.AccountType]: AccountType.M365, - ...getTriggerFromProperty(args), - }); - } else if (result.value.result === azureOption.id) { - await VS_CODE_UI.openUrl("https://azure.microsoft.com/en-us/free/"); - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccount, { - [TelemetryProperty.AccountType]: AccountType.Azure, - ...getTriggerFromProperty(args), - }); - } - } else { - ExtTelemetry.sendTelemetryErrorEvent(TelemetryEvent.CreateAccount, result.error, { - ...getTriggerFromProperty(args), - }); - } - return; -} - export async function openBuildIntelligentAppsWalkthroughHandler( ...args: unknown[] ): Promise> { @@ -1074,125 +1032,6 @@ export function saveTextDocumentHandler(document: vscode.TextDocumentWillSaveEve } } -export function registerAccountMenuCommands(context: ExtensionContext) { - // Register SignOut tree view command - context.subscriptions.push( - commands.registerCommand("fx-extension.signOut", async (node: TreeViewCommand) => { - try { - switch (node.contextValue) { - case "signedinM365": { - await Correlator.run(async () => { - await signOutM365(true); - }); - break; - } - case "signedinAzure": { - await Correlator.run(async () => { - await signOutAzure(true); - }); - break; - } - } - } catch (e) { - void showError(e as FxError); - } - }) - ); -} - -export function cmdHdlDisposeTreeView() { - TreeViewManagerInstance.dispose(); -} - -export async function cmpAccountsHandler(args: any[]) { - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.ManageAccount, getTriggerFromProperty(args)); - const signInAzureOption: VscQuickPickItem = { - id: "signInAzure", - label: localize("teamstoolkit.handlers.signInAzure"), - function: () => signInAzure(), - }; - - const signOutAzureOption: VscQuickPickItem = { - id: "signOutAzure", - label: localize("teamstoolkit.handlers.signOutOfAzure"), - function: async () => - await Correlator.run(async () => { - await signOutAzure(false); - }), - }; - - const signInM365Option: VscQuickPickItem = { - id: "signinM365", - label: localize("teamstoolkit.handlers.signIn365"), - function: () => signInM365(), - }; - - const signOutM365Option: VscQuickPickItem = { - id: "signOutM365", - label: localize("teamstoolkit.handlers.signOutOfM365"), - function: async () => - await Correlator.run(async () => { - await signOutM365(false); - }), - }; - - const createAccountsOption: VscQuickPickItem = { - id: "createAccounts", - label: `$(add) ${localize("teamstoolkit.commands.createAccount.title")}`, - function: async () => { - await Correlator.run(() => createAccountHandler([])); - }, - }; - - //TODO: hide subscription list until core or api expose the get subscription list API - // let selectSubscriptionOption: VscQuickPickItem = { - // id: "selectSubscription", - // label: "Specify an Azure Subscription", - // function: () => selectSubscription(), - // detail: "4 subscriptions discovered" - // }; - - const quickPick = window.createQuickPick(); - - const quickItemOptionArray: VscQuickPickItem[] = []; - - const m365AccountRes = await M365TokenInstance.getStatus({ scopes: AppStudioScopes }); - const m365Account = m365AccountRes.isOk() ? m365AccountRes.value : undefined; - if (m365Account && m365Account.status === "SignedIn") { - const accountInfo = m365Account.accountInfo; - const email = (accountInfo as any).upn ? (accountInfo as any).upn : undefined; - if (email !== undefined) { - signOutM365Option.label = signOutM365Option.label.concat(email); - } - quickItemOptionArray.push(signOutM365Option); - } else { - quickItemOptionArray.push(signInM365Option); - } - - const azureAccount = await azureAccountManager.getStatus(); - if (azureAccount.status === "SignedIn") { - const accountInfo = azureAccount.accountInfo; - const email = (accountInfo as any).email || (accountInfo as any).upn; - if (email !== undefined) { - signOutAzureOption.label = signOutAzureOption.label.concat(email); - } - quickItemOptionArray.push(signOutAzureOption); - } else { - quickItemOptionArray.push(signInAzureOption); - } - - quickItemOptionArray.push(createAccountsOption); - quickPick.items = quickItemOptionArray; - quickPick.onDidChangeSelection((selection) => { - if (selection[0]) { - (selection[0] as VscQuickPickItem).function().catch(console.error); - quickPick.hide(); - } - }); - quickPick.onDidHide(() => quickPick.dispose()); - quickPick.show(); -} - export async function decryptSecret(cipher: string, selection: vscode.Range): Promise { ExtTelemetry.sendTelemetryEvent(TelemetryEvent.EditSecretStart, { [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.Other, @@ -1488,50 +1327,6 @@ export function editAadManifestTemplate(args: any[]) { } } -export async function signOutAzure(isFromTreeView: boolean) { - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.SignOutStart, { - [TelemetryProperty.TriggerFrom]: isFromTreeView - ? TelemetryTriggerFrom.TreeView - : TelemetryTriggerFrom.CommandPalette, - [TelemetryProperty.AccountType]: AccountType.Azure, - }); - await vscode.window.showInformationMessage( - localize("teamstoolkit.commands.azureAccount.signOutHelp") - ); -} - -export async function signOutM365(isFromTreeView: boolean) { - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.SignOutStart, { - [TelemetryProperty.TriggerFrom]: isFromTreeView - ? TelemetryTriggerFrom.TreeView - : TelemetryTriggerFrom.CommandPalette, - [TelemetryProperty.AccountType]: AccountType.M365, - }); - let result = false; - result = await M365TokenInstance.signout(); - if (result) { - accountTreeViewProviderInstance.m365AccountNode.setSignedOut(); - await envTreeProviderInstance.refreshRemoteEnvWarning(); - } -} - -export async function signInAzure() { - await vscode.commands.executeCommand("fx-extension.signinAzure"); -} - -export async function signInM365() { - await vscode.commands.executeCommand("fx-extension.signinM365"); -} - -export interface VscQuickPickItem extends QuickPickItem { - /** - * Current id of the option item. - */ - id: string; - - function: () => Promise; -} - export async function migrateTeamsTabAppHandler(): Promise> { ExtTelemetry.sendTelemetryEvent(TelemetryEvent.MigrateTeamsTabAppStart); const selection = await VS_CODE_UI.showMessage( diff --git a/packages/vscode-extension/src/handlers/accountHandlers.ts b/packages/vscode-extension/src/handlers/accountHandlers.ts new file mode 100644 index 0000000000..cff6b191ea --- /dev/null +++ b/packages/vscode-extension/src/handlers/accountHandlers.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { QuickPickItem, window } from "vscode"; +import { OptionItem, SingleSelectConfig } from "@microsoft/teamsfx-api"; +import { Correlator, AppStudioScopes } from "@microsoft/teamsfx-core"; +import { ExtTelemetry } from "../telemetry/extTelemetry"; +import { AccountType, TelemetryEvent, TelemetryProperty } from "../telemetry/extTelemetryEvents"; +import { signInAzure, signOutAzure, signInM365, signOutM365 } from "../utils/accountUtils"; +import { localize } from "../utils/localizeUtils"; +import { getTriggerFromProperty } from "../utils/telemetryUtils"; +import azureAccountManager from "../commonlib/azureLogin"; +import M365TokenInstance from "../commonlib/m365Login"; +import { VS_CODE_UI } from "../qm/vsc_ui"; + +export interface VscQuickPickItem extends QuickPickItem { + /** + * Current id of the option item. + */ + id: string; + function: () => Promise; +} + +export async function createAccountHandler(args: any[]): Promise { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccountStart, getTriggerFromProperty(args)); + const m365Option: OptionItem = { + id: "createAccountM365", + label: `$(add) ${localize("teamstoolkit.commands.createAccount.m365")}`, + description: localize("teamstoolkit.commands.createAccount.requireSubscription"), + }; + const azureOption: OptionItem = { + id: "createAccountAzure", + label: `$(add) ${localize("teamstoolkit.commands.createAccount.azure")}`, + description: localize("teamstoolkit.commands.createAccount.free"), + }; + const option: SingleSelectConfig = { + name: "CreateAccounts", + title: localize("teamstoolkit.commands.createAccount.title"), + options: [m365Option, azureOption], + }; + const result = await VS_CODE_UI.selectOption(option); + if (result.isOk()) { + if (result.value.result === m365Option.id) { + await VS_CODE_UI.openUrl("https://developer.microsoft.com/microsoft-365/dev-program"); + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccount, { + [TelemetryProperty.AccountType]: AccountType.M365, + ...getTriggerFromProperty(args), + }); + } else if (result.value.result === azureOption.id) { + await VS_CODE_UI.openUrl("https://azure.microsoft.com/en-us/free/"); + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateAccount, { + [TelemetryProperty.AccountType]: AccountType.Azure, + ...getTriggerFromProperty(args), + }); + } + } else { + ExtTelemetry.sendTelemetryErrorEvent(TelemetryEvent.CreateAccount, result.error, { + ...getTriggerFromProperty(args), + }); + } + return; +} + +export async function cmpAccountsHandler(args: any[]) { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.ManageAccount, getTriggerFromProperty(args)); + const signInAzureOption: VscQuickPickItem = { + id: "signInAzure", + label: localize("teamstoolkit.handlers.signInAzure"), + function: () => signInAzure(), + }; + + const signOutAzureOption: VscQuickPickItem = { + id: "signOutAzure", + label: localize("teamstoolkit.handlers.signOutOfAzure"), + function: async () => + await Correlator.run(async () => { + await signOutAzure(false); + }), + }; + + const signInM365Option: VscQuickPickItem = { + id: "signinM365", + label: localize("teamstoolkit.handlers.signIn365"), + function: () => signInM365(), + }; + + const signOutM365Option: VscQuickPickItem = { + id: "signOutM365", + label: localize("teamstoolkit.handlers.signOutOfM365"), + function: async () => + await Correlator.run(async () => { + await signOutM365(false); + }), + }; + + const createAccountsOption: VscQuickPickItem = { + id: "createAccounts", + label: `$(add) ${localize("teamstoolkit.commands.createAccount.title")}`, + function: async () => { + await Correlator.run(() => createAccountHandler([])); + }, + }; + + const quickPick = window.createQuickPick(); + const quickItemOptionArray: VscQuickPickItem[] = []; + + const m365AccountRes = await M365TokenInstance.getStatus({ scopes: AppStudioScopes }); + const m365Account = m365AccountRes.isOk() ? m365AccountRes.value : undefined; + if (m365Account && m365Account.status === "SignedIn") { + const accountInfo = m365Account.accountInfo; + const email = (accountInfo as any).upn ? (accountInfo as any).upn : undefined; + if (email !== undefined) { + signOutM365Option.label = signOutM365Option.label.concat(email); + } + quickItemOptionArray.push(signOutM365Option); + } else { + quickItemOptionArray.push(signInM365Option); + } + + const azureAccount = await azureAccountManager.getStatus(); + if (azureAccount.status === "SignedIn") { + const accountInfo = azureAccount.accountInfo; + const email = (accountInfo as any).email || (accountInfo as any).upn; + if (email !== undefined) { + signOutAzureOption.label = signOutAzureOption.label.concat(email); + } + quickItemOptionArray.push(signOutAzureOption); + } else { + quickItemOptionArray.push(signInAzureOption); + } + + quickItemOptionArray.push(createAccountsOption); + quickPick.items = quickItemOptionArray; + quickPick.onDidChangeSelection((selection) => { + if (selection[0]) { + (selection[0] as VscQuickPickItem).function().catch(console.error); + quickPick.hide(); + } + }); + quickPick.onDidHide(() => quickPick.dispose()); + quickPick.show(); +} diff --git a/packages/vscode-extension/src/handlers/checkCopilotAccess.ts b/packages/vscode-extension/src/handlers/checkCopilotAccess.ts index f200f4f0d6..8d6b91c1b3 100644 --- a/packages/vscode-extension/src/handlers/checkCopilotAccess.ts +++ b/packages/vscode-extension/src/handlers/checkCopilotAccess.ts @@ -6,7 +6,6 @@ import M365TokenInstance from "../commonlib/m365Login"; import { signedIn } from "../commonlib/common/constant"; import { localize } from "../utils/localizeUtils"; import VsCodeLogInstance from "../commonlib/log"; -import { signInM365 } from "../handlers"; import { FxError, Result, err, ok } from "@microsoft/teamsfx-api"; import { AppStudioScopes, @@ -15,6 +14,7 @@ import { SummaryConstant, } from "@microsoft/teamsfx-core"; import { wrapError } from "../error/common"; +import { signInM365 } from "../utils/accountUtils"; export async function checkCopilotAccessHandler(): Promise> { // check m365 login status, if not logged in, pop up a message diff --git a/packages/vscode-extension/src/utils/accountUtils.ts b/packages/vscode-extension/src/utils/accountUtils.ts new file mode 100644 index 0000000000..39a74e1148 --- /dev/null +++ b/packages/vscode-extension/src/utils/accountUtils.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { ExtTelemetry } from "../telemetry/extTelemetry"; +import { + AccountType, + TelemetryEvent, + TelemetryProperty, + TelemetryTriggerFrom, +} from "../telemetry/extTelemetryEvents"; +import { localize } from "./localizeUtils"; +import accountTreeViewProviderInstance from "../treeview/account/accountTreeViewProvider"; +import envTreeProviderInstance from "../treeview/environmentTreeViewProvider"; +import M365TokenInstance from "../commonlib/m365Login"; + +export async function signInAzure() { + await vscode.commands.executeCommand("fx-extension.signinAzure"); +} + +export async function signInM365() { + await vscode.commands.executeCommand("fx-extension.signinM365"); +} + +export async function signOutAzure(isFromTreeView: boolean) { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.SignOutStart, { + [TelemetryProperty.TriggerFrom]: isFromTreeView + ? TelemetryTriggerFrom.TreeView + : TelemetryTriggerFrom.CommandPalette, + [TelemetryProperty.AccountType]: AccountType.Azure, + }); + await vscode.window.showInformationMessage( + localize("teamstoolkit.commands.azureAccount.signOutHelp") + ); +} + +export async function signOutM365(isFromTreeView: boolean) { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.SignOutStart, { + [TelemetryProperty.TriggerFrom]: isFromTreeView + ? TelemetryTriggerFrom.TreeView + : TelemetryTriggerFrom.CommandPalette, + [TelemetryProperty.AccountType]: AccountType.M365, + }); + let result = false; + result = await M365TokenInstance.signout(); + if (result) { + accountTreeViewProviderInstance.m365AccountNode.setSignedOut(); + await envTreeProviderInstance.refreshRemoteEnvWarning(); + } +} diff --git a/packages/vscode-extension/test/extension/handlers.test.ts b/packages/vscode-extension/test/extension/handlers.test.ts index 3e98da2553..0a002c0fb5 100644 --- a/packages/vscode-extension/test/extension/handlers.test.ts +++ b/packages/vscode-extension/test/extension/handlers.test.ts @@ -67,8 +67,6 @@ import { VsCodeUI } from "../../src/qm/vsc_ui"; import { ExtTelemetry } from "../../src/telemetry/extTelemetry"; import * as extTelemetryEvents from "../../src/telemetry/extTelemetryEvents"; import { TelemetryEvent } from "../../src/telemetry/extTelemetryEvents"; -import envTreeProviderInstance from "../../src/treeview/environmentTreeViewProvider"; -import TreeViewManagerInstance from "../../src/treeview/treeViewManager"; import * as appDefinitionUtils from "../../src/utils/appDefinitionUtils"; import { updateAutoOpenGlobalKey } from "../../src/utils/globalStateUtils"; import * as localizeUtils from "../../src/utils/localizeUtils"; @@ -771,28 +769,6 @@ describe("handlers", () => { ); }); - it("signOutM365", async () => { - const signOut = sandbox.stub(M365TokenInstance, "signout").resolves(true); - const sendTelemetryEvent = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); - sandbox.stub(envTreeProviderInstance, "reloadEnvironments"); - - await handlers.signOutM365(false); - - sandbox.assert.calledOnce(signOut); - }); - - it("signOutAzure", async () => { - Object.setPrototypeOf(AzureAccountManager, sandbox.stub()); - const showMessageStub = sandbox - .stub(vscode.window, "showInformationMessage") - .resolves(undefined); - const sendTelemetryEvent = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); - - await handlers.signOutAzure(false); - - sandbox.assert.calledOnce(showMessageStub); - }); - describe("decryptSecret", function () { const sandbox = sinon.createSandbox(); @@ -1773,54 +1749,6 @@ describe("handlers", () => { sandbox.restore(); }); - it("cmpAccountsHandler", async () => { - const showMessageStub = sandbox - .stub(vscode.window, "showInformationMessage") - .resolves(undefined); - const M365SignOutStub = sandbox.stub(M365TokenInstance, "signout"); - sandbox - .stub(M365TokenInstance, "getStatus") - .resolves(ok({ status: "SignedIn", accountInfo: { upn: "test.email.com" } })); - sandbox - .stub(AzureAccountManager.prototype, "getStatus") - .resolves({ status: "SignedIn", accountInfo: { upn: "test.email.com" } }); - let changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any = () => {}; - const stubQuickPick = { - items: [], - onDidChangeSelection: ( - _changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any - ) => { - changeSelectionCallback = _changeSelectionCallback; - return { - dispose: () => {}, - }; - }, - onDidHide: () => { - return { - dispose: () => {}, - }; - }, - show: () => {}, - hide: () => {}, - onDidAccept: () => {}, - }; - const hideStub = sandbox.stub(stubQuickPick, "hide"); - sandbox.stub(vscode.window, "createQuickPick").returns(stubQuickPick as any); - sandbox.stub(vsc_ui, "VS_CODE_UI").value(new VsCodeUI({})); - sandbox.stub(vsc_ui.VS_CODE_UI, "selectOption").resolves(ok({ result: "unknown" } as any)); - - await handlers.cmpAccountsHandler([]); - changeSelectionCallback([stubQuickPick.items[1]]); - - for (const i of stubQuickPick.items) { - await (i as any).function(); - } - - chai.assert.isTrue(showMessageStub.calledTwice); - chai.assert.isTrue(M365SignOutStub.calledOnce); - chai.assert.isTrue(hideStub.calledOnce); - }); - it("updatePreviewManifest", async () => { sandbox.stub(globalVariables, "core").value(new MockCore()); sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); @@ -2273,68 +2201,6 @@ describe("autoOpenProjectHandler", () => { chai.assert.isTrue(result.isErr()); }); - it("registerAccountMenuCommands() - signedinM365", async () => { - sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); - sandbox - .stub(vscode.commands, "registerCommand") - .callsFake((command: string, callback: (...args: any[]) => any) => { - callback({ contextValue: "signedinM365" }).then(() => {}); - return { - dispose: () => {}, - }; - }); - sandbox.stub(vscode.extensions, "getExtension"); - const signoutStub = sandbox.stub(M365TokenInstance, "signout"); - - await handlers.registerAccountMenuCommands({ - subscriptions: [], - } as unknown as vscode.ExtensionContext); - - chai.assert.isTrue(signoutStub.called); - }); - - it("registerAccountMenuCommands() - signedinAzure", async () => { - sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); - sandbox - .stub(vscode.commands, "registerCommand") - .callsFake((command: string, callback: (...args: any[]) => any) => { - callback({ contextValue: "signedinAzure" }).then(() => {}); - return { - dispose: () => {}, - }; - }); - sandbox.stub(vscode.extensions, "getExtension"); - const showMessageStub = sandbox - .stub(vscode.window, "showInformationMessage") - .resolves(undefined); - - await handlers.registerAccountMenuCommands({ - subscriptions: [], - } as unknown as vscode.ExtensionContext); - - chai.assert.isTrue(showMessageStub.called); - }); - - it("registerAccountMenuCommands() - error", async () => { - sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); - sandbox - .stub(vscode.commands, "registerCommand") - .callsFake((command: string, callback: (...args: any[]) => any) => { - callback({ contextValue: "signedinM365" }).then(() => {}); - return { - dispose: () => {}, - }; - }); - sandbox.stub(vscode.extensions, "getExtension"); - const signoutStub = sandbox.stub(M365Login.prototype, "signout").throws(new UserCancelError()); - - await handlers.registerAccountMenuCommands({ - subscriptions: [], - } as unknown as vscode.ExtensionContext); - - chai.assert.isTrue(signoutStub.called); - }); - it("openSampleReadmeHandler() - trigger from walkthrough", async () => { sandbox.stub(vscode.workspace, "workspaceFolders").value([{ uri: vscode.Uri.file("test") }]); sandbox.stub(vscode.workspace, "openTextDocument"); @@ -2584,22 +2450,6 @@ describe("autoOpenProjectHandler", () => { }); }); - it("signInAzure()", async () => { - const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand"); - - await handlers.signInAzure(); - - chai.assert.isTrue(executeCommandStub.calledOnce); - }); - - it("signInM365()", async () => { - const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand"); - - await handlers.signInM365(); - - chai.assert.isTrue(executeCommandStub.calledOnce); - }); - it("openLifecycleTreeview() - TeamsFx Project", async () => { sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); sandbox.stub(globalVariables, "isTeamsFxProject").value(true); diff --git a/packages/vscode-extension/test/handlers/accountHandlers.test.ts b/packages/vscode-extension/test/handlers/accountHandlers.test.ts new file mode 100644 index 0000000000..5e194d29f4 --- /dev/null +++ b/packages/vscode-extension/test/handlers/accountHandlers.test.ts @@ -0,0 +1,163 @@ +import * as vscode from "vscode"; +import * as sinon from "sinon"; +import * as chai from "chai"; +import M365TokenInstance from "../../src/commonlib/m365Login"; +import { err, ok } from "@microsoft/teamsfx-api"; +import { AzureAccountManager } from "../../src/commonlib/azureLogin"; +import * as vsc_ui from "../../src/qm/vsc_ui"; +import { cmpAccountsHandler, createAccountHandler } from "../../src/handlers/accountHandlers"; +import { ExtTelemetry } from "../../src/telemetry/extTelemetry"; +import * as localizeUtils from "../../src/utils/localizeUtils"; + +describe("AccountHandlers", () => { + describe("createAccountHandler", () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + + beforeEach(() => { + sandbox.stub(localizeUtils, "localize").returns("test"); + sandbox.stub(vsc_ui, "VS_CODE_UI").value(new vsc_ui.VsCodeUI({})); + }); + + it("create M365 account", async () => { + const selectOptionStub = sandbox + .stub(vsc_ui.VS_CODE_UI, "selectOption") + .resolves(ok({ result: "createAccountM365" } as any)); + const openUrlStub = sandbox.stub(vsc_ui.VS_CODE_UI, "openUrl"); + const sendTelemetryEventStub = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); + + await createAccountHandler([]); + + chai.expect(selectOptionStub.calledOnce).to.be.true; + chai.expect( + openUrlStub.calledOnceWith("https://developer.microsoft.com/microsoft-365/dev-program") + ).to.be.true; + chai.expect(sendTelemetryEventStub.args[1][1]).to.deep.equal({ + "account-type": "m365", + "trigger-from": "CommandPalette", + }); + }); + + it("create Azure account", async () => { + const selectOptionStub = sandbox + .stub(vsc_ui.VS_CODE_UI, "selectOption") + .resolves(ok({ result: "createAccountAzure" } as any)); + const openUrlStub = sandbox.stub(vsc_ui.VS_CODE_UI, "openUrl"); + const sendTelemetryEventStub = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); + + await createAccountHandler([]); + + chai.expect(selectOptionStub.calledOnce).to.be.true; + chai.expect(openUrlStub.calledOnceWith("https://azure.microsoft.com/en-us/free/")).to.be.true; + chai.expect(sendTelemetryEventStub.args[1][1]).to.deep.equal({ + "account-type": "azure", + "trigger-from": "CommandPalette", + }); + }); + + it("create account error", async () => { + const selectOptionStub = sandbox + .stub(vsc_ui.VS_CODE_UI, "selectOption") + .resolves(err("error") as any); + const sendTelemetryErrorEventStub = sandbox.stub(ExtTelemetry, "sendTelemetryErrorEvent"); + + await createAccountHandler([]); + + chai.expect(selectOptionStub.calledOnce).to.be.true; + chai.expect(sendTelemetryErrorEventStub.calledOnce).to.be.true; + }); + }); + + describe("cmpAccountsHandler", () => { + const sandbox = sinon.createSandbox(); + let changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any; + let stubQuickPick: any; + + afterEach(() => { + sandbox.restore(); + }); + + beforeEach(() => { + changeSelectionCallback = () => {}; + stubQuickPick = { + items: [], + onDidChangeSelection: ( + _changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any + ) => { + changeSelectionCallback = _changeSelectionCallback; + return { + dispose: () => {}, + }; + }, + onDidHide: () => { + return { + dispose: () => {}, + }; + }, + show: () => {}, + hide: () => {}, + onDidAccept: () => {}, + }; + sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); + sandbox.stub(vscode.window, "createQuickPick").returns(stubQuickPick as any); + sandbox.stub(vsc_ui, "VS_CODE_UI").value(new vsc_ui.VsCodeUI({})); + sandbox.stub(vsc_ui.VS_CODE_UI, "selectOption").resolves(ok({ result: "unknown" } as any)); + }); + + it("Sign out happy path", async () => { + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); + const M365SignOutStub = sandbox.stub(M365TokenInstance, "signout"); + sandbox + .stub(M365TokenInstance, "getStatus") + .resolves(ok({ status: "SignedIn", accountInfo: { upn: "test.email.com" } })); + sandbox + .stub(AzureAccountManager.prototype, "getStatus") + .resolves({ status: "SignedIn", accountInfo: { upn: "test.email.com" } }); + const hideStub = sandbox.stub(stubQuickPick, "hide"); + + await cmpAccountsHandler([]); + changeSelectionCallback([stubQuickPick.items[1]]); + + for (const i of stubQuickPick.items) { + await (i as any).function(); + } + + chai.assert.isTrue(showMessageStub.calledTwice); + chai.assert.isTrue(M365SignOutStub.calledOnce); + chai.assert.isTrue(hideStub.calledOnce); + }); + + it("Sign in happy path", async () => { + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); + const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand"); + sandbox + .stub(M365TokenInstance, "getStatus") + .resolves(ok({ status: "SignedOut", accountInfo: { upn: "test.email.com" } })); + sandbox + .stub(AzureAccountManager.prototype, "getStatus") + .resolves({ status: "SignedOut", accountInfo: { upn: "test.email.com" } }); + const hideStub = sandbox.stub(stubQuickPick, "hide"); + + await cmpAccountsHandler([]); + changeSelectionCallback([stubQuickPick.items[1]]); + + for (const i of stubQuickPick.items) { + await (i as any).function(); + } + + chai.assert.isTrue(showMessageStub.notCalled); + chai.assert.isTrue(executeCommandStub.calledThrice); + chai.expect(executeCommandStub.args[0][0]).to.be.equal("fx-extension.signinAzure"); + chai.expect(executeCommandStub.args[1][0]).to.be.equal("fx-extension.signinM365"); + chai.expect(executeCommandStub.args[2][0]).to.be.equal("fx-extension.signinAzure"); + chai.assert.isTrue(hideStub.calledOnce); + }); + }); +}); diff --git a/packages/vscode-extension/test/utils/accountUtils.test.ts b/packages/vscode-extension/test/utils/accountUtils.test.ts new file mode 100644 index 0000000000..a2c846d434 --- /dev/null +++ b/packages/vscode-extension/test/utils/accountUtils.test.ts @@ -0,0 +1,54 @@ +import * as sinon from "sinon"; +import * as chai from "chai"; +import * as vscode from "vscode"; +import { AzureAccountManager } from "../../src/commonlib/azureLogin"; +import { ExtTelemetry } from "../../src/telemetry/extTelemetry"; +import { signOutM365, signOutAzure, signInAzure, signInM365 } from "../../src/utils/accountUtils"; +import envTreeProviderInstance from "../../src/treeview/environmentTreeViewProvider"; +import M365TokenInstance from "../../src/commonlib/m365Login"; + +describe("accountUtils", () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + + it("signInAzure()", async () => { + const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand"); + + await signInAzure(); + + chai.assert.isTrue(executeCommandStub.calledOnce); + }); + + it("signInM365()", async () => { + const executeCommandStub = sandbox.stub(vscode.commands, "executeCommand"); + + await signInM365(); + + chai.assert.isTrue(executeCommandStub.calledOnce); + }); + + it("signOutM365", async () => { + const signOut = sandbox.stub(M365TokenInstance, "signout").resolves(true); + const sendTelemetryEvent = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); + sandbox.stub(envTreeProviderInstance, "reloadEnvironments"); + + await signOutM365(false); + + sandbox.assert.calledOnce(signOut); + }); + + it("signOutAzure", async () => { + Object.setPrototypeOf(AzureAccountManager, sandbox.stub()); + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); + const sendTelemetryEvent = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); + + await signOutAzure(false); + + sandbox.assert.calledOnce(showMessageStub); + }); +});