diff --git a/packages/tests/src/commonlib/functionValidator.ts b/packages/tests/src/commonlib/functionValidator.ts index eaf27081a1..d18dfedbeb 100644 --- a/packages/tests/src/commonlib/functionValidator.ts +++ b/packages/tests/src/commonlib/functionValidator.ts @@ -8,6 +8,7 @@ import * as chai from "chai"; import glob from "glob"; import path from "path"; import { EnvConstants, PluginId, StateConfigKey } from "./constants"; +// eslint-disable-next-line import/no-cycle import { getResourceGroupNameFromResourceId, getSiteNameFromResourceId, @@ -39,6 +40,7 @@ enum BaseConfig { API_ENDPOINT = "API_ENDPOINT", M365_APPLICATION_ID_URI = "M365_APPLICATION_ID_URI", IDENTITY_ID = "IDENTITY_ID", + WEBSITE_CONTENTSHARE = "WEBSITE_CONTENTSHARE", } enum SQLConfig { @@ -110,14 +112,11 @@ export class FunctionValidator { token as string ); chai.assert.exists(webappSettingsResponse); - const endpoint = - (this.ctx[EnvConstants.FUNCTION_ENDPOINT] as string) ?? - (this.ctx[EnvConstants.FUNCTION_ENDPOINT_2] as string); - chai.assert.equal( - webappSettingsResponse[BaseConfig.API_ENDPOINT], - endpoint - ); + const contentShare = + webappSettingsResponse[BaseConfig.WEBSITE_CONTENTSHARE]; + const functionNameInAzure = contentShare.split("-")[0]; + chai.assert.equal(functionNameInAzure, this.functionAppName); console.log("Successfully validate Function Provision."); } diff --git a/packages/tests/src/e2e/caseFactory.ts b/packages/tests/src/e2e/caseFactory.ts index dfad962e82..ddc9c0771e 100644 --- a/packages/tests/src/e2e/caseFactory.ts +++ b/packages/tests/src/e2e/caseFactory.ts @@ -51,8 +51,10 @@ export abstract class CaseFactory { skipDeploy?: boolean; skipValidate?: boolean; skipPackage?: boolean; + skipErrorMessage?: string; }; - public custimized?: Record; + public customized?: Record; + public processEnv?: NodeJS.ProcessEnv; public constructor( capability: Capability, @@ -74,8 +76,10 @@ export abstract class CaseFactory { skipDeploy?: boolean; skipValidate?: boolean; skipPackage?: boolean; + skipErrorMessage?: string; } = {}, - custimized?: Record + customized?: Record, + processEnv?: NodeJS.ProcessEnv ) { this.capability = capability; this.testPlanCaseId = testPlanCaseId; @@ -83,7 +87,8 @@ export abstract class CaseFactory { this.validate = validate; this.programmingLanguage = programmingLanguage; this.options = options; - this.custimized = custimized; + this.customized = customized; + this.processEnv = processEnv; } public onBefore(): Promise { @@ -103,14 +108,16 @@ export abstract class CaseFactory { testFolder: string, capability: Capability, programmingLanguage?: ProgrammingLanguage, - custimized?: Record + customized?: Record, + processEnv?: NodeJS.ProcessEnv ): Promise { await Executor.createProject( testFolder, appName, capability, programmingLanguage ? programmingLanguage : ProgrammingLanguage.TS, - custimized + customized, + processEnv ); } @@ -126,14 +133,15 @@ export abstract class CaseFactory { validate, programmingLanguage, options, - custimized, + customized, + processEnv, onBefore, onAfter, onAfterCreate, onBeforeProvision, onCreate, } = this; - describe(`template Test: ${capability}`, function () { + describe(`template Test: ${capability} - ${programmingLanguage}`, function () { const testFolder = getTestFolder(); const appName = getUniqueAppName(); const projectPath = path.resolve(testFolder, appName); @@ -153,7 +161,8 @@ export abstract class CaseFactory { testFolder, capability, programmingLanguage, - custimized + customized, + processEnv ); expect(fs.pathExistsSync(projectPath)).to.be.true; @@ -173,7 +182,12 @@ export abstract class CaseFactory { expect(result).to.be.true; process.env["AZURE_RESOURCE_GROUP_NAME"] = appName + "-rg"; - const { success } = await Executor.provision(projectPath); + const { success } = await Executor.provision( + projectPath, + "dev", + true, + options?.skipErrorMessage + ); expect(success).to.be.true; // Validate Provision diff --git a/packages/tests/src/e2e/copilotplugin/CopilotPluginWithApiKeyAuth.tests.ts b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithApiKeyAuth.tests.ts new file mode 100644 index 0000000000..c05c8556c0 --- /dev/null +++ b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithApiKeyAuth.tests.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * @author Yimin Jin + */ + +import { ProgrammingLanguage } from "@microsoft/teamsfx-core"; +import { CopilotPluginCommonTest } from "./copilotPluginCommonTest"; + +class CopilotPluginWithApiKeyAuthCase extends CopilotPluginCommonTest {} +const validateFiles = { + [ProgrammingLanguage.JS]: [ + "appPackage/ai-plugin.json", + "appPackage/manifest.json", + "src/keyGen.js", + ], + [ProgrammingLanguage.TS]: [ + "appPackage/ai-plugin.json", + "appPackage/manifest.json", + "src/keyGen.ts", + ], + [ProgrammingLanguage.CSharp]: [ + "appPackage/ai-plugin.json", + "appPackage/manifest.json", + "GenerateApiKey.ps1", + ], +}; +new CopilotPluginWithApiKeyAuthCase( + 28640069, + "yimin@microsoft.com", + "api-key", + ProgrammingLanguage.JS, + validateFiles[ProgrammingLanguage.JS] +).test(); + +new CopilotPluginWithApiKeyAuthCase( + 28640069, + "yimin@microsoft.com", + "api-key", + ProgrammingLanguage.TS, + validateFiles[ProgrammingLanguage.TS] +).test(); + +new CopilotPluginWithApiKeyAuthCase( + 28640069, + "yimin@microsoft.com", + "api-key", + ProgrammingLanguage.CSharp, + validateFiles[ProgrammingLanguage.CSharp] +).test(); diff --git a/packages/tests/src/e2e/copilotplugin/CopilotPluginWithNoneAuth.tests.ts b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithNoneAuth.tests.ts new file mode 100644 index 0000000000..160f1612c3 --- /dev/null +++ b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithNoneAuth.tests.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * @author Yimin Jin + */ + +import { ProgrammingLanguage } from "@microsoft/teamsfx-core"; +import { CopilotPluginCommonTest } from "./copilotPluginCommonTest"; + +class CopilotPluginWithNoneAuthCase extends CopilotPluginCommonTest {} +const validateFiles = ["appPackage/ai-plugin.json", "appPackage/manifest.json"]; + +new CopilotPluginWithNoneAuthCase( + 27569734, + "yimin@microsoft.com", + "none", + ProgrammingLanguage.JS, + validateFiles +).test(); + +new CopilotPluginWithNoneAuthCase( + 27569734, + "yimin@microsoft.com", + "none", + ProgrammingLanguage.TS, + validateFiles +).test(); + +new CopilotPluginWithNoneAuthCase( + 27569734, + "yimin@microsoft.com", + "none", + ProgrammingLanguage.CSharp, + validateFiles +).test(); diff --git a/packages/tests/src/e2e/copilotplugin/CopilotPluginWithOAuth.tests.ts b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithOAuth.tests.ts new file mode 100644 index 0000000000..af9646eba0 --- /dev/null +++ b/packages/tests/src/e2e/copilotplugin/CopilotPluginWithOAuth.tests.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * @author Hui Miao + */ + +import { ProgrammingLanguage } from "@microsoft/teamsfx-core"; +import { CopilotPluginCommonTest } from "./copilotPluginCommonTest"; + +class CopilotPluginWithOAuthCase extends CopilotPluginCommonTest {} +const validateFiles = [ + "appPackage/ai-plugin.dev.json", + "appPackage/manifest.json", +]; + +new CopilotPluginWithOAuthCase( + 28641204, + "huimiao@microsoft.com", + "oauth", + ProgrammingLanguage.JS, + validateFiles +).test(); + +new CopilotPluginWithOAuthCase( + 28641204, + "huimiao@microsoft.com", + "oauth", + ProgrammingLanguage.TS, + validateFiles +).test(); + +new CopilotPluginWithOAuthCase( + 28641204, + "huimiao@microsoft.com", + "oauth", + ProgrammingLanguage.CSharp, + validateFiles +).test(); diff --git a/packages/tests/src/e2e/copilotplugin/copilotPluginCommonTest.ts b/packages/tests/src/e2e/copilotplugin/copilotPluginCommonTest.ts new file mode 100644 index 0000000000..179e9dd570 --- /dev/null +++ b/packages/tests/src/e2e/copilotplugin/copilotPluginCommonTest.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * @author Hui Miao + */ + +import { Capability } from "../../utils/constants"; +import { CaseFactory } from "../caseFactory"; +import { ProgrammingLanguage } from "@microsoft/teamsfx-core"; +import { replaceSecretKey, validateFiles } from "./helper"; +import * as path from "path"; +export class CopilotPluginCommonTest extends CaseFactory { + validateFileList?: string[]; + authOption?: string; + + public constructor( + testPlanCaseId: number, + author: string, + authOption: "none" | "api-key" | "oauth", + programmingLanguage?: ProgrammingLanguage, + validateFileList?: string[] + ) { + const env = Object.assign({}, process.env); + env["DEVELOP_COPILOT_PLUGIN"] = "true"; + if (programmingLanguage === ProgrammingLanguage.CSharp) { + env["TEAMSFX_CLI_DOTNET"] = "true"; + } + + const skipOptions = { + skipValidate: true, + skipErrorMessage: "No elements found in the manifest", + }; + + const authOptions: Record = {}; + authOptions["api-auth"] = authOption; + + super( + Capability.CopilotPluginFromScratch, + testPlanCaseId, + author, + ["function"], + programmingLanguage, + skipOptions, + authOptions, + env + ); + this.validateFileList = validateFileList; + this.authOption = authOption; + this.onAfterCreate = this.onAfterCreate.bind(this); + } + + public override async onAfterCreate(projectPath: string): Promise { + await validateFiles(projectPath, this.validateFileList || []); + + if (this.authOption === "api-key") { + const userFile = path.resolve(projectPath, "env", `.env.dev.user`); + await replaceSecretKey(userFile); + } + } +} diff --git a/packages/tests/src/e2e/copilotplugin/helper.ts b/packages/tests/src/e2e/copilotplugin/helper.ts new file mode 100644 index 0000000000..0f8ef76df5 --- /dev/null +++ b/packages/tests/src/e2e/copilotplugin/helper.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import * as path from "path"; +import * as fs from "fs-extra"; +import { expect } from "chai"; + +export async function validateFiles( + projectPath: string, + files: string[] +): Promise { + for (const file of files) { + const filePath = path.join(projectPath, file); + expect(fs.existsSync(filePath), `${filePath} must exist.`).to.eq(true); + } + console.log("Files validation successful"); +} + +export async function replaceSecretKey(userFile: string): Promise { + const newSecretKey = 'SECRET_API_KEY="test-secret-api-key"'; + let fileContent = fs.readFileSync(userFile, "utf8"); + fileContent = fileContent.replace(/(SECRET_API_KEY=).*/, newSecretKey); + fs.writeFileSync(userFile, fileContent, "utf8"); + console.log(`Updated ${newSecretKey} in .env.dev.user file`); +} diff --git a/packages/tests/src/utils/executor.ts b/packages/tests/src/utils/executor.ts index 4212549ba1..f237df8b45 100644 --- a/packages/tests/src/utils/executor.ts +++ b/packages/tests/src/utils/executor.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { ProgrammingLanguage } from "@microsoft/teamsfx-core"; +// eslint-disable-next-line import/no-cycle import { execAsync, editDotEnvFile } from "./commonUtils"; import { TemplateProjectFolder, @@ -52,6 +53,10 @@ export class Executor { return { success: true, ...result }; } } catch (e: any) { + if (skipErrorMessage && e.message.includes(skipErrorMessage)) { + console.log(`[Skip Warning] ${e.message}`); + return { success: true, ...e }; + } if (e.killed && e.signal == "SIGTERM") { console.error(`[Failed] "${command}" in ${cwd}. Timeout and killed.`); } else { @@ -96,14 +101,19 @@ export class Executor { appName: string, capability: Capability, language: ProgrammingLanguage, - customized: Record = {} + customized: Record = {}, + processEnv?: NodeJS.ProcessEnv ) { + const langCommand = + language === ProgrammingLanguage.CSharp + ? "--runtime dotnet" + : `--programming-language ${language}`; const command = - `teamsapp new --interactive false --app-name ${appName} --capability ${capability} --programming-language ${language} ` + + `teamsapp new --interactive false --app-name ${appName} --capability ${capability} ${langCommand} ` + Object.entries(customized) .map(([key, value]) => "--" + key + " " + value) .join(" "); - return this.execute(command, workspace); + return this.execute(command, workspace, processEnv); } static async addEnv(workspace: string, newEnv: string, env = "dev") { @@ -152,8 +162,21 @@ export class Executor { ); } - static async provision(workspace: string, env = "dev", isV3 = true) { - return this.executeCmd(workspace, "provision", env, undefined, false, isV3); + static async provision( + workspace: string, + env = "dev", + isV3 = true, + skipErrorMessage?: string + ) { + return this.executeCmd( + workspace, + "provision", + env, + undefined, + false, + isV3, + skipErrorMessage + ); } static async provisionWithCustomizedProcessEnv(