diff --git a/packages/fx-core/package.json b/packages/fx-core/package.json index d95e2452315..6fff71ef395 100644 --- a/packages/fx-core/package.json +++ b/packages/fx-core/package.json @@ -79,6 +79,7 @@ "test:metadataUtil": "nyc mocha \"tests/component/util/metadataUtil.test.ts\"", "test:migrate": "nyc mocha \"tests/component/migrate.test.ts\"", "test:retry": "npx mocha \"tests/core/middleware/retry.test.ts\"", + "test:sharePointEmbeddedContainerType": "nyc mocha \"tests/component/driver/sharePointEmbeddedContainerType/*.test.ts\"", "clean": "rm -rf build", "prebuild": "npm run gen:cli", "build": "tsc -p ./ --incremental", diff --git a/packages/fx-core/resource/package.nls.json b/packages/fx-core/resource/package.nls.json index 1c901b9e680..acb4cb7db8a 100644 --- a/packages/fx-core/resource/package.nls.json +++ b/packages/fx-core/resource/package.nls.json @@ -940,6 +940,14 @@ "driver.devChannel.install.description": "Install app into sandboxed channel.", "driver.devChannel.install.progress.message": "Installing app...", "driver.devChannel.install.summary.exists": "App '%s' already installed in team '%s'. It will now be updated to the latest version.", + "driver.sharePointEmbeddedContainerType.description.createContainerType": "Create a SharePoint Embedded Container type", + "driver.sharePointEmbeddedContainerType.progressBar.createContainerTypeTitle": "Creating a SharePoint Embedded container type...", + "driver.sharePointEmbeddedContainerType.log.startExecuteDriver": "Executing action %s", + "driver.sharePointEmbeddedContainerType.log.successExecuteDriver": "Action %s executed successfully", + "driver.sharePointEmbeddedContainerType.log.failExecuteDriver": "Unable to execute action %s. Error message: %s", + "driver.sharePointEmbeddedContainerType.log.startCreateContainerType": "Environment variable %s does not exist, creating a new SharePointEmbedded container type...", + "driver.sharePointEmbeddedContainerType.log.successCreateContainerType": "Created SharePoint Embedded container type with container type id %s", + "driver.sharePointEmbeddedContainerType.log.skipCreateCreateContainerType": "Environment variable %s already exist, skipping new SharePoint Embedded container type creation step.", "error.installApp.outsideSandbox": "Unable to install app outside sandboxed Team. Please update TEAM_ID and CHANNEL_ID.", "error.yaml.InvalidYamlSchemaError": "Unable to parse yaml file: %s. Please open the yaml file for detailed errors.", "error.yaml.InvalidYamlSchemaErrorWithReason": "Unable to parse yaml file: %s. Reason: %s Please review the yaml file or upgrade to the latest Microsoft 365 Agents Toolkit.", diff --git a/packages/fx-core/resource/yaml-schema/v1.9/yaml.schema.json b/packages/fx-core/resource/yaml-schema/v1.9/yaml.schema.json index 4d779fadced..545a9514b60 100644 --- a/packages/fx-core/resource/yaml-schema/v1.9/yaml.schema.json +++ b/packages/fx-core/resource/yaml-schema/v1.9/yaml.schema.json @@ -84,7 +84,8 @@ { "$ref": "#/definitions/shareToOthers"}, { "$ref": "#/definitions/devChannelCreate"}, { "$ref": "#/definitions/devChannelInstallApp"}, - { "$ref": "#/definitions/typeSpecCompile"} + { "$ref": "#/definitions/typeSpecCompile"}, + { "$ref": "#/definitions/sharePointEmbeddedContainerTypeCreate"} ] } }, @@ -2102,6 +2103,65 @@ } } } + }, + "sharePointEmbeddedContainerTypeCreate": { + "type": "object", + "additionalProperties": false, + "description": "Create a new SharePointEmbedded container type.", + "required": ["uses", "with", "writeToEnvironmentFile"], + "properties": { + "name": { + "type": "string", + "description": "An optional name of this action." + }, + "env": { + "type": "object", + "description": "Define environment variables for this action.", + "additionalProperties": { + "type": "string" + } + }, + "uses": { + "type": "string", + "description": "This action will create a new SPE container type. TODO add more description", + "const": "sharePointEmbeddedContainerType/create" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["owningApplicationId", "billingClassification", "name", "discoverable"], + "properties": { + "owningApplicationId": { + "type": "string", + "description": "application Id for the owning application for the SPE container type" + }, + "billingClassification": { + "type": "string", + "description": "The billingClassification for the container type. Can be standard, trial, dtc" + }, + "name": { + "type": "string", + "description": "The name for the container type" + }, + "discoverable": { + "type": "boolean", + "description": "determines whether the drive items in the container of the container type are discoverable" + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["containerTypeId"], + "properties": { + "containerTypeId": { + "$ref": "#/definitions/envVarName" + } + } + } + } } } } diff --git a/packages/fx-core/resource/yaml-schema/yaml.schema.json b/packages/fx-core/resource/yaml-schema/yaml.schema.json index 4d779fadced..545a9514b60 100644 --- a/packages/fx-core/resource/yaml-schema/yaml.schema.json +++ b/packages/fx-core/resource/yaml-schema/yaml.schema.json @@ -84,7 +84,8 @@ { "$ref": "#/definitions/shareToOthers"}, { "$ref": "#/definitions/devChannelCreate"}, { "$ref": "#/definitions/devChannelInstallApp"}, - { "$ref": "#/definitions/typeSpecCompile"} + { "$ref": "#/definitions/typeSpecCompile"}, + { "$ref": "#/definitions/sharePointEmbeddedContainerTypeCreate"} ] } }, @@ -2102,6 +2103,65 @@ } } } + }, + "sharePointEmbeddedContainerTypeCreate": { + "type": "object", + "additionalProperties": false, + "description": "Create a new SharePointEmbedded container type.", + "required": ["uses", "with", "writeToEnvironmentFile"], + "properties": { + "name": { + "type": "string", + "description": "An optional name of this action." + }, + "env": { + "type": "object", + "description": "Define environment variables for this action.", + "additionalProperties": { + "type": "string" + } + }, + "uses": { + "type": "string", + "description": "This action will create a new SPE container type. TODO add more description", + "const": "sharePointEmbeddedContainerType/create" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["owningApplicationId", "billingClassification", "name", "discoverable"], + "properties": { + "owningApplicationId": { + "type": "string", + "description": "application Id for the owning application for the SPE container type" + }, + "billingClassification": { + "type": "string", + "description": "The billingClassification for the container type. Can be standard, trial, dtc" + }, + "name": { + "type": "string", + "description": "The name for the container type" + }, + "discoverable": { + "type": "boolean", + "description": "determines whether the drive items in the container of the container type are discoverable" + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["containerTypeId"], + "properties": { + "containerTypeId": { + "$ref": "#/definitions/envVarName" + } + } + } + } } } } diff --git a/packages/fx-core/src/component/driver/index.ts b/packages/fx-core/src/component/driver/index.ts index 7661efbdc18..097b64508a1 100644 --- a/packages/fx-core/src/component/driver/index.ts +++ b/packages/fx-core/src/component/driver/index.ts @@ -40,3 +40,4 @@ import "./oauth/create"; import "./oauth/update"; import "./share/shareToOthers"; import "./typeSpec/compile"; +import "./sharePointEmbeddedContainerType/create"; diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/create.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/create.ts new file mode 100644 index 00000000000..0cdf4c4b6db --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/create.ts @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { hooks } from "@feathersjs/hooks/lib"; +import { FxError, SystemError, UserError, err, ok, Result } from "@microsoft/teamsfx-api"; +import axios from "axios"; +import { Service } from "typedi"; +import { GraphScopes } from "../../../common/constants"; +import { getLocalizedString } from "../../../common/localizeUtils"; +import { CreateSPEContainerTypeArgs } from "./interface/createSPEContainerTypeArgs"; +import { CreateSPEContainerTypeOutput, OutputKeys } from "./interface/createSPEContainerTypeOutput"; +import { SPContainerTypeBillingClassification } from "./interface/sharePointEmbeddedContainerType"; +import { DriverContext } from "../interface/commonArgs"; +import { ExecutionResult, StepDriver } from "../interface/stepDriver"; +import { addStartAndEndTelemetry } from "../middleware/addStartAndEndTelemetry"; +import { loadStateFromEnv, mapStateToEnv } from "../util/utils"; +import { logMessageKeys, descriptionMessageKeys, progressBarKeys } from "./utility/constants"; +import { WrapDriverContext } from "../util/wrapUtil"; +import { SPEContainerTypeAppClient } from "./utility/speContainerTypeAppClient"; +import { OutputEnvironmentVariableUndefinedError } from "../error/outputEnvironmentVariableUndefinedError"; +import { HttpServerError, InvalidActionInputError, assembleError } from "../../../error/common"; + +const actionName = "sharePointEmbeddedContainerType/create"; // DO NOT MODIFY THE NAME + +@Service(actionName) // DO NOT MODIFY THE SERVICE NAME +export class CreateSharePointEmbeddedContainerTypeDriver implements StepDriver { + // TODO localize these strings + description = getLocalizedString(descriptionMessageKeys.createContainerType); + readonly progressTitle = getLocalizedString(progressBarKeys.progressBarTitle); + + public async execute( + args: CreateSPEContainerTypeArgs, + context: DriverContext, + outputEnvVarNames?: Map + ): Promise { + const wrapDriverContext = new WrapDriverContext(context, actionName, actionName); + return await this.executeInternal(args, wrapDriverContext, outputEnvVarNames); + } + + @hooks([addStartAndEndTelemetry(actionName, actionName)]) + private async executeInternal( + args: CreateSPEContainerTypeArgs, + context: WrapDriverContext, + outputEnvVarNames?: Map + ): Promise { + let outputs: Map = new Map(); + const summaries: string[] = []; + if (!outputEnvVarNames) { + const error = new OutputEnvironmentVariableUndefinedError(actionName); + context.logProvider?.error( + getLocalizedString(logMessageKeys.failExecuteDriver, actionName, error.displayMessage) + ); + return { + result: err(error), + summaries: summaries, + }; + } + + const speContainerTypeState: CreateSPEContainerTypeOutput = loadStateFromEnv(outputEnvVarNames); + + try { + context.logProvider?.info(getLocalizedString(logMessageKeys.startExecuteDriver, actionName)); + this.validateArgs(args); + + if (!speContainerTypeState.containerTypeId) { + context.logProvider?.info( + getLocalizedString( + logMessageKeys.startCreateContainerType, + outputEnvVarNames.get(OutputKeys.containerTypeId) + ) + ); + + const speContainerTypeClient = new SPEContainerTypeAppClient( + context.m365TokenProvider, + context.logProvider + ); + + const speContainerType = await speContainerTypeClient.createSPEContainerType( + args.owningApplicationId, + args.billingClassification, + args.name, + args.discoverable + ); + + speContainerTypeState.containerTypeId = speContainerType.id; + outputs = mapStateToEnv(speContainerTypeState, outputEnvVarNames); + + const summary = getLocalizedString( + logMessageKeys.successCreateContainerType, + speContainerType.id + ); + + context.logProvider?.info(summary); + summaries.push(summary); + } else { + context.logProvider?.info( + getLocalizedString( + logMessageKeys.skipCreateContainerType, + outputEnvVarNames.get(OutputKeys.containerTypeId) + ) + ); + } + return { + result: ok(outputs), + summaries: summaries, + }; + } catch (error) { + if (error instanceof UserError || error instanceof SystemError) { + context.logProvider?.error( + getLocalizedString(logMessageKeys.failExecuteDriver, actionName, error.displayMessage) + ); + return { + result: err(error), + summaries: summaries, + }; + } + + if (axios.isAxiosError(error)) { + const message = JSON.stringify(error.response!.data); + context.logProvider?.error( + getLocalizedString(logMessageKeys.failExecuteDriver, actionName, message) + ); + return { + result: err(new HttpServerError(error, actionName, message)), + summaries: summaries, + }; + } + const message = JSON.stringify(error); + context.logProvider?.error( + getLocalizedString(logMessageKeys.failExecuteDriver, actionName, message) + ); + + return { + result: err(assembleError(error as Error, actionName)), + summaries: summaries, + }; + } + } + + private validateArgs(args: CreateSPEContainerTypeArgs): void { + const invalidParameters: string[] = []; + if (typeof args.name !== "string" || !args.name) { + invalidParameters.push("name"); + } + + if (typeof args.owningApplicationId !== "string" || !args.owningApplicationId) { + invalidParameters.push("owningApplicationId"); + } + + if ( + (args.billingClassification && typeof args.billingClassification !== "string") || + !Object.values(SPContainerTypeBillingClassification).includes(args.billingClassification) + ) { + invalidParameters.push("billingClassification"); + } + + if (typeof args.discoverable !== "boolean") { + invalidParameters.push("discoverable"); + } + + if (invalidParameters.length > 0) { + throw new InvalidActionInputError(actionName, invalidParameters); + } + } +} diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeArgs.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeArgs.ts new file mode 100644 index 00000000000..c46d7a6efbd --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeArgs.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SPContainerTypeBillingClassification } from "./sharePointEmbeddedContainerType"; + +export interface CreateSPEContainerTypeArgs { + owningApplicationId: string; // The application ID of the Microsoft Entra app that will own the container type; + billingClassification: SPContainerTypeBillingClassification; // Billing classification for the container type; + name?: string; // The name for the container type; + discoverable?: boolean; // Whether the container type is discoverable by M365 apps, including Copilot; +} diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeOutput.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeOutput.ts new file mode 100644 index 00000000000..0a259091ee3 --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/createSPEContainerTypeOutput.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export type CreateSPEContainerTypeOutput = { + containerTypeId?: string; +}; + +// The const is used to reference the property name in CreateAadAppOutput. When renaming the properties in CreateAadAppOutput, you need to update the const as well. +export const OutputKeys = { + containerTypeId: "containerTypeId", +}; diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/sharePointEmbeddedContainerType.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/sharePointEmbeddedContainerType.ts new file mode 100644 index 00000000000..0906f1cfd2b --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/interface/sharePointEmbeddedContainerType.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export enum SPContainerTypeBillingClassification { + standard = "standard", // standard container type. + directToCustomer = "directToCustomer", // direct to customer container type. + trial = "trial", // trial container type. +} + +export enum SPContainerBillingStatus { + invalid, + valid, +} + +export enum SPContainerTypeSettingsOverride { + none, + isItemVersioningEnabled, + itemMajorVersionLimit, + maxStoragePerContainerInBytes, + unknownFutureValue, +} + +export interface ISharePointEmbeddedContainerType { + id?: string; // container type ID; + name?: string; // container type name; + owningAppId?: string; // The application ID of the Microsoft Entra app that owns the container type; + billingClassification?: SPContainerTypeBillingClassification; // Billing classification for the container type; + billingStatus?: SPContainerBillingStatus; // Billing status for the container type; + createdDateTime?: Date; // The date and time when the container type was created; + expirationDateTime?: Date; // The date and time when the container type will expire (only if it's trial container type); + settings?: SharePointEmbeddedContainerTypeSettings; // Settings for the container type; + etag?: string; //Used in update for optimistic concurrency control. +} + +export interface SharePointEmbeddedContainerTypeSettings { + sharingCapability?: string; // sharing capabilities permitted for containers. + urlTemplate?: string; // Pattern used to redirect files + isDiscoverabilityEnabled?: boolean; // Enables, disables surface of items from containers in experiences like my activity or M356. Optional. + isSearchEnabled?: boolean; // If search is enabled. Optional. + isItemVersioningEnabled?: boolean; // Controls item versioning. Optional. + itemMajorVersionLimit?: number; // Maximum number of versions. Versioning must be enabled. Optional. + maxStoragePerContainerInBytes?: number; // Controls maximum storage in bytes. Optional. + isSharingRestricted?: boolean; // Controls if sharing is restricted. Optional. + consumingTenantOverridables?: SPContainerTypeSettingsOverride; // Settings that can be overwritten in the consuming tenant, comma separated. Optional. +} diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/constants.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/constants.ts new file mode 100644 index 00000000000..e9e619faa7c --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/constants.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export const logMessageKeys = { + startExecuteDriver: "driver.sharePointEmbeddedContainerType.log.startExecuteDriver", + successExecuteDriver: "driver.sharePointEmbeddedContainerType.log.successExecuteDriver", + failExecuteDriver: "driver.sharePointEmbeddedContainerType.log.failExecuteDriver", + startCreateContainerType: "driver.sharePointEmbeddedContainerType.log.startCreateContainerType", + successCreateContainerType: + "driver.sharePointEmbeddedContainerType.log.successCreateContainerType", + skipCreateContainerType: + "driver.sharePointEmbeddedContainerType.log.skipCreateCreateContainerType", +}; + +export const descriptionMessageKeys = { + createContainerType: "driver.sharePointEmbeddedContainerType.description.createContainerType", +}; + +export const progressBarKeys = { + progressBarTitle: "driver.sharePointEmbeddedContainerType.progressBar.createContainerTypeTitle", +}; diff --git a/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/speContainerTypeAppClient.ts b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/speContainerTypeAppClient.ts new file mode 100644 index 00000000000..dd34de31d86 --- /dev/null +++ b/packages/fx-core/src/component/driver/sharePointEmbeddedContainerType/utility/speContainerTypeAppClient.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { hooks } from "@feathersjs/hooks/lib"; +import { LogProvider, M365TokenProvider } from "@microsoft/teamsfx-api"; +import axios, { AxiosError, AxiosInstance, AxiosRequestHeaders } from "axios"; +import axiosRetry, { IAxiosRetryConfig } from "axios-retry"; +import { GraphScopes } from "../../../../common/constants"; +import { getLocalizedString } from "../../../../common/localizeUtils"; +import { ErrorContextMW } from "../../../../common/globalVars"; +import { + ISharePointEmbeddedContainerType, + SharePointEmbeddedContainerTypeSettings, + SPContainerTypeBillingClassification, +} from "../interface/sharePointEmbeddedContainerType"; + +// Missing this part will cause build failure when adding 'axios-retry' in AxiosRequestConfig +declare module "axios" { + export interface AxiosRequestConfig { + "axios-retry"?: IAxiosRetryConfig; + } +} + +export class SPEContainerTypeAppClient { + private readonly baseUrl: string = "https://graph.microsoft.com/beta/storage/fileStorage"; + private readonly tokenProvider: M365TokenProvider; + private readonly logProvider: LogProvider | undefined; + private readonly retryNumber: number = 5; + private readonly axios: AxiosInstance; + + constructor(tokenProvider: M365TokenProvider, logProvider?: LogProvider) { + this.tokenProvider = tokenProvider; + this.logProvider = logProvider; + + // Create axios instance which sets authorization header automatically before each MS Graph request + this.axios = axios.create({ + baseURL: this.baseUrl, + }); + + this.axios.interceptors.request.use(async (config) => { + this.logProvider?.debug( + getLocalizedString("core.common.SendingApiRequest", config.url, JSON.stringify(config.data)) + ); + + const tokenResponse = await this.tokenProvider.getAccessToken({ scopes: GraphScopes }); + if (tokenResponse.isErr()) { + throw tokenResponse.error; + } + + const token = tokenResponse.value; + // harcode token for now + + if (!config.headers) { + config.headers = {} as AxiosRequestHeaders; + } + config.headers["Authorization"] = `Bearer ${token}`; + + return config; + }); + + this.axios.interceptors.response.use((response) => { + this.logProvider?.debug( + getLocalizedString("core.common.ReceiveApiResponse", JSON.stringify(response.data)) + ); + return response; + }); + + // Add retry logic. Retry post request may result in creating additional resources but should be fine in Microsoft Entra driver. + axiosRetry(this.axios, { + retries: this.retryNumber, + retryDelay: axiosRetry.exponentialDelay, // exponetial delay time: Math.pow(2, retryNumber) * 100 + retryCondition: (error) => + axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error), // retry when there's network error or 5xx error + }); + } + + @hooks([ErrorContextMW({ source: "Graph", component: "SharePointEmbeddedCTAppClient" })]) + public async createSPEContainerType( + owningApplicationId: string, + billingClassification: string, + name?: string, + discoverable?: boolean + ): Promise { + const speContainerTypeSettings: SharePointEmbeddedContainerTypeSettings = { + isDiscoverabilityEnabled: discoverable, + }; + + const requestBody: ISharePointEmbeddedContainerType = { + name: name, + owningAppId: owningApplicationId, + billingClassification: billingClassification as SPContainerTypeBillingClassification, + settings: speContainerTypeSettings, + }; + + const response = await this.axios.post("containerTypes", requestBody); + return response.data; + } +} diff --git a/packages/fx-core/tests/component/driver/sharePointEmbeddedContainerType/create.test.ts b/packages/fx-core/tests/component/driver/sharePointEmbeddedContainerType/create.test.ts new file mode 100644 index 00000000000..544c76d147f --- /dev/null +++ b/packages/fx-core/tests/component/driver/sharePointEmbeddedContainerType/create.test.ts @@ -0,0 +1,305 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "mocha"; +import * as sinon from "sinon"; +import mockedEnv, { RestoreFn } from "mocked-env"; +import * as chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { CreateSharePointEmbeddedContainerTypeDriver } from "../../../../src/component/driver/sharePointEmbeddedContainerType/create"; +import { SPEContainerTypeAppClient } from "../../../../src/component/driver/sharePointEmbeddedContainerType/utility/speContainerTypeAppClient"; +import { + SPContainerBillingStatus, + SPContainerTypeSettingsOverride, + ISharePointEmbeddedContainerType, + SharePointEmbeddedContainerTypeSettings, + SPContainerTypeBillingClassification, +} from "../../../../src/component/driver/sharePointEmbeddedContainerType/interface/sharePointEmbeddedContainerType"; +import { MockedM365Provider } from "../../../core/utils"; +import { + MockedLogProvider, + MockedTelemetryReporter, + MockedUserInteraction, +} from "../../../plugins/solution/util"; +import { HttpServerError, InvalidActionInputError } from "../../../../src/error"; +import { OutputEnvironmentVariableUndefinedError } from "../../../../src/component/driver/error/outputEnvironmentVariableUndefinedError"; +chai.use(chaiAsPromised); +const expect = chai.expect; + +const outputKeys = { + containerTypeId: "ContainerTypeId", +}; + +const outputEnvVarNames = new Map(Object.entries(outputKeys)); + +describe("sharePointEmbeddedContainerTypeCreate", async () => { + const speContainerTypeDriver = new CreateSharePointEmbeddedContainerTypeDriver(); + + const mockedDriverContext: any = { + m365TokenProvider: new MockedM365Provider(), + ui: new MockedUserInteraction(), + projectPath: "test", + }; + + const expectedContainerTypeId = "247e5bd2-edc2-4ee7-a9a3-0064eaf77ccb"; + const containerTypeName = "TestContainerType"; + const containerTypeOwningAppId = "00000000-0000-0000-0000-000000000001"; + const containerTypeBillingClassification: SPContainerTypeBillingClassification = + SPContainerTypeBillingClassification.standard; + const containerTypeBillingStatus: SPContainerBillingStatus = SPContainerBillingStatus.valid; + const containerTypeCreatedDateTime: Date = new Date("2024-06-24T12:00:00Z"); + const containerTypeExpirationDateTime: Date = new Date("2025-06-24T12:00:00Z"); + const containerTypeSettings: SharePointEmbeddedContainerTypeSettings = { + sharingCapability: "external", + urlTemplate: "https://example.com/{id}", + isDiscoverabilityEnabled: true, + isSearchEnabled: true, + isItemVersioningEnabled: true, + itemMajorVersionLimit: 10, + maxStoragePerContainerInBytes: 1000000000, + isSharingRestricted: false, + consumingTenantOverridables: SPContainerTypeSettingsOverride.none, + }; // settings for the container type + const containerTypeEtag = "eTag"; // etag for optimistic concurrency control + + const testContainerType: ISharePointEmbeddedContainerType = { + id: expectedContainerTypeId, + name: containerTypeName, + owningAppId: containerTypeOwningAppId, + billingClassification: containerTypeBillingClassification, + billingStatus: containerTypeBillingStatus, + createdDateTime: containerTypeCreatedDateTime, + expirationDateTime: containerTypeExpirationDateTime, + settings: containerTypeSettings, + etag: containerTypeEtag, + }; + + let envRestore: RestoreFn | undefined; + + afterEach(() => { + sinon.restore(); + if (envRestore) { + envRestore(); + envRestore = undefined; + } + }); + + it("should return error if arguments are invalid", async () => { + // name is invalid + let args: any = { + name: 123, + owningApplicationId: "app-id", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: true, + }; + let result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // owningApplicationId is invalid + args = { + name: "test", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: true, + owningApplicationId: 123, + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // billingClassification invalid + args = { + name: "test", + owningApplicationId: "app-id", + billingClassification: "invalid", + discoverable: true, + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // discoverable invalid + args = { + name: "test", + owningApplicationId: "app-id", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: "notBoolean", + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + }); + + it("should return error if arguments are missing", async () => { + // name missing + let args: any = { + owningApplicationId: "app-id", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: true, + }; + let result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // owningApplicationId missing + args = { + name: "test", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: true, + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // billingClassification missing + args = { + name: "test", + owningApplicationId: "app-id", + discoverable: true, + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + + // discoverable missing + args = { + name: "test", + owningApplicationId: "app-id", + billingClassification: SPContainerTypeBillingClassification.standard, + }; + result = await speContainerTypeDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf(InvalidActionInputError); + }); + + it("should return error if outputEnvVarNames is undefined", async () => { + const args = { + name: "test", + owningApplicationId: "app-id", + billingClassification: SPContainerTypeBillingClassification.standard, + discoverable: true, + }; + const result = await speContainerTypeDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()).to.be.instanceOf( + OutputEnvironmentVariableUndefinedError + ); + }); + + it("should create new SPEContainerType with empty .env", async () => { + sinon + .stub(SPEContainerTypeAppClient.prototype, "createSPEContainerType") + .resolves(testContainerType as ISharePointEmbeddedContainerType); + + const args: any = { + name: containerTypeName, + owningApplicationId: containerTypeOwningAppId, + billingClassification: containerTypeBillingClassification, + discoverable: true, + }; + + const result = await speContainerTypeDriver.execute( + args, + mockedDriverContext, + outputEnvVarNames + ); + + expect(result.result.isOk()).to.be.true; + expect(result.result._unsafeUnwrap().get(outputKeys.containerTypeId)).to.equal( + expectedContainerTypeId + ); + }); + + it("should output to specific environment variable based on writeToEnvironmentFile declaration", async () => { + sinon + .stub(SPEContainerTypeAppClient.prototype, "createSPEContainerType") + .resolves(testContainerType as ISharePointEmbeddedContainerType); + + const args: any = { + name: containerTypeName, + owningApplicationId: containerTypeOwningAppId, + billingClassification: containerTypeBillingClassification, + discoverable: true, + }; + + const outputEnvVarNames = new Map( + Object.entries({ + containerTypeId: "MY_CONTAINER_TYPE_ID", + }) + ); + + const result = await speContainerTypeDriver.execute( + args, + mockedDriverContext, + outputEnvVarNames + ); + + expect(result.result.isOk()).to.be.true; + expect(result.result._unsafeUnwrap().get("MY_CONTAINER_TYPE_ID")).to.equal( + expectedContainerTypeId + ); + }); + + it("should use existing SPE Container Type when CONTAINER_TYPE_ID exists", async () => { + sinon + .stub(SPEContainerTypeAppClient.prototype, "createSPEContainerType") + .rejects("createSPEContainerType should not be called"); + + envRestore = mockedEnv({ + [outputKeys.containerTypeId]: "5eb48390-7c1a-48af-be78-fe35cc24e956", + }); + + const args: any = { + name: containerTypeName, + owningApplicationId: containerTypeOwningAppId, + billingClassification: containerTypeBillingClassification, + discoverable: true, + }; + + const result = await speContainerTypeDriver.execute( + args, + mockedDriverContext, + outputEnvVarNames + ); + + expect(result.result.isOk()).to.be.true; + expect(result.result._unsafeUnwrap().size).to.equal(0); + expect(result.summaries.length).to.equal(0); + }); + + it("should throw error when SPEContainerTypeClient fails", async () => { + sinon.stub(SPEContainerTypeAppClient.prototype, "createSPEContainerType").rejects({ + isAxiosError: true, + response: { + status: 500, + data: { + error: { + code: "InternalServerError", + message: "Internal server error", + }, + }, + }, + }); + + const args: any = { + name: containerTypeName, + owningApplicationId: containerTypeOwningAppId, + billingClassification: containerTypeBillingClassification, + discoverable: true, + }; + + const result = await speContainerTypeDriver.execute( + args, + mockedDriverContext, + outputEnvVarNames + ); + + expect(result.result.isErr()).to.be.true; + expect(result.result._unsafeUnwrapErr()) + .is.instanceOf(HttpServerError) + .and.has.property("message") + .and.equals( + 'A http server error occurred while performing the sharePointEmbeddedContainerType/create task. Try again later. The error response is: {"error":{"code":"InternalServerError","message":"Internal server error"}}' + ); + }); +});