Skip to content

Commit

Permalink
feat: e2e support for multi-tenant (#12642)
Browse files Browse the repository at this point in the history
* feat: add tenantId parameter in debug launch url

* feat: append tenantId parameter in preview launch url

* fix: read tenant id from env file instead of state json file

* fix: fallback collaborator to display name

* fix: only load tenanted account cache when tenant cache exists

* test: fix ut

* fix: add feature flag

* test: fix ut

* test: add ut to raise coverage
  • Loading branch information
HuihuiWu-Microsoft authored Nov 19, 2024
1 parent 32231c0 commit decc8f6
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 37 deletions.
4 changes: 4 additions & 0 deletions packages/fx-core/src/component/m365/launchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { NotExtendedToM365Error } from "./errors";
import { PackageService } from "./packageService";
import { MosServiceEndpoint, MosServiceScope } from "./serviceConstant";
import { officeBaseUrl, outlookBaseUrl, outlookCopilotAppId } from "./constants";
import { featureFlagManager, FeatureFlags } from "../../common/featureFlags";

export class LaunchHelper {
private readonly m365TokenProvider: M365TokenProvider;
Expand Down Expand Up @@ -66,6 +67,9 @@ export class LaunchHelper {
url = new URL(baseUrl);
const tid = await this.getTidFromToken();
if (tid) {
if (featureFlagManager.getBooleanValue(FeatureFlags.MultiTenant)) {
url.searchParams.append("tenantId", tid);
}
url.searchParams.append("appTenantId", tid);
}
break;
Expand Down
3 changes: 2 additions & 1 deletion packages/fx-core/src/component/provisionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ class ProvisionUtils {
return err(new M365TokenJSONNotFoundError());
}
const tenantIdInToken = (appStudioTokenJson as any).tid;
const tenantUserName = (appStudioTokenJson as any).upn;
const tenantUserName =
(appStudioTokenJson as any).upn ?? (appStudioTokenJson as any).unique_name;
if (!tenantIdInToken || !(typeof tenantIdInToken === "string")) {
return err(new M365TenantIdNotFoundInTokenError());
}
Expand Down
5 changes: 4 additions & 1 deletion packages/fx-core/src/core/collaborator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,10 @@ export async function listCollaborator(
content: getLocalizedString("core.collaboration.TeamsAppOwner"),
color: Colors.BRIGHT_WHITE,
},
{ content: teamsAppOwner.userPrincipalName, color: Colors.BRIGHT_MAGENTA },
{
content: teamsAppOwner.userPrincipalName ?? teamsAppOwner.displayName,
color: Colors.BRIGHT_MAGENTA,
},
{ content: `.\n`, color: Colors.BRIGHT_WHITE }
);
}
Expand Down
48 changes: 44 additions & 4 deletions packages/fx-core/tests/component/m365/launchHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,57 @@ import { PackageService } from "../../../src/component/m365/packageService";
import { HubTypes } from "../../../src/question";
import { outlookCopilotAppId } from "../../../src/component/m365/constants";
import { MockedM365Provider } from "../../core/utils";
import mockedEnv, { RestoreFn } from "mocked-env";
import { FeatureFlagName } from "../../../src";

describe("LaunchHelper", () => {
const m365TokenProvider = new MockedM365Provider();
const launchHelper = new LaunchHelper(m365TokenProvider);
let mockedEnvRestore: RestoreFn = () => {};

afterEach(() => {
sinon.restore();
mockedEnvRestore();
});

beforeEach(() => {
mockedEnvRestore = mockedEnv({
[FeatureFlagName.MultiTenant]: "true",
});
});

describe("getLaunchUrl", () => {
it("getLaunchUrl: Teams, signed in", async () => {
sinon.stub(m365TokenProvider, "getStatus").resolves(
ok({
status: "",
accountInfo: {
tid: "test-tid",
upn: "test-upn",
},
})
);
const properties: ManifestProperties = {
capabilities: ["staticTab"],
id: "test-id",
version: "1.0.0",
manifestVersion: "1.16",
isApiME: false,
isSPFx: false,
isApiMeAAD: false,
};
const result = await launchHelper.getLaunchUrl(HubTypes.teams, "test-id", properties);
chai.assert(result.isOk());
chai.assert.equal(
(result as any).value,
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&tenantId=test-tid&appTenantId=test-tid&login_hint=test-upn"
);
});

it("getLaunchUrl: Teams, signed in - multi tenant off", async () => {
mockedEnvRestore = mockedEnv({
[FeatureFlagName.MultiTenant]: "false",
});
sinon.stub(m365TokenProvider, "getStatus").resolves(
ok({
status: "",
Expand Down Expand Up @@ -71,7 +111,7 @@ describe("LaunchHelper", () => {
chai.assert(result.isOk());
chai.assert.equal(
(result as any).value,
"https://teams.microsoft.com/?appTenantId=test-tid&login_hint=test-upn"
"https://teams.microsoft.com/?tenantId=test-tid&appTenantId=test-tid&login_hint=test-upn"
);
});

Expand All @@ -98,7 +138,7 @@ describe("LaunchHelper", () => {
chai.assert(result.isOk());
chai.assert.equal(
(result as any).value,
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&appTenantId=test-tid&login_hint=test-upn"
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&tenantId=test-tid&appTenantId=test-tid&login_hint=test-upn"
);
});

Expand All @@ -125,7 +165,7 @@ describe("LaunchHelper", () => {
chai.assert(result.isOk());
chai.assert.equal(
(result as any).value,
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&appTenantId=test-tid&login_hint=test-upn"
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&tenantId=test-tid&appTenantId=test-tid&login_hint=test-upn"
);
});

Expand All @@ -152,7 +192,7 @@ describe("LaunchHelper", () => {
chai.assert(result.isOk());
chai.assert.equal(
(result as any).value,
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&appTenantId=test-tid&login_hint=test-upn"
"https://teams.microsoft.com/l/app/test-id?installAppPackage=true&webjoin=true&tenantId=test-tid&appTenantId=test-tid&login_hint=test-upn"
);
});

Expand Down
30 changes: 30 additions & 0 deletions packages/fx-core/tests/component/provisionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,36 @@ describe("provisionUtils", () => {
chai.assert.isTrue(res.error instanceof M365TenantIdNotFoundInTokenError);
}
});

it("happy pass - upn", async () => {
mocker.stub(tools.tokenProvider.m365TokenProvider, "getAccessToken").resolves(ok(""));
mocker
.stub(tools.tokenProvider.m365TokenProvider, "getJsonObject")
.resolves(ok({ tid: "faked id", upn: "faked upn" }));
const res = await provisionUtils.getM365TenantId(tools.tokenProvider.m365TokenProvider);
chai.assert.isTrue(res.isOk());
if (res.isOk()) {
chai.assert.deepEqual(res.value, {
tenantIdInToken: "faked id",
tenantUserName: "faked upn",
});
}
});

it("happy pass - unique_name", async () => {
mocker.stub(tools.tokenProvider.m365TokenProvider, "getAccessToken").resolves(ok(""));
mocker
.stub(tools.tokenProvider.m365TokenProvider, "getJsonObject")
.resolves(ok({ tid: "faked id", unique_name: "faked unique name" }));
const res = await provisionUtils.getM365TenantId(tools.tokenProvider.m365TokenProvider);
chai.assert.isTrue(res.isOk());
if (res.isOk()) {
chai.assert.deepEqual(res.value, {
tenantIdInToken: "faked id",
tenantUserName: "faked unique name",
});
}
});
});
describe("arm", () => {
let mockedEnvRestore: RestoreFn | undefined;
Expand Down
11 changes: 10 additions & 1 deletion packages/vscode-extension/src/commonlib/codeFlowLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export class CodeFlowLogin {
this.account = dataCache;
this.status = loggedIn;
}

if (featureFlagManager.getBooleanValue(FeatureFlags.MultiTenant)) {
const tenantCache = await loadTenantId(this.accountName);
if (tenantCache) {
const allAccounts = await this.msalTokenCache.getAllAccounts();
this.account = allAccounts.find((account) => account.tenantId == tenantCache);
}
}
} else if (this.status !== loggingIn) {
this.account = undefined;
this.status = loggedOut;
Expand Down Expand Up @@ -392,11 +400,12 @@ export class CodeFlowLogin {
if (tenantId) {
const allAccounts = await this.msalTokenCache.getAllAccounts();
tenantedAccount = allAccounts.find((account) => account.tenantId == tenantId);
this.account = tenantedAccount ?? this.account;
}

try {
const res = await this.pca.acquireTokenSilent({
account: tenantedAccount ? tenantedAccount : this.account,
account: this.account,
scopes: scopes,
forceRefresh: tenantedAccount ? false : true,
authority: tenantId ? BASE_AUTHORITY + tenantId : this.config.auth.authority,
Expand Down
10 changes: 9 additions & 1 deletion packages/vscode-extension/src/debug/teamsfxDebugProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Correlator,
environmentNameManager,
envUtil,
featureFlagManager,
FeatureFlags,
Hub,
isValidProject,
isValidProjectV3,
Expand Down Expand Up @@ -223,7 +225,13 @@ async function generateAccountHint(includeTenantId = true): Promise<string> {
}
}
if (includeTenantId && tenantId) {
return loginHint ? `appTenantId=${tenantId}&login_hint=${loginHint}` : "";
if (featureFlagManager.getBooleanValue(FeatureFlags.MultiTenant)) {
return loginHint
? `tenantId=${tenantId}&appTenantId=${tenantId}&login_hint=${loginHint}`
: "";
} else {
return loginHint ? `appTenantId=${tenantId}&login_hint=${loginHint}` : "";
}
} else {
return loginHint ? `login_hint=${loginHint}` : "";
}
Expand Down
22 changes: 9 additions & 13 deletions packages/vscode-extension/src/utils/envTreeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { SubscriptionInfo } from "@microsoft/teamsfx-api";
import { getProvisionResultJson } from "./fileSystemUtils";
import { workspaceUri } from "../globalVariables";
import { getV3TeamsAppId } from "./appDefinitionUtils";
import path from "path";
import fs from "fs-extra";
import { dotenvUtil } from "@microsoft/teamsfx-core/build/component/utils/envUtil";

export async function getSubscriptionInfoFromEnv(
env: string
Expand Down Expand Up @@ -34,20 +37,13 @@ export async function getSubscriptionInfoFromEnv(
}

export async function getM365TenantFromEnv(env: string): Promise<string | undefined> {
let provisionResult: Record<string, any> | undefined;

try {
provisionResult = await getProvisionResultJson(env);
} catch (error) {
// ignore error on tree view when load provision result failed.
return undefined;
}

if (!provisionResult) {
return undefined;
const projectPath = workspaceUri!.fsPath;
const envFile = path.resolve(projectPath, "env", `.env.${env}`);
if (await fs.pathExists(envFile)) {
const envData = dotenvUtil.deserialize(fs.readFileSync(envFile, "utf-8"));
return envData.obj["TEAMS_APP_TENANT_ID"];
}

return provisionResult.solution?.teamsAppTenantId;
return undefined;
}

export async function getResourceGroupNameFromEnv(env: string): Promise<string | undefined> {
Expand Down
30 changes: 14 additions & 16 deletions packages/vscode-extension/test/utils/envTreeUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as chai from "chai";
import * as sinon from "sinon";
import fs from "fs-extra";
import * as globalVariables from "../../src/globalVariables";
import { Uri } from "vscode";
import { envUtil, metadataUtil, pathUtils } from "@microsoft/teamsfx-core";
Expand Down Expand Up @@ -54,36 +55,33 @@ describe("EnvTreeUtils", () => {
const m365TenantId = {
teamsAppTenantId: "fakeTenantId",
};
const provisionResult: Record<string, any> = {
solution: m365TenantId,
};

beforeEach(() => {
sandbox.stub(globalVariables, "workspaceUri").value({ fsPath: "/test" });
});

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

it("returns m365 tenantId successfully", async () => {
sandbox.stub(fileSystemUtils, "getProvisionResultJson").resolves(provisionResult);
sandbox.stub(fs, "pathExists").resolves(true);
sandbox.stub(fs, "readFileSync").returns("TEAMS_APP_TENANT_ID=fakeTenantId\n");
const result = await envTreeUtils.getM365TenantFromEnv("test");
chai.expect(result).equal("fakeTenantId");
});

it("returns undefined if get provision result throws error", async () => {
sandbox.stub(fileSystemUtils, "getProvisionResultJson").rejects(new Error());
it("returns undefined if env file doesn't exist", async () => {
sandbox.stub(fs, "pathExists").resolves(false);
const result = await envTreeUtils.getM365TenantFromEnv("test");
chai.expect(result).is.undefined;
chai.expect(result).equal(undefined);
});

it("returns undefined if get provision result returns undefined", async () => {
sandbox.stub(fileSystemUtils, "getProvisionResultJson").resolves(undefined);
const result = await envTreeUtils.getM365TenantFromEnv("test");
chai.expect(result).is.undefined;
});

it("returns undefined if get provision result does not contain solution", async () => {
sandbox.stub(fileSystemUtils, "getProvisionResultJson").resolves({});
it("returns undefined if tenant id doesn't exist in env file", async () => {
sandbox.stub(fs, "pathExists").resolves(true);
sandbox.stub(fs, "readFileSync").returns("");
const result = await envTreeUtils.getM365TenantFromEnv("test");
chai.expect(result).is.undefined;
chai.expect(result).equal(undefined);
});
});

Expand Down

0 comments on commit decc8f6

Please sign in to comment.