From 24547819bbbd5c8cf13b9117823f0e7c4eca78d9 Mon Sep 17 00:00:00 2001 From: Ning Tang Date: Thu, 7 Aug 2025 13:30:31 +0800 Subject: [PATCH 1/2] feat(yml): update teamsApp/extendToM365 --- .../resource/yaml-schema/yaml.schema.json | 9 +++++++ .../src/component/driver/m365/acquire.ts | 27 ++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/fx-core/resource/yaml-schema/yaml.schema.json b/packages/fx-core/resource/yaml-schema/yaml.schema.json index 4d779fadced..68202d985be 100644 --- a/packages/fx-core/resource/yaml-schema/yaml.schema.json +++ b/packages/fx-core/resource/yaml-schema/yaml.schema.json @@ -1385,6 +1385,11 @@ "appPackagePath": { "type": "string", "description": "path to app package" + }, + "scope": { + "type": "string", + "description": "The scope of the app. personal is only visible to the owners. shared is visible to other users in the tenant.", + "enum": ["personal", "shared"] } } }, @@ -1401,6 +1406,10 @@ "appId": { "description": "Required. The app ID of M365 title.", "$ref": "#/definitions/envVarName" + }, + "shareLink": { + "description": "Optional. The share link of the app.", + "$ref": "#/definitions/envVarName" } } } diff --git a/packages/fx-core/src/component/driver/m365/acquire.ts b/packages/fx-core/src/component/driver/m365/acquire.ts index 422ef316db9..a677bfe39fd 100644 --- a/packages/fx-core/src/component/driver/m365/acquire.ts +++ b/packages/fx-core/src/component/driver/m365/acquire.ts @@ -8,9 +8,9 @@ import { hooks } from "@feathersjs/hooks/lib"; import { FxError, Result, SystemError, UserError } from "@microsoft/teamsfx-api"; import { getLocalizedString } from "../../../common/localizeUtils"; -import { PackageService } from "../../m365/packageService"; -import { MosServiceEndpoint, MosServiceScope } from "../../m365/serviceConstant"; import { FileNotFoundError, InvalidActionInputError, assembleError } from "../../../error/common"; +import { AppScope, PackageService } from "../../m365/packageService"; +import { MosServiceEndpoint, MosServiceScope } from "../../m365/serviceConstant"; import { getAbsolutePath, wrapRun } from "../../utils/common"; import { logMessageKeys } from "../aad/utility/constants"; import { DriverContext } from "../interface/commonArgs"; @@ -19,6 +19,7 @@ import { addStartAndEndTelemetry } from "../middleware/addStartAndEndTelemetry"; interface AcquireArgs { appPackagePath?: string; // The path of the app package + scope?: AppScope; } const actionName = "teamsApp/extendToM365"; @@ -27,6 +28,7 @@ const helpLink = "https://aka.ms/teamsfx-actions/teamsapp-extendToM365"; const outputKeys = { titleId: "titleId", appId: "appId", + shareLink: "shareLink", }; @Service(actionName) // DO NOT MODIFY the service name @@ -92,13 +94,20 @@ export class M365TitleAcquireDriver implements StepDriver { throw sideloadingTokenRes.error; } const sideloadingToken = sideloadingTokenRes.value; - const sideloadingRes = await packageService.sideLoading(sideloadingToken, appPackagePath); + const sideloadingRes = await packageService.sideLoading( + sideloadingToken, + appPackagePath, + args.scope + ); + const mapping = new Map(); + mapping.set(outputEnvVarNames!.get(outputKeys.titleId)!, sideloadingRes[0]); + mapping.set(outputEnvVarNames!.get(outputKeys.appId)!, sideloadingRes[1]); + if (outputEnvVarNames?.get(outputKeys.shareLink)) { + mapping.set(outputEnvVarNames.get(outputKeys.shareLink)!, sideloadingRes[2]); + } return { - output: new Map([ - [outputEnvVarNames!.get(outputKeys.titleId)!, sideloadingRes[0]], - [outputEnvVarNames!.get(outputKeys.appId)!, sideloadingRes[1]], - ]), + output: mapping, summaries: [getLocalizedString("driver.m365.acquire.summary", sideloadingRes[0])], }; } catch (error) { @@ -124,6 +133,10 @@ export class M365TitleAcquireDriver implements StepDriver { invalidParameters.push("appPackagePath"); } + if (args.scope && !Object.values(AppScope).includes(args.scope)) { + invalidParameters.push("scope"); + } + if (invalidParameters.length > 0) { throw new InvalidActionInputError(actionName, invalidParameters, helpLink); } From 2163e2ace3734a3011c3bf6be320ca9a764c33fb Mon Sep 17 00:00:00 2001 From: Ning Tang Date: Thu, 7 Aug 2025 14:18:09 +0800 Subject: [PATCH 2/2] feat(core): support new parameter --- .../src/component/driver/m365/acquire.ts | 7 +- .../component/driver/m365/acquire.test.ts | 163 +++++++++++++++++- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/packages/fx-core/src/component/driver/m365/acquire.ts b/packages/fx-core/src/component/driver/m365/acquire.ts index a677bfe39fd..c688ac4bf24 100644 --- a/packages/fx-core/src/component/driver/m365/acquire.ts +++ b/packages/fx-core/src/component/driver/m365/acquire.ts @@ -133,7 +133,12 @@ export class M365TitleAcquireDriver implements StepDriver { invalidParameters.push("appPackagePath"); } - if (args.scope && !Object.values(AppScope).includes(args.scope)) { + if ( + args.scope && + !Object.values(AppScope) + .map((v) => v.toLowerCase()) + .includes(args.scope) + ) { invalidParameters.push("scope"); } diff --git a/packages/fx-core/tests/component/driver/m365/acquire.test.ts b/packages/fx-core/tests/component/driver/m365/acquire.test.ts index 64151727a22..f1c276cf31a 100644 --- a/packages/fx-core/tests/component/driver/m365/acquire.test.ts +++ b/packages/fx-core/tests/component/driver/m365/acquire.test.ts @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "mocha"; -import * as sinon from "sinon"; import chai from "chai"; import fs from "fs-extra"; -import { PackageService } from "../../../../src/component/m365/packageService"; +import "mocha"; +import * as sinon from "sinon"; import { M365TitleAcquireDriver } from "../../../../src/component/driver/m365/acquire"; -import { MockedLogProvider, MockedUserInteraction } from "../../../plugins/solution/util"; +import { PackageService } from "../../../../src/component/m365/packageService"; import { FileNotFoundError, InvalidActionInputError, UnhandledError, } from "../../../../src/error/common"; import { MockedM365Provider } from "../../../core/utils"; +import { MockedLogProvider, MockedUserInteraction } from "../../../plugins/solution/util"; describe("teamsApp/extendToM365", async () => { const acquireDriver = new M365TitleAcquireDriver(); @@ -153,4 +153,159 @@ describe("teamsApp/extendToM365", async () => { chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); }); + + it("execute: invalid scope parameter", async () => { + const args = { + appPackagePath: "fakePath", + scope: "invalid-scope" as any, + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ]); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert(result.result.isErr()); + if (result.result.isErr()) { + chai.assert.isTrue(result.result.error instanceof InvalidActionInputError); + chai.assert.isTrue(result.result.error.message.includes("scope")); + } + }); + + it("execute: valid scope personal", async () => { + const args = { + appPackagePath: "fakePath", + scope: "personal" as any, + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", "https://example.com/sharelink"]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + }); + + it("execute: valid scope shared", async () => { + const args = { + appPackagePath: "fakePath", + scope: "shared" as any, + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", "https://example.com/sharelink"]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + }); + + it("execute: with shareLink output key", async () => { + const args = { + appPackagePath: "fakePath", + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ["shareLink", "MY_SHARE_LINK"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", "https://example.com/sharelink"]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + chai.assert.equal( + (result.result as any).value.get("MY_SHARE_LINK"), + "https://example.com/sharelink" + ); + }); + + it("execute: without shareLink output key should not include it in result", async () => { + const args = { + appPackagePath: "fakePath", + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", "https://example.com/sharelink"]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + chai.assert.isFalse((result.result as any).value.has("MY_SHARE_LINK")); + chai.assert.equal((result.result as any).value.size, 2); + }); + + it("execute: with scope and shareLink together", async () => { + const args = { + appPackagePath: "fakePath", + scope: "shared" as any, + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ["shareLink", "MY_SHARE_LINK"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", "https://example.com/sharelink"]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + chai.assert.equal( + (result.result as any).value.get("MY_SHARE_LINK"), + "https://example.com/sharelink" + ); + }); + + it("execute: empty shareLink from service should still be set", async () => { + const args = { + appPackagePath: "fakePath", + }; + const outputEnvVarNames = new Map([ + ["titleId", "MY_TITLE_ID"], + ["appId", "MY_APP_ID"], + ["shareLink", "MY_SHARE_LINK"], + ]); + + sinon + .stub(PackageService.prototype, "sideLoading") + .resolves(["test-title-id", "test-app-id", ""]); + sinon.stub(fs, "pathExists").resolves(true); + + const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames); + chai.assert.isTrue(result.result.isOk()); + chai.assert.equal((result.result as any).value.get("MY_TITLE_ID"), "test-title-id"); + chai.assert.equal((result.result as any).value.get("MY_APP_ID"), "test-app-id"); + chai.assert.equal((result.result as any).value.get("MY_SHARE_LINK"), ""); + }); });