From 454e0d22a5c353dea3c5ab13f331b1ab34060911 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 13:01:22 +0800 Subject: [PATCH 01/17] feat: add builer api --- .../src/component/m365/packageService.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index 57b5ef6e843..5a344ab1e9c 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -20,6 +20,8 @@ import { waitSeconds } from "../../common/utils"; import { WrappedAxiosClient } from "../../common/wrappedAxiosClient"; import { NotExtendedToM365Error } from "./errors"; import { MosServiceEndpoint } from "./serviceConstant"; +import { manifestUtils } from "../driver/teamsApp/utils/ManifestUtils"; +import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker"; const M365ErrorSource = "M365"; const M365ErrorComponent = "PackageService"; @@ -140,6 +142,74 @@ export class PackageService { @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) public async sideLoading(token: string, manifestPath: string): Promise<[string, string]> { + const manifest = await manifestUtils.readAppManifest(manifestPath); + if (manifest.isErr()) { + throw manifest.error; + } + const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest.value); + if (isDelcarativeAgentApp) { + return await this.sideLoadingV2(token, manifestPath); + } else { + return await this.sideLoadingV1(token, manifestPath); + } + } + // Side loading using Builder API + @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) + public async sideLoadingV2(token: string, manifestPath: string): Promise<[string, string]> { + try { + this.checkZip(manifestPath); + const data = await fs.readFile(manifestPath); + const content = new FormData(); + content.append("package", data); + const serviceUrl = await this.getTitleServiceUrl(token); + this.logger?.verbose("Uploading package ..."); + const uploadHeaders = content.getHeaders(); + uploadHeaders["Authorization"] = `Bearer ${token}`; + const uploadResponse = await this.axiosInstance.post( + "/builder/v1/users/packages", + content.getBuffer(), + { + baseURL: serviceUrl, + headers: uploadHeaders, + params: { + scope: "Personal", + }, + } + ); + + const statusId = uploadResponse.data.statusId; + this.logger?.debug(`Acquiring package with statusId: ${statusId as string} ...`); + + do { + const statusResponse = await this.axiosInstance.get( + `/builder/v1/users/packages/status/${statusId as string}`, + { + baseURL: serviceUrl, + headers: { Authorization: `Bearer ${token}` }, + } + ); + const resCode = statusResponse.status; + this.logger?.debug(`Package status: ${resCode} ...`); + if (resCode === 200) { + const titleId: string = statusResponse.data.titleId; + const appId: string = statusResponse.data.appId; + this.logger?.info(`TitleId: ${titleId}`); + this.logger?.info(`AppId: ${appId}`); + this.logger?.verbose("Sideloading done."); + return [titleId, appId]; + } else { + await waitSeconds(2); + } + } while (true); + } catch (error: any) { + if (error.response) { + error = this.traceError(error); + } + throw assembleError(error, M365ErrorSource); + } + } + @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) + public async sideLoadingV1(token: string, manifestPath: string): Promise<[string, string]> { try { this.checkZip(manifestPath); const data = await fs.readFile(manifestPath); From c073a6c805eebd45f2c8c13ac80a7afe73b1689d Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 15:06:13 +0800 Subject: [PATCH 02/17] feat: update --- .../src/component/m365/packageService.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index 5a344ab1e9c..fcc2c6b1919 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { hooks } from "@feathersjs/hooks"; -import { LogProvider, SystemError, UserError } from "@microsoft/teamsfx-api"; +import { Err, LogProvider, SystemError, TeamsAppManifest, UserError } from "@microsoft/teamsfx-api"; import AdmZip from "adm-zip"; import FormData from "form-data"; import fs from "fs-extra"; @@ -22,6 +22,7 @@ import { NotExtendedToM365Error } from "./errors"; import { MosServiceEndpoint } from "./serviceConstant"; import { manifestUtils } from "../driver/teamsApp/utils/ManifestUtils"; import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker"; +import stripBom from "strip-bom"; const M365ErrorSource = "M365"; const M365ErrorComponent = "PackageService"; @@ -141,16 +142,16 @@ export class PackageService { } @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) - public async sideLoading(token: string, manifestPath: string): Promise<[string, string]> { - const manifest = await manifestUtils.readAppManifest(manifestPath); - if (manifest.isErr()) { - throw manifest.error; + public async sideLoading(token: string, packagePath: string): Promise<[string, string]> { + const manifest = this.getManifestFromZip(packagePath); + if (!manifest) { + throw new Error("Invalid app package zip. manifest.json is missing"); } - const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest.value); + const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest); if (isDelcarativeAgentApp) { - return await this.sideLoadingV2(token, manifestPath); + return await this.sideLoadingV2(token, packagePath); } else { - return await this.sideLoadingV1(token, manifestPath); + return await this.sideLoadingV1(token, packagePath); } } // Side loading using Builder API @@ -162,7 +163,7 @@ export class PackageService { const content = new FormData(); content.append("package", data); const serviceUrl = await this.getTitleServiceUrl(token); - this.logger?.verbose("Uploading package ..."); + this.logger?.debug("Uploading package with sideLoading V2 ..."); const uploadHeaders = content.getHeaders(); uploadHeaders["Authorization"] = `Bearer ${token}`; const uploadResponse = await this.axiosInstance.post( @@ -216,7 +217,7 @@ export class PackageService { const content = new FormData(); content.append("package", data); const serviceUrl = await this.getTitleServiceUrl(token); - this.logger?.verbose("Uploading package ..."); + this.logger?.debug("Uploading package with sideLoading V1 ..."); const uploadHeaders = content.getHeaders(); uploadHeaders["Authorization"] = `Bearer ${token}`; const uploadResponse = await this.axiosInstance.post( @@ -510,4 +511,15 @@ export class PackageService { this.logger?.warning(`Please make sure input path is a valid app package zip. ${path}`); } } + + private getManifestFromZip(path: string): TeamsAppManifest | undefined { + const zip = new AdmZip(path); + const manifestEntry = zip.getEntry("manifest.json"); + if (!manifestEntry) { + return undefined; + } + let manifestContent = manifestEntry.getData().toString("utf8"); + manifestContent = stripBom(manifestContent); + return JSON.parse(manifestContent) as TeamsAppManifest; + } } From b37a8650e6bc05e349a636bacf7d0a6b72fd83dc Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 15:27:22 +0800 Subject: [PATCH 03/17] feat: builder API uninstall --- .../src/component/m365/packageService.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index fcc2c6b1919..afa96f73542 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -364,6 +364,27 @@ export class PackageService { }); this.logger?.verbose("Unacquiring done."); } catch (error: any) { + if (error.response) { + error = this.traceError(error); + } + // try delete in the builder API + try { + const serviceUrl = await this.getTitleServiceUrl(token); + this.logger?.verbose(`Unacquiring package with TitleId ${titleId} in builder API...`); + await this.axiosInstance.delete(`/builder/v1/users/titles/${titleId}`, { + baseURL: serviceUrl, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + this.logger?.verbose("Unacquiring done."); + return; + } catch (subError: any) { + if (subError.response) { + subError = this.traceError(subError); + } + this.logger?.error(subError); + } if (error.response) { error = this.traceError(error); } From 6ffe9d278e4af3e195ce992e733c3f6b432e6e76 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 15:39:07 +0800 Subject: [PATCH 04/17] feat: update --- packages/fx-core/src/component/m365/packageService.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index afa96f73542..1d047660926 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -364,10 +364,7 @@ export class PackageService { }); this.logger?.verbose("Unacquiring done."); } catch (error: any) { - if (error.response) { - error = this.traceError(error); - } - // try delete in the builder API + // try to delete in the builder API try { const serviceUrl = await this.getTitleServiceUrl(token); this.logger?.verbose(`Unacquiring package with TitleId ${titleId} in builder API...`); @@ -377,7 +374,7 @@ export class PackageService { Authorization: `Bearer ${token}`, }, }); - this.logger?.verbose("Unacquiring done."); + this.logger?.verbose("Unacquiring using builder api done."); return; } catch (subError: any) { if (subError.response) { From 6c7744cb8db01f6474cfd5669e3a830747307c06 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 15:40:57 +0800 Subject: [PATCH 05/17] feat: update --- packages/fx-core/src/component/m365/packageService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index 1d047660926..f47e908513d 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { hooks } from "@feathersjs/hooks"; -import { Err, LogProvider, SystemError, TeamsAppManifest, UserError } from "@microsoft/teamsfx-api"; +import { LogProvider, SystemError, TeamsAppManifest, UserError } from "@microsoft/teamsfx-api"; import AdmZip from "adm-zip"; import FormData from "form-data"; import fs from "fs-extra"; @@ -20,7 +20,6 @@ import { waitSeconds } from "../../common/utils"; import { WrappedAxiosClient } from "../../common/wrappedAxiosClient"; import { NotExtendedToM365Error } from "./errors"; import { MosServiceEndpoint } from "./serviceConstant"; -import { manifestUtils } from "../driver/teamsApp/utils/ManifestUtils"; import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker"; import stripBom from "strip-bom"; From 1c97da721d5c5f81254cfdf827624ee0d598428a Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 15:50:09 +0800 Subject: [PATCH 06/17] feat: add scope --- .../src/component/m365/packageService.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index f47e908513d..efa7bbbac4d 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -26,6 +26,11 @@ import stripBom from "strip-bom"; const M365ErrorSource = "M365"; const M365ErrorComponent = "PackageService"; +enum AppScope { + Personal = "Personal", + Shared = "Shared", +} + // Call m365 service for package CRUD export class PackageService { private static sharedInstance: PackageService; @@ -141,21 +146,29 @@ export class PackageService { } @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) - public async sideLoading(token: string, packagePath: string): Promise<[string, string]> { + public async sideLoading( + token: string, + packagePath: string, + appScope = AppScope.Personal + ): Promise<[string, string]> { const manifest = this.getManifestFromZip(packagePath); if (!manifest) { throw new Error("Invalid app package zip. manifest.json is missing"); } const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest); if (isDelcarativeAgentApp) { - return await this.sideLoadingV2(token, packagePath); + return await this.sideLoadingV2(token, packagePath, appScope); } else { return await this.sideLoadingV1(token, packagePath); } } // Side loading using Builder API @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) - public async sideLoadingV2(token: string, manifestPath: string): Promise<[string, string]> { + public async sideLoadingV2( + token: string, + manifestPath: string, + appScope: AppScope + ): Promise<[string, string]> { try { this.checkZip(manifestPath); const data = await fs.readFile(manifestPath); @@ -172,7 +185,7 @@ export class PackageService { baseURL: serviceUrl, headers: uploadHeaders, params: { - scope: "Personal", + scope: appScope, }, } ); From f89ce9cddf54886c0be48320623509492d4620dd Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 16:00:09 +0800 Subject: [PATCH 07/17] feat: get share link --- .../src/component/m365/packageService.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index efa7bbbac4d..42ae10bb12e 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -294,6 +294,27 @@ export class PackageService { } } @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) + public async getShareLink(token: string, titleId: string): Promise { + const serviceUrl = await this.getTitleServiceUrl(token); + try { + const resp = await this.axiosInstance.get( + `/marketplace/v1/users/titles/${titleId}/sharingInfo`, + { + baseURL: serviceUrl, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + return resp.data.unifiedStoreLink; + } catch (error: any) { + if (error.response) { + error = this.traceError(error); + } + throw assembleError(error, M365ErrorSource); + } + } + @hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })]) public async getLaunchInfoByManifestId(token: string, manifestId: string): Promise { try { const serviceUrl = await this.getTitleServiceUrl(token); From bab36d49c2ed31d02d28b3a3291ef088087019a9 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 16:20:16 +0800 Subject: [PATCH 08/17] fix: ut --- .../fx-core/tests/component/m365/packageService.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index c46c81edd77..61e5fd3a2fa 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -358,6 +358,7 @@ describe("Package Service", () => { }; let packageService = new PackageService("https://test-endpoint"); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); let actualError: Error | undefined; try { const result = await packageService.sideLoading("test-token", "test-path"); @@ -368,8 +369,8 @@ describe("Package Service", () => { } chai.assert.isUndefined(actualError); - packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); try { const result = await packageService.sideLoading("test-token", "test-path"); chai.assert.equal(result[0], "test-title-id"); @@ -390,6 +391,7 @@ describe("Package Service", () => { axiosPostResponses["/dev/v1/users/packages"] = new Error("test-post"); let packageService = new PackageService("https://test-endpoint"); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); let actualError: Error | undefined; try { await packageService.sideLoading("test-token", "test-path"); @@ -401,6 +403,7 @@ describe("Package Service", () => { chai.assert.isTrue(actualError?.message.includes("test-post")); packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); try { await packageService.sideLoading("test-token", "test-path"); } catch (error: any) { @@ -469,6 +472,7 @@ describe("Package Service", () => { axiosPostResponses["/dev/v1/users/packages"] = expectedError; const packageService = new PackageService("https://test-endpoint"); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); let actualError: any; try { await packageService.sideLoading("test-token", "test-path"); From b8ef73a3d145095f9a8476b0d24e9b1fc3a8299e Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 15 Jan 2025 16:31:54 +0800 Subject: [PATCH 09/17] fix: ut --- packages/fx-core/tests/component/m365/packageService.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index 61e5fd3a2fa..c1d714a162c 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -432,6 +432,7 @@ describe("Package Service", () => { axiosPostResponses["/dev/v1/users/packages"] = expectedError; let packageService = new PackageService("https://test-endpoint"); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); let actualError: any; try { await packageService.sideLoading("test-token", "test-path"); @@ -443,6 +444,7 @@ describe("Package Service", () => { chai.assert.isTrue(actualError.message.includes("test-post")); packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); try { await packageService.sideLoading("test-token", "test-path"); } catch (error: any) { From 7293dd619d7b24520fde0691ecdcab8517e1eb8d Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Thu, 16 Jan 2025 14:14:20 +0800 Subject: [PATCH 10/17] fix: ut --- .../component/m365/packageService.test.ts | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index c1d714a162c..c41d1ae87a6 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -344,6 +344,14 @@ describe("Package Service", () => { }, }, }; + axiosPostResponses["/builder/v1/users/packages"] = { + data: { + statusId: "test-status-id-builder-api", + titlePreview: { + titleId: "test-title-id-preview-builder-api", + }, + }, + }; axiosPostResponses["/dev/v1/users/packages/acquisitions"] = { data: { statusId: "test-status-id", @@ -356,6 +364,13 @@ describe("Package Service", () => { appId: "test-app-id", }, }; + axiosGetResponses["/builder/v1/users/packages/status/test-status-id-builder-api"] = { + status: 200, + data: { + titleId: "test-title-id-builder-api", + appId: "test-app-id-builder-api", + }, + }; let packageService = new PackageService("https://test-endpoint"); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); @@ -370,7 +385,34 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); packageService = new PackageService("https://test-endpoint", logger); - sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ + $schema: + "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", + manifestVersion: "1.19", + version: "1.0.0", + id: "${{TEAMS_APP_ID}}", + developer: { + name: "Teams App, Inc.", + websiteUrl: "https://www.example.com", + privacyUrl: "https://www.example.com/privacy", + termsOfUseUrl: "https://www.example.com/termofuse", + }, + icons: { + color: "color.png", + outline: "outline.png", + }, + name: { + short: "test-manifest", + full: "test-manifest full name", + }, + description: { + short: "Short description for test-manifest", + full: "Full description for test-manifest", + }, + accentColor: "#FFFFFF", + composeExtensions: [], + permissions: ["identity", "messageTeamMembers"], + } as any); try { const result = await packageService.sideLoading("test-token", "test-path"); chai.assert.equal(result[0], "test-title-id"); @@ -380,6 +422,27 @@ describe("Package Service", () => { } chai.assert.isUndefined(actualError); + + packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ + copilotAgents: { + declarativeAgents: [ + { + id: "declarativeAgent", + file: "declarativeAgent.json", + }, + ], + }, + } as any); + try { + const result = await packageService.sideLoading("test-token", "test-path"); + chai.assert.equal(result[0], "test-title-id-builder-api"); + chai.assert.equal(result[1], "test-app-id-builder-api"); + } catch (error: any) { + actualError = error; + } + + chai.assert.isUndefined(actualError); }); it("sideLoading throws expected error", async () => { @@ -661,6 +724,7 @@ describe("Package Service", () => { }, }; axiosDeleteResponses["/catalog/v1/users/acquisitions/test-title-id"] = {}; + axiosDeleteResponses["/builder/v1/users/titles/test-title-id"] = {}; let packageService = new PackageService("https://test-endpoint"); let actualError: Error | undefined; @@ -682,7 +746,23 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); }); - + it("unacquire by builder api", async () => { + axiosGetResponses["/config/v1/environment"] = { + data: { + titlesServiceUrl: "https://test-url", + }, + }; + axiosDeleteResponses["/catalog/v1/users/acquisitions/test-title-id"] = new Error("test-delete"); + axiosDeleteResponses["/builder/v1/users/titles/test-title-id"] = {}; + const packageService = new PackageService("https://test-endpoint"); + let actualError: Error | undefined; + try { + await packageService.unacquire("test-token", "test-title-id"); + } catch (error: any) { + actualError = error; + } + chai.assert.isUndefined(actualError); + }); it("unacquire throws expected error", async () => { axiosGetResponses["/config/v1/environment"] = { data: { @@ -1075,4 +1155,20 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); chai.assert.isUndefined(result); }); + + it("get share link happy path", async () => { + axiosGetResponses["/config/v1/environment"] = { + data: { + titlesServiceUrl: "https://test-url", + }, + }; + axiosGetResponses["/marketplace/v1/users/titles/test-title-id/sharingInfo"] = { + data: { + unifiedStoreLink: "https://test-share-link", + }, + }; + const packageService = new PackageService("https://test-endpoint"); + const shareLink = await packageService.getShareLink("test-token", "test-title-id"); + chai.assert.equal(shareLink, "https://test-share-link"); + }); }); From 77f7067d88df03891ee06b016cc0cc61bf9e67ff Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Thu, 16 Jan 2025 14:39:11 +0800 Subject: [PATCH 11/17] feat: update --- .../src/component/m365/packageService.ts | 14 ++-- .../component/m365/packageService.test.ts | 65 +++++++++++++++++- .../fx-core/tests/component/m365/success.zip | Bin 0 -> 1839 bytes 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 packages/fx-core/tests/component/m365/success.zip diff --git a/packages/fx-core/src/component/m365/packageService.ts b/packages/fx-core/src/component/m365/packageService.ts index 42ae10bb12e..599015911bd 100644 --- a/packages/fx-core/src/component/m365/packageService.ts +++ b/packages/fx-core/src/component/m365/packageService.ts @@ -26,7 +26,7 @@ import stripBom from "strip-bom"; const M365ErrorSource = "M365"; const M365ErrorComponent = "PackageService"; -enum AppScope { +export enum AppScope { Personal = "Personal", Shared = "Shared", } @@ -150,16 +150,22 @@ export class PackageService { token: string, packagePath: string, appScope = AppScope.Personal - ): Promise<[string, string]> { + ): Promise<[string, string, string]> { const manifest = this.getManifestFromZip(packagePath); if (!manifest) { throw new Error("Invalid app package zip. manifest.json is missing"); } const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest); if (isDelcarativeAgentApp) { - return await this.sideLoadingV2(token, packagePath, appScope); + const res = await this.sideLoadingV2(token, packagePath, appScope); + let shareLink = ""; + if (appScope == AppScope.Shared) { + shareLink = await this.getShareLink(token, res[0]); + } + return [res[0], res[1], shareLink]; } else { - return await this.sideLoadingV1(token, packagePath); + const res = await this.sideLoadingV1(token, packagePath); + return [res[0], res[1], ""]; } } // Side loading using Builder API diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index c41d1ae87a6..7a8d6cd9be3 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -9,7 +9,7 @@ import fs from "fs-extra"; import "mocha"; import sinon from "sinon"; import { NotExtendedToM365Error } from "../../../src/component/m365/errors"; -import { PackageService } from "../../../src/component/m365/packageService"; +import { AppScope, PackageService } from "../../../src/component/m365/packageService"; import { setTools } from "../../../src/common/globalVars"; import { UnhandledError } from "../../../src/error/common"; import { MockLogProvider } from "../../core/utils"; @@ -371,6 +371,11 @@ describe("Package Service", () => { appId: "test-app-id-builder-api", }, }; + axiosGetResponses["/marketplace/v1/users/titles/test-title-id-builder-api/sharingInfo"] = { + data: { + unifiedStoreLink: "https://test-share-link", + }, + }; let packageService = new PackageService("https://test-endpoint"); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); @@ -423,6 +428,28 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); + packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ + copilotAgents: { + declarativeAgents: [ + { + id: "declarativeAgent", + file: "declarativeAgent.json", + }, + ], + }, + } as any); + try { + const result = await packageService.sideLoading("test-token", "test-path", AppScope.Shared); + chai.assert.equal(result[0], "test-title-id-builder-api"); + chai.assert.equal(result[1], "test-app-id-builder-api"); + chai.assert.equal(result[2], "https://test-share-link"); + } catch (error: any) { + actualError = error; + } + + chai.assert.isUndefined(actualError); + packageService = new PackageService("https://test-endpoint", logger); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ copilotAgents: { @@ -438,6 +465,21 @@ describe("Package Service", () => { const result = await packageService.sideLoading("test-token", "test-path"); chai.assert.equal(result[0], "test-title-id-builder-api"); chai.assert.equal(result[1], "test-app-id-builder-api"); + chai.assert.equal(result[2], ""); + } catch (error: any) { + actualError = error; + } + + chai.assert.isUndefined(actualError); + + packageService = new PackageService("https://test-endpoint"); + try { + const result = await packageService.sideLoading( + "test-token", + "./tests/component/m365/success.zip" + ); + chai.assert.equal(result[0], "test-title-id"); + chai.assert.equal(result[1], "test-app-id"); } catch (error: any) { actualError = error; } @@ -452,6 +494,7 @@ describe("Package Service", () => { }, }; axiosPostResponses["/dev/v1/users/packages"] = new Error("test-post"); + axiosPostResponses["/builder/v1/users/packages"] = new Error("test-post-builder-api"); let packageService = new PackageService("https://test-endpoint"); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); @@ -475,6 +518,26 @@ describe("Package Service", () => { chai.assert.isDefined(actualError); chai.assert.isTrue(actualError?.message.includes("test-post")); + + packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ + copilotAgents: { + declarativeAgents: [ + { + id: "declarativeAgent", + file: "declarativeAgent.json", + }, + ], + }, + } as any); + try { + await packageService.sideLoading("test-token", "test-path"); + } catch (error: any) { + actualError = error; + } + + chai.assert.isDefined(actualError); + chai.assert.isTrue(actualError?.message.includes("test-post-builder-api")); }); it("sideLoading throws expected reponse error", async () => { diff --git a/packages/fx-core/tests/component/m365/success.zip b/packages/fx-core/tests/component/m365/success.zip new file mode 100644 index 0000000000000000000000000000000000000000..2133c113938848d5a9730192b847cc10b1aa76eb GIT binary patch literal 1839 zcmZ`)2~d;Q7XBe5>{LaOB_s@uf*^}jK!}L1tqUfy7?ywt1YQVhf?x{Bk|J0uOF$&7 z0#6<&0wp3vKm-bi#@8$&7-R`yQ;GyaD1;@3H2QpRI!=4cN9oz zHvp)p0Ab$lKE~8b3*(9aV5S5BssI3lkfKPjmN7)ag2r;(+?)LA;Gm$%wbd9^#h1#* zi_23vyZ;X~w;k&{|kS1tf!Sud_wSp-Gfi`Mv$v*fz z8P;^NsE_K2IO}E?I%V4Sg!)*e(L%cd6U3Esu_6O8Kt!CY$8G1G) zyOZ?_Uwq>p$P|d^q(mfTo7o)YB;6zq3_o5ZP+j($N_@9f>?stPj9$1dD~d%}OGdUQ z-4-qeg%!F=jVLD@ZltjUT#!Vs6rFw4*GG$MkkDLBnG?!*kCs{Me7DNRt|d5u6m2LK za`+2w%{6@Q*A^3z*_;bqNZJJY5XZ5j-qc*M;(xMxbUv3GX65#kCLYTSF)5B4&uDrk zxcVz(B1k9QN3~a(Qv4jAFd*p5PnP7eujVLs2Ndz?9q;?bRwQCtd#5tnzgTn%Rw;}w zh=T^ukxtT-5}Dol#tQRM{Kc&Fc<=YF9pSWzNnPZ+w40rg$8w~c(1OM+oW*)yu5g)l z{b8~s+G<*_TJteBo9|_6x9V5$Ac`%iKom|T=ggFLmM1&1|JBuk?-(p4#u z-#e~wA~|CPlXQ>CH8;FG_aC?4i!7Q^N%Yhy7hYrM068$CRHs?R^srOtPWPzG?0b{F z)51vBg$M6KDJ!94{WfzoS``seM(QRMBP^=W#K2LmnVxMYYG$q7mdA!FY|2VCo%Oi9 zB;+D_tJP`EIWWbx^l+1K^Dc5Y;67)Y(_TXi-Dtfa!w9&;oKyrkXp7F&cc$=zd+hJZ z{Fu(N#F2wsw>t4$9Kn0j8_VB^fdVL{N001R00BFi35RD^- zhvMTdSw_T>h*vRZN&1I&bk6x;%V54%3g7N*HH)r>zVwdBN72%Uk?O=@_=@gnKM_CD zz$(GxWlRq;RmftNiTLW%lz!#e$h-z@e(>V09=whhC`;%qpvOFFSd?tBG9E;SCSlhuub^)Z)6!}w_~e7 z=A!eG?}dnZWA|2r4C4puoiu-Qs2ziRH@XR}95H0ZskLv^Hgn9Q?mV>PtPZ}kjmrg7 ze;*8eI-9w8C##0PLD^MX{i<;Fm$VoV<0orN^gLM#pE_;>9k58Wnm1VfN5oSH(|BG| zT%tBb+Bh@{-b*=FLn5^gs0416>-%3On`}7Mhpb`;@0I_!GLn(ho>On>3F6E&q~?>R z<7McdgAGObbGps}fme`YRKHp;Y*yUrLY72Fznx;(2cyRCf90vsSV0YxB)l^eg&kN9 z8wy8PIlRuxbycjQ{5mD&r8T5f1sgrYmxmpTeEy`}PyZmEVyAPCetJ(EBBn{E{Hant zHOup~J2R{D3fYvKy4*27ZZT0t*Y@!ShHs?dLY&ttzx6ii<_z{9s=gvxJbbDP0)`b56U7@T=yDq; z*NJndFcnrooQFgoO$TF!5w@k)*yU}-nVTmj8}Gd_F%Bq^6-=syX9tQ?^knTytU27Z z*g7ty1-HjR^mhJhSl@YZSs-`2_~G$#AID5vi?o?Xli-#a59wZ^U19u@uOP=Vovd~Y z{esvvRghfar1F5BD<=dmCHy$aktvv&l33$ z{b~JwvV5Sy0Xgk2^@sRGF_I$)^qCsffDd5+PyloRucREUcfhE^|8ULw?Z&*^|NK>- eC+*_}9}f$;;Qy3DcN7@%Ar6vnjvUW?^!@ Date: Thu, 16 Jan 2025 15:27:52 +0800 Subject: [PATCH 12/17] fix: ut --- packages/fx-core/tests/component/driver/m365/acquire.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 111f26cd6cf..64151727a22 100644 --- a/packages/fx-core/tests/component/driver/m365/acquire.test.ts +++ b/packages/fx-core/tests/component/driver/m365/acquire.test.ts @@ -143,7 +143,9 @@ describe("teamsApp/extendToM365", async () => { ["appId", "MY_APP_ID"], ]); - sinon.stub(PackageService.prototype, "sideLoading").resolves(["test-title-id", "test-app-id"]); + 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); From 7bb01c5c2b5621f64ba89515505bfaeb2ea91865 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Fri, 17 Jan 2025 09:40:44 +0800 Subject: [PATCH 13/17] fix: ut --- .../component/m365/packageService.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index 7a8d6cd9be3..45be2a8f80e 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -450,7 +450,8 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); - packageService = new PackageService("https://test-endpoint", logger); + // without logger + packageService = new PackageService("https://test-endpoint"); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ copilotAgents: { declarativeAgents: [ @@ -510,6 +511,7 @@ describe("Package Service", () => { packageService = new PackageService("https://test-endpoint", logger); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); + actualError = undefined; try { await packageService.sideLoading("test-token", "test-path"); } catch (error: any) { @@ -530,6 +532,7 @@ describe("Package Service", () => { ], }, } as any); + actualError = undefined; try { await packageService.sideLoading("test-token", "test-path"); } catch (error: any) { @@ -538,6 +541,22 @@ describe("Package Service", () => { chai.assert.isDefined(actualError); chai.assert.isTrue(actualError?.message.includes("test-post-builder-api")); + + packageService = new PackageService("https://test-endpoint", logger); + sandbox + .stub(packageService, "getManifestFromZip" as keyof PackageService) + .returns(undefined as any); + actualError = undefined; + try { + await packageService.sideLoading("test-token", "test-path"); + } catch (error: any) { + actualError = error; + } + + chai.assert.isDefined(actualError); + chai.assert.isTrue( + actualError?.message.includes("Invalid app package zip. manifest.json is missing") + ); }); it("sideLoading throws expected reponse error", async () => { @@ -571,6 +590,7 @@ describe("Package Service", () => { packageService = new PackageService("https://test-endpoint", logger); sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({} as any); + actualError = undefined; try { await packageService.sideLoading("test-token", "test-path"); } catch (error: any) { From 4f8ae5507203be671d45ebf1f4e40267d1b8b582 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Fri, 17 Jan 2025 14:32:38 +0800 Subject: [PATCH 14/17] fix: ut --- .../tests/component/m365/packageService.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index 45be2a8f80e..a06b95c02e6 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -837,7 +837,7 @@ describe("Package Service", () => { }; axiosDeleteResponses["/catalog/v1/users/acquisitions/test-title-id"] = new Error("test-delete"); axiosDeleteResponses["/builder/v1/users/titles/test-title-id"] = {}; - const packageService = new PackageService("https://test-endpoint"); + let packageService = new PackageService("https://test-endpoint"); let actualError: Error | undefined; try { await packageService.unacquire("test-token", "test-title-id"); @@ -845,6 +845,14 @@ describe("Package Service", () => { actualError = error; } chai.assert.isUndefined(actualError); + + packageService = new PackageService("https://test-endpoint", logger); + try { + await packageService.unacquire("test-token", "test-title-id"); + } catch (error: any) { + actualError = error; + } + chai.assert.isUndefined(actualError); }); it("unacquire throws expected error", async () => { axiosGetResponses["/config/v1/environment"] = { From 31ed3e89e796cf5b6793e718bbbee4654907c80a Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Fri, 17 Jan 2025 14:55:16 +0800 Subject: [PATCH 15/17] fix: ut --- .../component/m365/packageService.test.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index a06b95c02e6..2897c53f005 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -862,7 +862,7 @@ describe("Package Service", () => { }; axiosDeleteResponses["/catalog/v1/users/acquisitions/test-title-id"] = new Error("test-delete"); - const packageService = new PackageService("https://test-endpoint"); + let packageService = new PackageService("https://test-endpoint"); let actualError: Error | undefined; try { await packageService.unacquire("test-token", "test-title-id"); @@ -872,6 +872,17 @@ describe("Package Service", () => { chai.assert.isDefined(actualError); chai.assert.isTrue(actualError?.message.includes("test-delete")); + + packageService = new PackageService("https://test-endpoint", logger); + actualError = undefined; + try { + await packageService.unacquire("test-token", "test-title-id"); + } catch (error: any) { + actualError = error; + } + + chai.assert.isDefined(actualError); + chai.assert.isTrue(actualError?.message.includes("test-delete")); }); it("unacquire throws expected response error", async () => { @@ -885,6 +896,7 @@ describe("Package Service", () => { data: {}, }; axiosDeleteResponses["/catalog/v1/users/acquisitions/test-title-id"] = expectedError; + axiosDeleteResponses["/builder/v1/users/titles/test-title-id"] = expectedError; const packageService = new PackageService("https://test-endpoint"); let actualError: any; @@ -1262,4 +1274,20 @@ describe("Package Service", () => { const shareLink = await packageService.getShareLink("test-token", "test-title-id"); chai.assert.equal(shareLink, "https://test-share-link"); }); + + it("get share link - failure", async () => { + axiosGetResponses["/config/v1/environment"] = { + data: { + titlesServiceUrl: "https://test-url", + }, + }; + const packageService = new PackageService("https://test-endpoint"); + let actualError: boolean | undefined; + try { + const shareLink = await packageService.getShareLink("test-token", "test-title-id"); + } catch (error: any) { + actualError = error; + } + chai.assert.isDefined(actualError); + }); }); From 89668381f0ab58ebd853fe6b82bd24954dc68213 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Fri, 17 Jan 2025 17:03:27 +0800 Subject: [PATCH 16/17] fix: ut --- .../component/m365/packageService.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index 2897c53f005..2c3e7cc6f18 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -488,6 +488,40 @@ describe("Package Service", () => { chai.assert.isUndefined(actualError); }); + it("sideloading throws error in get status", async () => { + axiosGetResponses["/config/v1/environment"] = { + data: { + titlesServiceUrl: "https://test-url", + }, + }; + + axiosPostResponses["/builder/v1/users/packages"] = { + data: { + statusId: "test-status-id-builder-api", + titlePreview: { + titleId: "test-title-id-preview-builder-api", + }, + }, + }; + let actualError: Error | undefined; + const packageService = new PackageService("https://test-endpoint", logger); + sandbox.stub(packageService, "getManifestFromZip" as keyof PackageService).returns({ + copilotAgents: { + declarativeAgents: [ + { + id: "declarativeAgent", + file: "declarativeAgent.json", + }, + ], + }, + } as any); + try { + const result = await packageService.sideLoading("test-token", "test-path", AppScope.Shared); + } catch (error: any) { + actualError = error; + } + chai.assert.isDefined(actualError); + }); it("sideLoading throws expected error", async () => { axiosGetResponses["/config/v1/environment"] = { data: { From 543105696d68d7373f7cc889f77115c955969ce2 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Fri, 17 Jan 2025 17:33:13 +0800 Subject: [PATCH 17/17] fix: ut --- .../component/m365/packageService.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/fx-core/tests/component/m365/packageService.test.ts b/packages/fx-core/tests/component/m365/packageService.test.ts index 2c3e7cc6f18..0013cbf3194 100644 --- a/packages/fx-core/tests/component/m365/packageService.test.ts +++ b/packages/fx-core/tests/component/m365/packageService.test.ts @@ -521,6 +521,26 @@ describe("Package Service", () => { actualError = error; } chai.assert.isDefined(actualError); + + const expectedError = new Error("test-status") as any; + expectedError.response = { + data: { + foo: "bar", + }, + headers: { + traceresponse: "tracing-id", + }, + }; + axiosGetResponses["/builder/v1/users/packages/status/test-status-id-builder-api"] = + expectedError; + actualError = undefined; + try { + const result = await packageService.sideLoading("test-token", "test-path", AppScope.Shared); + } catch (error: any) { + actualError = error; + } + chai.assert.isDefined(actualError); + chai.assert.isTrue(actualError?.message.includes("test-status")); }); it("sideLoading throws expected error", async () => { axiosGetResponses["/config/v1/environment"] = {