diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e868c7dddd..73519c049c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,142 +9,60 @@ # In each subsection folders are ordered first by depth, then alphabetically. # This should make it easy to add new rules without breaking existing ones. -/packages/vscode-extension @1openwindow @HuihuiWu-Microsoft @nliu-ms @tecton -/packages/vscode-extension/src/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/vscode-extension/src/debug @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/vscode-extension/test/localdebug @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/vscode-extension/package.nls.json @timngmsft @sffamily @therealjohn @supkasar -/packages/vscode-extension/src/migration @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/vscode-extension/test/migration @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/vscode-extension/README.md @therealjohn @sffamily -/packages/vscode-extension/CHANGELOG.md @therealjohn @sffamily -/packages/vscode-extension/WHATISNEW.md @therealjohn @sffamily -/packages/vscode-extension/PRERELEASE.md @therealjohn @sffamily +./lerna.json @wenytang-ms @qinezh @Siglud @LongOddCode +./pnpm-workspace.yaml @wenytang-ms @qinezh @Siglud @LongOddCode +/.github/CODEOWNERS @adashen @eriolchan @kimizhu @MSFT-yiz @zhenjiao-ms +/.github/actions @xzf0587 @wenytang-ms @blackchoey +/.github/workflows/cd.yml @LongOddCode @Siglud @qinezh @wenytang-ms +/.github/workflows/templates-ci.yml @hund030 @eriolchan @huimiu + +/Localize @HuihuiWu-Microsoft @tecton @chagong + +/packages/api @jayzhang @LongOddCode @nliu-ms +/packages/api/src/schemas @dooriya @qinezh @a1exwang @kimizhu /packages/cli @chagong @Alive-Fish @LongOddCode @jayzhang -/packages/cli/src/resource/strings.json @timngmsft @sffamily @therealjohn @supkasar -/packages/cli/src/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -/packages/cli/src/commonlib/appStudioLoginUserPassword.ts @chagong @Alive-Fish -/packages/cli/src/commonlib/azureLoginUserPassword.ts @chagong @Alive-Fish -/packages/cli/src/commonlib/graphLoginUserPassword.ts @chagong @Alive-Fish -/packages/cli/src/commonlib/common/userPasswordConfig.ts @chagong @Alive-Fish /packages/cli/src/cmds/m365 @swatDong @kuojianlu @kimizhu /packages/cli/src/cmds/preview @swatDong @kuojianlu @qinezh @a1exwang -/packages/cli/src/commands @jayzhang @Alive-Fish @MSFT-yiz - +/packages/cli/src/commands @jayzhang @Alive-Fish /packages/cli/src/commands/models/account.ts @swatDong @xiaolang124 @kimizhu /packages/cli/src/commands/models/accountLogin.ts @swatDong @xiaolang124 @kimizhu /packages/cli/src/commands/models/accountLoginAzure.ts @swatDong @xiaolang124 @kimizhu /packages/cli/src/commands/models/accountLoginM365.ts @swatDong @xiaolang124 @kimizhu /packages/cli/src/commands/models/accountLogout.ts @swatDong @xiaolang124 @kimizhu /packages/cli/src/commands/models/accountShow.ts @swatDong @xiaolang124 @kimizhu - -/packages/cli/src/commands/models/add.ts @HuihuiWu-Microsoft @nliu-ms @jayzhang @MSFT-yiz -/packages/cli/src/commands/models/addSPFxWebpart.ts @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang @MSFT-yiz - -/packages/cli/src/commands/models/m365.ts @swatDong @kuojianlu @kimizhu +/packages/cli/src/commands/models/add.ts @HuihuiWu-Microsoft @nliu-ms @jayzhang +/packages/cli/src/commands/models/addSPFxWebpart.ts @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/cli/src/commands/models/doctor.ts @swatDong @jayzhang /packages/cli/src/commands/models/m365LaunchInfo.ts @swatDong @kuojianlu @kimizhu /packages/cli/src/commands/models/m365Sideloading.ts @swatDong @kuojianlu @kimizhu /packages/cli/src/commands/models/m365Unacquire.ts @swatDong @kuojianlu @kimizhu +/packages/cli/src/commands/models/package.ts @nliu-ms @jayzhang @anchenyi +/packages/cli/src/commands/models/permission.ts @KennethBWSong @SLdragon +/packages/cli/src/commands/models/permissionGrant.ts @KennethBWSong @SLdragon +/packages/cli/src/commands/models/permissionStatus.ts @KennethBWSong @SLdragon /packages/cli/src/commands/models/preview.ts @swatDong @kuojianlu @kimizhu - -/packages/cli/src/commands/models/package.ts @nliu-ms @jayzhang @MSFT-yiz -/packages/cli/src/commands/models/publish.ts @nliu-ms @jayzhang @MSFT-yiz -/packages/cli/src/commands/models/updateTeamsApp.ts @nliu-ms @jayzhang @MSFT-yiz -/packages/cli/src/commands/models/validate.ts @nliu-ms @jayzhang @MSFT-yiz - -/packages/cli/src/commands/models/permission.ts @KennethBWSong @SLdragon @adashen -/packages/cli/src/commands/models/permissionGrant.ts @KennethBWSong @SLdragon @adashen -/packages/cli/src/commands/models/permissionStatus.ts @KennethBWSong @SLdragon @adashen -/packages/cli/src/commands/models/updateAadApp.ts @KennethBWSong @xzf0587 @blackchoey @adashen -/packages/cli/src/commands/models/upgrade.ts @xzf0587 @blackchoey @adashen - -/packages/tests/src/e2e/collaboration @KennethBWSong @adashen @SLdragon -/packages/tests/src/e2e/frontend @hund030 @eriolchan @huimiu -/packages/tests/src/e2e/bot @JerryYangKai @eriolchan @Siglud @Yukun-dong -/packages/tests/src/e2e/bot/tdpIntegrationTemplates @yuqizhou77 @nliu-ms @MSFT-yiz -/packages/tests/src/e2e/m365/DebugLinkUnfurling.tests.ts @JerryYangKai @eriolchan @Siglud @Yukun-dong -/packages/tests/src/e2e/m365/DeployLinkUnfurling.tests.ts @JerryYangKai @eriolchan @Siglud @Yukun-dong -/packages/tests/src/e2e/scaffold @hund030 @eriolchan @huimiu -/packages/tests/src/e2e/multienv @a1exwang @dooriya @qinezh @xiaolang124 @kimizhu -/packages/tests/src/e2e/m365 @kimizhu @swatDong @kuojianlu -/packages/tests/src/e2e/m365/ProvisionApiSpecMessageExtension.tests.ts @yuqizhou77 @Alive-Fish -/packages/tests/src/e2e/debug @kimizhu @swatDong @kuojianlu -/packages/tests/src/e2e/samples @LongOddCode @ayachensiyuan -/packages/cli/tests/unit/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/cli/src/commands/models/publish.ts @nliu-ms @jayzhang @anchenyi +/packages/cli/src/commands/models/entraAppUpdate.ts @KennethBWSong @xzf0587 @blackchoey +/packages/cli/src/commands/models/teamsapp @nliu-ms @jayzhang @anchenyi +/packages/cli/src/commands/models/upgrade.ts @xzf0587 @blackchoey +/packages/cli/src/commands/models/validate.ts @nliu-ms @jayzhang @anchenyi +/packages/cli/src/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/cli/src/commonlib/appStudioLoginUserPassword.ts @chagong @Alive-Fish +/packages/cli/src/commonlib/azureLoginUserPassword.ts @chagong @Alive-Fish +/packages/cli/src/commonlib/common/userPasswordConfig.ts @chagong @Alive-Fish +/packages/cli/src/commonlib/graphLoginUserPassword.ts @chagong @Alive-Fish +/packages/cli/src/resource/commands.json @timngmsft @sffamily @therealjohn @supkasar +/packages/cli/src/resource/errors.json @timngmsft @sffamily @therealjohn @supkasar +/packages/cli/src/resource/strings.json @timngmsft @sffamily @therealjohn @supkasar /packages/cli/tests/unit/cmds/preview @swatDong @kuojianlu @qinezh @a1exwang -/packages/cli/tests/unit/cmds/m365 @swatDong @kuojianlu @kimizhu - - -/packages/api @jayzhang @LongOddCode @nliu-ms -/packages/api/src/schemas @dooriya @qinezh @a1exwang @kimizhu - -/packages/sdk @tecton @blackchoey @SLdragon @wenytang-ms @yiqing-zhao -/packages/sdk/src/conversation @kimizhu @swatDong @a1exwang @XiaofuHuang @dooriya @SLdragon -/packages/sdk/test/unit/node/conversation @kimizhu @swatDong @a1exwang @XiaofuHuang @dooriya - -/packages/simpleauth @adashen @blackchoey @wenytang-ms - -/packages/function-extension @adashen @blackchoey +/packages/cli/tests/unit/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya /packages/dotnet-sdk @tecton @JerryYangKai @yiqing-zhao -/packages/dotnet-sdk/src/TeamsFx/Conversation @swatDong @dooriya @kimizhu /packages/dotnet-sdk/src/TeamsFx.Test/Conversation @swatDong @dooriya @kimizhu +/packages/dotnet-sdk/src/TeamsFx/Conversation @swatDong @dooriya @kimizhu -/.github/workflows/cd.yml @adashen @LongOddCode @Siglud @qinezh @wenytang-ms -/.github/workflows/templates-ci.yml @hund030 @eriolchan @huimiu -/.github/actions @xzf0587 @wenytang-ms @blackchoey @adashen -/.github/CODEOWNERS @adashen @eriolchan @kimizhu @MSFT-yiz @zhenjiao-ms -/packages/server @chagong @Alive-Fish @jayzhang - -/Localize @HuihuiWu-Microsoft @tecton @chagong - -/packages/sdk-react @tecton @yiqing-zhao - -/templates/* @hund030 @eriolchan @huimiu -/templates/scripts @hund030 @eriolchan @huimiu -/templates/constraints/yml/actions @hund030 @eriolchan @huimiu - -/templates/**/api-plugin-from-scratch @hund030 @eriolchan @huimiu -/templates/**/copilot-plugin-from-scratch @hund030 @eriolchan @huimiu -/templates/**/copilot-plugin-from-scratch-api-key @hund030 @eriolchan @huimiu -/templates/**/dashboard-tab @hund030 @eriolchan @huimiu -/templates/**/non-sso-tab @hund030 @eriolchan @Yimin-Jin -/templates/**/non-sso-tab-ssr @Yimin-Jin @eriolchan @hund030 -/templates/**/sso-tab @hund030 @eriolchan @Yimin-Jin -/templates/**/sso-tab-ssr @Yimin-Jin @eriolchan @hund030 -/templates/**/default-bot @JerryYangKai @eriolchan @Siglud @Yukun-dong -/templates/**/link-unfurling @JerryYangKai @eriolchan @Siglud @Yukun-dong -/templates/**/message-extension-action @JerryYangKai @eriolchan @Siglud @Yukun-dong -/templates/**/message-extension-search @JerryYangKai @eriolchan @Siglud @Yukun-dong -/templates/**/message-extension-copilot @hund030 @eriolchan @huimiu - -/templates/**/non-sso-tab-default-bot @hund030 @yuqizhou77 -/templates/**/default-bot-message-extension @yuqizhou77 @MSFT-yiz -/templates/**/message-extension @yuqizhou77 @MSFT-yiz -/templates/**/office-addin @jayzhang @nliu-ms -/templates/**/copilot-plugin-existing-api @yuqizhou77 @Alive-Fish @jayzhang -/templates/**/copilot-plugin-existing-api-api-key @yuqizhou77 @Alive-Fish @jayzhang -/templates/**/api-plugin-existing-api @yuqizhou77 @Alive-Fish @jayzhang -/templates/**/spfx-tab @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/templates/**/spfx-tab-import @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang - -/templates/**/sso-tab-with-obo-flow @KennethBWSong @adashen @SLdragon - -/templates/**/command-and-response @kimizhu @dooriya @swatDong -/templates/**/notification-http-timer-trigger @kimizhu @dooriya @swatDong -/templates/**/notification-http-trigger @kimizhu @dooriya @swatDong -/templates/**/notification-restify @kimizhu @dooriya @swatDong -/templates/**/notification-timer-trigger @kimizhu @dooriya @swatDong -/templates/**/notification-webapi @kimizhu @dooriya @swatDong -/templates/**/workflow @kimizhu @dooriya @swatDong -/templates/**/m365-message-extension @kimizhu @swatDong @kuojianlu -/templates/**/ai-bot @kimizhu @swatDong @kuojianlu -/templates/**/ai-assistant-bot @kimizhu @swatDong @kuojianlu -/templates/**/custom-copilot-basic @kimizhu @swatDong @kuojianlu @XiaofuHuang -/templates/python/custom-copilot-basic @frankqianms @adashen -/templates/**/custom-copilot-assistant-new @kimizhu @swatDong @kuojianlu @XiaofuHuang -/templates/**/custom-copilot-assistant-assistants-api @kimizhu @swatDong @kuojianlu @XiaofuHuang +/packages/function-extension @adashen @blackchoey /packages/fx-core/.eslintignore @LongOddCode @wenytang-ms /packages/fx-core/.eslintrc.js @LongOddCode @wenytang-ms @@ -154,213 +72,248 @@ /packages/fx-core/.prettierignore @LongOddCode @wenytang-ms /packages/fx-core/.prettierrc.js @LongOddCode @wenytang-ms /packages/fx-core/.vscode/launch.json @LongOddCode @wenytang-ms -/packages/fx-core/CONTRIBUTING.md @jayzhang @LongOddCode @wenytang-ms -/packages/fx-core/LICENSE.txt @jayzhang @LongOddCode @wenytang-ms -/packages/fx-core/NOTICE.txt @jayzhang @LongOddCode @wenytang-ms -/packages/fx-core/README.md @jayzhang @LongOddCode @wenytang-ms +/packages/fx-core/CONTRIBUTING.md @LongOddCode @wenytang-ms +/packages/fx-core/LICENSE.txt @LongOddCode @wenytang-ms +/packages/fx-core/NOTICE.txt @LongOddCode @wenytang-ms +/packages/fx-core/README.md @LongOddCode @wenytang-ms /packages/fx-core/package-lock.json @LongOddCode @wenytang-ms /packages/fx-core/package.json @LongOddCode @wenytang-ms +/packages/fx-core/resource/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang +/packages/fx-core/resource/package.nls.cs.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.de.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.es.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.fr.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.it.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.ja.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.json @timngmsft @sffamily @therealjohn @supkasar +/packages/fx-core/resource/package.nls.ko.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.pl.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.pt-BR.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.ru.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.tr.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.zh-Hans.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.zh-Hant.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.zh-cn.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/package.nls.zh-tw.json @HuihuiWu-Microsoft @chagong @jayzhang +/packages/fx-core/resource/yaml-schema @jayzhang @wenytang-ms @kuojianlu @Siglud /packages/fx-core/scripts/delete-unused-strings.js @jayzhang /packages/fx-core/scripts/find-unused-strings.js @jayzhang /packages/fx-core/scripts/generate-appdef.ps1 @nliu-ms - /packages/fx-core/src/common/constants.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/constants.ts @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/src/question @jayzhang @xzf0587 @LongOddCode @yuqizhou77 @tecton -/packages/fx-core/tests/question @jayzhang @xzf0587 @LongOddCode @yuqizhou77 @tecton - /packages/fx-core/src/common/correlator.ts @chagong @jayzhang @LongOddCode - -/packages/fx-core/src/common/featureFlags.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/common/featureFlags.test.ts @jayzhang @xzf0587 @LongOddCode - +/packages/fx-core/src/common/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang +/packages/fx-core/src/common/featureFlags.ts @jayzhang @xzf0587 /packages/fx-core/src/common/globalState.ts @tecton @jayzhang @LongOddCode -/packages/fx-core/tests/common/globalState.test.ts @tecton @jayzhang @LongOddCode - /packages/fx-core/src/common/jsonUtils.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/src/common/local @kimizhu @swatDong @kuojianlu @XiaofuHuang /packages/fx-core/src/common/localizeUtils.ts @jayzhang @HuihuiWu-Microsoft @chagong +/packages/fx-core/src/common/m365 @kimizhu @swatDong @kuojianlu /packages/fx-core/src/common/permissionInterface.ts @SLdragon @KennethBWSong /packages/fx-core/src/common/projectSettingsHelper.ts @jayzhang @xzf0587 @LongOddCode /packages/fx-core/src/common/projectSettingsHelperV3.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/src/common/samples.ts @HuihuiWu-Microsoft @wenytang-ms @jayzhang @tecton /packages/fx-core/src/common/telemetry.ts @jayzhang @xzf0587 @LongOddCode /packages/fx-core/src/common/templates-config.json @hund030 @eriolchan @huimiu - /packages/fx-core/src/common/tools.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/common/tools.test.ts @jayzhang @xzf0587 @LongOddCode - /packages/fx-core/src/common/utils.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/common/utils.test.ts @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/src/common/versionMetadata.ts @xzf0587 @blackchoey @adashen +/packages/fx-core/src/common/versionMetadata.ts @xzf0587 @blackchoey +/packages/fx-core/src/component @jayzhang @xzf0587 @hund030 @LongOddCode +/packages/fx-core/src/component/configManager @jayzhang @wenytang-ms @kuojianlu @Siglud +/packages/fx-core/src/component/debugHandler @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/src/component/developerPortalScaffoldUtils.ts @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/src/component/driver/aad @blackchoey @wenytang-ms @KennethBWSong +/packages/fx-core/src/component/driver/add @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/src/component/driver/apiKey @KennethBWSong @SLdragon +/packages/fx-core/src/component/driver/arm @xzf0587 @blackchoey +/packages/fx-core/src/component/driver/botAadApp @blackchoey @wenytang-ms @KennethBWSong +/packages/fx-core/src/component/driver/botFramework @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/src/component/driver/deploy/azure @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/deploy/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/src/component/driver/devTool @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/src/component/driver/file @swatDong @XiaofuHuang @xiaolang124 @kuojianlu @kimizhu +/packages/fx-core/src/component/driver/m365 @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/src/component/driver/middleware @tecton @jayzhang @LongOddCode +/packages/fx-core/src/component/driver/script/baseBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/script/baseBuildStepDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/script/dotnetBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/script/npmBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/script/npxBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/src/component/driver/script/scriptDriver.ts @jayzhang @Siglud +/packages/fx-core/src/component/driver/teamsApp @nliu-ms @jayzhang @anchenyi +/packages/fx-core/src/component/driver/util/utils.ts @blackchoey @xzf0587 +/packages/fx-core/src/component/feature/collaboration.ts @KennethBWSong @SLdragon +/packages/fx-core/src/component/feature/createAuthFiles.ts @KennethBWSong @xzf0587 +/packages/fx-core/src/component/feature/sso.ts @KennethBWSong @xzf0587 +/packages/fx-core/src/component/generator @Yukun-dong @hund030 @JerryYangKai @eriolchan +/packages/fx-core/src/component/generator/copilotPlugin @yuqizhou77 @nliu-ms @Alive-Fish +/packages/fx-core/src/component/generator/officeAddin @jayzhang @tecton +/packages/fx-core/src/component/generator/officeXMLAddin @jayzhang @tecton +/packages/fx-core/src/component/generator/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms +/packages/fx-core/src/component/resource/aadApp @KennethBWSong @SLdragon +/packages/fx-core/src/component/resource/botService @kimizhu @swatDong @kuojianlu +/packages/fx-core/src/core @jayzhang @LongOddCode @jayzhang @nliu-ms @xzf0587 @hund030 +/packages/fx-core/src/core/middleware/projectMigrationV3 @xzf0587 @frankqianms @blackchoey +/packages/fx-core/src/core/middleware/utils/debug @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/src/error @jayzhang @xzf0587 @hund030 @LongOddCode /packages/fx-core/src/failpoint @jayzhang @LongOddCode -/packages/fx-core/tests/core/failpoint.test.ts @jayzhang @LongOddCode /packages/fx-core/src/folder.ts @jayzhang @xzf0587 @LongOddCode /packages/fx-core/src/index.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/src/ui/visitor.ts @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/src/component @jayzhang @xzf0587 @hund030 @LongOddCode +/packages/fx-core/src/question @jayzhang @xzf0587 @LongOddCode @yuqizhou77 @tecton +/packages/fx-core/src/ui @jayzhang @yuqizhou77 @tecton +/packages/fx-core/templates/core/v3Migration @xzf0587 @frankqianms @blackchoey +/packages/fx-core/templates/plugins/resource/aad/ @KennethBWSong @xzf0587 +/packages/fx-core/templates/plugins/resource/appstudio @nliu-ms @anchenyi /packages/fx-core/test/component @jayzhang @xzf0587 @hund030 @LongOddCode - +/packages/fx-core/tests/common/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang +/packages/fx-core/tests/common/featureFlags.test.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/common/globalState.test.ts @tecton @jayzhang @LongOddCode +/packages/fx-core/tests/common/local @kimizhu @swatDong @kuojianlu @XiaofuHuang +/packages/fx-core/tests/common/m365 @kimizhu @swatDong @kuojianlu +/packages/fx-core/tests/common/samples.test.ts @HuihuiWu-Microsoft @wenytang-ms @jayzhang @tecton +/packages/fx-core/tests/common/tools.test.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/common/utils.test.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/component/configManager @jayzhang @wenytang-ms @kuojianlu @Siglud /packages/fx-core/tests/component/coordinator @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/component/developerPortalScaffoldUtils.test.ts @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/tests/component/driver/aad @blackchoey @wenytang-ms @KennethBWSong +/packages/fx-core/tests/component/driver/add @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/tests/component/driver/apiKey @KennethBWSong @SLdragon +/packages/fx-core/tests/component/driver/arm @xzf0587 @blackchoey +/packages/fx-core/tests/component/driver/botAadApp @blackchoey @wenytang-ms @KennethBWSong +/packages/fx-core/tests/component/driver/botFramework @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/tests/component/driver/deploy/azure/ @Siglud @Yukun-dong @JerryYangKai @eriolchan +/packages/fx-core/tests/component/driver/deploy/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/tests/component/driver/devTool @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/tests/component/driver/file @swatDong @XiaofuHuang @xiaolang124 @kuojianlu @kimizhu +/packages/fx-core/tests/component/driver/m365 @swatDong @XiaofuHuang @kuojianlu @kimizhu +/packages/fx-core/tests/component/driver/middleware/updateProgress.test.ts @tecton @jayzhang @LongOddCode +/packages/fx-core/tests/component/driver/script @Siglud @Yukun-dong @JerryYangKai @eriolchan /packages/fx-core/tests/component/driver/script/scriptDriver.test.ts @jayzhang @LongOddCode +/packages/fx-core/tests/component/driver/teamsApp @nliu-ms @jayzhang @yuqizhou77 @anchenyi +/packages/fx-core/tests/component/driver/util/utils.test.ts @blackchoey @xzf0587 /packages/fx-core/tests/component/envUtil.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/component/error.test.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/component/error @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/component/feature @KennethBWSong @xzf0587 +/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @yuqizhou77 @nliu-ms @Alive-Fish @jayzhang +/packages/fx-core/tests/component/generator/generator.test.ts @Yukun-dong @hund030 @JerryYangKai @eriolchan +/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts @jayzhang @tecton +/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts @jayzhang @tecton +/packages/fx-core/tests/component/generator/spfxGenerator.test.ts @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang /packages/fx-core/tests/component/jsonUtils.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/component/middleware/helper.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/component/middleware/middleware.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/component/provisionUtils.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/component/resourceGroupHelper.test.ts @jayzhang @xzf0587 @LongOddCode - +/packages/fx-core/tests/component/resource/appManifest @nliu-ms @jayzhang @HuihuiWu-Microsoft @anchenyi +/packages/fx-core/tests/component/resource/botService @kimizhu @swatDong @kuojianlu /packages/fx-core/tests/component/util/azureAccountMock.ts @Siglud @xiaolang124 /packages/fx-core/tests/component/util/azureResourceOperation.test.ts @Siglud @Yukun-dong /packages/fx-core/tests/component/util/logProviderMock.ts @Siglud @Yukun-dong /packages/fx-core/tests/component/util/metadataUtil.test.ts @chagong @jayzhang -/packages/fx-core/tests/component/utils.test.ts @jayzhang @xzf0587 @LongOddCode - /packages/fx-core/tests/core/FxCore.create.test.ts @jayzhang @xzf0587 @LongOddCode /packages/fx-core/tests/core/FxCore.test.ts @jayzhang @xzf0587 @LongOddCode /packages/fx-core/tests/core/callback.test.ts @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/tests/core/middleware/ConcurrentLockerMW.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/core/middleware/ErrorHandlerMW.test.ts @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tests/core/collaborator.test.ts @KennethBWSong @SLdragon +/packages/fx-core/tests/core/failpoint.test.ts @jayzhang @LongOddCode /packages/fx-core/tests/core/middleware/VideoFilterAppBlockerMW.test.ts @a1exwang - -/packages/fx-core/tests/core/other.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/core/samples_v2.zip @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/core/tools.test.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/core/utils.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/plugins/solution/util.ts @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/tests/ui/qm.visitor.test.ts @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/tsconfig.json @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/webpack.config.js @jayzhang @xzf0587 @LongOddCode - -/packages/fx-core/src/component/debugHandler @swatDong @XiaofuHuang @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/file @swatDong @XiaofuHuang @xiaolang124 @kuojianlu @kimizhu -/packages/fx-core/tests/component/driver/file @swatDong @XiaofuHuang @xiaolang124 @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/botFramework @swatDong @XiaofuHuang @kuojianlu @kimizhu -/packages/fx-core/tests/component/driver/botFramework @swatDong @XiaofuHuang @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/m365 @swatDong @XiaofuHuang @kuojianlu @kimizhu -/packages/fx-core/tests/component/driver/m365 @swatDong @XiaofuHuang @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/devTool @swatDong @XiaofuHuang @kuojianlu @kimizhu -/packages/fx-core/tests/component/driver/devTool @swatDong @XiaofuHuang @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/arm @xzf0587 @blackchoey @adashen -/packages/fx-core/tests/component/driver/arm @xzf0587 @blackchoey @adashen - -/packages/fx-core/src/core/middleware/projectMigrationV3 @xzf0587 @frankqianms @blackchoey @adashen -/packages/fx-core/templates/core/v3Migration @xzf0587 @frankqianms @blackchoey @adashen -/packages/fx-core/tests/core/middleware/migration @xzf0587 @frankqianms @blackchoey @adashen -/packages/fx-core/tests/core/middleware/projectVersionChecker.test.ts @xzf0587 @frankqianms @blackchoey @adashen -/packages/fx-core/tests/core/middleware/testAssets @xzf0587 @frankqianms @blackchoey @adashen -/packages/fx-core/tests/samples/sampleV3 @xzf0587 @wenytang-ms @frankqianms @blackchoey @adashen - -/packages/fx-core/src/component/driver/aad @blackchoey @wenytang-ms @KennethBWSong @adashen -/packages/fx-core/tests/component/driver/aad @blackchoey @wenytang-ms @KennethBWSong @adashen -/packages/fx-core/src/component/driver/botAadApp @blackchoey @wenytang-ms @KennethBWSong @adashen -/packages/fx-core/tests/component/driver/botAadApp @blackchoey @wenytang-ms @KennethBWSong @adashen -/packages/fx-core/src/component/driver/util/utils.ts @blackchoey @xzf0587 @adashen -/packages/fx-core/tests/component/driver/util/utils.test.ts @blackchoey @xzf0587 @adashen -/packages/fx-core/src/component/driver/apiKey @KennethBWSong @SLdragon @adashen -/packages/fx-core/tests/component/driver/apiKey @KennethBWSong @SLdragon @adashen - -/packages/fx-core/src/core/middleware/utils/debug @swatDong @XiaofuHuang @kuojianlu @kimizhu /packages/fx-core/tests/core/middleware/debug @swatDong @XiaofuHuang @kuojianlu @kimizhu - -/packages/fx-core/src/component/driver/deploy/azure @Siglud @Yukun-dong @JerryYangKai @eriolchan -/packages/fx-core/tests/component/driver/deploy/azure/ @Siglud @Yukun-dong @JerryYangKai @eriolchan -/packages/fx-core/tests/component/driver/script @Siglud @Yukun-dong @JerryYangKai @eriolchan -packages/fx-core/src/component/driver/script/baseBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan -packages/fx-core/src/component/driver/script/baseBuildStepDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan -packages/fx-core/src/component/driver/script/dotnetBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan -packages/fx-core/src/component/driver/script/npmBuildDriver.ts @Siglud @Yukun-dong @JerryYangKai @eriolchan - -/packages/fx-core/resource/package.nls.json @timngmsft @sffamily @therealjohn @supkasar -/packages/fx-core/resource/package.nls.cs.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.de.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.es.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.fr.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.it.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.ja.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.ko.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.pl.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.pt-BR.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.ru.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.tr.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.zh-Hans.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.zh-Hant.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.zh-cn.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/package.nls.zh-tw.json @HuihuiWu-Microsoft @chagong @jayzhang -/packages/fx-core/resource/yaml-schema @jayzhang @wenytang-ms @kuojianlu @Siglud -/packages/fx-core/src/core @jayzhang @LongOddCode @jayzhang @nliu-ms @xzf0587 @hund030 - -/packages/fx-core/src/component/configManager @jayzhang @wenytang-ms @kuojianlu @Siglud -/packages/fx-core/tests/component/configManager @jayzhang @wenytang-ms @kuojianlu @Siglud - -/packages/fx-core/src/error @jayzhang @xzf0587 @hund030 @LongOddCode - -/packages/fx-core/src/common/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang -/packages/fx-core/tests/common/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang -/packages/fx-core/resource/deps-checker @qinezh @a1exwang @kimizhu @swatDong @XiaofuHuang - -/packages/fx-core/src/component/resource/aadApp @KennethBWSong @adashen @SLdragon -/packages/fx-core/tests/component/resource/aadApp @KennethBWSong @adashen @SLdragon - -/packages/fx-core/src/component/resource/appManifest @nliu-ms @jayzhang @HuihuiWu-Microsoft -/packages/fx-core/tests/component/resource/appManifest @nliu-ms @jayzhang @HuihuiWu-Microsoft - -/packages/fx-core/src/common/samples.ts @HuihuiWu-Microsoft @wenytang-ms @jayzhang @tecton -/packages/fx-core/tests/common/samples.test.ts @HuihuiWu-Microsoft @wenytang-ms @jayzhang @tecton +/packages/fx-core/tests/core/middleware/migration @xzf0587 @frankqianms @blackchoey +/packages/fx-core/tests/core/middleware/projectVersionChecker.test.ts @xzf0587 @frankqianms @blackchoey +/packages/fx-core/tests/core/middleware/testAssets @xzf0587 @frankqianms @blackchoey +/packages/fx-core/tests/plugins/resource/appstudio @nliu-ms @jayzhang @yuqizhou77 @anchenyi /packages/fx-core/tests/plugins/resource/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/packages/fx-core/tests/question @jayzhang @xzf0587 @LongOddCode @yuqizhou77 @tecton +/packages/fx-core/tests/samples/sampleV3 @xzf0587 @wenytang-ms @frankqianms @blackchoey +/packages/fx-core/tests/ui @jayzhang @xzf0587 @LongOddCode +/packages/fx-core/tsconfig.json @xzf0587 @LongOddCode +/packages/fx-core/webpack.config.js @jayzhang @xzf0587 @LongOddCode -/packages/fx-core/src/component/generator @Yukun-dong @hund030 @JerryYangKai @eriolchan -/packages/fx-core/src/component/generator/copilotPlugin @yuqizhou77 @nliu-ms @Alive-Fish @jayzhang -/packages/fx-core/src/component/generator/officeAddin @jayzhang @LongOddCode -/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts @jayzhang @LongOddCode -/packages/fx-core/src/component/driver/deploy/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/tests/component/driver/deploy/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/src/component/driver/add @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/tests/component/driver/add @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/src/component/generator/spfx @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/tests/component/generator/spfxGenerator.test.ts @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/tests/component/generator/generator.test.ts @Yukun-dong @hund030 @JerryYangKai @eriolchan -/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @yuqizhou77 @nliu-ms @Alive-Fish @jayzhang +/packages/manifest/ @jayzhang @nliu-ms @anchenyi -/packages/fx-core/src/component/developerPortalScaffoldUtils.ts @yuqizhou77 @nliu-ms @jayzhang -/packages/fx-core/tests/component/developerPortalScaffoldUtils.test.ts @yuqizhou77 @nliu-ms @jayzhang +/packages/sdk @tecton @blackchoey @SLdragon @wenytang-ms @yiqing-zhao -/packages/fx-core/src/common/local @kimizhu @swatDong @kuojianlu @XiaofuHuang -/packages/fx-core/tests/common/local @kimizhu @swatDong @kuojianlu @XiaofuHuang -/packages/fx-core/src/common/m365 @kimizhu @swatDong @kuojianlu -/packages/fx-core/tests/common/m365 @kimizhu @swatDong @kuojianlu -/packages/fx-core/src/component/resource/botService @kimizhu @swatDong @kuojianlu -/packages/fx-core/tests/component/resource/botService @kimizhu @swatDong @kuojianlu +/packages/sdk-react @tecton @yiqing-zhao +/packages/sdk/src/conversation @kimizhu @swatDong @a1exwang @XiaofuHuang @dooriya @SLdragon +/packages/sdk/test/unit/node/conversation @kimizhu @swatDong @a1exwang @XiaofuHuang @dooriya -/packages/fx-core/tests/component/driver/teamsApp @nliu-ms @jayzhang @yuqizhou77 -/packages/fx-core/tests/plugins/resource/appstudio @nliu-ms @jayzhang @yuqizhou77 -/packages/fx-core/templates/plugins/resource/appstudio @nliu-ms -/packages/fx-core/src/component/driver/middleware/updateProgress.ts @tecton @jayzhang @LongOddCode -/packages/fx-core/tests/component/driver/middleware/updateProgress.test.ts @tecton @jayzhang @LongOddCode +/packages/server @chagong @Alive-Fish @jayzhang -/packages/fx-core/templates/plugins/resource/aad/ @KennethBWSong @xzf0587 @adashen -/packages/fx-core/src/component/feature/collaboration.ts @KennethBWSong @SLdragon @adashen -/packages/fx-core/src/component/feature/createAuthFiles.ts @KennethBWSong @xzf0587 @adashen -/packages/fx-core/src/component/feature/sso.ts @KennethBWSong @xzf0587 @adashen -packages/fx-core/tests/component/feature/sso.test.ts @KennethBWSong @xzf0587 @adashen -packages/fx-core/tests/component/feature/collaborator.test.ts @KennethBWSong @SLdragon @adashen -packages/fx-core/tests/component/feature/collaboration.test.ts @KennethBWSong @SLdragon @adashen -packages/fx-core/tests/core/collaborator.test.ts @KennethBWSong @SLdragon @adashen +/packages/simpleauth @blackchoey @wenytang-ms -packages/fx-core/tests/helpers.ts @jayzhang @MSFT-yiz +/packages/spec-parser/ @KennethBWSong @SLdragon -packages/manifest/ @jayzhang @MSFT-yiz +/packages/tests/src/e2e/bot @JerryYangKai @eriolchan @Siglud @Yukun-dong +/packages/tests/src/e2e/bot/tdpIntegrationTemplates @yuqizhou77 @nliu-ms +/packages/tests/src/e2e/collaboration @KennethBWSong @SLdragon +/packages/tests/src/e2e/debug @kimizhu @swatDong @kuojianlu +/packages/tests/src/e2e/frontend @hund030 @eriolchan @huimiu +/packages/tests/src/e2e/m365 @kimizhu @swatDong @kuojianlu +/packages/tests/src/e2e/m365/DebugLinkUnfurling.tests.ts @JerryYangKai @eriolchan @Siglud @Yukun-dong +/packages/tests/src/e2e/m365/DeployLinkUnfurling.tests.ts @JerryYangKai @eriolchan @Siglud @Yukun-dong +/packages/tests/src/e2e/m365/ProvisionApiSpecMessageExtension.tests.ts @yuqizhou77 @Alive-Fish +/packages/tests/src/e2e/multienv @a1exwang @dooriya @qinezh @xiaolang124 @kimizhu +/packages/tests/src/e2e/samples @LongOddCode @ayachensiyuan +/packages/tests/src/e2e/scaffold @hund030 @eriolchan @huimiu -packages/spec-parser/ @KennethBWSong @SLdragon @adashen +/packages/vscode-extension @1openwindow @HuihuiWu-Microsoft @nliu-ms @tecton +/packages/vscode-extension/CHANGELOG.md @therealjohn @sffamily +/packages/vscode-extension/PRERELEASE.md @therealjohn @sffamily +/packages/vscode-extension/README.md @therealjohn @sffamily +/packages/vscode-extension/WHATISNEW.md @therealjohn @sffamily +/packages/vscode-extension/package.nls.json @timngmsft @sffamily @therealjohn @supkasar +/packages/vscode-extension/src/commonlib @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/vscode-extension/src/debug @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/vscode-extension/src/debug/taskTerminal/officeDevTerminal.ts @tecton @swatDong +/packages/vscode-extension/src/migration @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/vscode-extension/test/localdebug @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya +/packages/vscode-extension/test/migration @kimizhu @swatDong @kuojianlu @a1exwang @qinezh @XiaofuHuang @xiaolang124 @dooriya -./lerna.json @wenytang-ms @qinezh @Siglud @LongOddCode -./pnpm-workspace.yaml @wenytang-ms @qinezh @Siglud @LongOddCode \ No newline at end of file +/templates/* @hund030 @eriolchan @huimiu +/templates/**/ai-assistant-bot @kimizhu @swatDong @kuojianlu +/templates/**/ai-bot @kimizhu @swatDong @kuojianlu +/templates/**/api-plugin-existing-api @yuqizhou77 @Alive-Fish @jayzhang +/templates/**/api-plugin-from-scratch @hund030 @eriolchan @huimiu +/templates/**/command-and-response @kimizhu @dooriya @swatDong +/templates/**/copilot-plugin-existing-api @yuqizhou77 @Alive-Fish @jayzhang +/templates/**/copilot-plugin-existing-api-api-key @yuqizhou77 @Alive-Fish @jayzhang +/templates/**/copilot-plugin-from-scratch @hund030 @eriolchan @huimiu +/templates/**/copilot-plugin-from-scratch-api-key @hund030 @eriolchan @huimiu +/templates/**/custom-copilot-assistant-assistants-api @kimizhu @swatDong @kuojianlu @XiaofuHuang +/templates/**/custom-copilot-assistant-new @kimizhu @swatDong @kuojianlu @XiaofuHuang +/templates/**/custom-copilot-basic @kimizhu @swatDong @kuojianlu @XiaofuHuang +/templates/**/dashboard-tab @hund030 @eriolchan @huimiu +/templates/**/default-bot @JerryYangKai @eriolchan @Siglud @Yukun-dong +/templates/**/default-bot-message-extension @yuqizhou77 +/templates/**/link-unfurling @JerryYangKai @eriolchan @Siglud @Yukun-dong +/templates/**/m365-message-extension @kimizhu @swatDong @kuojianlu +/templates/**/message-extension @yuqizhou77 @Yukun-dong +/templates/**/message-extension-action @JerryYangKai @eriolchan @Siglud @Yukun-dong +/templates/**/message-extension-copilot @hund030 @eriolchan @huimiu +/templates/**/message-extension-search @JerryYangKai @eriolchan @Siglud @Yukun-dong +/templates/**/non-sso-tab @hund030 @eriolchan @Yimin-Jin +/templates/**/non-sso-tab-default-bot @hund030 @yuqizhou77 +/templates/**/non-sso-tab-ssr @Yimin-Jin @eriolchan @hund030 +/templates/**/notification-http-timer-trigger @kimizhu @dooriya @swatDong +/templates/**/notification-http-trigger @kimizhu @dooriya @swatDong +/templates/**/notification-restify @kimizhu @dooriya @swatDong +/templates/**/notification-timer-trigger @kimizhu @dooriya @swatDong +/templates/**/notification-webapi @kimizhu @dooriya @swatDong +/templates/**/office-addin @jayzhang @tecton +/templates/**/office-json-addin @jayzhang @tecton +/templates/**/office-xml-addin-excel-cf @jayzhang @tecton +/templates/**/office-xml-addin-excel-react @jayzhang @tecton +/templates/**/office-xml-addin-excel-sso @jayzhang @tecton +/templates/**/office-xml-addin-excel-taskpane @jayzhang @tecton +/templates/**/office-xml-addin-powerpoint-react @jayzhang @tecton +/templates/**/office-xml-addin-powerpoint-sso @jayzhang @tecton +/templates/**/office-xml-addin-powerpoint-taskpane @jayzhang @tecton +/templates/**/office-xml-addin-word-react @jayzhang @tecton +/templates/**/office-xml-addin-word-sso @jayzhang @tecton +/templates/**/office-xml-addin-word-taskpane @jayzhang @tecton +/templates/**/spfx-tab @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/templates/**/spfx-tab-import @HuihuiWu-Microsoft @yuqizhou77 @nliu-ms @jayzhang +/templates/**/sso-tab @hund030 @eriolchan @Yimin-Jin +/templates/**/sso-tab-ssr @Yimin-Jin @eriolchan @hund030 +/templates/**/sso-tab-with-obo-flow @Yimin-Jin @hund030 @huimiu @eriolchan +/templates/**/workflow @kimizhu @dooriya @swatDong +/templates/constraints/yml/actions @hund030 @eriolchan @huimiu +/templates/python/* @adashen @blackchoey @frankqianms +/templates/python/custom-copilot-basic @frankqianms @blackchoey +/templates/python/custom-copilot-rag-azure-ai-search @frankqianms @blackchoey +/templates/scripts @hund030 @eriolchan @huimiu diff --git a/.github/accounts.json b/.github/accounts.json index 4e7c472e63..21cbba1700 100644 --- a/.github/accounts.json +++ b/.github/accounts.json @@ -48,6 +48,7 @@ "yukun-dong": "yukundong", "huimiu": "huimiao", "Yimin-Jin": "yiminjin", + "anchenyi": "anchenyi", "yiqing-zhao": "yiqingzhao", "lijie-lee": "lijieli", "jaeyonglee05": "Jaeyonglee" diff --git a/.github/scripts/get-dailydigest-dependencies.js b/.github/scripts/get-dailydigest-dependencies.js index 140cdcd2f8..eef7e52c7d 100644 --- a/.github/scripts/get-dailydigest-dependencies.js +++ b/.github/scripts/get-dailydigest-dependencies.js @@ -41,6 +41,12 @@ const codeOwnerMap = new Map([ ["notification-http-timer-trigger-isolated", "tianyuan@microsoft.com"], ["notification-http-trigger-isolated", "tianyuan@microsoft.com"], ["notification-timer-trigger-isolated", "tianyuan@microsoft.com"], + ["custom-copilot-assistant-assistants-api", "kuojianlu@microsoft.com"], + ["custom-copilot-assistant-new", "kuojianlu@microsoft.com"], + ["custom-copilot-basic", "kuojianlu@microsoft.com"], + ["custom-copilot-rag-custom-api", "kuojianlu@microsoft.com"], + ["api-plugin-from-scratch", "huimiao@microsoft.com"], + ["api-message-extension-sso", "huimiao@microsoft.com"], ]); async function getTemplatesDependencies() { diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4863d6633b..ad3094129e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -23,9 +23,9 @@ on: - cron: "0 16 * * *" permissions: - actions: read - contents: read - + actions: read + contents: read + jobs: cd: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 712a57fe86..de9bc08307 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -212,7 +212,7 @@ jobs: path: packages/tests/src/e2e/resource - name: Download samples(rc) - if: github.event_name == 'workflow_dispatch' && startsWith(matrix.cases, './samples/') && contains(matrix.cases, 'ProactiveMessage') == false && contains(matrix.cases, 'SignatureOutlook') == false + if: github.event_name == 'workflow_dispatch' && startsWith(matrix.cases, './samples/') && contains(matrix.cases, 'ProactiveMessage') == false && contains(matrix.cases, 'SignatureOutlook') == false && contains(matrix.cases, 'ChefBot') == false uses: actions/checkout@v3 with: repository: OfficeDev/TeamsFx-Samples @@ -235,6 +235,14 @@ jobs: ref: main path: packages/tests/src/e2e/resource + - name: Download samples from ai repo + if: startsWith(matrix.cases, './samples/') && contains(matrix.cases, 'ChefBot') + uses: actions/checkout@v3 + with: + repository: microsoft/teams-ai + ref: main + path: packages/tests/src/e2e/resource + - name: run test working-directory: packages/tests/src/e2e run: | @@ -426,7 +434,7 @@ jobs: failed=$((failed+1)) label="FAILED" if [[ ! -z "$email" && ! "$emails" == *"$email"* ]]; then - emails="$emails;$email;zhendr@microsoft.com" + emails="$emails;$email;zhendr@microsoft.com;ccdevexperiencefc@microsoft.com" fi elif [[ ! -z `echo $test | jq 'select(.skipped==true)'` || ! -z `echo $test | jq 'select(.pending==true)'` ]]; then skipped=$((skipped+1)) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 09da3c6e58..b2759095a8 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -28,16 +28,17 @@ jobs: with: script: | const AZDO_TICKET_REGEX = 'https:\/\/(dev\.azure\.com\/msazure|msazure\.visualstudio\.com)\/Microsoft%20Teams%20Extensibility'; + const AZDO_TICKET_REGEX_WXP = 'https:\/\/office\.visualstudio\.com\/OC'; const pullRequest = context.payload.pull_request; if(pullRequest.title.startsWith("feat")) { const body = pullRequest.body; - const match = body?.match(AZDO_TICKET_REGEX); + const match = body?.match(AZDO_TICKET_REGEX) || body?.match(AZDO_TICKET_REGEX_WXP); if(!match) { core.setFailed("Feat PR should contains AZDO tickets"); } } else if(pullRequest.title.startsWith("fix")) { const body = pullRequest.body; - const match = body?.match(AZDO_TICKET_REGEX); + const match = body?.match(AZDO_TICKET_REGEX) || body?.match(AZDO_TICKET_REGEX_WXP); if(!match && !body) { core.setFailed("Fix PR should contains AZDO tickets or descrptions"); } @@ -131,7 +132,7 @@ jobs: then for obj in "$YMLTPL" do - mustache test.json $obj | yamllint - + mustache test.json $obj | yamllint -d "{extends: relaxed, rules: {line-length: {max: 100}}}" - done fi @@ -189,11 +190,6 @@ jobs: - name: Get branch name id: branch-name uses: tj-actions/branch-names@v7 - - name: Add Pull Request Reviewer - uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 - with: - reviewers: "MuyangAmigo" - token: ${{ secrets.GITHUB_TOKEN }} - name: check origin or remote id: remote run: | diff --git a/.github/workflows/rerun.yml b/.github/workflows/rerun.yml index 1d03500110..8044825709 100644 --- a/.github/workflows/rerun.yml +++ b/.github/workflows/rerun.yml @@ -26,9 +26,9 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest env: - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} steps: - name: wait for 60s run: | @@ -38,7 +38,7 @@ jobs: - name: clean devtunnel run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} ~/bin/devtunnel delete-all -f - name: re-run failed jobs diff --git a/.github/workflows/ui-test.yml b/.github/workflows/ui-test.yml index 5a17f2c7da..121a093989 100644 --- a/.github/workflows/ui-test.yml +++ b/.github/workflows/ui-test.yml @@ -64,9 +64,9 @@ jobs: ADO_TOKEN: ${{ secrets.ADO_PAT }} AUTO_TEST_PLAN_ID: ${{ github.event.inputs.source-testplan-id }} TARGET_TEST_PLAN_NAME: ${{ github.event.inputs.target-testplan-name }} - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} steps: - name: Init GitHub CLI @@ -158,6 +158,7 @@ jobs: run: | pnpm install testplanid=`npx ts-node src/scripts/testPlan.ts obtain vscode ${{ github.event.inputs.target-testplan-name }}` + echo "Testplan id is $testplanid" npx ts-node src/scripts/testPlan.ts archive $testplanid - name: Upload testplan to artifact @@ -171,7 +172,7 @@ jobs: - name: clean devtunnel run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} ~/bin/devtunnel delete-all -f outputs: @@ -198,8 +199,9 @@ jobs: CLEAN_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} CLEAN_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} M365_ACCOUNT_PASSWORD: ${{ secrets.TEST_M365_PASSWORD }} M365_USERNAME: "test14@xxbdw.onmicrosoft.com" @@ -270,7 +272,7 @@ jobs: if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} - name: Install devtunnel (windows) if: matrix.os == 'windows-latest' @@ -280,7 +282,7 @@ jobs: $currentDirectory = (Get-Location).Path $executablePath = Join-Path $currentDirectory "devtunnel.exe" [System.Environment]::SetEnvironmentVariable("Path", "$currentPath;$executablePath", [System.EnvironmentVariableTarget]::Machine) - ./devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ./devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} - name: Downgrade PowerShell (win) if: matrix.os == 'windows-latest' @@ -357,7 +359,7 @@ jobs: path: ./packages/tests/resource - name: Download samples - if: startsWith(matrix.test-case, 'sample-') && contains(matrix.test-case, 'proactive-message') == false && contains(matrix.test-case, 'upgrade') == false + if: startsWith(matrix.test-case, 'sample-') && contains(matrix.test-case, 'proactive-message') == false && contains(matrix.test-case, 'upgrade') == false && contains(matrix.test-case, 'chef-bot') == false uses: actions/checkout@v3 with: repository: OfficeDev/TeamsFx-Samples @@ -372,6 +374,14 @@ jobs: ref: main path: ./packages/tests/resource + - name: Download samples chef bot + if: contains(matrix.test-case, 'chef-bot') + uses: actions/checkout@v3 + with: + repository: microsoft/teams-ai + ref: main + path: ./packages/tests/resource + - name: Get VSCode & chromedriver working-directory: packages/tests run: | @@ -564,7 +574,7 @@ jobs: status=`echo $jobs | jq --arg case "$case" -r '.[] | select(.name == $case ) | .conclusion'` if [[ ! -z "$email" && ! "$emails" == *"$email"* && "$status" == "failure" ]]; then - emails="$emails;$email;zhendr@microsoft.com" + emails="$emails;$email;zhendr@microsoft.com;ccdevexperiencefc@microsoft.com" fi status=`echo $jobs | jq --arg case "$case" -r '.[] | select(.name == $case ) | .conclusion'` diff --git a/docs/images/visualstudio/debug/create-devtunnel-button.png b/docs/images/visualstudio/debug/create-devtunnel-button.png new file mode 100644 index 0000000000..a0a9b2b51d Binary files /dev/null and b/docs/images/visualstudio/debug/create-devtunnel-button.png differ diff --git a/docs/images/visualstudio/debug/debug-button.png b/docs/images/visualstudio/debug/debug-button.png new file mode 100644 index 0000000000..78ad12430c Binary files /dev/null and b/docs/images/visualstudio/debug/debug-button.png differ diff --git a/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png b/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png new file mode 100644 index 0000000000..86c0cc4f53 Binary files /dev/null and b/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png differ diff --git a/docs/images/visualstudio/debug/switch-to-copilot.png b/docs/images/visualstudio/debug/switch-to-copilot.png new file mode 100644 index 0000000000..758530f113 Binary files /dev/null and b/docs/images/visualstudio/debug/switch-to-copilot.png differ diff --git a/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png b/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png new file mode 100644 index 0000000000..5c847998e3 Binary files /dev/null and b/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png differ diff --git a/docs/images/visualstudio/debug/switch-to-outlook.png b/docs/images/visualstudio/debug/switch-to-outlook.png new file mode 100644 index 0000000000..35ac217e7d Binary files /dev/null and b/docs/images/visualstudio/debug/switch-to-outlook.png differ diff --git a/docs/images/visualstudio/debug/switch-to-teams.png b/docs/images/visualstudio/debug/switch-to-teams.png new file mode 100644 index 0000000000..55c4894330 Binary files /dev/null and b/docs/images/visualstudio/debug/switch-to-teams.png differ diff --git a/docs/images/visualstudio/debug/switch-to-test-tool.png b/docs/images/visualstudio/debug/switch-to-test-tool.png new file mode 100644 index 0000000000..10c276849b Binary files /dev/null and b/docs/images/visualstudio/debug/switch-to-test-tool.png differ diff --git a/packages/adaptivecards-tools-sdk/package.json b/packages/adaptivecards-tools-sdk/package.json index 0b0c109488..e67f6e65de 100644 --- a/packages/adaptivecards-tools-sdk/package.json +++ b/packages/adaptivecards-tools-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/adaptivecards-tools", - "version": "1.3.2", + "version": "1.3.3", "description": "Microsoft sdk for Adaptive Cards", "main": "lib/index.js", "types": "types/index.d.ts", @@ -47,7 +47,7 @@ "adaptive-expressions": "^4.20.0", "adaptivecards": "~2.10.0", "adaptivecards-templating": "^2.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^13.0.2", "react": "^17.0.2" }, "publishConfig": { diff --git a/packages/adaptivecards-tools-sdk/pnpm-lock.yaml b/packages/adaptivecards-tools-sdk/pnpm-lock.yaml index 99d351bd0a..e5b3a4f23f 100644 --- a/packages/adaptivecards-tools-sdk/pnpm-lock.yaml +++ b/packages/adaptivecards-tools-sdk/pnpm-lock.yaml @@ -15,8 +15,8 @@ dependencies: specifier: ^2.1.0 version: 2.1.0(adaptive-expressions@4.20.0) markdown-it: - specifier: ^12.3.2 - version: 12.3.2 + specifier: ^13.0.2 + version: 13.0.2 react: specifier: ^17.0.2 version: 17.0.2 @@ -920,8 +920,9 @@ packages: strip-ansi: 6.0.1 dev: true - /entities@2.1.0: - resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + /entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} dev: false /error-ex@1.3.2: @@ -1804,8 +1805,8 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /linkify-it@3.0.3: - resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + /linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} dependencies: uc.micro: 1.0.6 dev: false @@ -1912,13 +1913,13 @@ packages: semver: 6.3.1 dev: true - /markdown-it@12.3.2: - resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + /markdown-it@13.0.2: + resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} hasBin: true dependencies: argparse: 2.0.1 - entities: 2.1.0 - linkify-it: 3.0.3 + entities: 3.0.1 + linkify-it: 4.0.1 mdurl: 1.0.1 uc.micro: 1.0.6 dev: false diff --git a/packages/api/package.json b/packages/api/package.json index 1ac19622ef..e7ddd85165 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx-api", - "version": "0.22.6", + "version": "0.22.7", "description": "teamsfx framework api", "main": "build/index.js", "types": "build/index.d.ts", @@ -40,6 +40,7 @@ "@types/sinon": "^9.0.10", "@typescript-eslint/eslint-plugin": "^4.19.0", "@typescript-eslint/parser": "^4.19.0", + "chai": "4.3.4", "chai-as-promised": "^7.1.1", "chai-spies": "^1.0.0", "copyfiles": "^2.4.1", diff --git a/packages/cli/README.md b/packages/cli/README.md index 1a23cc619d..10e727f50f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -36,9 +36,6 @@ Telemetry collection is on by default. To opt out, please add the global option This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -## Extensibility Model - -Teams Toolkit CLI depends on [fx-core](/packages/fx-core) and [api](/packages/api) packages. [fx-core](/packages/fx-core) is designed to be extensible. See [EXTENSIBILITY.md](/packages/api/EXTENSIBILITY.md) for more information. ## Contributing diff --git a/packages/cli/package.json b/packages/cli/package.json index 5603205372..96a65cdab6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsapp-cli", - "version": "3.0.0-alpha", + "version": "3.0.0", "author": "Microsoft Corporation", "description": "", "license": "MIT", @@ -52,6 +52,7 @@ "@types/chai-as-promised": "^7.1.3", "@types/express": "^4.17.14", "@types/fs-extra": "^8.0.1", + "@types/inquirer": "7.3.3", "@types/keytar": "^4.4.2", "@types/lodash": "^4.14.170", "@types/mocha": "^8.0.4", @@ -113,6 +114,7 @@ "express": "^4.18.2", "figures": "^3.2.0", "fs-extra": "^9.1.0", + "inquirer": "^7.3.3", "lodash": "^4.17.21", "node-machine-id": "^1.1.12", "open": "^8.2.1", @@ -139,4 +141,4 @@ "npx eslint --cache --fix --quiet" ] } -} \ No newline at end of file +} diff --git a/packages/cli/pnpm-lock.yaml b/packages/cli/pnpm-lock.yaml index d0110f0391..6a09f2cca6 100644 --- a/packages/cli/pnpm-lock.yaml +++ b/packages/cli/pnpm-lock.yaml @@ -22,7 +22,7 @@ dependencies: version: 5.1.1 '@inquirer/prompts': specifier: ^3.3.0 - version: 3.3.0 + version: 3.3.2 '@inquirer/type': specifier: ^1.1.5 version: 1.1.5 @@ -59,6 +59,9 @@ dependencies: fs-extra: specifier: ^9.1.0 version: 9.1.0 + inquirer: + specifier: ^7.3.3 + version: 7.3.3 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -105,6 +108,9 @@ devDependencies: '@types/fs-extra': specifier: ^8.0.1 version: 8.0.1 + '@types/inquirer': + specifier: 7.3.3 + version: 7.3.3 '@types/keytar': specifier: ^4.4.2 version: 4.4.2 @@ -623,23 +629,23 @@ packages: - supports-color dev: true - /@inquirer/checkbox@1.5.0: - resolution: {integrity: sha512-3cKJkW1vIZAs4NaS0reFsnpAjP0azffYII4I2R7PTI7ZTMg5Y1at4vzXccOH3762b2c2L4drBhpJpf9uiaGNxA==} + /@inquirer/checkbox@1.5.2: + resolution: {integrity: sha512-CifrkgQjDkUkWexmgYYNyB5603HhTHI91vLFeQXh6qrTKiCMVASol01Rs1cv6LP/A2WccZSRlJKZhbaBIs/9ZA==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 figures: 3.2.0 dev: false - /@inquirer/confirm@2.0.15: - resolution: {integrity: sha512-hj8Q/z7sQXsF0DSpLQZVDhWYGN6KLM/gNjjqGkpKwBzljbQofGjn0ueHADy4HUY+OqDHmXuwk/bY+tZyIuuB0w==} + /@inquirer/confirm@2.0.17: + resolution: {integrity: sha512-EqzhGryzmGpy2aJf6LxJVhndxYmFs+m8cxXzf8nejb1DE3sabf6mUgBcp4J0jAUEiAcYzqmkqRr7LPFh/WdnXA==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false @@ -663,75 +669,95 @@ packages: wrap-ansi: 6.2.0 dev: false - /@inquirer/editor@1.2.13: - resolution: {integrity: sha512-gBxjqt0B9GLN0j6M/tkEcmcIvB2fo9Cw0f5NRqDTkYyB9AaCzj7qvgG0onQ3GVPbMyMbbP4tWYxrBOaOdKpzNA==} + /@inquirer/core@6.0.0: + resolution: {integrity: sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/type': 1.2.1 + '@types/mute-stream': 0.0.4 + '@types/node': 20.11.28 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + figures: 3.2.0 + mute-stream: 1.0.0 + run-async: 3.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /@inquirer/editor@1.2.15: + resolution: {integrity: sha512-gQ77Ls09x5vKLVNMH9q/7xvYPT6sIs5f7URksw+a2iJZ0j48tVS6crLqm2ugG33tgXHIwiEqkytY60Zyh5GkJQ==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 external-editor: 3.1.0 dev: false - /@inquirer/expand@1.1.14: - resolution: {integrity: sha512-yS6fJ8jZYAsxdxuw2c8XTFMTvMR1NxZAw3LxDaFnqh7BZ++wTQ6rSp/2gGJhMacdZ85osb+tHxjVgx7F+ilv5g==} + /@inquirer/expand@1.1.16: + resolution: {integrity: sha512-TGLU9egcuo+s7PxphKUCnJnpCIVY32/EwPCLLuu+gTvYiD8hZgx8Z2niNQD36sa6xcfpdLY6xXDBiL/+g1r2XQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 figures: 3.2.0 dev: false - /@inquirer/input@1.2.14: - resolution: {integrity: sha512-tISLGpUKXixIQue7jypNEShrdzJoLvEvZOJ4QRsw5XTfrIYfoWFqAjMQLerGs9CzR86yAI89JR6snHmKwnNddw==} + /@inquirer/input@1.2.16: + resolution: {integrity: sha512-Ou0LaSWvj1ni+egnyQ+NBtfM1885UwhRCMtsRt2bBO47DoC1dwtCa+ZUNgrxlnCHHF0IXsbQHYtIIjFGAavI4g==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false - /@inquirer/password@1.1.14: - resolution: {integrity: sha512-vL2BFxfMo8EvuGuZYlryiyAB3XsgtbxOcFs4H9WI9szAS/VZCAwdVqs8rqEeaAf/GV/eZOghIOYxvD91IsRWSg==} + /@inquirer/password@1.1.16: + resolution: {integrity: sha512-aZYZVHLUXZ2gbBot+i+zOJrks1WaiI95lvZCn1sKfcw6MtSSlYC8uDX8sTzQvAsQ8epHoP84UNvAIT0KVGOGqw==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/input': 1.2.14 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 dev: false - /@inquirer/prompts@3.3.0: - resolution: {integrity: sha512-BBCqdSnhNs+WziSIo4f/RNDu6HAj4R/Q5nMgJb5MNPFX8sJGCvj9BoALdmR0HTWXyDS7TO8euKj6W6vtqCQG7A==} + /@inquirer/prompts@3.3.2: + resolution: {integrity: sha512-k52mOMRvTUejrqyF1h8Z07chC+sbaoaUYzzr1KrJXyj7yaX7Nrh0a9vktv8TuocRwIJOQMaj5oZEmkspEcJFYQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/checkbox': 1.5.0 - '@inquirer/confirm': 2.0.15 - '@inquirer/core': 5.1.1 - '@inquirer/editor': 1.2.13 - '@inquirer/expand': 1.1.14 - '@inquirer/input': 1.2.14 - '@inquirer/password': 1.1.14 - '@inquirer/rawlist': 1.2.14 - '@inquirer/select': 1.3.1 + '@inquirer/checkbox': 1.5.2 + '@inquirer/confirm': 2.0.17 + '@inquirer/core': 6.0.0 + '@inquirer/editor': 1.2.15 + '@inquirer/expand': 1.1.16 + '@inquirer/input': 1.2.16 + '@inquirer/password': 1.1.16 + '@inquirer/rawlist': 1.2.16 + '@inquirer/select': 1.3.3 dev: false - /@inquirer/rawlist@1.2.14: - resolution: {integrity: sha512-xIYmDpYgfz2XGCKubSDLKEvadkIZAKbehHdWF082AyC2I4eHK44RUfXaoOAqnbqItZq4KHXS6jDJ78F2BmQvxg==} + /@inquirer/rawlist@1.2.16: + resolution: {integrity: sha512-pZ6TRg2qMwZAOZAV6TvghCtkr53dGnK29GMNQ3vMZXSNguvGqtOVc4j/h1T8kqGJFagjyfBZhUPGwNS55O5qPQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false - /@inquirer/select@1.3.1: - resolution: {integrity: sha512-EgOPHv7XOHEqiBwBJTyiMg9r57ySyW4oyYCumGp+pGyOaXQaLb2kTnccWI6NFd9HSi5kDJhF7YjA+3RfMQJ2JQ==} + /@inquirer/select@1.3.3: + resolution: {integrity: sha512-RzlRISXWqIKEf83FDC9ZtJ3JvuK1l7aGpretf41BCWYrvla2wU8W8MTRNMiPrPJ+1SIqrRC1nZdZ60hD9hRXLg==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 figures: 3.2.0 @@ -753,6 +779,11 @@ packages: resolution: {integrity: sha512-wmwHvHozpPo4IZkkNtbYenem/0wnfI6hvOcGKmPEa0DwuaH5XUQzFqy6OpEpjEegZMhYIk8HDYITI16BPLtrRA==} engines: {node: '>=14.18.0'} + /@inquirer/type@1.2.1: + resolution: {integrity: sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ==} + engines: {node: '>=18'} + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -969,6 +1000,13 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true + /@types/inquirer@7.3.3: + resolution: {integrity: sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==} + dependencies: + '@types/through': 0.0.33 + rxjs: 6.6.7 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1015,6 +1053,12 @@ packages: /@types/node@14.14.21: resolution: {integrity: sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==} + /@types/node@20.11.28: + resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.11.6: resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} dependencies: @@ -1061,6 +1105,12 @@ packages: resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} dev: true + /@types/through@0.0.33: + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + dependencies: + '@types/node': 14.14.21 + dev: true + /@types/underscore@1.11.0: resolution: {integrity: sha512-ipNAQLgRnG0EWN1cTtfdVHp5AyTW/PAMJ1PxLN4bAKSHbusSZbj48mIHiydQpN7GgQrYqwfnvZ573OVfJm5Nzg==} dev: true @@ -1850,7 +1900,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} @@ -1874,6 +1923,11 @@ packages: string-width: 4.2.3 dev: true + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: false + /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -3395,6 +3449,25 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} requiresBuild: true + /inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + dev: false + /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -4002,7 +4075,7 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} dependencies: - chalk: 4.1.0 + chalk: 4.1.2 is-unicode-supported: 0.1.0 dev: true @@ -4107,7 +4180,6 @@ packages: /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - dev: true /mimic-response@2.1.0: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} @@ -4201,6 +4273,10 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: false + /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4400,7 +4476,6 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 - dev: true /open@8.2.1: resolution: {integrity: sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==} @@ -4916,7 +4991,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -4942,6 +5016,11 @@ packages: glob: 10.3.10 dev: true + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: false + /run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} @@ -4953,6 +5032,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.14.1 + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -5537,7 +5622,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} @@ -5610,7 +5694,6 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -5948,7 +6031,7 @@ packages: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} requiresBuild: true dependencies: - string-width: 1.0.2 + string-width: 4.2.3 /wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} diff --git a/packages/cli/src/cmds/preview/constants.ts b/packages/cli/src/cmds/preview/constants.ts index 53f2c89520..46364dc567 100644 --- a/packages/cli/src/cmds/preview/constants.ts +++ b/packages/cli/src/cmds/preview/constants.ts @@ -104,7 +104,7 @@ export const doctorResult = { SideLoadingDisabled: "Your Microsoft 365 tenant admin hasn't enabled custom app upload permission for your account. You can't install your app to Teams!", NotSignIn: "No Microsoft 365 account login", - SignInSuccess: `Microsoft 365 Account (@account) is logged in and custom app upload permission is enabled`, + SignInSuccess: `Microsoft 365 Account (@account) is signed in and custom app upload permission is enabled`, SkipTrustingCert: "Skip trusting development certificate for localhost", HelpLink: `Please refer to @Link for more information.`, NgrokWarning: diff --git a/packages/cli/src/colorize.ts b/packages/cli/src/colorize.ts index 493d6317b7..811b3df03f 100644 --- a/packages/cli/src/colorize.ts +++ b/packages/cli/src/colorize.ts @@ -14,6 +14,7 @@ export enum TextType { Important = "important", Details = "details", // secondary text Commands = "commands", // commands, parameters, system inputs + Spinner = "spinner", } export function colorize(message: string, type: TextType): string { @@ -38,6 +39,8 @@ export function colorize(message: string, type: TextType): string { return chalk.gray(message); case TextType.Commands: return chalk.blueBright(message); + case TextType.Spinner: + return chalk.yellowBright(message); } } diff --git a/packages/cli/src/commands/common.ts b/packages/cli/src/commands/common.ts index a2aece3f21..587ceaa555 100644 --- a/packages/cli/src/commands/common.ts +++ b/packages/cli/src/commands/common.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { CLICommandOption } from "@microsoft/teamsfx-api"; +import { commands } from "../resource"; export const ProjectFolderOption: CLICommandOption = { name: "folder", @@ -68,7 +69,7 @@ export const IgnoreKeysOption: CLICommandOption = { export const ListFormatOption: CLICommandOption = { name: "format", shortName: "f", - description: "Specifies the format of the results.", + description: commands["list.templates"].options.format, type: "string", choices: ["table", "json"], default: "table", @@ -83,3 +84,18 @@ export const ShowDescriptionOption: CLICommandOption = { default: false, required: true, }; + +export const ConfigFilePathOption: CLICommandOption = { + type: "string", + name: "config-file-path", + shortName: "c", + description: "Specifies the path of the configuration yaml file.", +}; + +export const ValidateMethodOption: CLICommandOption = { + type: "string", + name: "validate-method", + shortName: "m", + choices: ["validation-rules", "test-cases"], + description: "Specifies validation method", +}; diff --git a/packages/cli/src/commands/engine.ts b/packages/cli/src/commands/engine.ts index ee1a04d39f..16fd547c21 100644 --- a/packages/cli/src/commands/engine.ts +++ b/packages/cli/src/commands/engine.ts @@ -204,7 +204,16 @@ class CLIEngine { // 6. version check const inputs = getSystemInputs(context.optionValues.projectPath as string); inputs.ignoreEnvInfo = true; - const skipCommands = ["new", "sample", "upgrade", "update", "package", "publish", "validate"]; + const skipCommands = [ + "new", + "sample", + "upgrade", + "update", + "package", + "publish", + "validate", + "deploy", + ]; if (!skipCommands.includes(context.command.name) && context.optionValues.projectPath) { const core = getFxCore(); const res = await core.projectVersionCheck(inputs); diff --git a/packages/cli/src/commands/models/account.ts b/packages/cli/src/commands/models/account.ts index 8899ff1b06..466690865e 100644 --- a/packages/cli/src/commands/models/account.ts +++ b/packages/cli/src/commands/models/account.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { accountLoginCommand } from "./accountLogin"; import { accountLogoutCommand } from "./accountLogout"; import { accountShowCommand } from "./accountShow"; @@ -9,6 +10,6 @@ import { accountShowCommand } from "./accountShow"; export const accountCommand: CLICommand = { name: "auth", aliases: ["account"], - description: "Manage Microsoft 365 and Azure accounts.", + description: commands.auth.description, commands: [accountShowCommand, accountLoginCommand, accountLogoutCommand], }; diff --git a/packages/cli/src/commands/models/accountLogin.ts b/packages/cli/src/commands/models/accountLogin.ts index 812abbb110..cb5e319bc5 100644 --- a/packages/cli/src/commands/models/accountLogin.ts +++ b/packages/cli/src/commands/models/accountLogin.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { accountLoginAzureCommand } from "./accountLoginAzure"; import { accountLoginM365Command } from "./accountLoginM365"; export const accountLoginCommand: CLICommand = { name: "login", - description: "Log in to Microsoft 365 or Azure account.", + description: commands["auth.login"].description, commands: [accountLoginM365Command, accountLoginAzureCommand], }; diff --git a/packages/cli/src/commands/models/accountLoginAzure.ts b/packages/cli/src/commands/models/accountLoginAzure.ts index 5db61d2898..635263ad13 100644 --- a/packages/cli/src/commands/models/accountLoginAzure.ts +++ b/packages/cli/src/commands/models/accountLoginAzure.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { UserError, err, ok, CLICommand } from "@microsoft/teamsfx-api"; +import { CLICommand, UserError, err, ok } from "@microsoft/teamsfx-api"; import AzureTokenProvider from "../../commonlib/azureLogin"; import { codeFlowLoginFormat, @@ -8,36 +8,37 @@ import { servicePrincipalLoginFormat, usageError, } from "../../commonlib/common/constant"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { accountUtils } from "./accountShow"; export const accountLoginAzureCommand: CLICommand = { name: "azure", - description: "Log in to Azure account.", + description: commands["auth.login.azure"].description, options: [ { name: "tenant", - description: "Authenticate with a specific Microsoft Entra tenant.", + description: commands["auth.login.azure"].options["tenant"], type: "string", default: "", }, { name: "service-principal", - description: "Authenticate Azure with a credential representing a service principal", + description: commands["auth.login.azure"].options["service-principal"], type: "boolean", default: false, }, { name: "username", shortName: "u", - description: "Client ID for service principal", + description: commands["auth.login.azure"].options.username, type: "string", default: "", }, { name: "password", shortName: "p", - description: "Provide client secret or a pem file with key and public certificate.", + description: commands["auth.login.azure"].options.password, type: "string", default: "", }, diff --git a/packages/cli/src/commands/models/accountLoginM365.ts b/packages/cli/src/commands/models/accountLoginM365.ts index e458f2d61e..5d5d1ffe56 100644 --- a/packages/cli/src/commands/models/accountLoginM365.ts +++ b/packages/cli/src/commands/models/accountLoginM365.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { CLICommand, ok } from "@microsoft/teamsfx-api"; import M365TokenProvider from "../../commonlib/m365Login"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { accountUtils } from "./accountShow"; export const accountLoginM365Command: CLICommand = { name: "m365", - description: "Log in to Microsoft 365 account.", + description: commands["auth.login.m365"].description, telemetry: { event: TelemetryEvent.AccountLoginM365, }, diff --git a/packages/cli/src/commands/models/accountLogout.ts b/packages/cli/src/commands/models/accountLogout.ts index cebb1b2f8f..bd8d61d60c 100644 --- a/packages/cli/src/commands/models/accountLogout.ts +++ b/packages/cli/src/commands/models/accountLogout.ts @@ -4,17 +4,17 @@ import { CLICommand, ok } from "@microsoft/teamsfx-api"; import AzureTokenProvider from "../../commonlib/azureLogin"; import { logger } from "../../commonlib/logger"; import M365TokenProvider from "../../commonlib/m365Login"; -import { cliSource } from "../../constants"; +import { commands, strings } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; export const accountLogoutCommand: CLICommand = { name: "logout", - description: "Log out of Microsoft 365 or Azure account.", + description: commands["auth.logout"].description, arguments: [ { type: "string", name: "service", - description: "Azure or Microsoft 365.", + description: commands["auth.logout"].arguments.service, choices: ["azure", "m365"], required: true, }, @@ -30,9 +30,9 @@ export const accountLogoutCommand: CLICommand = { ctx.telemetryProperties.service = "azure"; const result = await AzureTokenProvider.signout(); if (result) { - logger.info(`[${cliSource}] Successfully signed out of Azure.`); + logger.info(strings["account.logout.azure"]); } else { - logger.error(`[${cliSource}] Failed to sign out of Azure.`); + logger.error(strings["account.logout.azure.fail"]); } break; } @@ -40,9 +40,9 @@ export const accountLogoutCommand: CLICommand = { ctx.telemetryProperties.service = "m365"; const result = await M365TokenProvider.signout(); if (result) { - logger.info(`[${cliSource}] Successfully signed out of Microsoft 365.`); + logger.info(strings["account.logout.m365"]); } else { - logger.error(`[${cliSource}] Failed to sign out of Microsoft 365.`); + logger.error(strings["account.logout.m365.fail"]); } break; } diff --git a/packages/cli/src/commands/models/accountShow.ts b/packages/cli/src/commands/models/accountShow.ts index dcc3521335..7084c1d11d 100644 --- a/packages/cli/src/commands/models/accountShow.ts +++ b/packages/cli/src/commands/models/accountShow.ts @@ -9,8 +9,7 @@ import { checkIsOnline } from "../../commonlib/codeFlowLogin"; import { signedIn } from "../../commonlib/common/constant"; import { logger } from "../../commonlib/logger"; import M365TokenProvider from "../../commonlib/m365Login"; -import * as constants from "../../constants"; -import { strings } from "../../resource"; +import { commands, strings } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; class AccountUtils { @@ -37,7 +36,7 @@ class AccountUtils { return Promise.resolve(true); } else { if (commandType === "login") { - logger.outputError(`[${constants.cliSource}] Failed to sign in to Microsoft 365.`); + logger.outputError(strings["account.login.m365.fail"]); } } return Promise.resolve(result !== undefined); @@ -70,7 +69,7 @@ class AccountUtils { return Promise.resolve(true); } else { if (commandType === "login") { - logger.outputError(`[${constants.cliSource}] Failed to sign in to Azure.`); + logger.outputError(strings["account.login.azure.fail"]); } } return Promise.resolve(result !== undefined); @@ -86,7 +85,7 @@ export const accountUtils = new AccountUtils(); export const accountShowCommand: CLICommand = { name: "list", aliases: ["show"], - description: "Display all connected Microsoft 365 and Azure accounts.", + description: commands["auth.show"].description, telemetry: { event: TelemetryEvent.AccountShow, }, diff --git a/packages/cli/src/commands/models/add.ts b/packages/cli/src/commands/models/add.ts index 077e6e0714..accd76f140 100644 --- a/packages/cli/src/commands/models/add.ts +++ b/packages/cli/src/commands/models/add.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { addSPFxWebpartCommand } from "./addSPFxWebpart"; - export const addCommand: CLICommand = { name: "add", - description: "Add feature to your Microsoft Teams application.", + description: commands.add.description, commands: [addSPFxWebpartCommand], }; diff --git a/packages/cli/src/commands/models/addSPFxWebpart.ts b/packages/cli/src/commands/models/addSPFxWebpart.ts index c91876734b..aa44835d7e 100644 --- a/packages/cli/src/commands/models/addSPFxWebpart.ts +++ b/packages/cli/src/commands/models/addSPFxWebpart.ts @@ -3,12 +3,13 @@ import { CLICommand, Stage } from "@microsoft/teamsfx-api"; import { SPFxAddWebpartInputs, SPFxAddWebpartOptions } from "@microsoft/teamsfx-core"; import { getFxCore } from "../../activate"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; export const addSPFxWebpartCommand: CLICommand = { name: "spfx-web-part", - description: "Auto-hosted SPFx web part tightly integrated with Microsoft Teams.", + description: commands["add.spfx-web-part"].description, options: [...SPFxAddWebpartOptions, ProjectFolderOption], telemetry: { event: TelemetryEvent.AddWebpart, diff --git a/packages/cli/src/commands/models/create.ts b/packages/cli/src/commands/models/create.ts index a6c15e3023..166cbcaebc 100644 --- a/packages/cli/src/commands/models/create.ts +++ b/packages/cli/src/commands/models/create.ts @@ -16,7 +16,6 @@ import { CreateProjectOptions, MeArchitectureOptions, QuestionNames, - isApiCopilotPluginEnabled, } from "@microsoft/teamsfx-core"; import chalk from "chalk"; import { assign } from "lodash"; @@ -24,6 +23,7 @@ import * as path from "path"; import * as uuid from "uuid"; import { getFxCore } from "../../activate"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent, TelemetryProperty } from "../../telemetry/cliTelemetryEvents"; import { createSampleCommand } from "./createSample"; @@ -49,7 +49,7 @@ function adjustOptions(options: CLICommandOption[]) { export function getCreateCommand(): CLICommand { return { name: "new", - description: "Create a new Microsoft Teams application.", + description: commands.create.description, options: [...adjustOptions(CreateProjectOptions)], examples: [ { diff --git a/packages/cli/src/commands/models/createSample.ts b/packages/cli/src/commands/models/createSample.ts index ea79307066..057809f79e 100644 --- a/packages/cli/src/commands/models/createSample.ts +++ b/packages/cli/src/commands/models/createSample.ts @@ -9,14 +9,16 @@ import { } from "@microsoft/teamsfx-core"; import chalk from "chalk"; import { assign } from "lodash"; +import * as path from "path"; import * as uuid from "uuid"; import { getFxCore } from "../../activate"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent, TelemetryProperty } from "../../telemetry/cliTelemetryEvents"; -import * as path from "path"; + export const createSampleCommand: CLICommand = { name: "sample", - description: "Create an app from existing sample.", + description: commands["create.sample"].description, arguments: CreateSampleProjectArguments, options: CreateSampleProjectOptions, telemetry: { diff --git a/packages/cli/src/commands/models/deploy.ts b/packages/cli/src/commands/models/deploy.ts index 28a03c8dcf..923d6b1393 100644 --- a/packages/cli/src/commands/models/deploy.ts +++ b/packages/cli/src/commands/models/deploy.ts @@ -2,20 +2,28 @@ // Licensed under the MIT license. import { CLICommand, CLIContext, InputsWithProjectPath } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../activate"; -import { strings } from "../../resource"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; -import { EnvOption, IgnoreLoadEnvOption, ProjectFolderOption } from "../common"; +import { + ConfigFilePathOption, + EnvOption, + IgnoreLoadEnvOption, + ProjectFolderOption, +} from "../common"; export const deployCommand: CLICommand = { name: "deploy", - description: strings.command.deploy.description, - options: [EnvOption, ProjectFolderOption, IgnoreLoadEnvOption], + description: commands.deploy.description, + options: [EnvOption, ProjectFolderOption, IgnoreLoadEnvOption, ConfigFilePathOption], telemetry: { event: TelemetryEvent.Deploy, }, handler: async (ctx: CLIContext) => { const core = getFxCore(); const inputs = ctx.optionValues as InputsWithProjectPath; + if (inputs["config-file-path"]) { + process.env.TEAMSFX_CONFIG_FILE_PATH = inputs["config-file-path"]; + } const res = await core.deployArtifacts(inputs); return res; }, diff --git a/packages/cli/src/commands/models/entraAppUpdate.ts b/packages/cli/src/commands/models/entraAppUpdate.ts index a7d6336d41..f63cd3b65c 100644 --- a/packages/cli/src/commands/models/entraAppUpdate.ts +++ b/packages/cli/src/commands/models/entraAppUpdate.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { CLICommand, Inputs } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../activate"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EntraAppManifestFileOption, EnvOption, ProjectFolderOption } from "../common"; export const entraAppUpdateCommand: CLICommand = { name: "update", - description: "Update the Microsoft Entra app in the current application.", + description: commands["entra-app.update"].description, options: [EntraAppManifestFileOption, EnvOption, ProjectFolderOption], telemetry: { event: TelemetryEvent.UpdateAadApp, @@ -24,6 +25,6 @@ export const entraAppUpdateCommand: CLICommand = { export const entraAppCommand: CLICommand = { name: "entra-app", - description: "Manage the Microsoft Entra app in the current application.", + description: commands["entra-app"].description, commands: [entraAppUpdateCommand], }; diff --git a/packages/cli/src/commands/models/env.ts b/packages/cli/src/commands/models/env.ts index e0dd998d87..3f8aa2d467 100644 --- a/packages/cli/src/commands/models/env.ts +++ b/packages/cli/src/commands/models/env.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { envAddCommand } from "./envAdd"; import { envListCommand } from "./envList"; import { envResetCommand } from "./envReset"; export const envCommand: CLICommand = { name: "env", - description: "Manage environments.", + description: commands.env.description, commands: [envAddCommand, envListCommand, envResetCommand], }; diff --git a/packages/cli/src/commands/models/envAdd.ts b/packages/cli/src/commands/models/envAdd.ts index ea389d2f71..88be3683c3 100644 --- a/packages/cli/src/commands/models/envAdd.ts +++ b/packages/cli/src/commands/models/envAdd.ts @@ -9,12 +9,13 @@ import { } from "@microsoft/teamsfx-core"; import { getFxCore } from "../../activate"; import { WorkspaceNotSupported } from "../../cmds/preview/errors"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; export const envAddCommand: CLICommand = { name: "add", - description: "Add a new environment by copying from the specified environment.", + description: commands["env.add"].description, options: [...CreateEnvOptions, ProjectFolderOption], arguments: CreateEnvArguments, telemetry: { diff --git a/packages/cli/src/commands/models/envList.ts b/packages/cli/src/commands/models/envList.ts index 8b1106c5d6..e4b5ab547d 100644 --- a/packages/cli/src/commands/models/envList.ts +++ b/packages/cli/src/commands/models/envList.ts @@ -5,12 +5,13 @@ import { envUtil, isValidProjectV3 } from "@microsoft/teamsfx-core"; import os from "os"; import { WorkspaceNotSupported } from "../../cmds/preview/errors"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; export const envListCommand: CLICommand = { name: "list", - description: "List all environments.", + description: commands["env.list"].description, options: [ProjectFolderOption], telemetry: { event: TelemetryEvent.GrantPermission, diff --git a/packages/cli/src/commands/models/envReset.ts b/packages/cli/src/commands/models/envReset.ts index 1f151a6787..f3771ea58f 100644 --- a/packages/cli/src/commands/models/envReset.ts +++ b/packages/cli/src/commands/models/envReset.ts @@ -2,12 +2,13 @@ // Licensed under the MIT license. import { CLICommand, ok } from "@microsoft/teamsfx-api"; import { envUtil } from "@microsoft/teamsfx-core"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EnvFileOption, EnvOption, IgnoreKeysOption, ProjectFolderOption } from "../common"; export const envResetCommand: CLICommand = { name: "reset", - description: "Reset environment file.", + description: commands["env.reset"].description, options: [EnvOption, EnvFileOption, IgnoreKeysOption, ProjectFolderOption], telemetry: { event: TelemetryEvent.ResetEnvironment, diff --git a/packages/cli/src/commands/models/index.ts b/packages/cli/src/commands/models/index.ts index cc3022bb3d..a4b7ee71d2 100644 --- a/packages/cli/src/commands/models/index.ts +++ b/packages/cli/src/commands/models/index.ts @@ -18,7 +18,6 @@ export * from "./envList"; export * from "./list"; export * from "./listTemplates"; export * from "./listSamples"; -export * from "./m365"; export * from "./m365LaunchInfo"; export * from "./m365Sideloading"; export * from "./m365Unacquire"; @@ -30,8 +29,5 @@ export * from "./preview"; export * from "./provision"; export * from "./publish"; export * from "./root"; -export * from "./update"; -export * from "./updateAadApp"; -export * from "./updateTeamsApp"; export * from "./upgrade"; export * from "./validate"; diff --git a/packages/cli/src/commands/models/list.ts b/packages/cli/src/commands/models/list.ts index ce20be7e45..3613e1ac3b 100644 --- a/packages/cli/src/commands/models/list.ts +++ b/packages/cli/src/commands/models/list.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { listSamplesCommand } from "./listSamples"; import { listTemplatesCommand } from "./listTemplates"; export const listCommand: CLICommand = { name: "list", - description: "List available Microsoft Teams application templates and samples.", + description: commands.list.description, commands: [listSamplesCommand, listTemplatesCommand], }; diff --git a/packages/cli/src/commands/models/listSamples.ts b/packages/cli/src/commands/models/listSamples.ts index d980f67095..e77ebbde98 100644 --- a/packages/cli/src/commands/models/listSamples.ts +++ b/packages/cli/src/commands/models/listSamples.ts @@ -4,18 +4,19 @@ import { CLICommand, ok } from "@microsoft/teamsfx-api"; import chalk from "chalk"; import Table from "cli-table3"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { Sample, getTemplates } from "../../utils"; import { ListFormatOption, ShowDescriptionOption } from "../common"; export const listSamplesCommand: CLICommand = { name: "samples", - description: "List available Microsoft Teams application samples.", + description: commands["list.samples"].description, options: [ { name: "tag", shortName: "t", - description: "Specifies the tag to filter the samples.", + description: commands["list.samples"].options.tag, type: "string", }, ListFormatOption, diff --git a/packages/cli/src/commands/models/listTemplates.ts b/packages/cli/src/commands/models/listTemplates.ts index 76f7941d51..98cc05b196 100644 --- a/packages/cli/src/commands/models/listTemplates.ts +++ b/packages/cli/src/commands/models/listTemplates.ts @@ -5,12 +5,13 @@ import { CapabilityOptions } from "@microsoft/teamsfx-core"; import chalk from "chalk"; import Table from "cli-table3"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ListFormatOption } from "../common"; export const listTemplatesCommand: CLICommand = { name: "templates", - description: "List available Microsoft Teams application templates.", + description: commands["list.templates"].description, options: [ListFormatOption], defaultInteractiveOption: false, handler: (ctx) => { diff --git a/packages/cli/src/commands/models/m365.ts b/packages/cli/src/commands/models/m365.ts deleted file mode 100644 index 11dde6290d..0000000000 --- a/packages/cli/src/commands/models/m365.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -import { CLICommand } from "@microsoft/teamsfx-api"; -import { m365LaunchInfoCommand } from "./m365LaunchInfo"; -import { m365SideloadingCommand } from "./m365Sideloading"; -import { m365UnacquireCommand } from "./m365Unacquire"; - -export const m365Command: CLICommand = { - name: "m365", - description: "M365 App Management.", - commands: [m365SideloadingCommand, m365UnacquireCommand, m365LaunchInfoCommand], -}; diff --git a/packages/cli/src/commands/models/m365LaunchInfo.ts b/packages/cli/src/commands/models/m365LaunchInfo.ts index 702dd43335..e455c26333 100644 --- a/packages/cli/src/commands/models/m365LaunchInfo.ts +++ b/packages/cli/src/commands/models/m365LaunchInfo.ts @@ -1,24 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { CLICommand, LogLevel, err, ok } from "@microsoft/teamsfx-api"; +import { CLICommand, err, ok } from "@microsoft/teamsfx-api"; import { PackageService } from "@microsoft/teamsfx-core"; import { logger } from "../../commonlib/logger"; import { MissingRequiredOptionError } from "../../error"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { m365utils, sideloadingServiceEndpoint } from "./m365Sideloading"; export const m365LaunchInfoCommand: CLICommand = { name: "launchinfo", - description: "Get launch information of an acquired M365 App.", + description: commands.launchinfo.description, options: [ { name: "title-id", - description: "Title ID of the acquired M365 App.", + description: commands.launchinfo.options["title-id"], type: "string", }, { name: "manifest-id", - description: "Manifest ID of the acquired M365 App.", + description: commands.launchinfo.options["manifest-id"], type: "string", }, ], @@ -37,9 +38,6 @@ export const m365LaunchInfoCommand: CLICommand = { }, defaultInteractiveOption: false, handler: async (ctx) => { - // Command is preview, set log level to verbose - logger.logLevel = logger.logLevel > LogLevel.Verbose ? LogLevel.Verbose : logger.logLevel; - logger.warning("This command is in preview."); const packageService = new PackageService(sideloadingServiceEndpoint, logger); let titleId = ctx.optionValues["title-id"] as string; const manifestId = ctx.optionValues["manifest-id"] as string; diff --git a/packages/cli/src/commands/models/m365Sideloading.ts b/packages/cli/src/commands/models/m365Sideloading.ts index 1aee3283cf..33a26ab3b7 100644 --- a/packages/cli/src/commands/models/m365Sideloading.ts +++ b/packages/cli/src/commands/models/m365Sideloading.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { CLICommand, LogLevel, err, ok } from "@microsoft/teamsfx-api"; +import { CLICommand, err, ok } from "@microsoft/teamsfx-api"; import { PackageService, serviceEndpoint, serviceScope } from "@microsoft/teamsfx-core"; import { logger } from "../../commonlib/logger"; -import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; -import { ArgumentConflictError, MissingRequiredOptionError } from "../../error"; import M365TokenProvider from "../../commonlib/m365Login"; +import { ArgumentConflictError, MissingRequiredOptionError } from "../../error"; +import { commands } from "../../resource"; +import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; export const sideloadingServiceEndpoint = process.env.SIDELOADING_SERVICE_ENDPOINT ?? serviceEndpoint; @@ -48,16 +49,16 @@ export const m365utils = new M365Utils(); export const m365SideloadingCommand: CLICommand = { name: "install", aliases: ["sideloading"], - description: "Sideload a given application package across Microsoft 365.", + description: commands.install.description, options: [ { name: "file-path", - description: "Path to the App manifest zip package.", + description: commands.install.options["file-path"], type: "string", }, { name: "xml-path", - description: "Path to the XML manifest xml file.", + description: commands.install.options["xml-path"], type: "string", }, ], @@ -78,10 +79,6 @@ export const m365SideloadingCommand: CLICommand = { }, defaultInteractiveOption: false, handler: async (ctx) => { - // Command is preview, set log level to verbose - logger.logLevel = logger.logLevel > LogLevel.Verbose ? LogLevel.Verbose : logger.logLevel; - logger.warning("This command is in preview."); - const zipAppPackagePath = ctx.optionValues["file-path"] as string; const xmlPath = ctx.optionValues["xml-path"] as string; diff --git a/packages/cli/src/commands/models/m365Unacquire.ts b/packages/cli/src/commands/models/m365Unacquire.ts index f44a588515..9aef78fc3d 100644 --- a/packages/cli/src/commands/models/m365Unacquire.ts +++ b/packages/cli/src/commands/models/m365Unacquire.ts @@ -1,25 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { CLICommand, LogLevel, err, ok } from "@microsoft/teamsfx-api"; +import { CLICommand, err, ok } from "@microsoft/teamsfx-api"; import { PackageService } from "@microsoft/teamsfx-core"; import { logger } from "../../commonlib/logger"; import { MissingRequiredOptionError } from "../../error"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { m365utils, sideloadingServiceEndpoint } from "./m365Sideloading"; export const m365UnacquireCommand: CLICommand = { name: "uninstall", aliases: ["unacquire"], - description: "Remove an acquired M365 App.", + description: commands.uninstall.description, options: [ { name: "title-id", - description: "Title ID of the acquired M365 App.", + description: commands.uninstall.options["title-id"], type: "string", }, { name: "manifest-id", - description: "Manifest ID of the acquired M365 App.", + description: commands.uninstall.options["manifest-id"], type: "string", }, ], @@ -38,9 +39,6 @@ export const m365UnacquireCommand: CLICommand = { }, defaultInteractiveOption: false, handler: async (ctx) => { - // Command is preview, set log level to verbose - logger.logLevel = logger.logLevel > LogLevel.Verbose ? LogLevel.Verbose : logger.logLevel; - logger.warning("This command is in preview."); const packageService = new PackageService(sideloadingServiceEndpoint, logger); let titleId = ctx.optionValues["title-id"] as string; const manifestId = ctx.optionValues["manifest-id"] as string; diff --git a/packages/cli/src/commands/models/package.ts b/packages/cli/src/commands/models/package.ts index 3ffdb392fb..5d9246fd23 100644 --- a/packages/cli/src/commands/models/package.ts +++ b/packages/cli/src/commands/models/package.ts @@ -3,27 +3,26 @@ import { CLICommand, CLIContext, InputsWithProjectPath } from "@microsoft/teamsfx-api"; import { SelectTeamsManifestInputs, SelectTeamsManifestOptions } from "@microsoft/teamsfx-core"; import { getFxCore } from "../../activate"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EnvOption, ProjectFolderOption } from "../common"; export const packageCommand: CLICommand = { name: "package", - description: "Build your Microsoft Teams app into a package for publishing.", + description: commands.package.description, options: [ ...SelectTeamsManifestOptions, { name: "output-zip-path", type: "string", shortName: "oz", - description: - "Specifies the output path of the zipped app package, defaults to '${folder}/appPackage/build/appPackage.${env}.zip'.", + description: commands.package.options["output-zip-path"], }, { name: "output-manifest-path", type: "string", shortName: "om", - description: - "Specifies the output path of the generated manifest path, defaults to '${folder}/appPackage/build/manifest.${env}.json'", + description: commands.package.options["output-manifest-path"], }, EnvOption, ProjectFolderOption, diff --git a/packages/cli/src/commands/models/permission.ts b/packages/cli/src/commands/models/permission.ts index 59c8cb71f3..320c639d9f 100644 --- a/packages/cli/src/commands/models/permission.ts +++ b/packages/cli/src/commands/models/permission.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand } from "@microsoft/teamsfx-api"; +import { commands } from "../../resource"; import { permissionGrantCommand } from "./permissionGrant"; import { permissionStatusCommand } from "./permissionStatus"; export const permissionCommand: CLICommand = { name: "collaborator", aliases: ["permission"], - description: - "Check, grant and list permissions for who can access and manage Microsoft Teams application and Microsoft Entra application.", + description: commands.collaborator.description, commands: [permissionStatusCommand, permissionGrantCommand], }; diff --git a/packages/cli/src/commands/models/permissionGrant.ts b/packages/cli/src/commands/models/permissionGrant.ts index 352457c8d4..e6e6fecbf8 100644 --- a/packages/cli/src/commands/models/permissionGrant.ts +++ b/packages/cli/src/commands/models/permissionGrant.ts @@ -4,9 +4,10 @@ import { CLICommand, InputsWithProjectPath, err, ok } from "@microsoft/teamsfx-a import { PermissionGrantInputs, PermissionGrantOptions } from "@microsoft/teamsfx-core"; import { getFxCore } from "../../activate"; import { logger } from "../../commonlib/logger"; +import { MissingRequiredOptionError } from "../../error"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; -import { MissingRequiredOptionError } from "../../error"; export const azureMessage = "Notice: Azure resources permission needs to be handled by subscription owner since privileged account is " + @@ -21,7 +22,7 @@ export const spfxMessage = export const permissionGrantCommand: CLICommand = { name: "grant", - description: "Grant permission for another account.", + description: commands["collaborator.grant"].description, options: [...PermissionGrantOptions, ProjectFolderOption], telemetry: { event: TelemetryEvent.GrantPermission, diff --git a/packages/cli/src/commands/models/permissionStatus.ts b/packages/cli/src/commands/models/permissionStatus.ts index 4a9b280601..1fca548c39 100644 --- a/packages/cli/src/commands/models/permissionStatus.ts +++ b/packages/cli/src/commands/models/permissionStatus.ts @@ -4,19 +4,20 @@ import { CLICommand, InputsWithProjectPath, err, ok } from "@microsoft/teamsfx-a import { PermissionListInputs, PermissionListOptions } from "@microsoft/teamsfx-core"; import { getFxCore } from "../../activate"; import { logger } from "../../commonlib/logger"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; import { azureMessage, spfxMessage } from "./permissionGrant"; export const permissionStatusCommand: CLICommand = { name: "status", - description: "Check user's permission.", + description: commands["collaborator.status"].description, options: [ ...PermissionListOptions, { name: "all", shortName: "a", - description: "Whether to list all collaborators.", + description: commands["collaborator.status"].options["all"], type: "boolean", required: false, }, diff --git a/packages/cli/src/commands/models/preview.ts b/packages/cli/src/commands/models/preview.ts index a8f1084a5e..746cdf98eb 100644 --- a/packages/cli/src/commands/models/preview.ts +++ b/packages/cli/src/commands/models/preview.ts @@ -19,12 +19,13 @@ import { import * as constants from "../../cmds/preview/constants"; import { localTelemetryReporter } from "../../cmds/preview/localTelemetryReporter"; import PreviewEnv from "../../cmds/preview/previewEnv"; +import { commands } from "../../resource"; import { TelemetryEvent, TelemetryProperty } from "../../telemetry/cliTelemetryEvents"; import { ProjectFolderOption } from "../common"; export const previewCommand: CLICommand = { name: "preview", - description: "Preview the current application.", + description: commands.preview.description, options: [ ...PreviewTeamsAppOptions.map((option) => { if (option.name === "teams-manifest-file") { diff --git a/packages/cli/src/commands/models/provision.ts b/packages/cli/src/commands/models/provision.ts index 9426379de2..0004e051d1 100644 --- a/packages/cli/src/commands/models/provision.ts +++ b/packages/cli/src/commands/models/provision.ts @@ -1,28 +1,28 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { CLICommand, CLIContext, InputsWithProjectPath } from "@microsoft/teamsfx-api"; +import { CoreQuestionNames } from "@microsoft/teamsfx-core"; +import { newResourceGroupOption } from "@microsoft/teamsfx-core/build/question/other"; import { getFxCore } from "../../activate"; -import { strings } from "../../resource"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EnvOption, IgnoreLoadEnvOption, ProjectFolderOption } from "../common"; -import { CoreQuestionNames } from "@microsoft/teamsfx-core"; -import { newResourceGroupOption } from "@microsoft/teamsfx-core/build/question/other"; export const provisionCommand: CLICommand = { name: "provision", - description: strings.command.provision.description, + description: commands.provision.description, options: [ EnvOption, ProjectFolderOption, { name: "resource-group", - description: "Specifies resource group name.", + description: commands.provision.options["resource-group"], type: "string", hidden: true, }, { name: "region", - description: "Specifies resource group region.", + description: commands.provision.options.region, type: "string", hidden: true, }, diff --git a/packages/cli/src/commands/models/publish.ts b/packages/cli/src/commands/models/publish.ts index f52ec53d08..ad36bb50fa 100644 --- a/packages/cli/src/commands/models/publish.ts +++ b/packages/cli/src/commands/models/publish.ts @@ -2,13 +2,13 @@ // Licensed under the MIT license. import { CLICommand, CLIContext, InputsWithProjectPath } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../activate"; -import { strings } from "../../resource"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EnvOption, IgnoreLoadEnvOption, ProjectFolderOption } from "../common"; export const publishCommand: CLICommand = { name: "publish", - description: strings.command.publish.description, + description: commands.publish.description, options: [EnvOption, ProjectFolderOption, IgnoreLoadEnvOption], telemetry: { event: TelemetryEvent.Publish, diff --git a/packages/cli/src/commands/models/root.ts b/packages/cli/src/commands/models/root.ts index a3c6203c4f..a83806e64e 100644 --- a/packages/cli/src/commands/models/root.ts +++ b/packages/cli/src/commands/models/root.ts @@ -25,11 +25,12 @@ import { teamsappPublishCommand } from "./teamsapp/publish"; import { teamsappUpdateCommand } from "./teamsapp/update"; import { teamsappValidateCommand } from "./teamsapp/validate"; import { upgradeCommand } from "./upgrade"; +import { commands } from "../../resource"; export const helpCommand: CLICommand = { name: "help", - description: "Show Microsoft Teams Toolkit CLI help.", - handler: (ctx) => { + description: commands.help.description, + handler: () => { const helpText = helper.formatHelp(rootCommand, undefined); logger.info(helpText); return ok(undefined); @@ -69,37 +70,37 @@ export const rootCommand: CLICommand = { type: "boolean", name: "version", shortName: "v", - description: "Display Microsoft Teams Toolkit CLI version.", + description: commands.root.options.version, }, { type: "boolean", name: "help", shortName: "h", - description: "Show Microsoft Teams Toolkit CLI help.", + description: commands.root.options.help, }, { type: "boolean", name: "interactive", shortName: "i", - description: "Run the command in interactive mode.", + description: commands.root.options.interactive, default: true, }, { type: "boolean", name: "debug", - description: "Print debug information.", + description: commands.root.options.debug, default: false, }, { type: "boolean", name: "verbose", - description: "Print diagnostic information.", + description: commands.root.options.verbose, default: false, }, { type: "boolean", name: "telemetry", - description: "Whether to enable telemetry.", + description: commands.root.options.telemetry, default: true, }, ], diff --git a/packages/cli/src/commands/models/teamsapp/doctor.ts b/packages/cli/src/commands/models/teamsapp/doctor.ts index 95c916bcbe..3f8211f6f8 100644 --- a/packages/cli/src/commands/models/teamsapp/doctor.ts +++ b/packages/cli/src/commands/models/teamsapp/doctor.ts @@ -12,20 +12,19 @@ import { assembleError, getSideloadingStatus, } from "@microsoft/teamsfx-core"; -import { getFxCore } from "../../../activate"; -// import * as constants from "../../../cmds/preview/constants"; import * as util from "util"; +import { getFxCore } from "../../../activate"; import { DoneText, TextType, WarningText, colorize } from "../../../colorize"; import { signedOut } from "../../../commonlib/common/constant"; import { logger } from "../../../commonlib/logger"; import M365TokenInstance from "../../../commonlib/m365Login"; import { cliSource } from "../../../constants"; -import { strings } from "../../../resource"; +import { commands, strings } from "../../../resource"; import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents"; export const teamsappDoctorCommand: CLICommand = { name: "doctor", - description: "Prerequiste checker for building Microsoft Teams apps.", + description: commands.doctor.description, options: [], telemetry: { event: TelemetryEvent.Doctor, diff --git a/packages/cli/src/commands/models/teamsapp/package.ts b/packages/cli/src/commands/models/teamsapp/package.ts index 8b36efce27..8470d078ba 100644 --- a/packages/cli/src/commands/models/teamsapp/package.ts +++ b/packages/cli/src/commands/models/teamsapp/package.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { CLICommand, TeamsAppInputs } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../../activate"; +import { commands } from "../../../resource"; import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents"; import { EnvFileOption, @@ -14,7 +15,7 @@ import { export const teamsappPackageCommand: CLICommand = { name: "package", - description: "Build your Microsoft Teams app into a package for publishing.", + description: commands.package.description, options: [ TeamsAppManifestFileOption, TeamsAppOuputPackageOption, diff --git a/packages/cli/src/commands/models/teamsapp/publish.ts b/packages/cli/src/commands/models/teamsapp/publish.ts index 0a87c01daa..f0a9f843ad 100644 --- a/packages/cli/src/commands/models/teamsapp/publish.ts +++ b/packages/cli/src/commands/models/teamsapp/publish.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { CLICommand, TeamsAppInputs, err } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../../activate"; -import { strings } from "../../../resource"; +import { commands } from "../../../resource"; import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents"; import { EnvFileOption, @@ -17,7 +17,7 @@ import { validateArgumentConflict } from "./update"; export const teamsappPublishCommand: CLICommand = { name: "publish", - description: strings.command.publish.description, + description: commands.publish.description, options: [ TeamsAppManifestFileOption, TeamsAppPackageOption, diff --git a/packages/cli/src/commands/models/teamsapp/update.ts b/packages/cli/src/commands/models/teamsapp/update.ts index e888f29a4a..c6a1ec38f1 100644 --- a/packages/cli/src/commands/models/teamsapp/update.ts +++ b/packages/cli/src/commands/models/teamsapp/update.ts @@ -3,6 +3,7 @@ import { CLICommand, Result, TeamsAppInputs, err, ok } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../../activate"; import { ArgumentConflictError } from "../../../error"; +import { commands } from "../../../resource"; import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents"; import { EnvFileOption, @@ -16,7 +17,7 @@ import { export const teamsappUpdateCommand: CLICommand = { name: "update", - description: "Update the Microsoft Teams App manifest to Teams Developer Portal.", + description: commands.update.description, options: [ TeamsAppManifestFileOption, TeamsAppPackageOption, diff --git a/packages/cli/src/commands/models/teamsapp/validate.ts b/packages/cli/src/commands/models/teamsapp/validate.ts index 280063ce7f..4a003a75b4 100644 --- a/packages/cli/src/commands/models/teamsapp/validate.ts +++ b/packages/cli/src/commands/models/teamsapp/validate.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { CLICommand, TeamsAppInputs, err } from "@microsoft/teamsfx-api"; +import { CLICommand, CLICommandOption, TeamsAppInputs, err } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../../activate"; +import { commands } from "../../../resource"; import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents"; import { EnvFileOption, @@ -11,21 +12,15 @@ import { TeamsAppOuputPackageOption, TeamsAppOutputManifestFileOption, TeamsAppPackageOption, + ValidateMethodOption, } from "../../common"; import { validateArgumentConflict } from "./update"; +import { isAsyncAppValidationEnabled } from "../../../../../fx-core/build"; export const teamsappValidateCommand: CLICommand = { name: "validate", - description: "Validate the Microsoft Teams app using manifest schema or validation rules.", - options: [ - TeamsAppManifestFileOption, - TeamsAppPackageOption, - TeamsAppOuputPackageOption, - TeamsAppOutputManifestFileOption, - EnvOption, - EnvFileOption, - ProjectFolderOption, - ], + description: commands.validate.description, + options: getOptions(), telemetry: { event: TelemetryEvent.ValidateManifest, }, @@ -41,3 +36,21 @@ export const teamsappValidateCommand: CLICommand = { return res; }, }; + +function getOptions(): CLICommandOption[] { + const options = [ + TeamsAppManifestFileOption, + TeamsAppPackageOption, + TeamsAppOuputPackageOption, + TeamsAppOutputManifestFileOption, + EnvOption, + EnvFileOption, + ProjectFolderOption, + ]; + + if (isAsyncAppValidationEnabled()) { + options.push(ValidateMethodOption); + } + + return options; +} diff --git a/packages/cli/src/commands/models/update.ts b/packages/cli/src/commands/models/update.ts deleted file mode 100644 index 05d70ec9db..0000000000 --- a/packages/cli/src/commands/models/update.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -import { CLICommand } from "@microsoft/teamsfx-api"; -import { updateAadAppCommand } from "./updateAadApp"; -import { updateTeamsAppCommand } from "./updateTeamsApp"; - -export const updateCommand: CLICommand = { - name: "update", - description: "Update the specific application manifest file.", - commands: [updateAadAppCommand, updateTeamsAppCommand], -}; diff --git a/packages/cli/src/commands/models/updateAadApp.ts b/packages/cli/src/commands/models/updateAadApp.ts deleted file mode 100644 index 8aba3a921f..0000000000 --- a/packages/cli/src/commands/models/updateAadApp.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -import { CLICommand, InputsWithProjectPath } from "@microsoft/teamsfx-api"; -import { DeployAadManifestInputs, DeployAadManifestOptions } from "@microsoft/teamsfx-core"; -import { getFxCore } from "../../activate"; -import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; -import { ProjectFolderOption } from "../common"; -import * as path from "path"; - -export const updateAadAppCommand: CLICommand = { - name: "aad-app", - description: "Update the Microsoft Entra App in the current application.", - options: [...DeployAadManifestOptions, ProjectFolderOption], - telemetry: { - event: TelemetryEvent.UpdateAadApp, - }, - defaultInteractiveOption: false, - handler: async (ctx) => { - const inputs = ctx.optionValues as DeployAadManifestInputs & InputsWithProjectPath; - inputs.ignoreEnvInfo = false; - if (inputs["manifest-file-path"]) { - if (!path.isAbsolute(inputs["manifest-file-path"])) { - inputs["manifest-file-path"] = path.join(inputs.projectPath!, inputs["manifest-file-path"]); - } - } - const core = getFxCore(); - const res = await core.deployAadManifest(inputs); - return res; - }, -}; diff --git a/packages/cli/src/commands/models/updateTeamsApp.ts b/packages/cli/src/commands/models/updateTeamsApp.ts deleted file mode 100644 index eb366a4d84..0000000000 --- a/packages/cli/src/commands/models/updateTeamsApp.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -import { CLICommand, Result, err, ok } from "@microsoft/teamsfx-api"; -import { SelectTeamsManifestInputs, SelectTeamsManifestOptions } from "@microsoft/teamsfx-core"; -import { getFxCore } from "../../activate"; -import { MissingRequiredOptionError } from "../../error"; -import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; -import { EnvOption, ProjectFolderOption } from "../common"; -import * as path from "path"; - -export const updateTeamsAppCommand: CLICommand = { - name: "teams-app", - description: "Update the Microsoft Teams App manifest to Teams Developer Portal.", - options: [...SelectTeamsManifestOptions, EnvOption, ProjectFolderOption], - telemetry: { - event: TelemetryEvent.UpdateTeamsApp, - }, - defaultInteractiveOption: false, - handler: async (ctx) => { - const inputs = ctx.optionValues as SelectTeamsManifestInputs; - if (inputs["manifest-path"]) { - if (!path.isAbsolute(inputs["manifest-path"])) { - inputs["manifest-path"] = path.join(inputs.projectPath!, inputs["manifest-path"]); - } - } - const validateInputsRes = validateInputs(ctx.command.fullName, inputs); - if (validateInputsRes.isErr()) { - return err(validateInputsRes.error); - } - - const core = getFxCore(); - const res = await core.deployTeamsManifest(inputs); - return res; - }, -}; - -function validateInputs( - fullName: string, - inputs: SelectTeamsManifestInputs -): Result { - if (inputs["manifest-path"] && !inputs.env) { - return err(new MissingRequiredOptionError(fullName, "--env")); - } - return ok(undefined); -} diff --git a/packages/cli/src/commands/models/upgrade.ts b/packages/cli/src/commands/models/upgrade.ts index 473ec68dc3..6f659329aa 100644 --- a/packages/cli/src/commands/models/upgrade.ts +++ b/packages/cli/src/commands/models/upgrade.ts @@ -2,18 +2,18 @@ // Licensed under the MIT license. import { CLICommand, InputsWithProjectPath } from "@microsoft/teamsfx-api"; import { getFxCore } from "../../activate"; -import { strings } from "../../resource"; +import { commands } from "../../resource"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import UI from "../../userInteraction"; export const upgradeCommand: CLICommand = { name: "upgrade", - description: strings.command.upgrade.description, + description: commands.upgrade.description, options: [ { name: "force", shortName: "f", - description: strings.command.upgrade.options.force, + description: commands.upgrade.options.force, type: "boolean", default: false, required: true, @@ -34,7 +34,7 @@ export const upgradeCommand: CLICommand = { } const core = getFxCore(); const res = await core.phantomMigrationV3(inputs); - if (res.isOk()) await UI.showMessage("info", strings.command.upgrade.success, false); + if (res.isOk()) await UI.showMessage("info", commands.upgrade.success, false); return res; }, }; diff --git a/packages/cli/src/commands/models/validate.ts b/packages/cli/src/commands/models/validate.ts index 367621e39b..743920294f 100644 --- a/packages/cli/src/commands/models/validate.ts +++ b/packages/cli/src/commands/models/validate.ts @@ -7,10 +7,11 @@ import { ArgumentConflictError, MissingRequiredOptionError } from "../../error"; import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents"; import { EnvOption, ProjectFolderOption } from "../common"; import * as path from "path"; +import { commands } from "../../resource"; export const validateCommand: CLICommand = { name: "validate", - description: "Validate the Microsoft Teams app using manifest schema or validation rules.", + description: commands.validate.description, options: [...ValidateTeamsAppOptions, EnvOption, ProjectFolderOption], telemetry: { event: TelemetryEvent.ValidateManifest, diff --git a/packages/cli/src/error.ts b/packages/cli/src/error.ts index ac4b2015d3..3df7d01f1c 100644 --- a/packages/cli/src/error.ts +++ b/packages/cli/src/error.ts @@ -9,7 +9,7 @@ import { UserError, } from "@microsoft/teamsfx-api"; import * as constants from "./constants"; -import { strings } from "./resource"; +import { errors as strings } from "./resource"; import * as util from "util"; import { helper } from "./commands/helper"; diff --git a/packages/cli/src/resource/commands.json b/packages/cli/src/resource/commands.json new file mode 100644 index 0000000000..42295b3a94 --- /dev/null +++ b/packages/cli/src/resource/commands.json @@ -0,0 +1,164 @@ +{ + "root": { + "description": "Microsoft Teams Toolkit CLI.", + "options": { + "version": "Display Microsoft Teams Toolkit CLI version.", + "help": "Show Microsoft Teams Toolkit CLI help.", + "interactive": "Run the command in interactive mode.", + "debug": "Print debug information.", + "verbose": "Print diagnostic information.", + "telemetry": "Whether to enable telemetry." + } + }, + "help": { + "description": "Show Microsoft Teams Toolkit CLI help.", + "arguments": { + "command": "The command to get help for." + } + }, + "auth": { + "description": "Manage Microsoft 365 and Azure accounts." + }, + "auth.login": { + "description": "Log in to Microsoft 365 or Azure account." + }, + "auth.login.azure": { + "description": "Log in to Azure account.", + "options": { + "tenant": "Authenticate with a specific Microsoft Entra tenant.", + "service-principal": "Authenticate Azure with a credential representing a service principal", + "username": "Client ID for service principal", + "password": "Provide client secret or a pem file with key and public certificate." + } + }, + "auth.login.m365": { + "description": "Log in to Microsoft 365 account." + }, + "auth.logout": { + "description": "Log out of Microsoft 365 or Azure account.", + "arguments": { + "service": "Azure or Microsoft 365." + } + }, + "auth.show": { + "description": "Display all connected Microsoft 365 and Azure accounts." + }, + "add": { + "description": "Add feature to your Microsoft Teams application." + }, + "add.spfx-web-part": { + "description": "Auto-hosted SPFx web part tightly integrated with Microsoft Teams." + }, + "create": { + "description": "Create a new Microsoft Teams application." + }, + "create.sample": { + "description": "Create an app from existing sample." + }, + "collaborator": { + "description": "Check, grant and list permissions for who can access and manage Microsoft Teams application and Microsoft Entra application." + }, + "collaborator.grant": { + "description": "Grant permission for another account." + }, + "collaborator.status": { + "description": "Check user's permission.", + "options": { + "all": "Whether to list all collaborators." + } + }, + "deploy": { + "description": "Run the deploy stage in teamsapp.yml or teamsapp.local.yml." + }, + "doctor": { + "description": "Prerequiste checker for building Microsoft Teams apps." + }, + "entra-app": { + "description": "Manage the Microsoft Entra app in the current application." + }, + "entra-app.update": { + "description": "Update the Microsoft Entra app in the current application." + }, + "env": { + "description": "Manage environments." + }, + "env.add": { + "description": "Add a new environment by copying from the specified environment." + }, + "env.list": { + "description": "List all environments." + }, + "env.reset": { + "description": "Reset environment file." + }, + "list": { + "description": "List available Microsoft Teams application templates and samples." + }, + "list.samples": { + "description": "List available Microsoft Teams application samples.", + "options": { + "tag": "Specifies the tag to filter the samples." + } + }, + "list.templates": { + "description": "List available Microsoft Teams application templates.", + "options": { + "format": "Specifies the format of the results." + } + }, + "launchinfo": { + "description": "Get launch information of an acquired M365 App.", + "options": { + "title-id": "Title ID of the acquired M365 App.", + "manifest-id": "Manifest ID of the acquired M365 App." + } + }, + "install": { + "description": "Sideload a given application package across Microsoft 365.", + "options": { + "file-path": "Path to the App manifest zip package.", + "xml-path": "Path to the XML manifest xml file." + } + }, + "provision": { + "description": "Run the provision stage in teamsapp.yml or teamsapp.local.yml.", + "options": { + "resource-group": "Specifies resource group name.", + "region": "Specifies resource group region." + } + }, + "preview": { + "description": "Preview the current application." + }, + "package": { + "description": "Build your Microsoft Teams app into a package for publishing.", + "options": { + "output-zip-path": "Specifies the output path of the zipped app package, defaults to '${folder}/appPackage/build/appPackage.${env}.zip'.", + "output-manifest-path": "Specifies the output path of the generated manifest path, defaults to '${folder}/appPackage/build/manifest.${env}.json'" + } + }, + "publish": { + "description": "Run the publish stage in teamsapp.yml." + }, + "uninstall": { + "description": "Remove an acquired M365 App.", + "options": { + "title-id": "Title ID of the acquired M365 App.", + "manifest-id": "Manifest ID of the acquired M365 App." + } + }, + "update": { + "description": "Update the Microsoft Teams App manifest to Teams Developer Portal." + }, + "upgrade": { + "description": "Upgrade the project to work with the latest version of Teams Toolkit.", + "options": { + "force": "Force upgrade the project to work with the latest version of Teams Toolkit." + }, + "success": "Upgrade project successfully." + }, + "validate": { + "description": "Validate the Microsoft Teams app using manifest schema, validation rules, or test cases." + } +} + diff --git a/packages/cli/src/resource/errors.json b/packages/cli/src/resource/errors.json new file mode 100644 index 0000000000..85d0b7d8bb --- /dev/null +++ b/packages/cli/src/resource/errors.json @@ -0,0 +1,10 @@ +{ + "error.prefix": "(x) Error: ", + "error.MissingRequiredOptionError": "The command '%s' can not be executed for missing required option '%s'. Provide the option via '%s' and try again.", + "error.MissingRequiredArgumentError": "The command '%s' can not be executed for missing required argument '%s'. Provide the argument and try again.", + "error.ArgumentConflictError": "The command '%s' is designed to accept either argument '%s' or argument '%s', but not both simultaneously. Keep either of the two arguments and try again.", + "error.UnknownOptionError": "The command '%s' can not be executed for unknown option '%s'.", + "error.UnknownArgumentError": "The command '%s' can not be executed for unknown argument '%s'.", + "error.InvalidOptionErrorReason": "'%s' is not in the valid option list: %s.", + "error.InvalidChoiceError": "The command '%s' can not be executed for invalid choice '%s' for option/argument '%s', allowed values: %s." +} \ No newline at end of file diff --git a/packages/cli/src/resource/index.ts b/packages/cli/src/resource/index.ts index d4338450db..895a822003 100644 --- a/packages/cli/src/resource/index.ts +++ b/packages/cli/src/resource/index.ts @@ -2,4 +2,6 @@ // Licensed under the MIT license. import * as strings from "./strings.json"; -export { strings }; +import * as commands from "./commands.json"; +import * as errors from "./errors.json"; +export { strings, commands, errors }; diff --git a/packages/cli/src/resource/strings.json b/packages/cli/src/resource/strings.json index cd9b60bfbf..96e4710545 100644 --- a/packages/cli/src/resource/strings.json +++ b/packages/cli/src/resource/strings.json @@ -1,39 +1,21 @@ { - "account.login.azure": "You have successfully logged into Azure.", - "account.login.m365": "You have successfully logged into Microsoft 365.", + "account.login.azure": "Successfully signed into Azure.", + "account.login.azure.fail": "Unable to sign into Azure.", + "account.logout.azure": "Successfully signed out of Azure.", + "account.logout.azure.fail": "Unable to sign out of Azure.", + "account.login.m365": "Successfully signed into Microsoft 365.", + "account.login.m365.fail": "Unable to sign into Microsoft 365.", + "account.logout.m365": "Successfully signed out of Microsoft 365.", + "account.logout.m365.fail": "Unable to sign out of Microsoft 365.", "account.show.azure": "Your Azure account is: %s. Your subscriptions are: %s", "account.show.m365": "Your Microsoft 365 account is: %s.", "account.show.info": "Your %s account is: %s.", - "error.prefix": "(x) Error: ", - "error.MissingRequiredOptionError": "The command '%s' can not be executed for missing required option '%s'. Provide the option via '%s' and try again.", - "error.MissingRequiredArgumentError": "The command '%s' can not be executed for missing required argument '%s'. Provide the argument and try again.", - "error.ArgumentConflictError": "The command '%s' is designed to accept either argument '%s' or argument '%s', but not both simultaneously. Keep either of the two arguments and try again.", - "error.UnknownOptionError": "The command '%s' can not be executed for unknown option '%s'.", - "error.UnknownArgumentError": "The command '%s' can not be executed for unknown argument '%s'.", - "error.InvalidOptionErrorReason": "'%s' is not in the valid option list: %s.", - "error.InvalidChoiceError": "The command '%s' can not be executed for invalid choice '%s' for option/argument '%s', allowed values: %s.", "command": { - "provision": { - "description": "Run the provision stage in teamsapp.yml or teamsapp.local.yml." - }, - "deploy": { - "description": "Run the deploy stage in teamsapp.yml or teamsapp.local.yml." - }, - "publish": { - "description": "Run the publish stage in teamsapp.yml." - }, - "upgrade": { - "description": "Upgrade the project to work with the latest version of Teams Toolkit.", - "options": { - "force": "Force upgrade the project to work with the latest version of Teams Toolkit." - }, - "success": "Upgrade project successfully." - }, "doctor": { "account": { "SideLoadingDisabled": "Your Microsoft 365 tenant admin hasn't enabled custom app upload permission for your account. You can't install your app to Teams!", - "NotSignIn": "You have not logged in to your Microsoft 365 account yet. Please use teamsapp auth login command to login to your Microsoft 365 account.", - "SignInSuccess": "Microsoft 365 Account (%s) is logged in and custom app upload permission is enabled." + "NotSignIn": "You've not signed into your Microsoft 365 account yet. Please use teamsapp auth login command to sign into your Microsoft 365 account.", + "SignInSuccess": "Microsoft 365 Account (%s) is signed in and custom app upload permission is enabled." }, "node" : { "NotFound": "Cannot find Node.js. Node.js used for developing Teams apps with JavaScript or TypeScript using Teams Toolkit for Visual Studio Code. Visit https://nodejs.org to install the LTS version.", diff --git a/packages/cli/src/spinner.ts b/packages/cli/src/spinner.ts new file mode 100644 index 0000000000..3744fdf5c3 --- /dev/null +++ b/packages/cli/src/spinner.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TextType, colorize } from "./colorize"; + +const defaultSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const defaultTextType = TextType.Spinner; +const defaultRefreshInterval = 100; + +interface CustomizedSpinnerOptions { + spinnerFrames?: string[]; + textType?: TextType; + refreshInterval?: number; +} + +export class CustomizedSpinner { + public spinnerFrames: string[] = defaultSpinnerFrames; + public textType: TextType = defaultTextType; + public refreshInterval: number = defaultRefreshInterval; // refresh internal in milliseconds + private intervalId: NodeJS.Timeout | null = null; + + constructor(options: CustomizedSpinnerOptions = {}) { + if (options.spinnerFrames) { + this.spinnerFrames = options.spinnerFrames; + } + if (options.textType) { + this.textType = options.textType; + } + if (options.refreshInterval) { + this.refreshInterval = options.refreshInterval; + } + } + + public start(): void { + // hide cursor + process.stdout.write("\x1b[?25l"); + let currentFrameIndex = 0; + this.intervalId = setInterval(() => { + const frame = this.spinnerFrames[currentFrameIndex % this.spinnerFrames.length]; + const message = colorize(frame, this.textType); + process.stdout.write(`\r${message}`); + currentFrameIndex++; + }, this.refreshInterval); + } + + public stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + // show cursor + process.stdout.write("\x1b[?25h"); + } + } +} diff --git a/packages/cli/src/userInteraction.ts b/packages/cli/src/userInteraction.ts index eb31cfbe71..4d35015478 100644 --- a/packages/cli/src/userInteraction.ts +++ b/packages/cli/src/userInteraction.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { confirm, input, password } from "@inquirer/prompts"; +import { confirm, password } from "@inquirer/prompts"; +import { prompt } from "inquirer"; import { Colors, ConfirmConfig, @@ -43,9 +44,9 @@ import Progress from "./console/progress"; import ScreenManager from "./console/screen"; import { cliSource } from "./constants"; import { CheckboxChoice, SelectChoice, checkbox, select } from "./prompts"; -import { strings } from "./resource"; +import { errors } from "./resource"; import { getColorizedString } from "./utils"; - +import { CustomizedSpinner } from "./spinner"; /// TODO: input can be undefined type ValidationType = (input: T) => string | boolean | Promise; @@ -113,13 +114,17 @@ class CLIUserInteraction implements UserInteraction { return ok(defaultValue || ""); } ScreenManager.pause(); - const answer = await input({ - message, - default: defaultValue, - validate, - }); + const answer = await prompt([ + { + type: "input", + name: name, + message: message, + default: defaultValue, + validate: validate, + }, + ]); ScreenManager.continue(); - return ok(answer); + return ok(answer[name]); } async password( @@ -281,7 +286,7 @@ class CLIUserInteraction implements UserInteraction { const error = new InputValidationError( config.name, util.format( - strings["error.InvalidOptionErrorReason"], + errors["error.InvalidOptionErrorReason"], result.value, choices.map((choice) => choice.id).join(",") ) @@ -390,7 +395,7 @@ class CLIUserInteraction implements UserInteraction { const error = new InputValidationError( config.name, util.format( - strings["error.InvalidOptionErrorReason"], + errors["error.InvalidOptionErrorReason"], result.value.join(","), choices.map((choice) => choice.id).join(",") ) @@ -431,6 +436,8 @@ class CLIUserInteraction implements UserInteraction { if (config.validation || config.additionalValidationOnAccept) { validationFunc = async (input: string) => { let res: string | undefined = undefined; + const spinner = new CustomizedSpinner(); + spinner.start(); if (config.validation) { res = await config.validation(input); } @@ -438,7 +445,7 @@ class CLIUserInteraction implements UserInteraction { if (!res && !!config.additionalValidationOnAccept) { res = await config.additionalValidationOnAccept(input); } - + spinner.stop(); return res; }; } diff --git a/packages/cli/tests/unit/colorize.tests.ts b/packages/cli/tests/unit/colorize.tests.ts index 6b6cb5593f..86c1e31d7f 100644 --- a/packages/cli/tests/unit/colorize.tests.ts +++ b/packages/cli/tests/unit/colorize.tests.ts @@ -57,6 +57,9 @@ describe("colorize", () => { it("colorize - Commands", async () => { colorize("test", TextType.Commands); }); + it("colorize - Spinner", async () => { + colorize("test", TextType.Spinner); + }); it("replace template string", async () => { const template = "test %s"; const result = replaceTemplateString(template, "test"); diff --git a/packages/cli/tests/unit/commands.tests.ts b/packages/cli/tests/unit/commands.tests.ts index 0bafa94a3d..50f13eea6f 100644 --- a/packages/cli/tests/unit/commands.tests.ts +++ b/packages/cli/tests/unit/commands.tests.ts @@ -44,8 +44,6 @@ import { previewCommand, provisionCommand, publishCommand, - updateAadAppCommand, - updateTeamsAppCommand, upgradeCommand, validateCommand, } from "../../src/commands/models"; @@ -271,6 +269,18 @@ describe("CLI commands", () => { const res = await deployCommand.handler!(ctx); assert.isTrue(res.isOk()); }); + it("success for customized yaml path", async () => { + sandbox.stub(FxCore.prototype, "deployArtifacts").resolves(ok(undefined)); + const ctx: CLIContext = { + command: { ...deployCommand, fullName: "teamsfx" }, + optionValues: { "config-file-path": "fakePath" }, + globalOptionValues: {}, + argumentValues: [], + telemetryProperties: {}, + }; + const res = await deployCommand.handler!(ctx); + assert.isTrue(res.isOk()); + }); }); describe("envAddCommand", async () => { it("success", async () => { @@ -522,24 +532,6 @@ describe("CLI commands", () => { assert.isTrue(res.isErr()); }); }); - describe("updateAadAppCommand", async () => { - it("success", async () => { - sandbox.stub(FxCore.prototype, "deployAadManifest").resolves(ok(undefined)); - const ctx: CLIContext = { - command: { ...updateAadAppCommand, fullName: "teamsfx" }, - optionValues: { - env: "local", - projectPath: "./", - "manifest-file-path": "./aad.manifest.json", - }, - globalOptionValues: {}, - argumentValues: [], - telemetryProperties: {}, - }; - const res = await updateAadAppCommand.handler!(ctx); - assert.isTrue(res.isOk()); - }); - }); describe("entraAppUpdateCommand", async () => { it("success", async () => { sandbox.stub(FxCore.prototype, "deployAadManifest").resolves(ok(undefined)); @@ -558,36 +550,6 @@ describe("CLI commands", () => { assert.isTrue(res.isOk()); }); }); - describe("updateTeamsAppCommand", async () => { - it("success", async () => { - sandbox.stub(FxCore.prototype, "deployTeamsManifest").resolves(ok(undefined)); - const ctx: CLIContext = { - command: { ...updateTeamsAppCommand, fullName: "teamsfx" }, - optionValues: { env: "local" }, - globalOptionValues: {}, - argumentValues: [], - telemetryProperties: {}, - }; - const res = await updateTeamsAppCommand.handler!(ctx); - assert.isTrue(res.isOk()); - }); - - it("MissingRequiredOptionError", async () => { - sandbox.stub(FxCore.prototype, "deployTeamsManifest").resolves(ok(undefined)); - const ctx: CLIContext = { - command: { ...updateTeamsAppCommand, fullName: "teamsfx" }, - optionValues: { "manifest-path": "fakePath", projectPath: "./" }, - globalOptionValues: {}, - argumentValues: [], - telemetryProperties: {}, - }; - const res = await updateTeamsAppCommand.handler!(ctx); - assert.isTrue(res.isErr()); - if (res.isErr()) { - assert.equal(res.error.name, MissingRequiredOptionError.name); - } - }); - }); describe("upgradeCommand", async () => { it("success", async () => { sandbox.stub(FxCore.prototype, "phantomMigrationV3").resolves(ok(undefined)); @@ -1322,7 +1284,7 @@ describe("CLI read-only commands", () => { const accountRes = await checker.checkM365Account(); assert.isTrue(accountRes.isOk()); const account = (accountRes as any).value; - assert.include(account, "is logged in and custom app upload permission is enabled"); + assert.include(account, "is signed in and custom app upload permission is enabled"); }); it("checkM365Account - error", async () => { sandbox.stub(M365TokenProvider, "getStatus").resolves(err(new UserCancelError())); @@ -1331,7 +1293,7 @@ describe("CLI read-only commands", () => { const accountRes = await checker.checkM365Account(); assert.isTrue(accountRes.isOk()); const account = (accountRes as any).value; - assert.include(account, "You have not logged in"); + assert.include(account, "You've not signed into your Microsoft 365 account yet."); }); it("checkM365Account - error2", async () => { sandbox.stub(M365TokenProvider, "getStatus").rejects(new Error("test")); @@ -1366,7 +1328,7 @@ describe("CLI read-only commands", () => { const accountRes = await checker.checkM365Account(); assert.isTrue(accountRes.isOk()); const account = (accountRes as any).value; - assert.include(account, "is logged in and custom app upload permission is enabled"); + assert.include(account, "is signed in and custom app upload permission is enabled"); }); it("checkM365Account - no custom app upload permission", async () => { diff --git a/packages/cli/tests/unit/spinner.tests.ts b/packages/cli/tests/unit/spinner.tests.ts new file mode 100644 index 0000000000..e7672b874d --- /dev/null +++ b/packages/cli/tests/unit/spinner.tests.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { expect } from "chai"; +import "mocha"; +import sinon from "sinon"; +import { CustomizedSpinner } from "../../src/spinner"; +import { TextType } from "../../src/colorize"; + +describe("CustomizedSpinner", function () { + let clock: sinon.SinonFakeTimers; + let writeStub: sinon.SinonStub; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + writeStub = sinon.stub(process.stdout, "write"); + }); + + afterEach(() => { + clock.restore(); + writeStub.restore(); + }); + + describe("should correctly cycle through spinner frames on start", async () => { + it("", async () => { + const spinner = new CustomizedSpinner(); + spinner.start(); + + clock.tick(spinner.refreshInterval * 3); + + expect(writeStub.callCount).to.equal(4); + expect(writeStub.lastCall.args[0]).to.include(spinner.spinnerFrames[2]); + + spinner.stop(); + }); + }); + + describe("should hide and show the cursor on start and stop", async () => { + it("", async () => { + const spinner = new CustomizedSpinner(); + spinner.start(); + + expect(writeStub.firstCall.args[0]).to.equal("\x1b[?25l"); + + spinner.stop(); + + expect(writeStub.lastCall.args[0]).to.equal("\x1b[?25h"); + }); + }); + + describe("should allow custom spinner frames, text type, and refresh interval", async () => { + it("", async () => { + const customFrames = ["-", "\\", "|", "/"]; + const customTextType = TextType.Info; + const customInterval = 200; + const spinner = new CustomizedSpinner({ + spinnerFrames: customFrames, + textType: customTextType, + refreshInterval: customInterval, + }); + expect(spinner.spinnerFrames).to.deep.equal(customFrames); + expect(spinner.textType).to.equal(customTextType); + expect(spinner.refreshInterval).to.equal(customInterval); + }); + }); +}); diff --git a/packages/cli/tests/unit/telemetry/cliTelemetry.tests.ts b/packages/cli/tests/unit/telemetry/cliTelemetry.tests.ts index 4f8fa8f730..b7b0f58631 100644 --- a/packages/cli/tests/unit/telemetry/cliTelemetry.tests.ts +++ b/packages/cli/tests/unit/telemetry/cliTelemetry.tests.ts @@ -52,11 +52,11 @@ describe("Telemetry", function () { if (eventName === "UserError") { expect(properties[TelemetryProperty.ErrorType]).equals(TelemetryErrorType.UserError); expect(properties[TelemetryProperty.ErrorCode]).equals("ut.user"); - expect(properties[TelemetryProperty.ErrorMessage]).equals("UserError"); + // expect(properties[TelemetryProperty.ErrorMessage]).equals("UserError"); } else { expect(properties[TelemetryProperty.ErrorType]).equals(TelemetryErrorType.SystemError); expect(properties[TelemetryProperty.ErrorCode]).equals("ut.system"); - expect(properties[TelemetryProperty.ErrorMessage]).equals("SystemError"); + // expect(properties[TelemetryProperty.ErrorMessage]).equals("SystemError"); } }); const reporter = new CliTelemetryReporter("real", "real", "real", "real"); diff --git a/packages/cli/tests/unit/ui.tests.ts b/packages/cli/tests/unit/ui.tests.ts index 4f82908f19..3d44cd1879 100644 --- a/packages/cli/tests/unit/ui.tests.ts +++ b/packages/cli/tests/unit/ui.tests.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as prompts from "@inquirer/prompts"; +import inquirer from "inquirer"; import { Colors, LogLevel, @@ -373,7 +374,7 @@ describe("User Interaction Tests", function () { }); it("interactive", async () => { sandbox.stub(UI, "interactive").value(true); - sandbox.stub(prompts, "input").resolves("abc"); + sandbox.stub(inquirer, "prompt").resolves({ test: "abc" }); const result = await UI.input("test", "Input the password", "default string"); expect(result.isOk() ? result.value : result.error).equals("abc"); }); diff --git a/packages/cli/tests/unit/ui2.tests.ts b/packages/cli/tests/unit/ui2.tests.ts index 1b4d164001..b98a0fbf7c 100644 --- a/packages/cli/tests/unit/ui2.tests.ts +++ b/packages/cli/tests/unit/ui2.tests.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as inquirer from "@inquirer/prompts"; +import inquirer from "inquirer"; import { InputTextConfig, MultiSelectConfig, @@ -213,7 +213,7 @@ describe("UserInteraction(CLI) 2", () => { describe("selectFileOrInput", () => { it("happy path", async () => { - sandbox.stub(inquirer, "input").resolves("somevalue"); + sandbox.stub(inquirer, "prompt").resolves({ test: "somevalue" }); const res = await UI.selectFileOrInput({ name: "test", title: "test", diff --git a/packages/fx-core/package.json b/packages/fx-core/package.json index 7dd72d4b87..8fb29e1487 100644 --- a/packages/fx-core/package.json +++ b/packages/fx-core/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx-core", - "version": "2.0.6", + "version": "2.0.7", "main": "build/index.js", "types": "build/index.d.ts", "license": "MIT", @@ -41,6 +41,7 @@ "test:generator": "nyc mocha \"tests/component/generator/*.test.ts\"", "test:upgrade": "npx mocha \"tests/core/middleware/migration/*.test.ts\"", "test:officeAddinGenerator": "nyc mocha \"tests/component/generator/officeAddinGenerator.test.ts\"", + "test:officeXMLAddinGenerator": "nyc mocha \"tests/component/generator/officeXMLAddinGenerator.test.ts\"", "test:scriptDriver": "nyc mocha \"tests/component/driver/script/scriptDriver.test.ts\"", "test:rghelper": "nyc mocha \"tests/component/resourceGroupHelper.test.ts\"", "test:manifestUtil": "nyc mocha \"tests/component/resource/appManifest/manifestUtils.test.ts\"", diff --git a/packages/fx-core/resource/package.nls.json b/packages/fx-core/resource/package.nls.json index b5f30f0886..e356aa7547 100644 --- a/packages/fx-core/resource/package.nls.json +++ b/packages/fx-core/resource/package.nls.json @@ -1,20 +1,20 @@ { "core.provision.provision": "Provision", - "core.provision.learnMore": "Learn more", + "core.provision.learnMore": "More info", "core.provision.azureAccount": "Azure account: %s", "core.provision.azureSubscription": "Azure subscription: %s", "core.provision.m365Account": "Microsoft 365 account: %s", "core.provision.confirmEnvAndCostNotice": "Costs may apply based on usage. Do you want to provision resources in %s environment using listed accounts?", "core.deploy.confirmEnvNoticeV3": "Do you want to deploy resources in %s environment?", "core.provision.viewResources": "View provisioned resources", - "core.deploy.aadManifestSuccessNotice": "Your Microsoft Entra app has been deployed successfully. To view that, click \"Learn more\"", + "core.deploy.aadManifestSuccessNotice": "Your Microsoft Entra app has been deployed successfully. To view that, click \"More info\"", "core.deploy.aadManifestOnCLISuccessNotice": "Your Microsoft Entra app has been updated successfully.", - "core.deploy.aadManifestLearnMore": "Learn more", - "core.deploy.botTroubleShoot": "To troubleshoot your bot application in Azure, click \"Learn more\" for documentation.", - "core.deploy.botTroubleShoot.learnMore": "Learn more", + "core.deploy.aadManifestLearnMore": "More info", + "core.deploy.botTroubleShoot": "To troubleshoot your bot application in Azure, click \"More info\" for documentation.", + "core.deploy.botTroubleShoot.learnMore": "More info", "core.option.deploy": "Deploy", "core.option.confirm": "Confirm", - "core.option.learnMore": "Learn more", + "core.option.learnMore": "More info", "core.option.upgrade": "Upgrade", "core.option.moreInfo": "More Info", "core.progress.create": "Create", @@ -32,13 +32,13 @@ "core.migrationV3.aadManifestNotExist": "templates/appPackage/aad.manifest.template.json does not exist. You may be trying to upgrade a project created by Teams Toolkit for Visual Studio Code v3.x / Teams Toolkit CLI v0.x / Teams Toolkit for Visual Studio v17.3. Please install Teams Toolkit for Visual Studio Code v4.x / Teams Toolkit CLI v1.x / Teams Toolkit for Visual Studio v17.4 and run upgrade first.", "core.migrationV3.manifestNotExist": "templates/appPackage/manifest.template.json does not exist. You may be trying to upgrade a project created by Teams Toolkit for Visual Studio Code v3.x / Teams Toolkit CLI v0.x / Teams Toolkit for Visual Studio v17.3. Install Teams Toolkit for Visual Studio Code v4.x / Teams Toolkit CLI v1.x / Teams Toolkit for Visual Studio v17.4 and run upgrade first.", "core.migrationV3.manifestInvalid": "templates/appPackage/manifest.template.json is invalid.", - "core.migrationV3.abandonedProject": "This project is for previewing only and won't be supported by Teams Toolkit. To use Teams Toolkit, create a new project", - "core.migrationV3.notAllowedMigration": "The Teams Toolkit Pre-Release version has new project settings and doesn't work with older versions. Create a new project to try it, or run \"teamsapp upgrade\" to update your existing project.", - "core.projectVersionChecker.cliUseNewVersion": "Your TeamFx CLI version is outdated and doesn't support current project. Upgrade to the latest version using command below:\nnpm install -g @microsoft/teamsapp-cli@latest", + "core.migrationV3.abandonedProject": "This project is only for previewing and won't be supported by Teams Toolkit. Create a new project to use Teams Toolkit", + "core.migrationV3.notAllowedMigration": "Teams Toolkit Pre-Release version has new project settings and doesn't work with older versions. Create a new project to try it, or run \"teamsapp upgrade\" to update your existing project.", + "core.projectVersionChecker.cliUseNewVersion": "Your TeamFx CLI version is outdated and doesn't support current project. Upgrade to the latest version using below command:\nnpm install -g @microsoft/teamsapp-cli@latest", "core.projectVersionChecker.incompatibleProject": "The project isn't compatible with the current Teams Toolkit version.", "core.projectVersionChecker.vs.incompatibleProject": "The project contains a preview feature - \"Teams App Configuration Improvements.\" Turn on the preview feature to continue.", "core.deployArmTemplates.ActionSuccess": "ARM templates deployed successfully. Resource group name: %s. Deployment name: %s", - "core.collaboration.ListCollaboratorsSuccess": "'List Microsoft 365 App owners' succeeded, you can view it in [Output panel](%s).", + "core.collaboration.ListCollaboratorsSuccess": "'List of Microsoft 365 App owners is successful, you can view it in [Output panel](%s).", "core.collaboration.GrantingPermission": "Granting permission", "core.collaboration.EmailCannotBeEmptyOrSame": "Provide collaborator's email and make sure it's not the current user's email.", "core.collaboration.CannotFindUserInCurrentTenant": "User not found in current tenant. Provide correct email address", @@ -98,24 +98,24 @@ "plugins.spfx.scaffold.updateManifest": "Update webpart manifest", "plugins.spfx.GetTenantFailedError": "Unable to get tenant %s %s", "plugins.spfx.error.installLatestDependencyError": "Unable to set up SPFx environment in %s folder. To set up global SPFx environment, follow [Set up your SharePoint Framework development environment | Microsoft Learn](%s).", - "_plugins.spfx.error.installLatestDependencyError.comment": "'Microsoft Learn' and 'SharePoint' are the product brand names which should not be localized.", - "plugins.spfx.error.scaffoldError": "Project creation failed. A possible reason could be from Yeoman SharePoint Generator. Check [Output panel](%s) for details.", - "plugins.spfx.error.import.retrieveSolutionInfo": "Failed to retrieve existing SPFx solution information. Please make sure your SPFx solution is valid.", - "plugins.spfx.error.import.copySPFxSolution": "Failed to copy existing SPFx solution: %s", - "plugins.spfx.error.import.updateSPFxTemplate": "Failed to update project templates with existing SPFx solution: %s", - "plugins.spfx.error.import.common": "Failed to import existing SPFx solution to Teams Toolkit: %s", + "_plugins.spfx.error.installLatestDependencyError.comment": "The product brand names 'Microsoft Learn' and 'SharePoint' should not be localized.", + "plugins.spfx.error.scaffoldError": "Project creation is unsuccessful, which may be due to Yeoman SharePoint Generator. Check [Output panel](%s) for details.", + "plugins.spfx.error.import.retrieveSolutionInfo": "Unable to get existing SPFx solution information. Ensure your SPFx solution is valid.", + "plugins.spfx.error.import.copySPFxSolution": "Unable to copy existing SPFx solution: %s", + "plugins.spfx.error.import.updateSPFxTemplate": "Unable to update project templates with existing SPFx solution: %s", + "plugins.spfx.error.import.common": "Unable to import existing SPFx solution to Teams Toolkit: %s", "plugins.spfx.import.title": "Importing SPFx solution", "plugins.spfx.import.copyExistingSPFxSolution": "Copying existing SPFx solution...", "plugins.spfx.import.generateSPFxTemplates": "Generating templates based on solution info...", "plugins.spfx.import.updateTemplates": "Updating templates...", - "plugins.spfx.import.success": "Your SPFx solution has been successfully imported to %s.", - "plugins.spfx.import.log.success": "Teams Toolkit has imported your SPFx solution successfully. A complete log of import details can be found in %s.", - "plugins.spfx.import.log.fail": "Teams Toolkit failed to import your SPFx solution. A complete log of import details can be found in %s.", - "plugins.spfx.addWebPart.confirmInstall": "SPFx version in your solution is %s, not yet installed on your machine. Do you want to install SPFx %s in Teams Toolkit directory to continue adding web part?", + "plugins.spfx.import.success": "Your SPFx solution is successfully imported to %s.", + "plugins.spfx.import.log.success": "Teams Toolkit has successfully imported your SPFx solution. Find complete log of import details in %s.", + "plugins.spfx.import.log.fail": "Teams Toolkit is unable to import your SPFx solution. Find complete log of important details in %s.", + "plugins.spfx.addWebPart.confirmInstall": "SPFx %s version in your solution isn't installed on your machine. Do you want to install it in Teams Toolkit directory to continue adding web parts?", "plugins.spfx.addWebPart.install": "Install", - "plugins.spfx.addWebPart.confirmUpgrade": "Teams Toolkit is using SPFx version %s, but SPFx version in your solution is %s. Do you want to upgrade SPFx version in Teams Toolkit directory to %s and add web part?", + "plugins.spfx.addWebPart.confirmUpgrade": "Teams Toolkit is using SPFx version %s and your solution has SPFx version %s. Do you want to upgrade it to version %s in Teams Toolkit directory and add web parts?", "plugins.spfx.addWebPart.upgrade": "Upgrade", - "plugins.spfx.addWebPart.versionMismatch.continueConfirm": "SPFx version in your solution is %s, not yet installed on this machine. Teams Toolkit uses SPFx installed in Teams Toolkit directory by default (%s). The version mismatch may cause unexpected error. Do you want to continue?", + "plugins.spfx.addWebPart.versionMismatch.continueConfirm": "SPFx version %s in your solution isn't installed on this machine. Teams Toolkit uses the SPFx installed in its directory by default (%s). The version mismatch may cause unexpected error. Do you still want to continue?", "plugins.spfx.addWebPart.versionMismatch.help": "Help", "plugins.spfx.addWebPart.versionMismatch.continue": "Continue", "plugins.spfx.addWebPart.versionMismatch.output": "SPFx version in your solution is %s. You've installed %s globally and %s in Teams Toolkit directory, which is used as default (%s) by Teams Toolkit. The version mismatch may cause unexpected error. Find possible solutions in %s.", @@ -199,54 +199,55 @@ "error.appstudio.validateFetchSchemaFailed": "Unable to get schema from %s, message: %s", "error.appstudio.validateSchemaNotDefined": "Manifest schema is not defined", "error.appstudio.publishInDevPortalSuggestionForValidationError": "Generate package from \"Zip Teams app package\" and try again.", - "error.appstudio.teamsAppCreateConflict": "Unable to create Teams app with 409 Conflict error. That may come from your app id is conflicting with another app in your tenant. Click Get Help to learn more.", - "error.appstudio.teamsAppCreateConflictWithPublishedApp": "A Teams app with that id already exists in your organization's app store. Manually update the app id and try again.", - "error.appstudio.teamsAppPublishConflict": "Unable to publish Teams app. Teams app with id %s already exists in the staged apps. Manually update the app id and try again.", - "error.appstudio.NotAllowedToAcquireBotFrameworkToken": "The current account is not allowed to acquire botframework token.", + "error.appstudio.teamsAppCreateConflict": "Unable to create Teams app, which may be because your app ID is conflicting with another app's ID in your tenant. Click 'Get Help' to resolve this issue.", + "error.appstudio.teamsAppCreateConflictWithPublishedApp": "Teams app with the same ID already exists in your organization's app store. Update the app and try again.", + "error.appstudio.teamsAppPublishConflict": "Unable to publish Teams app because Teams app with this ID already exists in staged apps. Update the app ID and try again.", + "error.appstudio.NotAllowedToAcquireBotFrameworkToken": "This account can't get a botframework token.", "_error.appstudio.NotAllowedToAcquireBotFrameworkToken.comment": "This is to describe API call, no need to translate 'botframework'.", - "error.appstudio.BotProvisionReturnsForbiddenResult": "Botframework provisioning returns forbidden result from attempting to create bot registration.", + "error.appstudio.BotProvisionReturnsForbiddenResult": "Botframework provisioning returns forbidden result when attempting to create bot registration.", "_error.appstudio.BotProvisionReturnsForbiddenResult.comment": "This is to describe API call, no need to translate 'Botframework'.", - "error.appstudio.BotProvisionReturnsConflictResult": "Botframework provisioning returns conflict result from attempting to create bot registration.", + "error.appstudio.BotProvisionReturnsConflictResult": "Botframework provisioning returns conflict result when attempting to create bot registration.", "_error.appstudio.BotProvisionReturnsConflictResult.comment": "This is to describe API call, no need to translate 'Botframework'.", "error.generator.ScaffoldLocalTemplateError": "Unable to scaffold template based on local zip package.", "error.generator.TemplateNotFoundError": "Unable to find template: %s.", "error.generator.SampleNotFoundError": "Unable to find sample: %s.", - "error.generator.UnzipError": "Unable to unzip templates and write to disk.", + "error.generator.UnzipError": "Unable to extract templates and save them to disk.", "error.generator.MissKeyError": "Unable to find key %s", "error.generator.FetchSampleInfoError": "Unable to fetch sample info", - "error.generator.DownloadSampleApiLimitError": "Unable to download sample due to throttling. Retry later after rate limit reset (This may take up to 1 hour). Alternatively, you can go to %s to git clone the repo manually", - "error.generator.DownloadSampleNetworkError": "Unable to download sample due to network error. Check your network connection and retry. Alternatively, you can go to %s to git clone the repo manually", + "error.generator.DownloadSampleApiLimitError": "Unable to download sample due to rate limitation. Try again in an hour after rate limit reset or you can manually clone the repo from %s.", + "error.generator.DownloadSampleNetworkError": "Unable to download sample due to network error. Check your network connection and try again or you can manually clone the repo from %s", "error.copilotPlugin.apiSpecNotUsedInPlugin": "\"%s\" is not used in the plugin.", "error.copilotPlugin.openAiPluginManifest.CannotGetManifest": "Unable to get OpenAI plugin manifest from '%s'.", - "error.copilotPlugin.noExtraAPICanBeAdded": "No API can be added. Only GET and POST methods with at most one required parameter and no auth are supported. Methods defined in manifest.json are not listed.", + "error.copilotPlugin.noExtraAPICanBeAdded": "Unable to add API as only GET and POST methods with at most 5 required parameter and no authentication are supported. Also, methods defined in manifest.json are not listed.", "error.m365.NotExtendedToM365Error": "Unable to extend Teams app to Microsoft 365. Use 'teamsApp/extendToM365' action to extend your Teams app to Microsoft 365.", - "core.QuestionAppName.validation.pattern": "Application name must start with letters and contain at least two letters or digits. It can not contain some special characters.", - "core.QuestionAppName.validation.maxlength": "Application name is longer than the maximum length of 30.", - "core.QuestionAppName.validation.pathExist": "Path exists: %s. Select a different application name.", - "core.QuestionAppName.validation.lengthWarning": "Your application name may exceed the maximum length of 30 characters because Teams Toolkit will automatically append a \"local\" suffix for the Teams application registered for local debugging purposes. You may continue, but make sure to update your application name in the \"manifest.json\" file.", + "core.QuestionAppName.validation.pattern": "App name needs to begin with letters, include minimum two letters or digits, and exclude certain special characters.", + "core.QuestionAppName.validation.maxlength": "App name is longer than the 30 characters.", + "core.QuestionAppName.validation.pathExist": "Path exists: %s. Select a different app name.", + "core.QuestionAppName.validation.lengthWarning": "Your app name may exceed 30 characters due to a \"local\" suffix added by Teams Toolkit for local debugging. Please update your app name in \"manifest.json\" file.", "core.ProgrammingLanguageQuestion.title": "Programming Language", "core.ProgrammingLanguageQuestion.placeholder": "Select a programming language", "core.ProgrammingLanguageQuestion.placeholder.spfx": "SPFx is currently supporting TypeScript only.", "core.option.tutorial": "Open tutorial", "core.option.github": "Open a GitHub guide", - "core.option.inProduct": "Open a in-product guide", + "core.option.inProduct": "Open an in-product guide", "core.TabOption.label": "Tab", - "core.generator.officeAddin.importProject.title": "Importing an existing Outlook add-in project", - "core.generator.officeAddin.importProject.copyFiles": "Copying files", - "core.generator.officeAddin.importProject.convertProject": "Converting project", - "core.generator.officeAddin.importProject.updateManifest": "Modifying manifest", + "core.generator.officeAddin.importProject.title": "Importing Existing Outlook Add-in Project", + "core.generator.officeAddin.importProject.copyFiles": "Copying files...", + "core.generator.officeAddin.importProject.convertProject": "Converting project...", + "core.generator.officeAddin.importProject.updateManifest": "Modifying manifest...", + "core.generator.officeAddin.importOfficeProject.title": "Importing Existing Office Add-in Project", "core.TabOption.description": "UI-based app", "core.TabOption.detail": "Teams-aware webpages embedded in Microsoft Teams", "core.DashboardOption.label": "Dashboard", "core.DashboardOption.detail": "A canvas with cards and widgets for displaying important information", "core.BotNewUIOption.label": "Basic Bot", - "core.BotNewUIOption.detail": "A simple implementation of an echo bot that's ready to customize", + "core.BotNewUIOption.detail": "A simple implementation of an echo bot that's ready for customization", "core.LinkUnfurlingOption.label": "Link Unfurling", - "core.LinkUnfurlingOption.detail": "Display information and actions when a URL is pasted into the compose message area", + "core.LinkUnfurlingOption.detail": "Display information and actions when a URL is pasted into the text input field", "core.MessageExtensionOption.labelNew": "Collect Form Input and Process Data", "core.MessageExtensionOption.label": "Message Extension", "core.MessageExtensionOption.description": "Custom UI when users compose messages in Teams", - "core.MessageExtensionOption.detail": "Get user input, do something with it, and send customized results back", + "core.MessageExtensionOption.detail": "Receive user input, process it, and send customized results", "core.NotificationOption.label": "Chat Notification Message", "core.NotificationOption.detail": "Notify and inform with a message that displays in Teams chats", "core.CommandAndResponseOption.label": "Chat Command", @@ -280,24 +281,24 @@ "core.createProjectQuestion.projectType.bot.detail": "Conversational or informative chat experiences that can automate repetitive tasks", "core.createProjectQuestion.projectType.bot.label": "Bot", "core.createProjectQuestion.projectType.bot.title": "App Features Using a Bot", - "core.createProjectQuestion.projectType.messageExtension.detail": "Search or initiate actions from the message composing area of Teams and Outlook", + "core.createProjectQuestion.projectType.messageExtension.detail": "Search and take actions from the text input box in Teams and Outlook", "core.createProjectQuestion.projectType.messageExtension.copilotEnabled.detail": "Search or initiate actions from the message composing area of Teams, Outlook and Copilot", "core.createProjectQuestion.projectType.messageExtension.title": "App Features Using a Message Extension", - "core.createProjectQuestion.projectType.outlookAddin.detail": "Customize the ribbon and Task Pane with your web content", + "core.createProjectQuestion.projectType.outlookAddin.detail": "Customize the ribbon and Task Pane with your web content for seamless user experience", "core.createProjectQuestion.projectType.outlookAddin.label": "Outlook Add-in", "core.createProjectQuestion.projectType.outlookAddin.title": "App Features Using an Outlook Add-in", "core.createProjectQuestion.projectType.officeAddin.detail": "Extend Office apps to interact with content in Office documents and Outlook mails", "core.createProjectQuestion.projectType.officeAddin.label": "Office Add-in", "core.createProjectQuestion.projectType.officeAddin.title": "App Features Using an Office Add-in", "core.createProjectQuestion.projectType.officeAddin.framework.title": "Framework", - "core.createProjectQuestion.projectType.officeAddin.framework.placeholder": "Select a framework.", + "core.createProjectQuestion.projectType.officeAddin.framework.placeholder": "Select a framework", "core.createProjectQuestion.projectType.tab.detail": "Embed your own web content in Teams, Outlook, and the Microsoft 365 app", "core.createProjectQuestion.projectType.tab.title": "App Features Using a Tab", "core.createProjectQuestion.projectType.copilotPlugin.detail": "Create a plugin to extend Microsoft Copilot for Microsoft 365 using your APIs", "core.createProjectQuestion.projectType.copilotPlugin.label": "Copilot Plugin", "core.createProjectQuestion.projectType.copilotPlugin.title": "Copilot Plugin", "core.createProjectQuestion.projectType.copilotPlugin.placeholder": "Select an option", - "core.createProjectQuestion.projectType.customCopilot.detail": "Build intelligent chatbot as your own copilot in Microsoft Teams using Teams AI Library", + "core.createProjectQuestion.projectType.customCopilot.detail": "Build intelligent chatbot in Microsoft Teams easily using Teams AI Library", "core.createProjectQuestion.projectType.customCopilot.label": "Custom Copilot", "core.createProjectQuestion.projectType.customCopilot.title": "App Features Using Teams AI Library", "core.createProjectQuestion.projectType.customCopilot.placeholder": "Select an option", @@ -305,10 +306,10 @@ "core.createProjectQuestion.capability.botMessageExtension.label": "Start with a Bot", "core.createProjectQuestion.capability.botMessageExtension.detail": "Create a message extension using Bot Framework", "core.createProjectQuestion.capability.copilotPluginNewApiOption.label": "Start with a new API", - "core.createProjectQuestion.capability.copilotPluginNewApiOption.detail": "Create an API plugin with a new API from Azure Functions", + "core.createProjectQuestion.capability.copilotPluginNewApiOption.detail": "Create a plugin with a new API from Azure Functions", "core.createProjectQuestion.capability.messageExtensionNewApiOption.detail": "Create a message extension with a new API from Azure Functions", "core.createProjectQuestion.capability.copilotPluginApiSpecOption.label": "Start with an OpenAPI Description Document", - "core.createProjectQuestion.capability.copilotPluginApiSpecOption.detail": "Create an API plugin from your existing API", + "core.createProjectQuestion.capability.copilotPluginApiSpecOption.detail": "Create a plugin from your existing API", "core.createProjectQuestion.capability.messageExtensionApiSpecOption.detail": "Create a message extension from your existing API", "core.createProjectQuestion.capability.copilotPluginAIPluginOption.label": "Start with an OpenAI Plugin", "core.createProjectQuestion.capability.copilotPluginAIPluginOption.detail": "Convert an OpenAI Plugin to Microsoft 365 Copilot plugin", @@ -359,8 +360,8 @@ "core.createProjectQuestion.invalidUrl.message": "Enter a valid HTTP URL without authentication to access your OpenAPI description document.", "core.createProjectQuestion.apiSpec.operation.title": "Select Operation(s) Teams Can Interact with", "core.createProjectQuestion.apiSpec.copilotOperation.title": "Select Operation(s) Copilot Can Interact with", - "core.createProjectQuestion.apiSpec.operation.placeholder": "GET/POST methods with at most one required parameter and no auth are listed", - "core.createProjectQuestion.apiSpec.operation.apikey.placeholder": "GET/POST methods with at most one required parameter and API key are listed", + "core.createProjectQuestion.apiSpec.operation.placeholder": "GET/POST methods with at most 5 required parameter and no auth are listed", + "core.createProjectQuestion.apiSpec.operation.apikey.placeholder": "GET/POST methods with at most 5 required parameter and API key are listed", "core.createProjectQuestion.apiSpec.operation.invalidMessage": "%s API(s) selected. You can select at least one and at most %s APIs.", "core.createProjectQuestion.apiSpec.operation.multipleAuth": "Your selected APIs have multiple authorizations %s which are not supported.", "core.createProjectQuestion.apiSpec.operation.multipleServer": "Your selected APIs have multiple server URLs %s which are not supported.", @@ -375,36 +376,36 @@ "core.createProjectQuestion.officeXMLAddin.bar.detail": "Creating Project.", "core.createProjectQuestion.officeXMLAddin.mainEntry.title": "Office Add-in", "core.createProjectQuestion.officeXMLAddin.mainEntry.detail": "Create integration with Outlook, Word, Excel, or PowerPoint", - "core.createProjectQuestion.officeXMLAddin.create.title": "Select to create an Outlook, Word, Excel, or PowerPoint Add-in", + "core.createProjectQuestion.officeXMLAddin.create.title": "Select to Create an Outlook, Word, Excel, or PowerPoint Add-in", "core.createProjectQuestion.officeXMLAddin.word.title": "Word Add-in", "core.createProjectQuestion.officeXMLAddin.word.detail": "Create an add-in that can run in Word across multiple platforms", "core.createProjectQuestion.officeXMLAddin.word.sso.title": "Add-in with Single Sign On", "core.createProjectQuestion.officeXMLAddin.word.sso.detail": "Create a Word add-in with Single Sign On capabilities", - "core.createProjectQuestion.officeXMLAddin.word.react.title": "Add-in with React framework", + "core.createProjectQuestion.officeXMLAddin.word.react.title": "Add-in with React Framework", "core.createProjectQuestion.officeXMLAddin.word.react.detail": "Create a Word add-in with React framework", "core.createProjectQuestion.officeXMLAddin.word.create.title": "Create a Word Add-in", "core.createProjectQuestion.officeXMLAddin.excel.title": "Excel Add-in", "core.createProjectQuestion.officeXMLAddin.excel.detail": "Extend Excel functionality and access Excel data on multiple platforms", "core.createProjectQuestion.officeXMLAddin.excel.sso.title": "Add-in with Single Sign On", "core.createProjectQuestion.officeXMLAddin.excel.sso.detail": "Create an Excel add-in with Single Sign On capabilities", - "core.createProjectQuestion.officeXMLAddin.excel.react.title": "Add-in with React framework", + "core.createProjectQuestion.officeXMLAddin.excel.react.title": "Add-in with React Framework", "core.createProjectQuestion.officeXMLAddin.excel.react.detail": "Create an Excel add-in with React framework", - "core.createProjectQuestion.officeXMLAddin.excel.cf.shared.title": "Excel Custom Functions using a Shared Runtime", + "core.createProjectQuestion.officeXMLAddin.excel.cf.shared.title": "Excel Custom Functions Using a Shared Runtime", "core.createProjectQuestion.officeXMLAddin.excel.cf.shared.detail": "Create an Excel add-in leveraging Custom Functions using a Shared Runtime", - "core.createProjectQuestion.officeXMLAddin.excel.cf.js.title": "Excel Custom Functions using a JavaScript-only Runtime", + "core.createProjectQuestion.officeXMLAddin.excel.cf.js.title": "Excel Custom Functions Using a JavaScript-only Runtime", "core.createProjectQuestion.officeXMLAddin.excel.cf.js.detail": "Create an Excel add-in leveraging Custom Functions using a JavaScript-only Runtime", "core.createProjectQuestion.officeXMLAddin.excel.create.title": "Create an Excel Add-in", "core.createProjectQuestion.officeXMLAddin.powerpoint.title": "PowerPoint Add-in", "core.createProjectQuestion.officeXMLAddin.powerpoint.detail": "Build engaging solutions for presentations across platform", - "core.createProjectQuestion.officeXMLAddin.powerpoint.sso.title": "Create a PowerPoint add-in with Single Sign On capabilities", + "core.createProjectQuestion.officeXMLAddin.powerpoint.sso.title": "Add-in with Single Sign On", "core.createProjectQuestion.officeXMLAddin.powerpoint.sso.detail": "PowerPoint add-in with Single Sign On capabilities", - "core.createProjectQuestion.officeXMLAddin.powerpoint.react.title": "Add-in with React framework", + "core.createProjectQuestion.officeXMLAddin.powerpoint.react.title": "Add-in with React Framework", "core.createProjectQuestion.officeXMLAddin.powerpoint.react.detail": "Create a PowerPoint add-in with React framework", "core.createProjectQuestion.officeXMLAddin.powerpoint.create.title": "Create a PowerPoint Add-in", - "core.createProjectQuestion.officeXMLAddin.taskpane.title": "Add-in with Basic Taskpane", - "core.createProjectQuestion.officeXMLAddin.taskpane.detail": "Customize the Ribbon with a button and embed content in the Taskpane", - "core.createProjectQuestion.officeXMLAddin.manifestOnly.title": "Add-in project containing the manifest only", - "core.createProjectQuestion.officeXMLAddin.manifestOnly.detail": "Create a simple add-in project with a manifest file only", + "core.createProjectQuestion.officeXMLAddin.taskpane.title": "Add-in with Basic Task Pane", + "core.createProjectQuestion.officeXMLAddin.taskpane.detail": "Customize the Ribbon with a button and create a dashboard in the Task Pane", + "core.createProjectQuestion.officeXMLAddin.manifestOnly.title": "Add-in Project With only Manifest File", + "core.createProjectQuestion.officeXMLAddin.manifestOnly.detail": "Create an add-in project that includes only the manifest file", "core.aiAssistantBotOption.label": "AI Assistant Bot", "core.aiAssistantBotOption.detail": "A custom AI assistant bot in Teams using Teams AI library and OpenAI Assistants API", "core.aiBotOption.label": "AI Chat Bot", @@ -472,7 +473,9 @@ "core.selectValidateMethodQuestion.validate.schemaOptionDescription": "Validate using manifest schema", "core.selectValidateMethodQuestion.validate.appPackageOption": "Validate app package using validation rules", "core.selectValidateMethodQuestion.validate.appPackageOptionDescription": "Validate app package using validation rules", - "core.confirmManifestQuestion.placeholder": "Confirm manifest is correctly selected", + "core.selectValidateMethodQuestion.validate.testCasesOption": "Publish Readiness", + "core.selectValidateMethodQuestion.validate.testCasesOptionDescription": "Check your app with the test cases Microsoft uses before they publish it", + "core.confirmManifestQuestion.placeholder": "Confirm you've selected the correct manifest file", "core.aadAppQuestion.label": "Microsoft Entra app", "core.aadAppQuestion.description": "Your Microsoft Entra app for Single Sign On", "core.teamsAppQuestion.label": "Teams app", @@ -500,10 +503,11 @@ "core.common.OutlookWebClientName2": "Outlook web access client id 2", "core.common.CancelledMessage": "Operation is cancelled.", "core.common.SwaggerNotSupported": "Swagger 2.0 is not supported. Please convert it to OpenAPI 3.0 first.", + "core.common.SpecVersionNotSupported": "Unsupported OpenAPI version %s. Please use version 3.0.x.", "core.common.NoServerInformation": "No server information is found in the OpenAPI description document.", "core.common.RemoteRefNotSupported": "Remote reference is not supported: %s.", "core.common.MissingOperationId": "Missing operationIds: %s.", - "core.common.NoSupportedApi": "No supported API is found in the OpenAPI description document: only GET and POST methods are supported, additionally, there can be at most one required parameter, and no auth is allowed. \nFor more information visit: \"https://aka.ms/build-api-based-message-extension\".", + "core.common.NoSupportedApi": "No supported API is found in the OpenAPI description document: only GET and POST methods are supported, additionally, there can be at most 5 required parameter, and no auth is allowed. \nFor more information visit: \"https://aka.ms/build-api-based-message-extension\".", "core.common.UrlProtocolNotSupported": "Server url is not correct: protocol %s is not supported, you should use https protocol instead.", "core.common.RelativeServerUrlNotSupported": "Server url is not correct: relative server url is not supported.", "core.common.ErrorFetchApiSpec": "Your OpenAPI description document should be accessible without authentication, otherwise download and start from a local copy.", @@ -512,6 +516,8 @@ "core.importAddin.label": "Import an Existing Outlook Add-ins", "core.importAddin.detail": "Upgrade an Add-ins project to the latest app manifest and project structure", "core.importOfficeAddin.label": "Import an Existing Office Add-ins", + "core.officeContentAddin.label": "Content Add-in", + "core.officeContentAddin.detail": "Create new objects for Excel or PowerPoint", "core.newTaskpaneAddin.label": "Taskpane", "core.newTaskpaneAddin.detail": "Customize the Ribbon with a button and embed content in the Taskpane", "core.summary.actionDescription": "Action %s%s", @@ -534,6 +540,8 @@ "error.aad.manifest.PreAuthorizedApplicationsIsMissing": "preAuthorizedApplications is missing\n", "error.aad.manifest.ResourceAppIdIsMissing": "Some item(s) in requiredResourceAccess misses resourceAppId property.", "error.aad.manifest.ResourceAccessIdIsMissing": "Some item(s) in resourceAccess misses id property.", + "error.aad.manifest.ResourceAccessShouldBeArray": "resourceAccess should be an array.", + "error.aad.manifest.RequiredResourceAccessShouldBeArray": "requiredResourceAccess should be an array.", "error.aad.manifest.AccessTokenAcceptedVersionIs1": "accessTokenAcceptedVersion is 1\n", "error.aad.manifest.OptionalClaimsIsMissing": "optionalClaims is missing\n", "error.aad.manifest.OptionalClaimsMissingIdtypClaim": "optionalClaims access token doesn't contain idtyp claim\n", @@ -594,6 +602,8 @@ "driver.aadApp.error.generateSecretFailed": "Cannot generate client secret.", "driver.aadApp.error.invalidFieldInManifest": "Field %s is missing or invalid in Microsoft Entra app manifest.", "driver.aadApp.error.appNameTooLong": "The name for this Microsoft Entra app is too long. The maximum length is 120.", + "driver.aadApp.error.credentialInvalidLifetimeAsPerAppPolicy": "The client secret lifetime is too long for your tenant. Use a shorter value with the clientSecretExpireDays parameter.", + "driver.aadApp.error.credentialTypeNotAllowedAsPerAppPolicy": "Your tenant doesn't allow creating a client secret for Microsoft Entra app. Create and configure the app manually.", "driver.aadApp.progressBar.createAadAppTitle": "Creating Microsoft Entra application...", "driver.aadApp.progressBar.updateAadAppTitle": "Updating Microsoft Entra application...", "driver.aadApp.log.startExecuteDriver": "Executing action %s", @@ -691,6 +701,9 @@ "driver.teamsApp.progressBar.publishTeamsAppStep1": "Checking if the Teams app has already been submitted to tenant App Catalog", "driver.teamsApp.progressBar.publishTeamsAppStep2.1": "Update published Teams app", "driver.teamsApp.progressBar.publishTeamsAppStep2.2": "Publishing Teams app...", + "driver.teamsApp.progressBar.validateWithTestCases": "Submitting validation request...", + "driver.teamsApp.progressBar.validateWithTestCases.step": "Validation request submitted, status: %s. You will be notified when the result is ready or you can check all your validation records in [Teams Developer Portal](%s).", + "driver.teamsApp.progressBar.validateWithTestCases.conflict": "A validation is currently in progress, please submit later. You can find this existing validation in [Teams Developer Portal](%s).", "driver.teamsApp.summary.createTeamsAppAlreadyExists": "Teams app with id %s already exists, skipped creating a new Teams app.", "driver.teamsApp.summary.publishTeamsAppExists": "Teams app with id %s already exists in the organization's app store.", "driver.teamsApp.summary.publishTeamsAppNotExists": "Teams app with id %s does not exist in the organization's app store.", @@ -704,7 +717,11 @@ "driver.teamsApp.summary.validate.succeed": "%s passed", "driver.teamsApp.summary.validate.failed": "%s failed", "driver.teamsApp.summary.validate.warning": "%s warning", + "driver.teamsApp.summary.validate.skipped": "%s skipped", "driver.teamsApp.summary.validate.all": "All", + "driver.teamsApp.summary.validateWithTestCases": "Validation request completed, status: %s. \n\nSummary:\n%s. View the result from: %s.%s", + "driver.teamsApp.summary.validateWithTestCases.result": "Validation request completed, status: %s. %s. Check [Output panel](command:fx-extension.showOutputChannel) for details.", + "driver.teamsApp.summary.validateWithTestCases.result.detail": "%s Validation title: %s. Message: %s", "driver.teamsApp.validate.result": "Teams Toolkit has completed checking your app package against validation rules. %s.", "driver.teamsApp.validate.result.display": "Teams Toolkit has completed checking your app package against validation rules. %s. Check [Output panel](command:fx-extension.showOutputChannel) for details.", "error.teamsApp.validate.apiFailed": "Teams app package validation failed due to %s", @@ -778,8 +795,8 @@ "error.deploy.DeployZipPackageError": "Unable to deploy zip package to endpoint '%s' in Azure due to error: %s. \nSuggestions:\n 1. Verify that your Azure account has the necessary permissions to access the API. \n 2. Verify that the endpoint is properly configured in Azure and that the required resources have been provisioned. \n 3. Ensure that the zip package is valid and free of errors. \n 4. If the error message specifies the reason, such as an authentication failure or a network issue, fix the error and try again. \n 5. If the error still persists, you can attempt to deploy the package manually following the guidelines in this link: '%s'", "error.deploy.CheckDeploymentStatusError": "Unable to check deployment status for location: '%s' due to error: %s. If the issue persists, please review the deployment logs (Deployment -> Deployment center -> Logs) in Azure portal to identify any issues that may have occurred.", "error.deploy.DeployRemoteStartError": "The package has been successfully deployed to Azure for location: '%s', but the application is not able to start due to error: %s.\n If the reason is not clearly specified, here are some suggestions to troubleshoot:\n 1. Check the application logs: Look for any error messages or stack traces in the application logs to identify the root cause of the problem.\n 2. Check the Azure configuration: Ensure that the Azure configuration is correct, including connection strings and application settings.\n 3. Check the application code: Review the code to see if there are any syntax or logic errors that could be causing the issue.\n 4. Check the dependencies: Verify that all dependencies required by the application are correctly installed and updated.\n 5. Restart the application: Try restarting the application in Azure to see if that resolves the issue.\n 6. Check the resource allocation: Make sure that the resource allocation for the Azure instance is appropriate for the application and its workload.\n 7. Seek help from Azure support: If the issue persists, reach out to Azure support for further assistance.", - "error.script.ScriptTimeoutError": "Script execution timeout: %s. Adjust 'timeout' parameter in yaml or improve your script's efficiency.", - "error.script.ScriptExecutionError": "Script ('%s') execution error: %s", + "error.script.ScriptTimeoutError": "Script execution timeout. Adjust 'timeout' parameter in yaml or improve your script's efficiency.", + "error.script.ScriptExecutionError": "Unable to execute script action.", "error.deploy.AzureStorageClearBlobsError.Notification": "Unable to clear blob files in Azure Storage Account '%s'. Refer to the [Output panel](command:fx-extension.showOutputChannel) for more details.", "error.deploy.AzureStorageClearBlobsError": "Unable to clear blob files in Azure Storage Account '%s'. The error responses from Azure are:\n %s. \nIf the error message specifies the reason, fix the error and try again.", "error.deploy.AzureStorageUploadFilesError.Notification": "Unable to upload local folder '%s' to Azure Storage Account '%s'. Refer to the [Output panel](command:fx-extension.showOutputChannel) for more details.", @@ -794,6 +811,12 @@ "error.core.appIdNotExist": "Cannot find app id: %s. Either your current M365 account does not have permission, or the app has alredy been deleted.", "driver.apiKey.description.create": "Create an API key on Developer Portal for authentication in Open API spec.", "driver.aadApp.apiKey.title.create": "Creating API key...", + "driver.apiKey.description.update": "Update an API key on Developer Portal for authentication in Open API spec.", + "driver.aadApp.apiKey.title.update": "Updating API key...", + "driver.apiKey.log.skipUpdateApiKey": "Skip updating API key as the same property exists.", + "driver.apiKey.log.successUpdateApiKey": "API key updated successfully!", + "driver.apiKey.confirm.update": "The following parameters will be updated:\n%s\nDo you want to continue?", + "driver.apiKey.info.update": "API key updated successfully! The following parameters have been updated:\n%s", "driver.apiKey.log.startExecuteDriver": "Executing action %s", "driver.apiKey.log.skipCreateApiKey": "Environment variable %s exists. Skip creating API key.", "driver.apiKey.log.apiKeyNotFound": "Environment variable %s exists but failed to retrieve API key from Developer Portal. Check manually if API key exists.", diff --git a/packages/fx-core/resource/yaml-schema/v1.5/yaml.schema.json b/packages/fx-core/resource/yaml-schema/v1.5/yaml.schema.json new file mode 100644 index 0000000000..69bcc8be3e --- /dev/null +++ b/packages/fx-core/resource/yaml-schema/v1.5/yaml.schema.json @@ -0,0 +1,1663 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "type": "string", + "description": "The projectId used for telemetry." + }, + "environmentFolderPath": { + "type": "string", + "description": "The folder path of .env files used for variables and different envrironments." + }, + "version": { + "type": "string", + "description": "The version of the yaml file schema", + "const": "v1.5" + }, + "additionalMetadata": { + "type": "object", + "description": "Metadata property, used by Teams Toolkit only.", + "additionalProperties": true, + "properties": { + "sampleTag": { + "type": ["number","string","boolean","object","array", "null", "integer"], + "description": "A tag for the sample app used to track telemetry events sent by Teams Toolkit associated with that sample. Pattern: :" + } + } + }, + "provision": { + "$ref": "#/definitions/lifeCycleArray", + "description": "Called by `teamsfx provision`" + }, + "deploy": { + "$ref": "#/definitions/lifeCycleArray", + "description": "Called by `teamsfx deploy`" + }, + "publish": { + "$ref": "#/definitions/lifeCycleArray", + "description": "Called by `teamsfx publish`" + } + }, + "required": ["version"], + "definitions": { + "lifeCycleArray": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/definitions/aadAppCreate" }, + { "$ref": "#/definitions/aadAppUpdate" }, + { "$ref": "#/definitions/armDeploy" }, + { "$ref": "#/definitions/azureStorageEnableStaticWebsite" }, + { "$ref": "#/definitions/cliRunNpmCommand" }, + { "$ref": "#/definitions/cliRunDotnetCommand" }, + { "$ref": "#/definitions/cliRunNpxCommand" }, + { "$ref": "#/definitions/azureStorageDeploy" }, + { "$ref": "#/definitions/azureAppServiceZipDeploy" }, + { "$ref": "#/definitions/azureFunctionsZipDeploy" }, + { "$ref": "#/definitions/teamsAppCreate" }, + { "$ref": "#/definitions/teamsAppValidateManifest" }, + { "$ref": "#/definitions/teamsAppValidateAppPackage" }, + { "$ref": "#/definitions/teamsAppZipAppPackage" }, + { "$ref": "#/definitions/teamsAppUpdate" }, + { "$ref": "#/definitions/teamsAppPublishAppPackage" }, + { "$ref": "#/definitions/botAadAppCreate" }, + { "$ref": "#/definitions/botframeworkCreate" }, + { "$ref": "#/definitions/fileCreateOrUpdateEnvironmentFile" }, + { "$ref": "#/definitions/fileCreateOrUpdateJsonFile" }, + { "$ref": "#/definitions/devToolInstall" }, + { "$ref": "#/definitions/teamsAppExtendToM365" }, + { "$ref": "#/definitions/spfxDeploy" }, + { "$ref": "#/definitions/teamsAppCopyAppPackageToSPFx" }, + { "$ref": "#/definitions/script" }, + { "$ref": "#/definitions/apiKeyRegister"}, + { "$ref": "#/definitions/azureStaticWebAppGetDeploymentKey"}, + { "$ref": "#/definitions/apiKeyUpdate"} + ] + } + }, + "aadAppCreateBase": { + "type": "object", + "description": "Create Microsoft Entra application and client secret (optional). Refer to https://aka.ms/teamsfx-actions/aadapp-create for more details.", + "required": ["uses", "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": "Create Microsoft Entra application when the environment variable that stores clientId is empty. Also create client secret for the application when generateClientSecret parameter is true and the environment variable that stores clientSecret is empty. When creating new Microsoft Entra application, this action generates clientId, objectId, tenantId, authority and authorityHost. When creating new client secret, this action generates clientSecret. Refer to https://aka.ms/teamsfx-actions/aadapp-create for more details.", + "const": "aadApp/create" + } + } + }, + "aadAppCreateWithSecret": { + "type": "object", + "additionalProperties": false, + "allOf": [ { "$ref": "#/definitions/aadAppCreateBase" } ], + "required": ["with", "writeToEnvironmentFile"], + "properties": { + "name": {}, + "uses": {}, + "env": {}, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name", "generateClientSecret", "signInAudience"], + "properties": { + "name": { + "type": "string", + "description": "The name of Microsoft Entra application. Note: when you run aadApp/update, the Microsoft Entra application name will be updated based on the definition in manifest. If you don't want to change the name, ensure the name in Microsoft Entra application manifest is the same with the name defined here." + }, + "generateClientSecret": { + "type": "boolean", + "description": "Whether to generate client secret for the Microsoft Entra application. When the value is true, you need to specify the name of environment variable that stores the value of client secret in writeToEnvironmentVariable. For example: `clientSecret: SECRET_MY_AAD_APP_CLIENT_SECRET`.", + "const": true + }, + "signInAudience": { + "type": "string", + "description": "Specifies what Microsoft accounts are supported for the current application.", + "enum": ["AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"] + }, + "serviceManagementReference": { + "type": "string", + "description": "References application or service contact information from a Service or Asset Management database." + }, + "clientSecretExpireDays": { + "type": "integer", + "description": "The number of days the client secret is valid.", + "minimum": 1 + }, + "clientSecretDescription": { + "type": "string", + "description": "The description of the client secret." + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["clientId", "objectId", "clientSecret"], + "properties": { + "clientId": { + "description": "Required. The client (application) id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "objectId": { + "description": "Required. The object id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "clientSecret": { + "description": "Required when generateClientSecret is true. The generated client secret of the Microsoft Entra application.", + "$ref": "#/definitions/secretEnvVarName" + }, + "tenantId": { + "description": "The tenant id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "authority": { + "description": "The authority of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "authorityHost": { + "description": "The authority host name of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "aadAppCreateWithoutSecret": { + "type": "object", + "allOf": [ { "$ref": "#/definitions/aadAppCreateBase" } ], + "required": ["with", "writeToEnvironmentFile"], + "additionalProperties": false, + "properties": { + "name": {}, + "uses": {}, + "env": {}, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name", "generateClientSecret", "signInAudience"], + "properties": { + "name": { + "type": "string", + "description": "The name of Microsoft Entra application. Note: when you run aadApp/update, the Microsoft Entra application name will be updated based on the definition in manifest. If you don't want to change the name, ensure the name in Microsoft Entra application manifest is the same with the name defined here." + }, + "generateClientSecret": { + "type": "boolean", + "description": "Whether to generate client secret for the Microsoft Entra application. When the value is true, you need to specify the name of environment variable that stores the value of client secret in writeToEnvironmentVariable. For example: `clientSecret: SECRET_MY_AAD_APP_CLIENT_SECRET`.", + "const": false + }, + "signInAudience": { + "type": "string", + "description": "Specifies what Microsoft accounts are supported for the current application.", + "enum": ["AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"] + }, + "serviceManagementReference": { + "type": "string", + "description": "References application or service contact information from a Service or Asset Management database." + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["clientId", "objectId"], + "properties": { + "clientId": { + "description": "Required. The client (application) id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "objectId": { + "description": "Required. The object id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "tenantId": { + "description": "The tenant id of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "authority": { + "description": "The authority of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + }, + "authorityHost": { + "description": "The authority host name of created Microsoft Entra application.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "aadAppCreate": { + "type": "object", + "oneOf": [ + { "$ref": "#/definitions/aadAppCreateWithoutSecret" }, + { "$ref": "#/definitions/aadAppCreateWithSecret" } + ] + }, + "aadAppUpdate": { + "type": "object", + "additionalProperties": false, + "description": "Update Microsoft Entra application. Refer to https://aka.ms/teamsfx-actions/aadapp-update for more details.", + "required": ["uses", "with"], + "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": "Update Microsoft Entra application based on the given Microsoft Entra application manifest. If the manifest uses AAD_APP_ACCESS_AS_USER_PERMISSION_ID and the environment variable is empty, this action will generate a random id and output it. Refer to https://aka.ms/teamsfx-actions/aadapp-update for more details.", + "const": "aadApp/update" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["manifestPath", "outputFilePath"], + "properties": { + "manifestPath": { + "type": "string", + "description": "Path of Microsoft Entra application manifest. Environment variables in the manifest will be replaced before applying the manifest to Microsoft Entra application." + }, + "outputFilePath": { + "type": "string", + "description": "Generate the final Microsoft Entra application manifest used to update Microsoft Entra application to this path." + } + } + } + } + }, + "armDeploy": { + "type": "object", + "additionalProperties": false, + "description": "Create Azure resources using the referenced Bicep/JSON files. Refer to https://aka.ms/teamsfx-actions/arm-deploy for more details on the naming convertion rule.", + "required": ["uses", "with"], + "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": "Create Azure resources using the referenced Bicep/JSON files. Outputs from Bicep/JSON will be persisted in the current Teams Toolkit environment following certain naming convertion. Refer to https://aka.ms/teamsfx-actions/arm-deploy for more details on the naming convertion rule.", + "const": "arm/deploy" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["subscriptionId", "resourceGroupName", "templates"], + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id to deploy to" + }, + "resourceGroupName": { + "type": "string", + "description": "The resource group name to deploy to" + }, + "bicepCliVersion": { + "type": "string", + "description": "The Bicep CLI version. Bicep CLI will be downloaded to {Home}/.fx/bin/bicep.\n Teams Toolkit defaults to Bicep in PATH if version is not defined." + }, + "templates": { + "type": "array", + "description": "The list of templates to deploy", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["deploymentName", "path"], + "properties": { + "deploymentName": { + "type": "string", + "description": "The name of ARM deployment" + }, + "path": { + "type": "string", + "description": "Relative path to ARM template. Both Bicep and JSON format are supported." + }, + "parameters": { + "type": "string", + "description": "Relative path to ARM parameters file. Teams Toolkit will expand the environment variable in the parameters file" + } + } + } + } + } + } + } + }, + "azureStorageEnableStaticWebsite": { + "type": "object", + "additionalProperties": false, + "description": "Enable static website config for Azure Storage. Refer to https://aka.ms/teamsfx-actions/azure-storage-enable-static-website for more details.", + "required": ["uses", "with"], + "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": "Enable static website config for Azure Storage. This action has no output. Refer to https://aka.ms/teamsfx-actions/azure-storage-enable-static-website for more details.", + "const": "azureStorage/enableStaticWebsite" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["storageResourceId"], + "properties": { + "storageResourceId": { + "type": "string", + "description": "The resource id of the storage account" + }, + "indexPage": { + "type": "string", + "description": "The index page of the static website, default to 'index.html'" + }, + "errorPage": { + "type": "string", + "description": "The error page of the static website, default to 'index.html'" + } + } + } + } + }, + "cliRunNpmCommand": { + "type": "object", + "additionalProperties": false, + "description": "Execute npm command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-npm-command for more details.", + "required": ["uses", "with"], + "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": "Execute npm command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-npm-command for more details.", + "const": "cli/runNpmCommand" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["args"], + "properties": { + "args": { + "type": "string", + "description": "The arguments passed to the npm command" + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, default to './'" + } + } + } + } + }, + "cliRunDotnetCommand": { + "type": "object", + "additionalProperties": false, + "description": "Execute dotnet command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-dotnet-command for more details.", + "required": ["uses", "with"], + "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": "Execute dotnet command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-dotnet-command for more details.", + "const": "cli/runDotnetCommand" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["args"], + "properties": { + "args": { + "type": "string", + "description": "The arguments passed to the dotnet command" + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, default to './'" + }, + "execPath": { + "type": "string", + "description": "The path to the dotnet executable, default to system path." + } + } + } + } + }, + "cliRunNpxCommand": { + "type": "object", + "additionalProperties": false, + "description": "Execute npx command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-npx-command for more details.", + "required": ["uses", "with"], + "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": "Execute npx command with arguments. Refer to https://aka.ms/teamsfx-actions/cli-run-npx-command for more details.", + "const": "cli/runNpxCommand" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["args"], + "properties": { + "args": { + "type": "string", + "description": "The arguments passed to the npm command" + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, default to './'" + } + } + } + } + }, + "azureStorageDeploy": { + "type": "object", + "additionalProperties": false, + "description": "Upload and deploy the project to Azure Storage Service. Refer to https://aka.ms/teamsfx-actions/azure-storage-deploy for more details.", + "required": ["uses", "with"], + "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 upload and deploy the project to Azure Storage Service. This action has no output. Refer to https://aka.ms/teamsfx-actions/azure-storage-deploy for more details.", + "const": "azureStorage/deploy" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["artifactFolder", "resourceId"], + "properties": { + "artifactFolder": { + "type": "string", + "description": "Path to the distribution folder that contains the files to deploy." + }, + "resourceId": { + "type": "string", + "description": "The resource id of the storage account." + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, deploy program will find ignore file and create upload package file based on this directory, default to './'" + }, + "ignoreFile": { + "type": "string", + "description": "The path to the ignore file. Any files listed in this file will be ignored during upload. default ignores nothing." + } + } + } + } + }, + "azureAppServiceZipDeploy": { + "type": "object", + "additionalProperties": false, + "description": "Upload and deploy the project to Azure App Service. Refer to https://aka.ms/teamsfx-actions/azure-app-service-deploy for more details.", + "required": ["uses", "with"], + "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 upload and deploy the project to Azure App Service. This action has no output. Refer to https://aka.ms/teamsfx-actions/azure-app-service-deploy for more details.", + "const": "azureAppService/zipDeploy" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["artifactFolder", "resourceId"], + "properties": { + "artifactFolder": { + "type": "string", + "description": "Path to the distribution folder that contains the files to deploy." + }, + "resourceId": { + "type": "string", + "description": "The resource id of the Azure App Service." + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, deploy program will find ignore file and create upload package file based on this directory, default to './'" + }, + "ignoreFile": { + "type": "string", + "description": "The path to the ignore file. Any files listed in this file will be ignored during upload. default ignores nothing." + }, + "dryRun": { + "type": "boolean", + "description": "If true, the action will only package the files to be deployed without actually deploying them. Default to false." + }, + "outputZipFile": { + "type": "string", + "description": "The path to the packaged zip file. If not specified, the zip file will be saved to the workingDirectory/.deployment/deployment.zip." + } + } + } + } + }, + "azureFunctionsZipDeploy": { + "type": "object", + "additionalProperties": false, + "description": "Upload and deploy the project to Azure Functions. Refer to https://aka.ms/teamsfx-actions/azure-functions-deploy for more details.", + "required": ["uses", "with"], + "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 upload and deploy the project to Azure Functions. This action has no output. Refer to https://aka.ms/teamsfx-actions/azure-functions-deploy for more details.", + "const": "azureFunctions/zipDeploy" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["artifactFolder", "resourceId"], + "properties": { + "artifactFolder": { + "type": "string", + "description": "Path to the distribution folder that contains the files to deploy." + }, + "resourceId": { + "type": "string", + "description": "The resource id of the Azure Functions." + }, + "workingDirectory": { + "type": "string", + "description": "The working directory, deploy program will find ignore file based on this directory, default to './'" + }, + "ignoreFile": { + "type": "string", + "description": "The path to the ignore file. Any files listed in this file will be ignored during upload. default ignores nothing." + }, + "dryRun": { + "type": "boolean", + "description": "If true, the action will only package the files to be deployed without actually deploying them. Default to false." + }, + "outputZipFile": { + "type": "string", + "description": "The path to the packaged zip file. If not specified, the zip file will be saved to the workingDirectory/.deployment/deployment.zip." + } + } + } + } + }, + "teamsAppCreate": { + "type": "object", + "additionalProperties": false, + "description": "Create a Teams app in Teams Developer Portal. Refer to https://aka.ms/teamsfx-actions/teamsapp-create for more details.", + "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 Teams app for you if TEAMS_APP_ID environment variable is empty or the app with TEAMS_APP_ID is not found from Teams Developer Portal. Refer to https://aka.ms/teamsfx-actions/teamsapp-create for more details", + "const": "teamsApp/create" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Name of the Teams app" + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["teamsAppId"], + "properties": { + "teamsAppId": { + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "teamsAppValidateManifest": { + "type": "object", + "additionalProperties": false, + "description": "Validate Teams app manifest. Refer to https://aka.ms/teamsfx-actions/teamsapp-validate for more details.", + "required": ["uses", "with"], + "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 validate Teams app manifest with manifest schema. Refer to https://aka.ms/teamsfx-actions/teamsapp-validate for more details.", + "const": "teamsApp/validateManifest" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["manifestPath"], + "properties": { + "manifestPath": { + "type": "string", + "description": "Path to Teams app manifest file." + } + } + } + } + }, + "teamsAppValidateAppPackage": { + "type": "object", + "additionalProperties": false, + "description": "Validate Teams app manifest. Refer to https://aka.ms/teamsfx-actions/teamsapp-validate for more details.", + "required": ["uses", "with"], + "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 validate Teams app package file using validation rules. Refer to https://aka.ms/teamsfx-actions/teamsapp-validate for more details.", + "const": "teamsApp/validateAppPackage" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["appPackagePath"], + "properties": { + "appPackagePath": { + "type": "string", + "description": "Path to zipped Teams app package file." + } + } + } + } + }, + "teamsAppZipAppPackage": { + "type": "object", + "additionalProperties": false, + "description": "Zip app package with manifest file and icons. Refer to https://aka.ms/teamsfx-actions/teamsapp-zipAppPackage for more details.", + "required": ["uses", "with"], + "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 render Teams app manifest template with environment variables, and zip manifest file with two icons. Refer to https://aka.ms/teamsfx-actions/teamsapp-zipAppPackage for more details.", + "const": "teamsApp/zipAppPackage" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["manifestPath", "outputZipPath", "outputJsonPath"], + "properties": { + "manifestPath": { + "type": "string", + "description": "Path to Teams app manifest file" + }, + "outputZipPath": { + "type": "string", + "description": "Path to the output zip package" + }, + "outputJsonPath": { + "type": "string", + "description": "Path to the output manifest file" + } + } + } + } + }, + "teamsAppUpdate": { + "type": "object", + "additionalProperties": false, + "description": "Update Teams app in Teams Developer Portal. Refer to https://aka.ms/teamsfx-actions/teamsapp-update for more details.", + "required": ["uses", "with"], + "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": "Apply the Teams app manifest to an existing Teams app in Teams Developer Portal. Will use the app id in manifest.json file to determine which Teams app to update. Refer to https://aka.ms/teamsfx-actions/teamsapp-update for more details.", + "const": "teamsApp/update" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["appPackagePath"], + "properties": { + "appPackagePath": { + "type": "string", + "description": "Path to Teams app package" + } + } + } + } + }, + "teamsAppPublishAppPackage": { + "type": "object", + "additionalProperties": false, + "description": "Publish Teams app package to Teams Admin center. Refer to https://aka.ms/teamsfx-actions/teamsapp-publish for more details.", + "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": "Publish Teams app package to Teams Admin center. Refer to https://aka.ms/teamsfx-actions/teamsapp-publish for more details.", + "const": "teamsApp/publishAppPackage" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["appPackagePath"], + "properties": { + "appPackagePath": { + "type": "string", + "description": "Path to Teams app package to be published." + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["publishedAppId"], + "properties": { + "publishedAppId": { + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "botAadAppCreate": { + "type": "object", + "additionalProperties": false, + "description": "Create a new or reuse an existing Microsoft Entra application for bot. Refer to https://aka.ms/teamsfx-actions/botaadapp-create for more details.", + "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": "Create a new or reuse an existing Microsoft Entra application for bot. Refer to https://aka.ms/teamsfx-actions/botaadapp-create for more details.", + "const": "botAadApp/create" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The user-facing display name for this Microsoft Entra application" + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["botId", "botPassword"], + "properties": { + "botId": { + "description": "Required. Client (application) id of the Microsoft Entra application created for bot.", + "$ref": "#/definitions/envVarName" + }, + "botPassword": { + "description": "Required. Client secret of the Microsoft Entra application created for bot.", + "$ref": "#/definitions/secretEnvVarName" + } + } + } + } + }, + "botframeworkCreate": { + "type": "object", + "additionalProperties": false, + "description": "Create or update the bot registration on dev.botframework.com. Refer to https://aka.ms/teamsfx-actions/botframework-create for more details.", + "required": ["uses", "with"], + "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": "Create or update the bot registration on dev.botframework.com. Refer to https://aka.ms/teamsfx-actions/botframework-create for more details.", + "const": "botFramework/create" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["botId", "name", "messagingEndpoint"], + "properties": { + "botId": { + "type": "string", + "description": "the Microsoft Entra app client id of the bot" + }, + "name": { + "type": "string", + "description": "the name of the bot" + }, + "messagingEndpoint": { + "type": "string", + "description": "the messaging endpoint of the bot" + }, + "description": { + "type": "string", + "description": "the long description of the bot" + }, + "iconUrl": { + "type": "string", + "description": "the icon of the bot, pointed to an existing URL" + }, + "channels": { + "type": "array", + "description": "the channel(s) to be enabled of the bot", + "items": { + "oneOf": [{ "$ref": "#/definitions/MsTeamsChannel" }, { "$ref": "#/definitions/M365ExtensionsChannel" }] + } + } + } + } + } + }, + "MsTeamsChannel": { + "type": "object", + "additionalProperties": false, + "description": "Microsoft Teams channel", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Microsoft Teams channel", + "enum": ["msteams"] + }, + "callingWebhook": { + "type": "string", + "description": "Webhook for Microsoft Teams channel calls" + } + } + }, + "M365ExtensionsChannel": { + "type": "object", + "additionalProperties": false, + "description": "Microsoft 365 Extensions channel", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Microsoft 365 Extensions channel", + "enum": ["m365extensions"] + } + } + }, + "fileCreateOrUpdateEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Create or update variables to environment file. Refer to https://aka.ms/teamsfx-actions/file-createorupdateenvironmentfile for more details.", + "required": ["uses", "with"], + "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": "Create or update variables to environment file. Refer to https://aka.ms/teamsfx-actions/file-createorupdateenvironmentfile for more details.", + "const": "file/createOrUpdateEnvironmentFile" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["envs", "target"], + "properties": { + "envs": { + "type": "object", + "description": "the environment variable(s) to be generated", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "number" + } + ] + } + }, + "target": { + "type": "string", + "description": "The target environment file to be created or updated" + } + } + } + } + }, + "fileCreateOrUpdateJsonFile": { + "type": "object", + "additionalProperties": false, + "description": "Create or update JSON file. Refer to https://aka.ms/teamsfx-actions/file-createorupdatejsonfile for more details.", + "required": ["uses", "with"], + "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": "Create or update JSON file. Refer to https://aka.ms/teamsfx-actions/file-createorupdatejsonfile for more details.", + "const": "file/createOrUpdateJsonFile" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["target"], + "properties": { + "appsettings": { + "type": "object", + "description": "the app settings to be generated" + }, + "target": { + "type": "string", + "description": "the target file" + }, + "content": { + "type": "object", + "description": "the json content to be created or updated, will be merged with existing content" + } + } + } + } + }, + "devToolInstall": { + "type": "object", + "additionalProperties": false, + "description": "Install development tool(s). Refer to https://aka.ms/teamsfx-actions/devtool-install for more details.", + "required": ["uses", "with"], + "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": "Install development tool(s). Refer to https://aka.ms/teamsfx-actions/devtool-install for more details.", + "const": "devTool/install" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "properties": { + "devCert": { + "type": "object", + "description": "Generate an SSL certificate and install it to the system certificate management center. This will output environment variables specified by `sslCertFile` and `sslKeyFile` to current environment's .env file.", + "additionalProperties": false, + "required": ["trust"], + "properties": { + "trust": { + "type": "boolean", + "description": "whether to trust the SSL certificate or not" + } + } + }, + "func": { + "type": "object", + "description": "Install Azure Functions Core Tools. This will output environment variable specified by `funcPath` to current environment's .env file.", + "additionalProperties": false, + "required": [ + "version" + ], + "properties": { + "version": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "description": "The version number of Azure Functions Core Tools that follow the Semantic Versioning scheme." + }, + "symlinkDir": { + "type": "string", + "description": "The path of the symlink target for the folder containing Azure Functions Core Tools binaries." + } + } + }, + "dotnet": { + "type": "boolean", + "description": "Install .NET SDK. This will output environment variables specified by `dotnetPath` to current environment's .env file." + }, + "testTool": { + "type": "object", + "description": "Install Teams App Test Tool. This will output environment variables specified by `testToolPath` to current environment's .env file.", + "additionalProperties": false, + "required": ["version", "symlinkDir"], + "properties": { + "version": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "description": "The version number of Teams App Test Tool npm package that follow the Semantic Versioning scheme." + }, + "symlinkDir": { + "type": "string", + "description": "The path of the symlink target for the folder containing Teams App Test Tool binaries." + } + } + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "properties": { + "sslCertFile": { + "description": "The path of the certificate file of the SSL certificate. This parameter takes effect only when `devCert` is specified.", + "$ref": "#/definitions/envVarName" + }, + "sslKeyFile": { + "description": "The path of the key file of the SSL certificate. This parameter takes effect only when `devCert` is specified.", + "$ref": "#/definitions/envVarName" + }, + "funcPath": { + "description": "The path of the Azure Functions Core Tools binary. This parameter takes effect only when `func` is `true`.", + "$ref": "#/definitions/envVarName" + }, + "dotnetPath": { + "description": "The path of the .NET binary. This parameter takes effect only when `dotnet` is `true`.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "teamsAppExtendToM365": { + "type": "object", + "additionalProperties": false, + "description": "Extend Teams app across Microsoft 365. Refer to https://aka.ms/teamsfx-actions/teamsapp-extendToM365 for more details.", + "required": ["uses", "with"], + "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": "Extend Teams app across Microsoft 365. Refer to https://aka.ms/teamsfx-actions/teamsapp-extendToM365 for more details.", + "const": "teamsApp/extendToM365" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "parameters for this action", + "required": ["appPackagePath"], + "properties": { + "appPackagePath": { + "type": "string", + "description": "path to Teams app package" + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["titleId", "appId"], + "properties": { + "titleId": { + "description": "Required. The ID of M365 title.", + "$ref": "#/definitions/envVarName" + }, + "appId": { + "description": "Required. The app ID of M365 title.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "spfxDeploy": { + "type": "object", + "additionalProperties": false, + "description": "Deploy the SPFx package to SharePoint app catalog. Refer to https://aka.ms/teamsfx-actions/spfx-deploy for more details.", + "required": ["uses", "with"], + "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": "Deploy the SPFx package to SharePoint app catalog. Refer to https://aka.ms/teamsfx-actions/spfx-deploy for more details.", + "const": "spfx/deploy" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["packageSolutionPath"], + "properties": { + "createAppCatalogIfNotExist": { + "type": "boolean", + "description": "Whether to create tenant app catalog first if not exist, default value is `false`" + }, + "packageSolutionPath": { + "type": "string", + "description": "The path to package-solution.json in SPFx project" + } + } + } + } + }, + "teamsAppCopyAppPackageToSPFx": { + "type": "object", + "additionalProperties": false, + "description": "Copy the generated Teams app package to SPFx solution. Refer to https://aka.ms/teamsfx-actions/teams-app-copy-app-package-to-spfx for more details.", + "required": ["uses", "with"], + "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": "Copy the generated Teams app package to SPFx solution. Refer to https://aka.ms/teamsfx-actions/teams-app-copy-app-package-to-spfx for more details.", + "const": "teamsApp/copyAppPackageToSPFx" + }, + "with": { + "type": "object", + "description": "parameters for this action", + "additionalProperties": false, + "required": ["appPackagePath", "spfxFolder"], + "properties": { + "spfxFolder": { + "type": "string", + "description": "The source folder to the SPFx project" + }, + "appPackagePath": { + "type": "string", + "description": "The path to the zipped Teams app package" + } + } + } + } + }, + "script": { + "type": "object", + "additionalProperties": false, + "description": "Execute a user defined script. Refer to https://aka.ms/teamsfx-actions/script for more details.", + "required": ["uses", "with"], + "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": "Execute a user defined script. Refer to https://aka.ms/teamsfx-actions/script for more details.", + "const": "script" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["run"], + "properties": { + "run": { + "type": "string", + "description": "The command for this action to run or path to the script. Succeeds if exit code is 0." + }, + "workingDirectory": { + "type": "string", + "description": "Current working directory. Defaults to the directory of this file." + }, + "shell": { + "type": "string", + "description": "Shell command. If not specified, use default shell for current platform. The rule is: 1) use the value of the 'SHELL' environment variable if it is set. Otherwise the shell depends on operation system: 2) on macOS, use '/bin/zsh' if it exists, otherwise use '/bin/bash'; 3) On Windows, use the value of the 'ComSpec' environment variable if it exists, otherwise use 'cmd.exe'; 4) On Linux or other OS, use '/bin/sh' if it extis." + }, + "timeout": { + "type": "number", + "description": "timeout in ms" + }, + "redirectTo": { + "type": "string", + "description": "redirect stdout and stderr to a file" + } + } + } + } + }, + "relativePath": { + "type": "string", + "maxLength": 2048 + }, + "httpsUrl": { + "type": "string", + "maxLength": 2048, + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://" + }, + "semver": { + "type": "string", + "maxLength": 256, + "pattern": "^([0-9]|[1-9]+[0-9]*)\\.([0-9]|[1-9]+[0-9]*)\\.([0-9]|[1-9]+[0-9]*)$" + }, + "hexColor": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "guid": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$" + }, + "languageTag": { + "type": "string", + "pattern": "^[A-Za-z0-9]{1,8}(-[A-Za-z0-9]{1,8}){0,2}$" + }, + "taskInfoDimension": { + "type": "string", + "pattern": "^((([0-9]*\\.)?[0-9]+)|[lL][aA][rR][gG][eE]|[mM][eE][dD][iI][uU][mM]|[sS][mM][aA][lL][lL])$", + "maxLength": 16 + }, + "secretEnvVarName": { + "type": "string", + "pattern": "^SECRET_[A-Z0-9_]+$", + "maxLength": 256 + }, + "envVarName": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$", + "maxLength": 256 + }, + "apiKeyRegister": { + "type": "object", + "additionalProperties": false, + "description": "Create an API key.", + "required": ["uses", "with", "writeToEnvironmentFile"], + "properties": { + "uses": { + "type": "string", + "description": "Register API key. Refer to https://aka.ms/teamsfx-actions/apiKey-register for more details.", + "const": "apiKey/register" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name", "appId", "apiSpecPath"], + "properties": { + "name": { + "type": "string", + "description": "The name of API key." + }, + "appId": { + "type": "string", + "description": "The app ID of Teams app." + }, + "primaryClientSecret": { + "type": "string", + "description": "Primary client secret of API key. Length of client secret >= 10 and <= 128" + }, + "secondaryClientSecret": { + "type": "string", + "description": "Secondary callingWebhook secret of API key. Length of client secret >= 10 and <= 128" + }, + "apiSpecPath": { + "type": "string", + "description": "The path of API specification file." + }, + "applicableToApps": { + "type": "string", + "description": "Which app can access the API key? Values can be \"SpecificApp\" or \"AnyApp\". Default is \"AnyApp\".", + "enum": ["SpecificApp", "AnyApp"] + }, + "targetAudience": { + "type": "string", + "description": "Which tenant can access the API key? Values can be \"HomeTenant\" or \"AnyTenant\". Default is \"AnyTenant\".", + "enum": ["HomeTenant", "AnyTenant"] + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["registrationId"], + "properties": { + "registrationId": { + "description": "Required. The registration id of created API key.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "azureStaticWebAppGetDeploymentKey": { + "type": "object", + "additionalProperties": false, + "description": "Get deployment key from Azure Static Web App.", + "required": ["uses", "with", "writeToEnvironmentFile"], + "properties": { + "uses": { + "type": "string", + "description": "Get deployment key. Refer to https://aka.ms/teamsfx-actions/swa-get-deployment-key for more details.", + "const": "azureStaticWebApps/getDeploymentToken" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["resourceId"], + "properties": { + "resourceId": { + "type": "string", + "description": "The resource ID of Azure Static Web App." + } + } + }, + "writeToEnvironmentFile": { + "type": "object", + "additionalProperties": false, + "description": "Write environment variables to environment file", + "required": ["deploymentToken"], + "properties": { + "deploymentToken": { + "description": "Required. The deployment token of Azure Static Web App.", + "$ref": "#/definitions/envVarName" + } + } + } + } + }, + "apiKeyUpdate": { + "type": "object", + "additionalProperties": false, + "description": "Update an API key.", + "required": ["uses", "with"], + "properties": { + "uses": { + "type": "string", + "description": "Updagte API key. Refer to https://aka.ms/teamsfx-actions/apiKey-update for more details.", + "const": "apiKey/update" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name", "appId", "apiSpecPath", "registrationId"], + "properties": { + "name": { + "type": "string", + "description": "The name of API key." + }, + "appId": { + "type": "string", + "description": "The app ID of Teams app." + }, + "apiSpecPath": { + "type": "string", + "description": "The path of API specification file." + }, + "registrationId": { + "type": "string", + "description": "The registration id of API key." + }, + "applicableToApps": { + "type": "string", + "description": "Which app can access the API key? Values can be \"SpecificApp\" or \"AnyApp\". Default is \"AnyApp\".", + "enum": ["SpecificApp", "AnyApp"] + }, + "targetAudience": { + "type": "string", + "description": "Which tenant can access the API key? Values can be \"HomeTenant\" or \"AnyTenant\". Default is \"AnyTenant\".", + "enum": ["HomeTenant", "AnyTenant"] + } + } + } + } + } + } +} diff --git a/packages/fx-core/resource/yaml-schema/yaml.schema.json b/packages/fx-core/resource/yaml-schema/yaml.schema.json index 82c7dd151c..69bcc8be3e 100644 --- a/packages/fx-core/resource/yaml-schema/yaml.schema.json +++ b/packages/fx-core/resource/yaml-schema/yaml.schema.json @@ -14,7 +14,7 @@ "version": { "type": "string", "description": "The version of the yaml file schema", - "const": "v1.4" + "const": "v1.5" }, "additionalMetadata": { "type": "object", @@ -72,7 +72,8 @@ { "$ref": "#/definitions/teamsAppCopyAppPackageToSPFx" }, { "$ref": "#/definitions/script" }, { "$ref": "#/definitions/apiKeyRegister"}, - { "$ref": "#/definitions/azureStaticWebAppGetDeploymentKey"} + { "$ref": "#/definitions/azureStaticWebAppGetDeploymentKey"}, + { "$ref": "#/definitions/apiKeyUpdate"} ] } }, @@ -127,6 +128,19 @@ "type": "string", "description": "Specifies what Microsoft accounts are supported for the current application.", "enum": ["AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"] + }, + "serviceManagementReference": { + "type": "string", + "description": "References application or service contact information from a Service or Asset Management database." + }, + "clientSecretExpireDays": { + "type": "integer", + "description": "The number of days the client secret is valid.", + "minimum": 1 + }, + "clientSecretDescription": { + "type": "string", + "description": "The description of the client secret." } } }, @@ -192,6 +206,10 @@ "type": "string", "description": "Specifies what Microsoft accounts are supported for the current application.", "enum": ["AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"] + }, + "serviceManagementReference": { + "type": "string", + "description": "References application or service contact information from a Service or Asset Management database." } } }, @@ -1530,6 +1548,16 @@ "apiSpecPath": { "type": "string", "description": "The path of API specification file." + }, + "applicableToApps": { + "type": "string", + "description": "Which app can access the API key? Values can be \"SpecificApp\" or \"AnyApp\". Default is \"AnyApp\".", + "enum": ["SpecificApp", "AnyApp"] + }, + "targetAudience": { + "type": "string", + "description": "Which tenant can access the API key? Values can be \"HomeTenant\" or \"AnyTenant\". Default is \"AnyTenant\".", + "enum": ["HomeTenant", "AnyTenant"] } } }, @@ -1583,6 +1611,53 @@ } } } + }, + "apiKeyUpdate": { + "type": "object", + "additionalProperties": false, + "description": "Update an API key.", + "required": ["uses", "with"], + "properties": { + "uses": { + "type": "string", + "description": "Updagte API key. Refer to https://aka.ms/teamsfx-actions/apiKey-update for more details.", + "const": "apiKey/update" + }, + "with": { + "type": "object", + "additionalProperties": false, + "description": "Parameters for this action", + "required": ["name", "appId", "apiSpecPath", "registrationId"], + "properties": { + "name": { + "type": "string", + "description": "The name of API key." + }, + "appId": { + "type": "string", + "description": "The app ID of Teams app." + }, + "apiSpecPath": { + "type": "string", + "description": "The path of API specification file." + }, + "registrationId": { + "type": "string", + "description": "The registration id of API key." + }, + "applicableToApps": { + "type": "string", + "description": "Which app can access the API key? Values can be \"SpecificApp\" or \"AnyApp\". Default is \"AnyApp\".", + "enum": ["SpecificApp", "AnyApp"] + }, + "targetAudience": { + "type": "string", + "description": "Which tenant can access the API key? Values can be \"HomeTenant\" or \"AnyTenant\". Default is \"AnyTenant\".", + "enum": ["HomeTenant", "AnyTenant"] + } + } + } + } } } } diff --git a/packages/fx-core/src/common/constants.ts b/packages/fx-core/src/common/constants.ts index af3ddccfd7..eae66a0815 100644 --- a/packages/fx-core/src/common/constants.ts +++ b/packages/fx-core/src/common/constants.ts @@ -58,12 +58,14 @@ export class FeatureFlagName { static readonly OfficeXMLAddin = "TEAMSFX_OFFICE_XML_ADDIN"; static readonly CopilotPlugin = "DEVELOP_COPILOT_PLUGIN"; static readonly ApiCopilotPlugin = "API_COPILOT_PLUGIN"; - static readonly TeamsSampleConfigBranch = "TEAMSFX_SAMPLE_CONFIG_BRANCH"; - static readonly OfficeSampleConfigBranch = "TEAMSFX_OFFICE_SAMPLE_CONFIG_BRANCH"; + static readonly SampleConfigBranch = "TEAMSFX_SAMPLE_CONFIG_BRANCH"; static readonly TestTool = "TEAMSFX_TEST_TOOL"; + static readonly METestTool = "TEAMSFX_ME_TEST_TOOL"; static readonly ApiKey = "API_COPILOT_API_KEY"; static readonly MultipleParameters = "API_COPILOT_MULTIPLE_PARAMETERS"; static readonly TeamsFxRebranding = "TEAMSFX_REBRANDING"; static readonly TdpTemplateCliTest = "TEAMSFX_TDP_TEMPLATE_CLI_TEST"; + static readonly AsyncAppValidation = "TEAMSFX_ASYNC_APP_VALIDATION"; static readonly NewProjectType = "TEAMSFX_NEW_PROJECT_TYPE"; + static readonly ApiMeSSO = "API_ME_SSO"; } diff --git a/packages/fx-core/src/common/featureFlags.ts b/packages/fx-core/src/common/featureFlags.ts index dd63c4fec5..49eb55159b 100644 --- a/packages/fx-core/src/common/featureFlags.ts +++ b/packages/fx-core/src/common/featureFlags.ts @@ -21,7 +21,7 @@ export function initializePreviewFeatureFlags(): void { process.env[FeatureFlagName.AadManifest] = "true"; process.env[FeatureFlagName.ApiConnect] = "true"; process.env[FeatureFlagName.DeployManifest] = "true"; - process.env[FeatureFlagName.OfficeXMLAddin] = "true"; + // Force the feature to close until it needs to be released. process.env[FeatureFlagName.OfficeAddin] = "false"; } @@ -50,18 +50,26 @@ export function enableTestToolByDefault(): boolean { return isFeatureFlagEnabled(FeatureFlagName.TestTool, true); } +export function enableMETestToolByDefault(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.METestTool, true); +} + export function isApiKeyEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.ApiKey, false); } export function isMultipleParametersEnabled(): boolean { - return isFeatureFlagEnabled(FeatureFlagName.MultipleParameters, false); + return isFeatureFlagEnabled(FeatureFlagName.MultipleParameters, true); } export function isOfficeXMLAddinEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.OfficeXMLAddin, false); } +export function isOfficeJSONAddinEnabled(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.OfficeAddin, false); +} + export function isTeamsFxRebrandingEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.TeamsFxRebranding, false); } @@ -70,10 +78,68 @@ export function isTdpTemplateCliTestEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.TdpTemplateCliTest, false); } +export function isAsyncAppValidationEnabled(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.AsyncAppValidation, false); +} + export function isNewProjectTypeEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.NewProjectType, true); } -export function isOfficeJSONAddinEnabled(): boolean { - return isFeatureFlagEnabled(FeatureFlagName.OfficeAddin, false); +export function isApiMeSSOEnabled(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.ApiMeSSO, false); } + +/////////////////////////////////////////////////////////////////////////////// +// Notes for Office Addin Feature flags: +// Case 1: TEAMSFX_OFFICE_ADDIN = false, TEAMSFX_OFFICE_XML_ADDIN = false +// 1.1 project-type option: `outlook-addin-type` +// 1.2 addin-host: not show but use `outlook` internally +// 1.3 capabilities options: [`json-taskpane`, `outlook-addin-import`] +// 1.4 programming-language options: [`typescript`] (skip in UI) +// 1.5 office-addin-framework-type: not show question but use `default_old` internally +// 1.6 generator class: OfficeAddinGenerator +// 1.7 template link: config.json.json-taskpane.default_old.typescript +// Case 2: TEAMSFX_OFFICE_ADDIN = false AND TEAMSFX_OFFICE_XML_ADDIN = true +// 2.1 project-type option: `office-xml-addin-type` +// 2.2 addin-host options: [`outlook`, `word`, `excel`, `powerpoint`] +// 2.3 capabilities options: +// if (addin-host == `outlook`) then [`json-taskpane`, `outlook-addin-import`] +// else if (addin-host == `word`) then [`word-taskpane`, `word-xxx`, ...] +// else if (addin-host == `excel`) then [`excel-taskpane`, `excel-xxx`, ...] +// else if (addin-host === `powerpoint`) then [`powerpoint-taskpane`, `powerpoint-xxx`, ...] +// 2.4 programming-language options: +// if (addin-host == `outlook`) then [`typescript`] (skip in UI) +// else two options: [`typescript`, `javascript`] +// 2.5 office-addin-framework-type options: +// if (word excel and powerpoint) use `default` internally +// else if (outlook) use `default_old` internally +// 2.6 generator class: +// if (addin-host == `outlook`) then OfficeAddinGenerator +// else OfficeXMLAddinGenerator +// 2.7 template link: +// if (addin-host == `outlook`) config.json.json-taskpane.default.[programming-language] +// else config[addin-host].[capabilities].default.[programming-language] +// Case 3: TEAMSFX_OFFICE_ADDIN = true AND TEAMSFX_OFFICE_XML_ADDIN = true +// 3.1 project-type option: `office-addin-type` +// 3.2 addin-host: not show but will use `wxpo` internally +// 3.3 capabilities options: [`json-taskpane`, `office-addin-import`, `office-content-addin`] +// 3.4 programming-language options: [`typescript`, `javascript`] +// 3.5 office-addin-framework-type options: [`default`, `react`] +// if (capabilities == `json-taskpane`) then [`default`, `react`] +// else if (capabilities == `office-addin-import`) then [`default`] (skip in UI) +// else if (capabilities == `office-content-addin`) then [`default`] (skip in UI) +// 3.6 generator class: OfficeAddinGenerator +// 3.7 template link: config.json.[capabilities].[office-addin-framework-type].[programming-language] +// case 4: TEAMSFX_OFFICE_ADDIN = true AND TEAMSFX_OFFICE_XML_ADDIN = fasle +// 4.1 project-type option: `office-addin-type` +// 4.2 addin-host: not show but will use `wxpo` internally +// 4.3 capabilities options: [`json-taskpane`, `office-addin-import`] +// 4.4 programming-language options: [`typescript`, `javascript`] +// 4.5 office-addin-framework-type options: [`default`, `react`] +// if (capabilities == `json-taskpane`) then [`default`, `react`] +// else if (capabilities == `office-addin-import`) then [`default`] (skip in UI) +// else if (capabilities == `office-content-addin`) then [`default`] (skip in UI) +// 4.6 generator class: OfficeAddinGenerator +// 4.7 template link: config.json.[capabilities].[office-addin-framework-type].[programming-language] +/////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/fx-core/src/common/projectSettingsHelper.ts b/packages/fx-core/src/common/projectSettingsHelper.ts index 66c931f172..012cd66552 100644 --- a/packages/fx-core/src/common/projectSettingsHelper.ts +++ b/packages/fx-core/src/common/projectSettingsHelper.ts @@ -5,6 +5,11 @@ import fs from "fs-extra"; import * as path from "path"; import { MetadataV3 } from "./versionMetadata"; +export enum OfficeManifestType { + XmlAddIn, + MetaOsAddIn, +} + export function validateProjectSettings(projectSettings: any): string | undefined { if (!projectSettings) return "empty projectSettings"; if (!projectSettings.solutionSettings) return undefined; @@ -58,9 +63,14 @@ export function isValidProject(workspacePath?: string): boolean { } export function isValidOfficeAddInProject(workspacePath?: string): boolean { - const manifestList = fetchManifestList(workspacePath); + const xmlManifestList = fetchManifestList(workspacePath, OfficeManifestType.XmlAddIn); + const metaOsManifestList = fetchManifestList(workspacePath, OfficeManifestType.MetaOsAddIn); try { - if (manifestList && manifestList.length > 0) { + if ( + xmlManifestList && + xmlManifestList.length > 0 && + (!metaOsManifestList || metaOsManifestList.length == 0) + ) { return true; } else { return false; @@ -70,21 +80,38 @@ export function isValidOfficeAddInProject(workspacePath?: string): boolean { } } -export function fetchManifestList(workspacePath?: string): string[] | undefined { +export function fetchManifestList( + workspacePath?: string, + officeManifestType?: OfficeManifestType +): string[] | undefined { if (!workspacePath) return undefined; const list = fs.readdirSync(workspacePath); - const manifestList = list.filter((fileName) => isOfficeAddInManifest(fileName)); + const manifestList = list.filter((fileName) => + officeManifestType == OfficeManifestType.XmlAddIn + ? isOfficeXmlAddInManifest(fileName) + : isOfficeMetaOsAddInManifest(fileName) + ); return manifestList; } -export function isOfficeAddInManifest(inputFileName: string): boolean { +export function isOfficeXmlAddInManifest(inputFileName: string): boolean { return ( inputFileName.toLocaleLowerCase().indexOf("manifest") != -1 && inputFileName.toLocaleLowerCase().endsWith(".xml") ); } +export function isOfficeMetaOsAddInManifest(inputFileName: string): boolean { + return ( + inputFileName.toLocaleLowerCase().indexOf("manifest") != -1 && + inputFileName.toLocaleLowerCase().endsWith(".json") + ); +} + export function isValidProjectV3(workspacePath: string): boolean { + if (isValidOfficeAddInProject(workspacePath)) { + return false; + } const ymlFilePath = path.join(workspacePath, MetadataV3.configFile); const localYmlPath = path.join(workspacePath, MetadataV3.localConfigFile); if (fs.pathExistsSync(ymlFilePath) || fs.pathExistsSync(localYmlPath)) { diff --git a/packages/fx-core/src/common/projectTypeChecker.ts b/packages/fx-core/src/common/projectTypeChecker.ts index f048ac82a7..1ab3f04b38 100644 --- a/packages/fx-core/src/common/projectTypeChecker.ts +++ b/packages/fx-core/src/common/projectTypeChecker.ts @@ -6,6 +6,7 @@ import path from "path"; import semver from "semver"; import { parseDocument } from "yaml"; import { MetadataV2, MetadataV3 } from "./versionMetadata"; +import { isValidOfficeAddInProject } from "./projectSettingsHelper"; export enum TeamsfxConfigType { projectSettingsJson = "projectSettings.json", @@ -34,6 +35,7 @@ export interface ProjectTypeResult { manifestVersion?: string; dependsOnTeamsJs?: boolean; isSPFx?: boolean; + officeAddinProjectType?: string; lauguages: ("ts" | "js" | "csharp" | "java" | "python" | "c")[]; } @@ -195,6 +197,16 @@ class ProjectTypeChecker { } return true; } + + findOfficeAddinProject(filePath: string, data: ProjectTypeResult): boolean { + if (isValidOfficeAddInProject(filePath)) { + data.officeAddinProjectType = "XML"; + data.isTeamsFx = false; + return false; + } + return true; + } + async checkProjectType(projectPath: string) { const result: ProjectTypeResult = { isTeamsFx: false, @@ -236,6 +248,7 @@ class ProjectTypeChecker { 2, 0 ); + this.findOfficeAddinProject(projectPath, result); } catch (e) {} return result; } diff --git a/packages/fx-core/src/common/samples.ts b/packages/fx-core/src/common/samples.ts index 29e6074814..d4341ba45f 100644 --- a/packages/fx-core/src/common/samples.ts +++ b/packages/fx-core/src/common/samples.ts @@ -13,12 +13,9 @@ import { FeatureFlagName } from "./constants"; const packageJson = require("../../package.json"); const SampleConfigOwner = "OfficeDev"; -const TeamsSampleConfigRepo = "TeamsFx-Samples"; -const TeamsSampleConfigFile = ".config/samples-config-v3.json"; -const OfficeSampleConfigRepo = "Office-Samples"; -const OfficeSampleConfigFile = ".config/samples-config-v1.json"; -export const TeamsSampleConfigTag = "v2.4.0"; -export const OfficeSampleConfigTag = "v0.0.1"; +const SampleConfigRepo = "TeamsFx-Samples"; +const SampleConfigFile = ".config/samples-config-v3.json"; +export const SampleConfigTag = "v2.4.0"; // prerelease tag is always using a branch. export const SampleConfigBranchForPrerelease = "main"; @@ -70,133 +67,56 @@ class SampleProvider { } public async refreshSampleConfig(): Promise { - const teamsRet = await this.fetchOnlineSampleConfig( - TeamsSampleConfigRepo, - TeamsSampleConfigFile - ); - const teamsSampleCollection = await this.parseOnlineSampleConfig( - SampleConfigOwner, - TeamsSampleConfigRepo, - teamsRet.samplesConfig, - teamsRet.ref - ); - const officeRet = await this.fetchOnlineSampleConfig( - OfficeSampleConfigRepo, - OfficeSampleConfigFile - ); - const officeSampleCollection = await this.parseOnlineSampleConfig( - SampleConfigOwner, - OfficeSampleConfigRepo, - officeRet.samplesConfig, - officeRet.ref - ); - // merge samples from TeamsFx-Samples and Office-Samples - // use Set to remove duplicates - this.sampleCollection = { - samples: [...teamsSampleCollection.samples, ...officeSampleCollection.samples], - filterOptions: { - capabilities: Array.from( - new Set([ - ...teamsSampleCollection.filterOptions.capabilities, - ...officeSampleCollection.filterOptions.capabilities, - ]) - ), - languages: Array.from( - new Set([ - ...teamsSampleCollection.filterOptions.languages, - ...officeSampleCollection.filterOptions.languages, - ]) - ), - technologies: Array.from( - new Set([ - ...teamsSampleCollection.filterOptions.technologies, - ...officeSampleCollection.filterOptions.technologies, - ]) - ), - }, - }; + const { samplesConfig, ref } = await this.fetchOnlineSampleConfig(); + this.sampleCollection = this.parseOnlineSampleConfig(samplesConfig, ref); return this.sampleCollection; } - private async fetchOnlineSampleConfig(configRepo: string, configFile: string) { - const getRef = (configRepo: string, version: string) => { - if (configRepo === TeamsSampleConfigRepo) { - // Set default value for branchOrTag - if (version.includes("alpha")) { - // daily build version always use 'dev' branch - return "dev"; - } else if (version.includes("beta")) { - // prerelease build version always use branch head for prerelease. - return SampleConfigBranchForPrerelease; - } else if (version.includes("rc")) { - // if there is a breaking change, the tag is not used by any stable version. - return TeamsSampleConfigTag; - } else { - // stable version uses the head of branch defined by feature flag when available - return TeamsSampleConfigTag; - } - } else { - // Office Samples - if (version.includes("alpha")) { - return "dev"; - } else if (version.includes("beta")) { - return SampleConfigBranchForPrerelease; - } else if (version.includes("rc")) { - return OfficeSampleConfigTag; - } else { - return OfficeSampleConfigTag; - } - // return "dev"; - } - }; + private async fetchOnlineSampleConfig() { const version: string = packageJson.version; - const configBranchInEnv = - process.env[ - configRepo === TeamsSampleConfigRepo - ? FeatureFlagName.TeamsSampleConfigBranch - : FeatureFlagName.OfficeSampleConfigBranch - ]; + const configBranchInEnv = process.env[FeatureFlagName.SampleConfigBranch]; let samplesConfig: SampleConfigType | undefined; - let ref = getRef(configRepo, version); + let ref = SampleConfigTag; + + // Set default value for branchOrTag + if (version.includes("alpha")) { + // daily build version always use 'dev' branch + ref = "dev"; + } else if (version.includes("beta")) { + // prerelease build version always use branch head for prerelease. + ref = SampleConfigBranchForPrerelease; + } else if (version.includes("rc")) { + // if there is a breaking change, the tag is not used by any stable version. + ref = SampleConfigTag; + } else { + // stable version uses the head of branch defined by feature flag when available + ref = SampleConfigTag; + } + // Set branchOrTag value if branch in env is valid if (configBranchInEnv) { try { - const data = await this.fetchRawFileContent( - SampleConfigOwner, - configRepo, - configBranchInEnv, - configFile - ); + const data = await this.fetchRawFileContent(configBranchInEnv); ref = configBranchInEnv; samplesConfig = data as SampleConfigType; } catch (e: unknown) {} } if (samplesConfig === undefined) { - samplesConfig = (await this.fetchRawFileContent( - SampleConfigOwner, - configRepo, - ref, - configFile - )) as SampleConfigType; + samplesConfig = (await this.fetchRawFileContent(ref)) as SampleConfigType; } return { samplesConfig, ref }; } @hooks([ErrorContextMW({ component: "SampleProvider" })]) - private parseOnlineSampleConfig( - samplesOnwer: string, - samplesRepo: string, - samplesConfig: SampleConfigType, - ref: string - ): Promise { + private parseOnlineSampleConfig(samplesConfig: SampleConfigType, ref: string): SampleCollection { const samples = samplesConfig?.samples.map((sample) => { const isExternal = sample["downloadUrlInfo"] ? true : false; let gifUrl = sample["gifPath"] !== undefined - ? `https://raw.githubusercontent.com/${samplesOnwer}/${samplesRepo}/${ref}/${ + ? `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${ref}/${ sample["id"] as string }/${sample["gifPath"] as string}` : undefined; @@ -216,7 +136,7 @@ class SampleProvider { ? sample["downloadUrlInfo"] : { owner: SampleConfigOwner, - repository: samplesRepo, + repository: SampleConfigRepo, ref: ref, dir: sample["id"] as string, }, @@ -224,14 +144,14 @@ class SampleProvider { } as SampleConfig; }) || []; - return Promise.resolve({ + return { samples, filterOptions: { capabilities: samplesConfig?.filterOptions["capabilities"] || [], languages: samplesConfig?.filterOptions["languages"] || [], technologies: samplesConfig?.filterOptions["technologies"] || [], }, - }); + }; } public async getSampleReadmeHtml(sample: SampleConfig): Promise { @@ -261,13 +181,8 @@ class SampleProvider { } } - private async fetchRawFileContent( - configOwner: string, - configRepo: string, - branchOrTag: string, - configFile: string - ): Promise { - const url = `https://raw.githubusercontent.com/${configOwner}/${configRepo}/${branchOrTag}/${configFile}`; + private async fetchRawFileContent(branchOrTag: string): Promise { + const url = `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${branchOrTag}/${SampleConfigFile}`; try { const fileResponse = await sendRequestWithTimeout( async () => { @@ -276,7 +191,6 @@ class SampleProvider { 1000, 3 ); - if (fileResponse && fileResponse.data) { return fileResponse.data; } diff --git a/packages/fx-core/src/common/telemetry.ts b/packages/fx-core/src/common/telemetry.ts index fed44f7bfe..a86785958a 100644 --- a/packages/fx-core/src/common/telemetry.ts +++ b/packages/fx-core/src/common/telemetry.ts @@ -6,6 +6,7 @@ import { TelemetryConstants } from "../component/constants"; import { TOOLS, globalVars } from "../core/globalVars"; import { ProjectTypeResult } from "./projectTypeChecker"; import { assign } from "lodash"; +import { ProjectType } from "@microsoft/m365-spec-parser"; export enum TelemetryProperty { TriggerFrom = "trigger-from", @@ -56,6 +57,10 @@ export enum TelemetryProperty { GraphPermissionHasRole = "graph-permission-has-role", GraphPermissionHasAdminScope = "graph-permission-has-admin-scope", GraphPermissionScopes = "graph-permission-scopes", + GraphPermissionRoles = "graph-permission-roles", + RscApplication = "rsc-application", + RscDelegated = "rsc-delegated", + AadManifest = "aad-manifest", } @@ -140,6 +145,7 @@ export enum ProjectTypeProps { TeamsManifestCapabilities = "manifest-capabilities", TeamsJs = "teams-js", Lauguages = "languages", + OfficeAddinProjectType = "office-addin-project-type", } export enum TelemetrySuccess { @@ -235,8 +241,8 @@ export function fillInTelemetryPropsForFxError( props[TelemetryConstants.properties.errorCode] = props[TelemetryConstants.properties.errorCode] || errorCode; props[TelemetryConstants.properties.errorType] = errorType; - props[TelemetryConstants.properties.errorMessage] = error.message; - props[TelemetryConstants.properties.errorStack] = error.stack !== undefined ? error.stack : ""; // error stack will not append in error-message any more + // props[TelemetryConstants.properties.errorMessage] = error.message; // error-message is retired + // props[TelemetryConstants.properties.errorStack] = error.stack !== undefined ? error.stack : ""; // error stack will not append in error-message any more props[TelemetryConstants.properties.errorName] = error.name; // append global context properties @@ -248,12 +254,12 @@ export function fillInTelemetryPropsForFxError( props[TelemetryConstants.properties.errorInnerCode] = error.innerError["code"]; } - if (error.innerError) { - props[TelemetryConstants.properties.innerError] = JSON.stringify( - error.innerError, - Object.getOwnPropertyNames(error.innerError) - ); - } + // if (error.innerError) { // inner-error is retired + // props[TelemetryConstants.properties.innerError] = JSON.stringify( + // error.innerError, + // Object.getOwnPropertyNames(error.innerError) + // ); + // } if (error.categories) { props[TelemetryConstants.properties.errorCat] = error.categories.join("|"); @@ -280,6 +286,7 @@ export function fillinProjectTypeProperties( [ProjectTypeProps.Lauguages]: projectTypeRes.lauguages.join(","), [ProjectTypeProps.TeamsManifestCapabilities]: projectTypeRes.manifestCapabilities?.join(",") || "", + [ProjectTypeProps.OfficeAddinProjectType]: projectTypeRes.officeAddinProjectType || "", }; assign(props, newProps); } diff --git a/packages/fx-core/src/common/templates-config.json b/packages/fx-core/src/common/templates-config.json index 681dbede8a..477ba8cfcd 100644 --- a/packages/fx-core/src/common/templates-config.json +++ b/packages/fx-core/src/common/templates-config.json @@ -1,11 +1,12 @@ { - "version": "~4.1", - "localVersion": "4.1.0", - "tagPrefix": "templates@", - "tagListURL": "https://github.com/OfficeDev/TeamsFx/releases/download/template-tag-list/template-tags.txt", - "templateDownloadBaseURL": "https://github.com/OfficeDev/TeamsFx/releases/download", - "templateReleaseURL": "https://github.com/OfficeDev/TeamsFx/releases/expanded_assets", - "templateDownloadBasePath": "/OfficeDev/TeamsFx/releases/download", - "templateExt": ".zip", - "useLocalTemplate": true -} \ No newline at end of file + "version": "~4.1", + "localVersion": "4.1.0", + "tagPrefix": "templates@", + "tagListURL": "https://github.com/OfficeDev/TeamsFx/releases/download/template-tag-list/template-tags.txt", + "templateDownloadBaseURL": "https://github.com/OfficeDev/TeamsFx/releases/download", + "templateReleaseURL": "https://github.com/OfficeDev/TeamsFx/releases/expanded_assets", + "templateDownloadBasePath": "/OfficeDev/TeamsFx/releases/download", + "templateExt": ".zip", + "useLocalTemplate": true +} + diff --git a/packages/fx-core/src/common/wrappedAxiosClient.ts b/packages/fx-core/src/common/wrappedAxiosClient.ts index 64b6311852..75f4499ff5 100644 --- a/packages/fx-core/src/common/wrappedAxiosClient.ts +++ b/packages/fx-core/src/common/wrappedAxiosClient.ts @@ -115,14 +115,16 @@ export class WrappedAxiosClient { method: method, params: this.generateParameters(error.config!.params), [TelemetryPropertyKey.success]: TelemetryPropertyValue.failure, - [TelemetryPropertyKey.errorMessage]: JSON.stringify(error.response!.data), - "status-code": error.response!.status.toString() ?? "undefined", + [TelemetryPropertyKey.errorMessage]: error.response + ? JSON.stringify(error.response.data) + : error.message ?? "undefined", + "status-code": error.response?.status.toString() ?? "undefined", ...this.generateExtraProperties(fullPath, requestData), }; let eventName: string; if (this.isTDPApi(fullPath)) { - const correlationId = error.response!.headers[Constants.CORRELATION_ID]; + const correlationId = error.response?.headers[Constants.CORRELATION_ID] ?? "undefined"; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const extraData = error.response?.data ? `data: ${JSON.stringify(error.response.data)}` : ""; const TDPApiFailedError = new DeveloperPortalAPIFailedError( diff --git a/packages/fx-core/src/component/configManager/constant.ts b/packages/fx-core/src/component/configManager/constant.ts index b5553882ba..9066f9f110 100644 --- a/packages/fx-core/src/component/configManager/constant.ts +++ b/packages/fx-core/src/component/configManager/constant.ts @@ -9,6 +9,7 @@ export enum SummaryConstant { Succeeded = "(√) Done:", Failed = "(×) Error:", NotExecuted = "(!) Warning:", + Warning = "(!) Warning:", } export const component = "ConfigManager"; diff --git a/packages/fx-core/src/component/configManager/validator.ts b/packages/fx-core/src/component/configManager/validator.ts index ce7b4bc22e..8554cf8e44 100644 --- a/packages/fx-core/src/component/configManager/validator.ts +++ b/packages/fx-core/src/component/configManager/validator.ts @@ -7,7 +7,7 @@ import path from "path"; import { getResourceFolder } from "../../folder"; type Version = string; -const supportedVersions = ["1.0.0", "1.1.0", "v1.2", "v1.3", "v1.4"]; +const supportedVersions = ["1.0.0", "1.1.0", "v1.2", "v1.3", "v1.4", "v1.5"]; export class Validator { impl: Map; diff --git a/packages/fx-core/src/component/coordinator/index.ts b/packages/fx-core/src/component/coordinator/index.ts index 908974ff5a..0fe0d2cf51 100644 --- a/packages/fx-core/src/component/coordinator/index.ts +++ b/packages/fx-core/src/component/coordinator/index.ts @@ -23,10 +23,13 @@ import { EOL } from "os"; import * as path from "path"; import * as uuid from "uuid"; import * as xml2js from "xml2js"; +import { isApiKeyEnabled, isApiMeSSOEnabled } from "../../common/featureFlags"; import { getLocalizedString } from "../../common/localizeUtils"; import { TelemetryEvent, TelemetryProperty } from "../../common/telemetry"; import { getResourceGroupInPortal } from "../../common/tools"; +import { convertToAlphanumericOnly } from "../../common/utils"; import { MetadataV3 } from "../../common/versionMetadata"; +import { environmentNameManager } from "../../core/environmentName"; import { ObjectIsUndefinedError } from "../../core/error"; import { ErrorContextMW, globalVars } from "../../core/globalVars"; import { ResourceGroupConflictError, SelectSubscriptionError } from "../../error/azure"; @@ -38,15 +41,16 @@ import { } from "../../error/common"; import { LifeCycleUndefinedError } from "../../error/yml"; import { - MeArchitectureOptions, + ApiMessageExtensionAuthOptions, AppNamePattern, CapabilityOptions, + CustomCopilotAssistantOptions, + CustomCopilotRagOptions, + MeArchitectureOptions, NotificationTriggerOptions, + OfficeAddinHostOptions, ProjectTypeOptions, ScratchOptions, - ApiMessageExtensionAuthOptions, - CustomCopilotRagOptions, - CustomCopilotAssistantOptions, } from "../../question/create"; import { QuestionNames } from "../../question/questionNames"; import { ExecutionError, ExecutionOutput, ILifecycle } from "../configManager/interface"; @@ -60,6 +64,7 @@ import { AppStudioScopes, Constants } from "../driver/teamsApp/constants"; import { CopilotPluginGenerator } from "../generator/copilotPlugin/generator"; import { Generator } from "../generator/generator"; import { OfficeAddinGenerator } from "../generator/officeAddin/generator"; +import { OfficeXMLAddinGenerator } from "../generator/officeXMLAddin/generator"; import { SPFxGenerator } from "../generator/spfx/spfxGenerator"; import { convertToLangKey } from "../generator/utils"; import { ActionContext, ActionExecutionMW } from "../middleware/actionExecutionMW"; @@ -70,10 +75,6 @@ import { metadataUtil } from "../utils/metadataUtil"; import { pathUtils } from "../utils/pathUtils"; import { settingsUtil } from "../utils/settingsUtil"; import { SummaryReporter } from "./summary"; -import { convertToAlphanumericOnly } from "../../common/utils"; -import { isApiKeyEnabled, isOfficeXMLAddinEnabled } from "../../common/featureFlags"; -import { environmentNameManager } from "../../core/environmentName"; -import { OfficeXMLAddinGenerator } from "../generator/officeXMLAddin/generator"; export enum TemplateNames { Tab = "non-sso-tab", @@ -103,6 +104,8 @@ export enum TemplateNames { LinkUnfurling = "link-unfurling", CopilotPluginFromScratch = "copilot-plugin-from-scratch", CopilotPluginFromScratchApiKey = "copilot-plugin-from-scratch-api-key", + ApiMessageExtensionSso = "api-message-extension-sso", + ApiPluginFromScratch = "api-plugin-from-scratch", AIBot = "ai-bot", AIAssistantBot = "ai-assistant-bot", CustomCopilotBasic = "custom-copilot-basic", @@ -154,18 +157,16 @@ const Feature2TemplateName: any = { [`${CapabilityOptions.nonSsoTabAndBot().id}:undefined`]: TemplateNames.TabAndDefaultBot, [`${CapabilityOptions.botAndMe().id}:undefined`]: TemplateNames.BotAndMessageExtension, [`${CapabilityOptions.linkUnfurling().id}:undefined`]: TemplateNames.LinkUnfurling, - [`${CapabilityOptions.copilotPluginNewApi().id}:undefined:${ - ApiMessageExtensionAuthOptions.none().id - }`]: TemplateNames.CopilotPluginFromScratch, - [`${CapabilityOptions.copilotPluginNewApi().id}:undefined:${ - ApiMessageExtensionAuthOptions.apiKey().id - }`]: TemplateNames.CopilotPluginFromScratchApiKey, + [`${CapabilityOptions.copilotPluginNewApi().id}:undefined`]: TemplateNames.ApiPluginFromScratch, [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ ApiMessageExtensionAuthOptions.none().id }`]: TemplateNames.CopilotPluginFromScratch, [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ ApiMessageExtensionAuthOptions.apiKey().id }`]: TemplateNames.CopilotPluginFromScratchApiKey, + [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ + ApiMessageExtensionAuthOptions.microsoftEntra().id + }`]: TemplateNames.ApiMessageExtensionSso, [`${CapabilityOptions.aiBot().id}:undefined`]: TemplateNames.AIBot, [`${CapabilityOptions.aiAssistantBot().id}:undefined`]: TemplateNames.AIAssistantBot, [`${CapabilityOptions.tab().id}:ssr`]: TemplateNames.SsoTabSSR, @@ -201,11 +202,6 @@ const M365Actions = [ "teamsApp/extendToM365", ]; const AzureActions = ["arm/deploy"]; -const AzureDeployActions = [ - "azureAppService/zipDeploy", - "azureFunctions/zipDeploy", - "azureStorage/deploy", -]; const needTenantCheckActions = ["botAadApp/create", "aadApp/create", "botFramework/create"]; class Coordinator { @@ -271,6 +267,7 @@ class Coordinator { const language = inputs[QuestionNames.ProgrammingLanguage]; globalVars.isVS = language === "csharp"; const capability = inputs.capabilities as string; + const projectType = inputs[QuestionNames.ProjectType]; const meArchitecture = inputs[QuestionNames.MeArchitectureType] as string; const apiMEAuthType = inputs[QuestionNames.ApiMEAuth] as string; delete inputs.folder; @@ -283,32 +280,18 @@ class Coordinator { if (capability === CapabilityOptions.SPFxTab().id) { const res = await SPFxGenerator.generate(context, inputs, projectPath); if (res.isErr()) return err(res.error); - } else if ( - !isOfficeXMLAddinEnabled() && - (inputs[QuestionNames.ProjectType] === ProjectTypeOptions.outlookAddin().id || - CapabilityOptions.outlookAddinItems() - .map((i) => i.id) - .includes(capability)) - ) { - const res = await OfficeAddinGenerator.generate(context, inputs, projectPath); - if (res.isErr()) { - return err(res.error); - } - } else if ( - isOfficeXMLAddinEnabled() && - inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeXMLAddin().id - ) { - const res = - inputs[QuestionNames.OfficeAddinCapability] === ProjectTypeOptions.outlookAddin().id - ? await OfficeAddinGenerator.generate(context, inputs, projectPath) - : await OfficeXMLAddinGenerator.generate(context, inputs, projectPath); - if (res.isErr()) { - return err(res.error); - } - } else if (inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id) { - const res = await OfficeAddinGenerator.generate(context, inputs, projectPath); - if (res.isErr()) { - return err(res.error); + } else if (ProjectTypeOptions.officeAddinAllIds().includes(projectType)) { + const addinHost = inputs[QuestionNames.OfficeAddinHost]; + if ( + projectType === ProjectTypeOptions.officeXMLAddin().id && + addinHost && + addinHost !== OfficeAddinHostOptions.outlook().id + ) { + const res = await OfficeXMLAddinGenerator.generate(context, inputs, projectPath); + if (res.isErr()) return err(res.error); + } else { + const res = await OfficeAddinGenerator.generate(context, inputs, projectPath); + if (res.isErr()) return err(res.error); } } else if (capability === CapabilityOptions.copilotPluginApiSpec().id) { const res = await CopilotPluginGenerator.generatePluginFromApiSpec( @@ -375,11 +358,10 @@ class Coordinator { } if ( - capability === CapabilityOptions.copilotPluginNewApi().id || - (capability === CapabilityOptions.m365SearchMe().id && - meArchitecture === MeArchitectureOptions.newApi().id) + capability === CapabilityOptions.m365SearchMe().id && + meArchitecture === MeArchitectureOptions.newApi().id ) { - if (isApiKeyEnabled() && apiMEAuthType) { + if ((isApiKeyEnabled() || isApiMeSSOEnabled()) && apiMEAuthType) { feature = `${feature}:${apiMEAuthType}`; } else { feature = `${feature}:none`; diff --git a/packages/fx-core/src/component/driver/aad/create.ts b/packages/fx-core/src/component/driver/aad/create.ts index ee07530f88..4b4b7f2a06 100644 --- a/packages/fx-core/src/component/driver/aad/create.ts +++ b/packages/fx-core/src/component/driver/aad/create.ts @@ -25,6 +25,8 @@ import { CreateAadAppOutput, OutputKeys } from "./interface/createAadAppOutput"; import { SignInAudience } from "./interface/signInAudience"; import { AadAppClient } from "./utility/aadAppClient"; import { constants, descriptionMessageKeys, logMessageKeys } from "./utility/constants"; +import { WrapDriverContext } from "../util/wrapUtil"; +import { telemetryKeys } from "./utility/constants"; const actionName = "aadApp/create"; // DO NOT MODIFY the name const helpLink = "https://aka.ms/teamsfx-actions/aadapp-create"; @@ -37,11 +39,20 @@ export class CreateAadAppDriver implements StepDriver { description = getLocalizedString(descriptionMessageKeys.create); readonly progressTitle = getLocalizedString("driver.aadApp.progressBar.createAadAppTitle"); - @hooks([addStartAndEndTelemetry(actionName, actionName)]) public async execute( args: CreateAadAppArgs, 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: CreateAadAppArgs, + context: WrapDriverContext, + outputEnvVarNames?: Map ): Promise { const summaries: string[] = []; let outputs: Map = new Map(); @@ -62,11 +73,16 @@ export class CreateAadAppDriver implements StepDriver { outputEnvVarNames.get(OutputKeys.clientId) ) ); + context.addTelemetryProperties({ [telemetryKeys.newAadApp]: "true" }); // Create new Microsoft Entra app if no client id exists const signInAudience = args.signInAudience ? args.signInAudience : SignInAudience.AzureADMyOrg; - const aadApp = await aadAppClient.createAadApp(args.name, signInAudience); + const aadApp = await aadAppClient.createAadApp( + args.name, + signInAudience, + args.serviceManagementReference + ); aadAppState.clientId = aadApp.appId!; aadAppState.objectId = aadApp.id!; await this.setAadEndpointInfo(context.m365TokenProvider, aadAppState); @@ -82,6 +98,7 @@ export class CreateAadAppDriver implements StepDriver { outputEnvVarNames.get(OutputKeys.clientId) ) ); + context.addTelemetryProperties({ [telemetryKeys.newAadApp]: "false" }); } if (args.generateClientSecret) { @@ -101,7 +118,14 @@ export class CreateAadAppDriver implements StepDriver { driverConstants.generateSecretErrorMessageKey ); } - aadAppState.clientSecret = await aadAppClient.generateClientSecret(aadAppState.objectId); + + const clientSecretExpireDays = args.clientSecretExpireDays ?? 180; // Recommended lifetime from Azure Portal + const clientSecretDescription = args.clientSecretDescription ?? "default"; + aadAppState.clientSecret = await aadAppClient.generateClientSecret( + aadAppState.objectId, + clientSecretExpireDays, + clientSecretDescription + ); outputs.set(outputEnvVarNames.get(OutputKeys.clientSecret)!, aadAppState.clientSecret); const summary = getLocalizedString( diff --git a/packages/fx-core/src/component/driver/aad/error/aadManifestError.ts b/packages/fx-core/src/component/driver/aad/error/aadManifestError.ts index b5494d133f..96ae5115a3 100644 --- a/packages/fx-core/src/component/driver/aad/error/aadManifestError.ts +++ b/packages/fx-core/src/component/driver/aad/error/aadManifestError.ts @@ -66,6 +66,30 @@ export class MissingResourceAccessIdUserError extends UserError { } } +export class ResourceAccessShouldBeArrayUserError extends UserError { + constructor(actionName: string) { + super({ + source: actionName, + name: "ResourceAccessShouldBeArray", + message: getDefaultString("error.aad.manifest.ResourceAccessShouldBeArray"), + displayMessage: getLocalizedString("error.aad.manifest.ResourceAccessShouldBeArray"), + helpLink: "https://aka.ms/teamsfx-aad-manifest", + }); + } +} + +export class RequiredResourceAccessShouldBeArrayUserError extends UserError { + constructor(actionName: string) { + super({ + source: actionName, + name: "RequiredResourceAccessShouldBeArray", + message: getDefaultString("error.aad.manifest.RequiredResourceAccessShouldBeArray"), + displayMessage: getLocalizedString("error.aad.manifest.RequiredResourceAccessShouldBeArray"), + helpLink: "https://aka.ms/teamsfx-aad-manifest", + }); + } +} + export class UnknownResourceAccessIdUserError extends UserError { constructor(actionName: string, unknownId: string) { super({ diff --git a/packages/fx-core/src/component/driver/aad/error/clientSecretNotAllowedError.ts b/packages/fx-core/src/component/driver/aad/error/clientSecretNotAllowedError.ts new file mode 100644 index 0000000000..b79b49e1c6 --- /dev/null +++ b/packages/fx-core/src/component/driver/aad/error/clientSecretNotAllowedError.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { UserError } from "@microsoft/teamsfx-api"; +import { getDefaultString, getLocalizedString } from "../../../../common/localizeUtils"; +import { constants } from "../utility/constants"; + +const errorCode = "ClientSecretNotAllowed"; +const messageKey = "driver.aadApp.error.credentialTypeNotAllowedAsPerAppPolicy"; + +export class ClientSecretNotAllowedError extends UserError { + constructor(actionName: string) { + super({ + source: actionName, + name: errorCode, + message: getDefaultString(messageKey), + displayMessage: getLocalizedString(messageKey), + helpLink: constants.defaultHelpLink, + }); + } +} diff --git a/packages/fx-core/src/component/driver/aad/error/credentialInvalidLifetimeError.ts b/packages/fx-core/src/component/driver/aad/error/credentialInvalidLifetimeError.ts new file mode 100644 index 0000000000..fb813b6ff7 --- /dev/null +++ b/packages/fx-core/src/component/driver/aad/error/credentialInvalidLifetimeError.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { UserError } from "@microsoft/teamsfx-api"; +import { getDefaultString, getLocalizedString } from "../../../../common/localizeUtils"; +import { constants } from "../utility/constants"; + +const errorCode = "CredentialInvalidLifetime"; +const messageKey = "driver.aadApp.error.credentialInvalidLifetimeAsPerAppPolicy"; + +export class CredentialInvalidLifetimeError extends UserError { + constructor(actionName: string) { + super({ + source: actionName, + name: errorCode, + message: getDefaultString(messageKey), + displayMessage: getLocalizedString(messageKey), + helpLink: constants.defaultHelpLink, + }); + } +} diff --git a/packages/fx-core/src/component/driver/aad/interface/IAADDefinition.ts b/packages/fx-core/src/component/driver/aad/interface/IAADDefinition.ts index e50bb887bb..72fa00dcdc 100644 --- a/packages/fx-core/src/component/driver/aad/interface/IAADDefinition.ts +++ b/packages/fx-core/src/component/driver/aad/interface/IAADDefinition.ts @@ -72,4 +72,5 @@ export interface IAADDefinition { requiredResourceAccess?: RequiredResourceAccess[]; passwordCredentials?: PasswordCredential[]; spa?: Spa; + serviceManagementReference?: string; } diff --git a/packages/fx-core/src/component/driver/aad/interface/createAadAppArgs.ts b/packages/fx-core/src/component/driver/aad/interface/createAadAppArgs.ts index 7d80f7c319..df6058065d 100644 --- a/packages/fx-core/src/component/driver/aad/interface/createAadAppArgs.ts +++ b/packages/fx-core/src/component/driver/aad/interface/createAadAppArgs.ts @@ -7,4 +7,7 @@ export interface CreateAadAppArgs { name: string; // The name of AAD app generateClientSecret: boolean; // Whether generate client secret for the app signInAudience?: SignInAudience; // Specifies what Microsoft accounts are supported for the current application. + clientSecretExpireDays?: number; // The number of days the client secret is valid + clientSecretDescription?: string; // The description of the client secret + serviceManagementReference?: string; // Used as service tree id } diff --git a/packages/fx-core/src/component/driver/aad/permissions/index.ts b/packages/fx-core/src/component/driver/aad/permissions/index.ts index 9b9445d5a0..446d8726db 100644 --- a/packages/fx-core/src/component/driver/aad/permissions/index.ts +++ b/packages/fx-core/src/component/driver/aad/permissions/index.ts @@ -53,6 +53,8 @@ export function getDetailedGraphPermissionMap(): any { const map: any = {}; map.scopeIds = {}; map.scopes = {}; + map.roleIds = {}; + map.roles = {}; graphPermission.oauth2PermissionScopes.forEach((scope) => { map.scopeIds[scope.id] = { @@ -64,6 +66,14 @@ export function getDetailedGraphPermissionMap(): any { map.scopes[scope.value] = scope.id; }); + graphPermission.appRoles.forEach((role) => { + map.roleIds[role.id] = { + // value is the role name + value: role.value, + }; + map.roles[role.value] = role.id; + }); + loadedGraphPermissionMap = map; return map; } diff --git a/packages/fx-core/src/component/driver/aad/utility/aadAppClient.ts b/packages/fx-core/src/component/driver/aad/utility/aadAppClient.ts index 469d524afd..af02e95ac4 100644 --- a/packages/fx-core/src/component/driver/aad/utility/aadAppClient.ts +++ b/packages/fx-core/src/component/driver/aad/utility/aadAppClient.ts @@ -19,6 +19,8 @@ import { IAADDefinition } from "../interface/IAADDefinition"; import { SignInAudience } from "../interface/signInAudience"; import { AadManifestHelper } from "./aadManifestHelper"; import { aadErrorCode, constants } from "./constants"; +import { CredentialInvalidLifetimeError } from "../error/credentialInvalidLifetimeError"; +import { ClientSecretNotAllowedError } from "../error/clientSecretNotAllowedError"; // Another implementation of src\component\resource\aadApp\graph.ts to reduce call stacks // It's our internal utility so make sure pass valid parameters to it instead of relying on it to handle parameter errors @@ -78,12 +80,14 @@ export class AadAppClient { @hooks([ErrorContextMW({ source: "Graph", component: "AadAppClient" })]) public async createAadApp( displayName: string, - signInAudience = SignInAudience.AzureADMyOrg + signInAudience: SignInAudience = SignInAudience.AzureADMyOrg, + serviceManagementReference?: string ): Promise { const requestBody: IAADDefinition = { displayName: displayName, signInAudience: signInAudience, - }; // Create a Microsoft Entra app without setting anything + serviceManagementReference: serviceManagementReference, + }; // Create a Microsoft Entra app and optionally set service tree id const response = await this.axios.post("applications", requestBody); @@ -96,30 +100,50 @@ export class AadAppClient { } @hooks([ErrorContextMW({ source: "Graph", component: "AadAppClient" })]) - public async generateClientSecret(objectId: string): Promise { + public async generateClientSecret( + objectId: string, + clientSecretExpireDays = 180, // Recommended lifetime from Azure Portal + clientSecretDescription = "default" + ): Promise { const startDate = new Date(); const endDate = new Date(startDate.getTime()); - endDate.setDate(endDate.getDate() + 180); // Recommended lifetime from Azure Portal + endDate.setDate(endDate.getDate() + clientSecretExpireDays); const requestBody = { passwordCredential: { - displayName: constants.aadAppPasswordDisplayName, + displayName: clientSecretDescription, endDateTime: endDate.toISOString(), startDateTime: startDate.toISOString(), }, }; - const response = await this.axios.post(`applications/${objectId}/addPassword`, requestBody, { - "axios-retry": { - retries: this.retryNumber, - retryDelay: axiosRetry.exponentialDelay, - retryCondition: (error) => - axiosRetry.isNetworkError(error) || - axiosRetry.isRetryableError(error) || - this.is404Error(error), // also retry 404 error since Microsoft Entra need sometime to sync created Microsoft Entra app data - }, - }); + try { + const response = await this.axios.post(`applications/${objectId}/addPassword`, requestBody, { + "axios-retry": { + retries: this.retryNumber, + retryDelay: axiosRetry.exponentialDelay, + retryCondition: (error) => + axiosRetry.isNetworkError(error) || + axiosRetry.isRetryableError(error) || + this.is404Error(error), // also retry 404 error since Microsoft Entra need sometime to sync created Microsoft Entra app data + }, + }); - return response.data.secretText; + return response.data.secretText; + } catch (err) { + if (axios.isAxiosError(err) && err.response) { + if ( + err.response.data?.error?.code === aadErrorCode.credentialInvalidLifetimeAsPerAppPolicy + ) { + throw new CredentialInvalidLifetimeError(AadAppClient.name); + } + if ( + err.response.data?.error?.code === aadErrorCode.credentialTypeNotAllowedAsPerAppPolicy + ) { + throw new ClientSecretNotAllowedError(AadAppClient.name); + } + } + throw err; + } } @hooks([ErrorContextMW({ source: "Graph", component: "AadAppClient" })]) diff --git a/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts b/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts index f0baa98b9f..9bd6d1ac41 100644 --- a/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts +++ b/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts @@ -9,6 +9,8 @@ import { AadManifestErrorMessage, MissingResourceAccessIdUserError, MissingResourceAppIdUserError, + RequiredResourceAccessShouldBeArrayUserError, + ResourceAccessShouldBeArrayUserError, UnknownResourceAccessIdUserError, UnknownResourceAccessTypeUserError, UnknownResourceAppIdUserError, @@ -204,7 +206,7 @@ export class AadManifestHelper { // if manifest doesn't contain optionalClaims or access token doesn't contain idtyp clams if (!manifest.optionalClaims) { warningMsg += AadManifestErrorMessage.OptionalClaimsIsMissing; - } else if (!manifest.optionalClaims.accessToken.find((item) => item.name === "idtyp")) { + } else if (!manifest.optionalClaims.accessToken?.find((item) => item.name === "idtyp")) { warningMsg += AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim; } @@ -216,6 +218,11 @@ export class AadManifestHelper { public static processRequiredResourceAccessInManifest(manifest: AADManifest): void { const map = getPermissionMap(); + + if (manifest.requiredResourceAccess && !Array.isArray(manifest.requiredResourceAccess)) { + throw new RequiredResourceAccessShouldBeArrayUserError(componentName); + } + manifest.requiredResourceAccess?.forEach((requiredResourceAccessItem) => { const resourceIdOrName = requiredResourceAccessItem.resourceAppId; let resourceId = resourceIdOrName; @@ -230,6 +237,13 @@ export class AadManifestHelper { requiredResourceAccessItem.resourceAppId = resourceId; } + if ( + requiredResourceAccessItem.resourceAccess && + !Array.isArray(requiredResourceAccessItem.resourceAccess) + ) { + throw new ResourceAccessShouldBeArrayUserError(componentName); + } + requiredResourceAccessItem.resourceAccess?.forEach((resourceAccessItem) => { const resourceAccessIdOrName = resourceAccessItem.id; if (!resourceAccessIdOrName) { diff --git a/packages/fx-core/src/component/driver/aad/utility/constants.ts b/packages/fx-core/src/component/driver/aad/utility/constants.ts index cf211b2cb4..c403a4ec9a 100644 --- a/packages/fx-core/src/component/driver/aad/utility/constants.ts +++ b/packages/fx-core/src/component/driver/aad/utility/constants.ts @@ -30,9 +30,16 @@ export const permissionsKeys = { export const aadErrorCode = { permissionErrorCode: "CannotDeleteOrUpdateEnabledEntitlement", hostNameNotOnVerifiedDomain: "HostNameNotOnVerifiedDomain", // Using unverified domain in multi tenant scenario + credentialInvalidLifetimeAsPerAppPolicy: "CredentialInvalidLifetimeAsPerAppPolicy", + credentialTypeNotAllowedAsPerAppPolicy: "CredentialTypeNotAllowedAsPerAppPolicy", }; export const constants = { aadAppPasswordDisplayName: "default", oauthAuthorityPrefix: "https://login.microsoftonline.com", + defaultHelpLink: "https://aka.ms/teamsfx-actions/aadapp-create", +}; + +export const telemetryKeys = { + newAadApp: "new-aad-app", }; diff --git a/packages/fx-core/src/component/driver/apiKey/create.ts b/packages/fx-core/src/component/driver/apiKey/create.ts index 955da300e2..7e6fc6767a 100644 --- a/packages/fx-core/src/component/driver/apiKey/create.ts +++ b/packages/fx-core/src/component/driver/apiKey/create.ts @@ -30,12 +30,8 @@ import { ApiKeyFailedToGetDomainError } from "./error/apiKeyFailedToGetDomain"; import { ApiKeyNameTooLongError } from "./error/apiKeyNameTooLong"; import { CreateApiKeyArgs } from "./interface/createApiKeyArgs"; import { CreateApiKeyOutputs, OutputKeys } from "./interface/createApiKeyOutputs"; -import { - logMessageKeys, - maxDomainPerApiKey, - maxSecretLength, - minSecretLength, -} from "./utility/constants"; +import { logMessageKeys, maxSecretLength, minSecretLength } from "./utility/constants"; +import { getDomain, loadStateFromEnv, validateDomain } from "./utility/utility"; const actionName = "apiKey/register"; // DO NOT MODIFY the name const helpLink = "https://aka.ms/teamsfx-actions/apiKey-register"; @@ -61,7 +57,7 @@ export class CreateApiKeyDriver implements StepDriver { throw new OutputEnvironmentVariableUndefinedError(actionName); } - const state = this.loadStateFromEnv(outputEnvVarNames) as CreateApiKeyOutputs; + const state = loadStateFromEnv(outputEnvVarNames) as CreateApiKeyOutputs; const appStudioTokenRes = await context.m365TokenProvider.getAccessToken({ scopes: AppStudioScopes, }); @@ -95,8 +91,8 @@ export class CreateApiKeyDriver implements StepDriver { this.validateArgs(args); - const domains = await this.getDomain(args, context); - this.validateDomain(domains); + const domains = await getDomain(args, context); + validateDomain(domains, actionName); const apiKey = await this.mapArgsToApiSecretRegistration( context.m365TokenProvider, @@ -144,17 +140,6 @@ export class CreateApiKeyDriver implements StepDriver { } } - // Needs to validate the parameters outside of the function - private loadStateFromEnv( - outputEnvVarNames: Map - ): Record { - const result: Record = {}; - for (const [propertyName, envVarName] of outputEnvVarNames) { - result[propertyName] = process.env[envVarName]; - } - return result; - } - private loadClientSecret(): string | undefined { const clientSecret = process.env[QuestionNames.ApiSpecApiKey]; return clientSecret; @@ -172,37 +157,6 @@ export class CreateApiKeyDriver implements StepDriver { return true; } - // TODO: need to add logic to read domain from env if need to support non-lifecycle commands - private async getDomain(args: CreateApiKeyArgs, context: DriverContext): Promise { - const absolutePath = getAbsolutePath(args.apiSpecPath, context.projectPath); - const parser = new SpecParser(absolutePath, { - allowAPIKeyAuth: isApiKeyEnabled(), - allowMultipleParameters: isMultipleParametersEnabled(), - }); - const operations = await parser.list(); - const domains = operations - .filter((value) => { - return value.auth?.type === "apiKey" && value.auth?.name === args.name; - }) - .map((value) => { - return value.server; - }) - .filter((value, index, self) => { - return self.indexOf(value) === index; - }); - return domains; - } - - private validateDomain(domain: string[]): void { - if (domain.length > maxDomainPerApiKey) { - throw new ApiKeyDomainInvalidError(actionName); - } - - if (domain.length === 0) { - throw new ApiKeyFailedToGetDomainError(actionName); - } - } - private validateArgs(args: CreateApiKeyArgs): void { const invalidParameters: string[] = []; if (typeof args.name !== "string" || !args.name) { @@ -229,6 +183,22 @@ export class CreateApiKeyDriver implements StepDriver { invalidParameters.push("apiSpecPath"); } + if ( + args.applicableToApps && + args.applicableToApps !== ApiSecretRegistrationAppType.AnyApp && + args.applicableToApps !== ApiSecretRegistrationAppType.SpecificApp + ) { + invalidParameters.push("applicableToApps"); + } + + if ( + args.targetAudience && + args.targetAudience !== ApiSecretRegistrationTargetAudience.AnyTenant && + args.targetAudience !== ApiSecretRegistrationTargetAudience.HomeTenant + ) { + invalidParameters.push("targetAudience"); + } + if (invalidParameters.length > 0) { throw new InvalidActionInputError(actionName, invalidParameters, helpLink); } @@ -265,12 +235,20 @@ export class CreateApiKeyDriver implements StepDriver { return clientSecret; }); + const targetAudience: ApiSecretRegistrationTargetAudience = args.targetAudience + ? (args.targetAudience as ApiSecretRegistrationTargetAudience) + : ApiSecretRegistrationTargetAudience.AnyTenant; + const applicableToApps: ApiSecretRegistrationAppType = args.applicableToApps + ? (args.applicableToApps as ApiSecretRegistrationAppType) + : ApiSecretRegistrationAppType.AnyApp; + const apiKey: ApiSecretRegistration = { description: args.name, targetUrlsShouldStartWith: domain, - applicableToApps: ApiSecretRegistrationAppType.SpecificApp, - specificAppId: args.appId, - targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + applicableToApps: applicableToApps, + specificAppId: + applicableToApps === ApiSecretRegistrationAppType.SpecificApp ? args.appId : "", + targetAudience: targetAudience, clientSecrets: clientSecrets, manageableByUsers: [ { diff --git a/packages/fx-core/src/component/driver/apiKey/interface/createApiKeyArgs.ts b/packages/fx-core/src/component/driver/apiKey/interface/createApiKeyArgs.ts index c2cd1c23c2..25d3bcbaa1 100644 --- a/packages/fx-core/src/component/driver/apiKey/interface/createApiKeyArgs.ts +++ b/packages/fx-core/src/component/driver/apiKey/interface/createApiKeyArgs.ts @@ -7,4 +7,6 @@ export interface CreateApiKeyArgs { primaryClientSecret?: string; // The primary api secret secondaryClientSecret?: string; // The secondary api secret apiSpecPath: string; // The location of api spec file + applicableToApps?: string; // What app can access the api key. Values can be "SpecificApp" or "AnyApp". Default is "AnyApp". + targetAudience?: string; // What tenant can access the api key. Values can be "HomeTenant" or "AnyTenant". Default is "HomeTenant". } diff --git a/packages/fx-core/src/component/driver/apiKey/interface/updateApiKeyArgs.ts b/packages/fx-core/src/component/driver/apiKey/interface/updateApiKeyArgs.ts new file mode 100644 index 0000000000..2ea925892a --- /dev/null +++ b/packages/fx-core/src/component/driver/apiKey/interface/updateApiKeyArgs.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface UpdateApiKeyArgs { + registrationId: string; // The registration id of the api key + name: string; // The name of Api Secret + appId: string; // Teams app id + apiSpecPath: string; // The location of api spec file + applicableToApps?: string; // Which app can access the API key? Values can be "SpecificApp" or "AnyApp". Default is "AnyApp". + targetAudience?: string; // Which tenant can access the API key? Values can be "HomeTenant" or "AnyTenant". Default is "AnyTenant". +} diff --git a/packages/fx-core/src/component/driver/apiKey/update.ts b/packages/fx-core/src/component/driver/apiKey/update.ts new file mode 100644 index 0000000000..7009b8c58b --- /dev/null +++ b/packages/fx-core/src/component/driver/apiKey/update.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Service } from "typedi"; +import { ExecutionResult, StepDriver } from "../interface/stepDriver"; +import { getLocalizedString } from "../../../common/localizeUtils"; +import { hooks } from "@feathersjs/hooks"; +import { addStartAndEndTelemetry } from "../middleware/addStartAndEndTelemetry"; +import { UpdateApiKeyArgs } from "./interface/updateApiKeyArgs"; +import { DriverContext } from "../interface/commonArgs"; +import { SystemError, UserError, err, ok } from "@microsoft/teamsfx-api"; +import { logMessageKeys } from "./utility/constants"; +import { InvalidActionInputError, assembleError } from "../../../error"; +import { AppStudioScopes } from "../teamsApp/constants"; +import { ApiKeyNameTooLongError } from "./error/apiKeyNameTooLong"; +import { + ApiSecretRegistration, + ApiSecretRegistrationAppType, + ApiSecretRegistrationTargetAudience, + ApiSecretRegistrationUpdate, +} from "../teamsApp/interfaces/ApiSecretRegistration"; +import { AppStudioClient } from "../teamsApp/clients/appStudioClient"; +import { getDomain, validateDomain } from "./utility/utility"; + +const actionName = "apiKey/update"; // DO NOT MODIFY the name +const helpLink = "https://aka.ms/teamsfx-actions/apiKey-update"; + +@Service(actionName) // DO NOT MODIFY the service name +export class UpdateApiKeyDriver implements StepDriver { + description = getLocalizedString("driver.apiKey.description.update"); + readonly progressTitle = getLocalizedString("driver.aadApp.apiKey.title.update"); + + @hooks([addStartAndEndTelemetry(actionName, actionName)]) + public async execute(args: UpdateApiKeyArgs, context: DriverContext): Promise { + const summaries: string[] = []; + const outputs: Map = new Map(); + + try { + context.logProvider?.info(getLocalizedString(logMessageKeys.startExecuteDriver, actionName)); + this.validateArgs(args); + + const domain = await getDomain(args, context); + validateDomain(domain, actionName); + + const appStudioTokenRes = await context.m365TokenProvider.getAccessToken({ + scopes: AppStudioScopes, + }); + if (appStudioTokenRes.isErr()) { + throw appStudioTokenRes.error; + } + const appStudioToken = appStudioTokenRes.value; + + const getApiKeyRes = await AppStudioClient.getApiKeyRegistrationById( + appStudioToken, + args.registrationId + ); + const diffMsgs = this.compareApiKeyRegistration(getApiKeyRes, args, domain); + // If there is no difference, skip the update + if (!diffMsgs || diffMsgs.length === 0) { + const summary = getLocalizedString(logMessageKeys.skipUpdateApiKey); + context.logProvider?.info(summary); + summaries.push(summary); + + return { + result: ok(outputs), + summaries: summaries, + }; + } + + // If there is difference, ask user to confirm the update + // Skip confirm if only targetUrlsShouldStartWith is different when the url contains devtunnel + if (!this.shouldSkipConfirm(diffMsgs, getApiKeyRes.targetUrlsShouldStartWith, domain)) { + const userConfirm = await context.ui!.confirm!({ + name: "confirm-update-api-key", + title: getLocalizedString("driver.apiKey.confirm.update", diffMsgs.join(",\n")), + default: true, + }); + if (userConfirm.isErr()) { + throw userConfirm.error; + } + } + + const apiKey = this.mapArgsToApiSecretRegistration(args, domain); + const updateApiKeyRes = await AppStudioClient.updateApiKeyRegistration( + appStudioToken, + apiKey, + args.registrationId + ); + + await context.ui!.showMessage( + "info", + getLocalizedString("driver.apiKey.info.update", diffMsgs.join(",\n")), + false + ); + const summary = getLocalizedString(logMessageKeys.successUpdateApiKey); + context.logProvider?.info(summary); + summaries.push(summary); + + return { + result: ok(outputs), + summaries: summaries, + }; + } catch (error) { + if (error instanceof UserError || error instanceof SystemError) { + context.logProvider?.error( + getLocalizedString(logMessageKeys.failedExecuteDriver, actionName, error.displayMessage) + ); + return { + result: err(error), + summaries: summaries, + }; + } + + const message = JSON.stringify(error); + context.logProvider?.error( + getLocalizedString(logMessageKeys.failedExecuteDriver, actionName, message) + ); + return { + result: err(assembleError(error as Error, actionName)), + summaries: summaries, + }; + } + } + + private validateArgs(args: UpdateApiKeyArgs): void { + const invalidParameters: string[] = []; + if (typeof args.registrationId !== "string" || !args.registrationId) { + invalidParameters.push("registrationId"); + } + + if (typeof args.name !== "string" || !args.name) { + invalidParameters.push("name"); + } + + if (args.name.length > 128) { + throw new ApiKeyNameTooLongError(actionName); + } + + if (typeof args.appId !== "string" || !args.appId) { + invalidParameters.push("appId"); + } + + if (typeof args.apiSpecPath !== "string" || !args.apiSpecPath) { + invalidParameters.push("apiSpecPath"); + } + + if ( + args.applicableToApps && + args.applicableToApps !== ApiSecretRegistrationAppType.AnyApp && + args.applicableToApps !== ApiSecretRegistrationAppType.SpecificApp + ) { + invalidParameters.push("applicableToApps"); + } + + if ( + args.targetAudience && + args.targetAudience !== ApiSecretRegistrationTargetAudience.AnyTenant && + args.targetAudience !== ApiSecretRegistrationTargetAudience.HomeTenant + ) { + invalidParameters.push("targetAudience"); + } + + if (invalidParameters.length > 0) { + throw new InvalidActionInputError(actionName, invalidParameters, helpLink); + } + } + + private compareApiKeyRegistration( + current: ApiSecretRegistration, + input: UpdateApiKeyArgs, + domain: string[] + ): string[] { + const diffMsgs: string[] = []; + if (current.description !== input.name) { + diffMsgs.push(`description: ${current.description as string} => ${input.name}`); + } + + if (input.applicableToApps && current.applicableToApps !== input.applicableToApps) { + let msg = `applicableToApps: ${current.applicableToApps} => ${input.applicableToApps}`; + if (input.applicableToApps === "SpecificApp") { + msg += `, specificAppId: ${input.appId}`; + } + diffMsgs.push(msg); + } + + if (input.targetAudience && current.targetAudience !== input.targetAudience) { + diffMsgs.push( + `targetAudience: ${current.targetAudience as string} => ${input.targetAudience}` + ); + } + + // Compare domain + if ( + current.targetUrlsShouldStartWith.length !== domain.length || + !current.targetUrlsShouldStartWith.every((value) => domain.includes(value)) || + !domain.every((value) => current.targetUrlsShouldStartWith.includes(value)) + ) { + diffMsgs.push( + `targetUrlsShouldStartWith: ${current.targetUrlsShouldStartWith.join(",")} => ${domain.join( + "," + )}` + ); + } + + return diffMsgs; + } + + private mapArgsToApiSecretRegistration( + args: UpdateApiKeyArgs, + domain: string[] + ): ApiSecretRegistrationUpdate { + const targetAudience = args.targetAudience + ? (args.targetAudience as ApiSecretRegistrationTargetAudience) + : undefined; + const applicableToApps = args.applicableToApps + ? (args.applicableToApps as ApiSecretRegistrationAppType) + : undefined; + + const apiKey: ApiSecretRegistrationUpdate = { + description: args.name, + targetUrlsShouldStartWith: domain, + applicableToApps: applicableToApps, + specificAppId: + applicableToApps === ApiSecretRegistrationAppType.SpecificApp ? args.appId : "", + targetAudience: targetAudience, + }; + + return apiKey; + } + + // Should skip confirm box if only targetUrlsShouldStartWith is different and the url contains devtunnel + private shouldSkipConfirm(diffMsgs: string[], getDomain: string[], domain: string[]): boolean { + return ( + diffMsgs.length === 1 && + diffMsgs[0].includes("targetUrlsShouldStartWith") && + getDomain.length === domain.length && + getDomain.every((value) => value.includes("devtunnel")) && + domain.every((value) => value.includes("devtunnel")) + ); + } +} diff --git a/packages/fx-core/src/component/driver/apiKey/utility/constants.ts b/packages/fx-core/src/component/driver/apiKey/utility/constants.ts index 0eb1858900..2d03cc01f3 100644 --- a/packages/fx-core/src/component/driver/apiKey/utility/constants.ts +++ b/packages/fx-core/src/component/driver/apiKey/utility/constants.ts @@ -7,6 +7,8 @@ export const logMessageKeys = { apiKeyNotFound: "driver.apiKey.log.apiKeyNotFound", successCreateApiKey: "driver.apiKey.log.successCreateApiKey", failedExecuteDriver: "driver.apiKey.log.failedExecuteDriver", + skipUpdateApiKey: "driver.apiKey.log.skipUpdateApiKey", + successUpdateApiKey: "driver.apiKey.log.successUpdateApiKey", }; export const maxDomainPerApiKey = 1; diff --git a/packages/fx-core/src/component/driver/apiKey/utility/utility.ts b/packages/fx-core/src/component/driver/apiKey/utility/utility.ts new file mode 100644 index 0000000000..33bbdca923 --- /dev/null +++ b/packages/fx-core/src/component/driver/apiKey/utility/utility.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SpecParser } from "@microsoft/m365-spec-parser"; +import { getAbsolutePath } from "../../../utils/common"; +import { DriverContext } from "../../interface/commonArgs"; +import { CreateApiKeyArgs } from "../interface/createApiKeyArgs"; +import { UpdateApiKeyArgs } from "../interface/updateApiKeyArgs"; +import { isApiKeyEnabled, isMultipleParametersEnabled } from "../../../../common/featureFlags"; +import { maxDomainPerApiKey } from "./constants"; +import { ApiKeyDomainInvalidError } from "../error/apiKeyDomainInvalid"; +import { ApiKeyFailedToGetDomainError } from "../error/apiKeyFailedToGetDomain"; + +// Needs to validate the parameters outside of the function +export function loadStateFromEnv( + outputEnvVarNames: Map +): Record { + const result: Record = {}; + for (const [propertyName, envVarName] of outputEnvVarNames) { + result[propertyName] = process.env[envVarName]; + } + return result; +} + +// TODO: need to add logic to read domain from env if need to support non-lifecycle commands +export async function getDomain( + args: CreateApiKeyArgs | UpdateApiKeyArgs, + context: DriverContext +): Promise { + const absolutePath = getAbsolutePath(args.apiSpecPath, context.projectPath); + const parser = new SpecParser(absolutePath, { + allowBearerTokenAuth: isApiKeyEnabled(), // Currently, API key auth support is actually bearer token auth + allowMultipleParameters: isMultipleParametersEnabled(), + }); + const listResult = await parser.list(); + const operations = listResult.APIs.filter((value) => value.isValid); + const domains = operations + .filter((value) => { + const auth = value.auth; + return ( + auth && + auth.authScheme.type === "http" && + auth.authScheme.scheme === "bearer" && + auth.name === args.name + ); + }) + .map((value) => { + return value.server; + }) + .filter((value, index, self) => { + return self.indexOf(value) === index; + }); + return domains; +} + +export function validateDomain(domain: string[], actionName: string): void { + if (domain.length > maxDomainPerApiKey) { + throw new ApiKeyDomainInvalidError(actionName); + } + + if (domain.length === 0) { + throw new ApiKeyFailedToGetDomainError(actionName); + } +} diff --git a/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts b/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts index ff0b4e7ec3..1dc0113291 100644 --- a/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts +++ b/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts @@ -56,7 +56,13 @@ export namespace SPOClient { file: Buffer ): Promise { const requester = createRequesterWithToken(spoToken); - await requester.post(`/_api/web/tenantappcatalog/Add(overwrite=true, url='${fileName}')`, file); + await requester.post( + `/_api/web/tenantappcatalog/Add(overwrite=true, url='${fileName}')`, + file, + { + maxBodyLength: Infinity, + } + ); } /** diff --git a/packages/fx-core/src/component/driver/index.ts b/packages/fx-core/src/component/driver/index.ts index 6d779bbf04..3e8da622f0 100644 --- a/packages/fx-core/src/component/driver/index.ts +++ b/packages/fx-core/src/component/driver/index.ts @@ -7,6 +7,7 @@ import "./teamsApp/create"; import "./teamsApp/validate"; import "./teamsApp/validateAppPackage"; +import "./teamsApp/validateTestCases"; import "./teamsApp/configure"; import "./teamsApp/copyAppPackageToSPFx"; import "./teamsApp/publishAppPackage"; @@ -31,3 +32,4 @@ import "./botFramework/createOrUpdateBot"; import "./m365/acquire"; import "./add/addWebPart"; import "./apiKey/create"; +import "./apiKey/update"; diff --git a/packages/fx-core/src/component/driver/script/scriptDriver.ts b/packages/fx-core/src/component/driver/script/scriptDriver.ts index c040f87549..8f8a678bd2 100644 --- a/packages/fx-core/src/component/driver/script/scriptDriver.ts +++ b/packages/fx-core/src/component/driver/script/scriptDriver.ts @@ -204,9 +204,9 @@ export function convertScriptErrorToFxError( run: string ): ScriptTimeoutError | ScriptExecutionError { if (error.killed) { - return new ScriptTimeoutError(run, error); + return new ScriptTimeoutError(error); } else { - return new ScriptExecutionError(run, error.message, error); + return new ScriptExecutionError(error); } } diff --git a/packages/fx-core/src/component/driver/teamsApp/clients/appStudioClient.ts b/packages/fx-core/src/component/driver/teamsApp/clients/appStudioClient.ts index 227acbb2e4..0bce33a7b3 100644 --- a/packages/fx-core/src/component/driver/teamsApp/clients/appStudioClient.ts +++ b/packages/fx-core/src/component/driver/teamsApp/clients/appStudioClient.ts @@ -37,7 +37,10 @@ import { CheckSideloadingPermissionFailedError, DeveloperPortalAPIFailedError, } from "../../../../error/teamsApp"; -import { ApiSecretRegistration } from "../interfaces/ApiSecretRegistration"; +import { + ApiSecretRegistration, + ApiSecretRegistrationUpdate, +} from "../interfaces/ApiSecretRegistration"; import { WrappedAxiosClient } from "../../../../common/wrappedAxiosClient"; import { AsyncAppValidationResponse } from "../interfaces/AsyncAppValidationResponse"; import { AsyncAppValidationResultsResponse } from "../interfaces/AsyncAppValidationResultsResponse"; @@ -626,13 +629,16 @@ export namespace AppStudioClient { * Submit App Validation Request (In-App) for which App Definitions are stored at TDP. * @param teamsAppId * @param appStudioToken + * @param timeoutSeconds * @returns */ export async function submitAppValidationRequest( teamsAppId: string, - appStudioToken: string + appStudioToken: string, + timeoutSeconds = 10 ): Promise { const requester = createRequesterWithToken(appStudioToken, region); + requester.defaults.timeout = timeoutSeconds * 1000; try { const response = await RetryHandler.Retry(() => requester.post(`/api/v1.0/appvalidations/appdefinition/validate`, { @@ -673,13 +679,16 @@ export namespace AppStudioClient { * Get App validation results by provided app validation id * @param appValidationId * @param appStudioToken + * @param timeoutSeconds * @returns */ export async function getAppValidationById( appValidationId: string, - appStudioToken: string + appStudioToken: string, + timeoutSeconds = 10 ): Promise { const requester = createRequesterWithToken(appStudioToken, region); + requester.defaults.timeout = timeoutSeconds * 1000; try { const response = await RetryHandler.Retry(() => requester.get(`/api/v1.0/appvalidations/${appValidationId}`) @@ -810,4 +819,24 @@ export namespace AppStudioClient { throw error; } } + + export async function updateApiKeyRegistration( + appStudioToken: string, + apiKeyRegistration: ApiSecretRegistrationUpdate, + apiKeyRegistrationId: string + ): Promise { + const requester = createRequesterWithToken(appStudioToken); + try { + const response = await RetryHandler.Retry(() => + requester.patch( + `/api/v1.0/apiSecretRegistrations/${apiKeyRegistrationId}`, + apiKeyRegistration + ) + ); + return response?.data; + } catch (e) { + const error = wrapException(e, APP_STUDIO_API_NAMES.UPDATE_API_KEY); + throw error; + } + } } diff --git a/packages/fx-core/src/component/driver/teamsApp/constants.ts b/packages/fx-core/src/component/driver/teamsApp/constants.ts index 376b697fbd..e2c0ac0019 100644 --- a/packages/fx-core/src/component/driver/teamsApp/constants.ts +++ b/packages/fx-core/src/component/driver/teamsApp/constants.ts @@ -252,6 +252,7 @@ export class APP_STUDIO_API_NAMES { public static readonly DELETE_BOT = "delete-bot"; public static readonly UPDATE_BOT = "update-bot"; public static readonly CREATE_API_KEY = "create-api-key"; + public static readonly UPDATE_API_KEY = "update-api-key"; public static readonly GET_API_KEY = "get-api-key"; public static readonly SUMIT_APP_VALIDATION = "submit-app-validation"; public static readonly GET_APP_VALIDATION_RESULT = "get-app-validation-result"; @@ -331,6 +332,8 @@ export const STATIC_TABS_TPL_EXISTING_APP: IStaticTab[] = [ export const TEAMS_APP_SHORT_NAME_MAX_LENGTH = 30; export const STATIC_TABS_MAX_ITEMS = 16; +// Check validation result interval +export const CEHCK_VALIDATION_RESULTS_INTERVAL_SECONDS = 60; /** * Language codes. diff --git a/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts b/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts index 37ff93dea8..de9c1143df 100644 --- a/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts +++ b/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts @@ -25,6 +25,7 @@ import { manifestUtils } from "./utils/ManifestUtils"; import { expandEnvironmentVariable, getEnvironmentVariables } from "../../utils/common"; import { TelemetryPropertyKey } from "./utils/telemetry"; import { InvalidFileOutsideOfTheDirectotryError } from "../../../error/teamsApp"; +import { normalizePath } from "./utils/utils"; export const actionName = "teamsApp/zipAppPackage"; @@ -183,21 +184,17 @@ export class CreateAppPackageDriver implements StepDriver { if (checkExistenceRes.isErr()) { return err(checkExistenceRes.error); } - const expandedEnvVarResult = await CreateAppPackageDriver.expandOpenAPIEnvVars( + + const addFileWithVariableRes = await this.addFileWithVariable( + zip, + manifest.composeExtensions[0].apiSpecificationFile, apiSpecificationFile, + TelemetryPropertyKey.customizedOpenAPIKeys, context ); - if (expandedEnvVarResult.isErr()) { - return err(expandedEnvVarResult.error); + if (addFileWithVariableRes.isErr()) { + return err(addFileWithVariableRes.error); } - const openAPIContent = expandedEnvVarResult.value; - const attr = await fs.stat(apiSpecificationFile); - zip.addFile( - manifest.composeExtensions[0].apiSpecificationFile, - Buffer.from(openAPIContent), - "", - attr.mode - ); if (manifest.composeExtensions[0].commands.length > 0) { for (const command of manifest.composeExtensions[0].commands) { @@ -227,10 +224,24 @@ export class CreateAppPackageDriver implements StepDriver { if (checkExistenceRes.isErr()) { return err(checkExistenceRes.error); } - const dir = path.dirname(manifest.plugins[0].pluginFile); - this.addFileInZip(zip, dir, pluginFile); - const addFilesRes = await this.addPluginRelatedFiles(zip, pluginFile, appDirectory); + const addFileWithVariableRes = await this.addFileWithVariable( + zip, + manifest.plugins[0].pluginFile, + pluginFile, + TelemetryPropertyKey.customizedAIPluginKeys, + context + ); + if (addFileWithVariableRes.isErr()) { + return err(addFileWithVariableRes.error); + } + + const addFilesRes = await this.addPluginRelatedFiles( + zip, + manifest.plugins[0].pluginFile, + appDirectory, + context + ); if (addFilesRes.isErr()) { return err(addFilesRes.error); } @@ -254,20 +265,21 @@ export class CreateAppPackageDriver implements StepDriver { return ok(new Map()); } - private static async expandOpenAPIEnvVars( - openAPISpecPath: string, - ctx: WrapDriverContext + private static async expandEnvVars( + filePath: string, + ctx: WrapDriverContext, + telemetryKey: TelemetryPropertyKey ): Promise> { - const content = await fs.readFile(openAPISpecPath, "utf8"); + const content = await fs.readFile(filePath, "utf8"); const vars = getEnvironmentVariables(content); ctx.addTelemetryProperties({ - [TelemetryPropertyKey.customizedOpenAPIKeys]: vars.join(";"), + [telemetryKey]: vars.join(";"), }); const result = expandEnvironmentVariable(content); const notExpandedVars = getEnvironmentVariables(result); if (notExpandedVars.length > 0) { return err( - new MissingEnvironmentVariablesError("teamsApp", notExpandedVars.join(","), openAPISpecPath) + new MissingEnvironmentVariablesError("teamsApp", notExpandedVars.join(","), filePath) ); } return ok(result); @@ -322,27 +334,40 @@ export class CreateAppPackageDriver implements StepDriver { private async addPluginRelatedFiles( zip: AdmZip, pluginFile: string, - appDirectory: string + appDirectory: string, + context: WrapDriverContext ): Promise> { + const pluginFilePath = path.join(appDirectory, pluginFile); let pluginContent; try { - pluginContent = (await fs.readJSON(pluginFile)) as PluginManifestSchema; + pluginContent = (await fs.readJSON(pluginFilePath)) as PluginManifestSchema; } catch (e) { - return err(new JSONSyntaxError(pluginFile, e, actionName)); + return err(new JSONSyntaxError(pluginFilePath, e, actionName)); } const runtimes = pluginContent.runtimes; if (runtimes && runtimes.length > 0) { for (const runtime of runtimes) { if (runtime.type === "OpenApi" && runtime.spec?.url) { - const specFile = path.resolve(path.dirname(pluginFile), runtime.spec.url); + const specFile = path.resolve(path.dirname(pluginFilePath), runtime.spec.url); // add openapi spec const checkExistenceRes = await this.validateReferencedFile(specFile, appDirectory); if (checkExistenceRes.isErr()) { return err(checkExistenceRes.error); } - const dir = path.relative(appDirectory, path.dirname(specFile)); - this.addFileInZip(zip, dir, specFile); + const entryName = path.relative(appDirectory, specFile); + const useForwardSlash = pluginFile.concat(runtime.spec.url).includes("/"); + + const addFileWithVariableRes = await this.addFileWithVariable( + zip, + normalizePath(entryName, useForwardSlash), + specFile, + TelemetryPropertyKey.customizedOpenAPIKeys, + context + ); + if (addFileWithVariableRes.isErr()) { + return err(addFileWithVariableRes.error); + } } } } @@ -350,7 +375,30 @@ export class CreateAppPackageDriver implements StepDriver { return ok(undefined); } - private addFileInZip(zip: AdmZip, dir: string, filePath: string) { - zip.addLocalFile(filePath, dir === "." ? "" : dir); + private async addFileWithVariable( + zip: AdmZip, + entryName: string, + filePath: string, + telemetryKey: TelemetryPropertyKey, + context: WrapDriverContext + ): Promise> { + const expandedEnvVarResult = await CreateAppPackageDriver.expandEnvVars( + filePath, + context, + telemetryKey + ); + if (expandedEnvVarResult.isErr()) { + return err(expandedEnvVarResult.error); + } + const content = expandedEnvVarResult.value; + + const attr = await fs.stat(filePath); + zip.addFile(entryName, Buffer.from(content), "", attr.mode); + + return ok(undefined); + } + + private addFileInZip(zip: AdmZip, zipPath: string, filePath: string) { + zip.addLocalFile(filePath, zipPath === "." ? "" : zipPath); } } diff --git a/packages/fx-core/src/component/driver/teamsApp/interfaces/ApiSecretRegistration.ts b/packages/fx-core/src/component/driver/teamsApp/interfaces/ApiSecretRegistration.ts index 5f19dfe5f0..cfaaed1d78 100644 --- a/packages/fx-core/src/component/driver/teamsApp/interfaces/ApiSecretRegistration.ts +++ b/packages/fx-core/src/component/driver/teamsApp/interfaces/ApiSecretRegistration.ts @@ -30,6 +30,27 @@ export interface ApiSecretRegistration { manageableByUsers?: ApiSecretRegistrationUser[]; } +export interface ApiSecretRegistrationUpdate { + /** + * Max 128 characters + */ + description?: string; + /** + * Currently max length 1 + */ + targetUrlsShouldStartWith: string[]; + /** + * Teams app Id associated with the ApiSecretRegistration, should be required if applicableToApps === "SpecificType" + */ + specificAppId?: string; + applicableToApps?: ApiSecretRegistrationAppType; + /** + * Default to be "HomeTenant" + */ + targetAudience?: ApiSecretRegistrationTargetAudience; + manageableByUsers?: ApiSecretRegistrationUser[]; +} + export enum ApiSecretRegistrationAppType { SpecificApp = "SpecificApp", AnyApp = "AnyApp", diff --git a/packages/fx-core/src/component/driver/teamsApp/interfaces/AsyncAppValidationDetailsResponse.ts b/packages/fx-core/src/component/driver/teamsApp/interfaces/AsyncAppValidationDetailsResponse.ts index 3c52856b8a..3be23af718 100644 --- a/packages/fx-core/src/component/driver/teamsApp/interfaces/AsyncAppValidationDetailsResponse.ts +++ b/packages/fx-core/src/component/driver/teamsApp/interfaces/AsyncAppValidationDetailsResponse.ts @@ -4,8 +4,8 @@ import { AsyncAppValidationStatus } from "./AsyncAppValidationResponse"; export interface AsyncAppValidationDetailsResponse { - continuationToken: string; - appValidations: AsyncAppValidationDetailsViewModel[]; + continuationToken?: string; + appValidations?: AsyncAppValidationDetailsViewModel[]; } export interface AsyncAppValidationDetailsViewModel { diff --git a/packages/fx-core/src/component/driver/teamsApp/interfaces/ValidateWithTestCasesArgs.ts b/packages/fx-core/src/component/driver/teamsApp/interfaces/ValidateWithTestCasesArgs.ts new file mode 100644 index 0000000000..c3bf412c16 --- /dev/null +++ b/packages/fx-core/src/component/driver/teamsApp/interfaces/ValidateWithTestCasesArgs.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export interface ValidateWithTestCasesArgs { + /** + * Teams app package path + */ + appPackagePath: string; + /** + * Internal arguments + * Show message for non-life cycle command + */ + showMessage?: boolean; + /** + * Internal arguments + * Show progress bar for non-life cycle command + */ + showProgressBar?: boolean; +} diff --git a/packages/fx-core/src/component/driver/teamsApp/teamsappMgr.ts b/packages/fx-core/src/component/driver/teamsApp/teamsappMgr.ts index b034613a1f..f54293f482 100644 --- a/packages/fx-core/src/component/driver/teamsApp/teamsappMgr.ts +++ b/packages/fx-core/src/component/driver/teamsApp/teamsappMgr.ts @@ -40,6 +40,8 @@ import { import { manifestUtils } from "./utils/ManifestUtils"; import { ValidateManifestDriver } from "./validate"; import { ValidateAppPackageDriver } from "./validateAppPackage"; +import { ValidateWithTestCasesArgs } from "./interfaces/ValidateWithTestCasesArgs"; +import { ValidateWithTestCasesDriver } from "./validateTestCases"; class TeamsAppMgr { async ensureAppPackageFile(inputs: TeamsAppInputs): Promise> { @@ -206,14 +208,27 @@ class TeamsAppMgr { } } else if (inputs["package-file"]) { const teamsAppPackageFilePath = inputs["package-file"]; - const args: ValidateAppPackageArgs = { - appPackagePath: teamsAppPackageFilePath, - showMessage: true, - }; - const driver: ValidateAppPackageDriver = Container.get("teamsApp/validateAppPackage"); - const result = (await driver.execute(args, context)).result; - if (result.isErr()) { - return err(result.error); + if (inputs["validate-method"] == "test-cases") { + const args: ValidateWithTestCasesArgs = { + appPackagePath: teamsAppPackageFilePath, + showProgressBar: false, + showMessage: true, + }; + const driver: ValidateWithTestCasesDriver = Container.get("teamsApp/validateWithTestCases"); + const result = (await driver.execute(args, context)).result; + if (result.isErr()) { + return err(result.error); + } + } else { + const args: ValidateAppPackageArgs = { + appPackagePath: teamsAppPackageFilePath, + showMessage: true, + }; + const driver: ValidateAppPackageDriver = Container.get("teamsApp/validateAppPackage"); + const result = (await driver.execute(args, context)).result; + if (result.isErr()) { + return err(result.error); + } } } return ok(undefined); diff --git a/packages/fx-core/src/component/driver/teamsApp/utils/telemetry.ts b/packages/fx-core/src/component/driver/teamsApp/utils/telemetry.ts index cf7bac1077..e7f344ff47 100644 --- a/packages/fx-core/src/component/driver/teamsApp/utils/telemetry.ts +++ b/packages/fx-core/src/component/driver/teamsApp/utils/telemetry.ts @@ -13,6 +13,7 @@ export enum TelemetryPropertyKey { publishedAppId = "published-app-id", customizedKeys = "customized-manifest-keys", customizedOpenAPIKeys = "customized-openapi-keys", + customizedAIPluginKeys = "customized-ai-plugin-keys", validationErrors = "validation-errors", validationWarnings = "validation-warnings", OverwriteIfAppAlreadyExists = "overwrite-if-app-already-exists", diff --git a/packages/fx-core/src/component/driver/teamsApp/utils/utils.ts b/packages/fx-core/src/component/driver/teamsApp/utils/utils.ts index c0bcfd3670..695eb00adc 100644 --- a/packages/fx-core/src/component/driver/teamsApp/utils/utils.ts +++ b/packages/fx-core/src/component/driver/teamsApp/utils/utils.ts @@ -210,3 +210,7 @@ export class RetryHandler { } } } + +export function normalizePath(path: string, useForwardSlash: boolean): string { + return useForwardSlash ? path.replace(/\\/g, "/") : path; +} diff --git a/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts b/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts new file mode 100644 index 0000000000..05f2ad6a7b --- /dev/null +++ b/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { EOL } from "os"; +import { + Result, + FxError, + ok, + err, + TeamsAppManifest, + ManifestUtil, + Platform, + Colors, +} from "@microsoft/teamsfx-api"; +import { hooks } from "@feathersjs/hooks/lib"; +import { Service } from "typedi"; +import fs from "fs-extra"; +import * as path from "path"; +import { merge } from "lodash"; +import { StepDriver, ExecutionResult } from "../interface/stepDriver"; +import { DriverContext } from "../interface/commonArgs"; +import { WrapDriverContext } from "../util/wrapUtil"; +import { ValidateWithTestCasesArgs } from "./interfaces/ValidateWithTestCasesArgs"; +import { addStartAndEndTelemetry } from "../middleware/addStartAndEndTelemetry"; +import { AppStudioClient } from "./clients/appStudioClient"; +import { getLocalizedString } from "../../../common/localizeUtils"; +import { AppStudioScopes, waitSeconds } from "../../../common/tools"; +import AdmZip from "adm-zip"; +import { + Constants, + getAppStudioEndpoint, + CEHCK_VALIDATION_RESULTS_INTERVAL_SECONDS, +} from "./constants"; +import { metadataUtil } from "../../utils/metadataUtil"; +import { FileNotFoundError, InvalidActionInputError } from "../../../error/common"; +import { + AsyncAppValidationResponse, + AsyncAppValidationStatus, +} from "./interfaces/AsyncAppValidationResponse"; +import { AsyncAppValidationResultsResponse } from "./interfaces/AsyncAppValidationResultsResponse"; +import { SummaryConstant } from "../../configManager/constant"; + +const actionName = "teamsApp/validateWithTestCases"; + +@Service(actionName) +export class ValidateWithTestCasesDriver implements StepDriver { + description = getLocalizedString( + "core.selectValidateMethodQuestion.validate.testCasesOptionDescription" + ); + readonly progressTitle = getLocalizedString("driver.teamsApp.progressBar.validateWithTestCases"); + + public async execute( + args: ValidateWithTestCasesArgs, + context: DriverContext + ): Promise { + const wrapContext = new WrapDriverContext(context, actionName, actionName); + const res = await this.validate(args, wrapContext); + return { + result: res, + summaries: wrapContext.summaries, + }; + } + + @hooks([addStartAndEndTelemetry(actionName, actionName)]) + public async validate( + args: ValidateWithTestCasesArgs, + context: WrapDriverContext + ): Promise, FxError>> { + const result = this.validateArgs(args); + if (result.isErr()) { + return err(result.error); + } + + let appPackagePath = args.appPackagePath; + if (!path.isAbsolute(appPackagePath)) { + appPackagePath = path.join(context.projectPath, appPackagePath); + } + if (!(await fs.pathExists(appPackagePath))) { + return err(new FileNotFoundError(actionName, appPackagePath)); + } + + const archivedFile = await fs.readFile(appPackagePath); + + const zipEntries = new AdmZip(archivedFile).getEntries(); + const manifestFile = zipEntries.find((x) => x.entryName === Constants.MANIFEST_FILE); + if (manifestFile) { + const manifestContent = manifestFile.getData().toString(); + const manifest = JSON.parse(manifestContent) as TeamsAppManifest; + metadataUtil.parseManifest(manifest); + + // Add common properties like isCopilotPlugin: boolean + const manifestTelemetries = ManifestUtil.parseCommonTelemetryProperties(manifest); + merge(context.telemetryProperties, manifestTelemetries); + + const appStudioTokenRes = await context.m365TokenProvider.getAccessToken({ + scopes: AppStudioScopes, + }); + if (appStudioTokenRes.isErr()) { + return err(appStudioTokenRes.error); + } + const appStudioToken = appStudioTokenRes.value; + // Check if the app has ongoing validation + const existingValidationResponse = await AppStudioClient.getAppValidationRequestList( + manifest.id, + appStudioToken + ); + if (existingValidationResponse.appValidations) { + for (const validation of existingValidationResponse.appValidations) { + if ( + validation.status === AsyncAppValidationStatus.InProgress || + validation.status === AsyncAppValidationStatus.Created + ) { + if (context.platform === Platform.CLI) { + const message: Array<{ content: string; color: Colors }> = [ + { + content: `A validation is currently in progress, please submit later. You can find this existing validation from `, + color: Colors.BRIGHT_YELLOW, + }, + { + content: `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation/${ + validation.id + }`, + color: Colors.BRIGHT_CYAN, + }, + ]; + context.ui?.showMessage("warn", message, false); + } else { + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.conflict", + `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation/${validation.id}` + ); + context.logProvider.warning(message); + } + return ok(new Map()); + } + } + } + const response: AsyncAppValidationResponse = await AppStudioClient.submitAppValidationRequest( + manifest.id, + appStudioToken + ); + + if (context.platform === Platform.CLI) { + const message: Array<{ content: string; color: Colors }> = [ + { + content: `Validation request submitted, status: ${response.status}. View the validation result from `, + color: Colors.BRIGHT_WHITE, + }, + { + content: `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation/${ + response.appValidationId + }`, + color: Colors.BRIGHT_CYAN, + }, + ]; + context.ui?.showMessage("info", message, false); + } else { + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.step", + response.status, + `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation` + ); + context.logProvider.info(message); + + // Do not await the final validation result, return immediately + void this.runningBackgroundJob(args, context, appStudioToken, response, manifest.id); + } + return ok(new Map()); + } else { + return err(new FileNotFoundError(actionName, "manifest.json")); + } + } + + /** + * Periodically check the result until it's completed or aborted + * @param args + * @param context + * @param appStudioToken + * @param response + * @param teamsAppId + */ + public async runningBackgroundJob( + args: ValidateWithTestCasesArgs, + context: WrapDriverContext, + appStudioToken: string, + response: AsyncAppValidationResponse, + teamsAppId: string + ): Promise { + const validationRequestListUrl = `${getAppStudioEndpoint()}/apps/${teamsAppId}/app-validation`; + + try { + if (args.showProgressBar && context.ui) { + context.progressBar = context.ui.createProgressBar(this.progressTitle, 1); + await context.progressBar.start(); + + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.step", + response.status, + validationRequestListUrl + ); + await context.progressBar.next(message); + } + let resultResp = response as AsyncAppValidationResultsResponse; + while ( + resultResp.status !== AsyncAppValidationStatus.Completed && + resultResp.status !== AsyncAppValidationStatus.Aborted + ) { + await waitSeconds(CEHCK_VALIDATION_RESULTS_INTERVAL_SECONDS); + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.step", + resultResp.status, + validationRequestListUrl + ); + context.logProvider.info(message); + resultResp = await AppStudioClient.getAppValidationById( + resultResp.appValidationId, + appStudioToken + ); + } + this.evaluateValidationResults(args, context, resultResp, teamsAppId); + } finally { + if (args.showProgressBar && context.progressBar) { + await context.progressBar.end(true); + } + } + } + + /** + * Evaluate the validation results and log the summary + * @param args + * @param context + * @param resultResp + * @param teamsAppId + */ + private evaluateValidationResults( + args: ValidateWithTestCasesArgs, + context: WrapDriverContext, + resultResp: AsyncAppValidationResultsResponse, + teamsAppId: string + ): void { + const validationStatusUrl = `${getAppStudioEndpoint()}/apps/${teamsAppId}/app-validation/${ + resultResp.appValidationId + }`; + const failed = resultResp.validationResults?.failures?.length ?? 0; + const warns = resultResp.validationResults?.warnings?.length ?? 0; + const skipped = resultResp.validationResults?.skipped?.length ?? 0; + const passed = resultResp.validationResults?.successes?.length ?? 0; + const summaryStrArr = []; + const detailStrArr = []; + if (failed > 0) { + summaryStrArr.push(getLocalizedString("driver.teamsApp.summary.validate.failed", failed)); + for (const failure of resultResp.validationResults.failures) { + detailStrArr.push( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases.result.detail", + SummaryConstant.Failed, + failure.title, + failure.message + ) + ); + } + } + if (warns > 0) { + summaryStrArr.push(getLocalizedString("driver.teamsApp.summary.validate.warning", warns)); + for (const warning of resultResp.validationResults.warnings) { + detailStrArr.push( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases.result.detail", + SummaryConstant.Warning, + warning.title, + warning.message + ) + ); + } + } + if (skipped > 0) { + summaryStrArr.push(getLocalizedString("driver.teamsApp.summary.validate.skipped", skipped)); + } + if (passed > 0) { + summaryStrArr.push(getLocalizedString("driver.teamsApp.summary.validate.succeed", passed)); + } + const summaryStr = summaryStrArr.join(", "); + let detailStr = detailStrArr.join(EOL); + // start a new line if the detail is not empty. + if (detailStr.length > 0) { + detailStr = EOL + detailStr; + } + if (resultResp.status === AsyncAppValidationStatus.Completed) { + if (args.showMessage && context.ui) { + void context.ui.showMessage( + "info", + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases.result", + resultResp.status, + summaryStr + ), + false + ); + } + context.logProvider.info( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases", + resultResp.status, + summaryStr, + validationStatusUrl, + detailStr + ) + ); + } else { + if (args.showMessage && context.ui) { + void context.ui.showMessage( + "error", + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases.result", + resultResp.status, + "" + ), + false + ); + } + context.logProvider.error( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases", + resultResp.status, + "", + validationStatusUrl, + "" + ) + ); + } + } + + private validateArgs(args: ValidateWithTestCasesArgs): Result { + if (!args || !args.appPackagePath) { + return err(new InvalidActionInputError(actionName, ["appPackagePath"])); + } + return ok(undefined); + } +} diff --git a/packages/fx-core/src/component/generator/copilotPlugin/generator.ts b/packages/fx-core/src/component/generator/copilotPlugin/generator.ts index 7c3cd08fe8..52367359f8 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/generator.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/generator.ts @@ -297,7 +297,7 @@ export class CopilotPluginGenerator { isPlugin ? copilotPluginParserOptions : { - allowAPIKeyAuth, + allowBearerTokenAuth: allowAPIKeyAuth, // Currently, API key auth support is actually bearer token auth allowMultipleParameters, projectType: type, } diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index e4fd06edc4..e8aa2b1fde 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -39,6 +39,7 @@ import { ProjectType, ParseOptions, AdaptiveCardGenerator, + Utils, } from "@microsoft/m365-spec-parser"; import fs from "fs-extra"; import { getLocalizedString } from "../../../common/localizeUtils"; @@ -52,7 +53,8 @@ import { QuestionNames } from "../../../question/questionNames"; import { pluginManifestUtils } from "../../driver/teamsApp/utils/PluginManifestUtils"; import { copilotPluginApiSpecOptionId } from "../../../question/constants"; import { OpenAPIV3 } from "openapi-types"; -import { ProgrammingLanguage } from "../../../question"; +import { CustomCopilotRagOptions, ProgrammingLanguage } from "../../../question"; +import { ListAPIInfo } from "@microsoft/m365-spec-parser/dist/src/interfaces"; const manifestFilePath = "/.well-known/ai-plugin.json"; const componentName = "OpenAIPluginManifestHelper"; @@ -61,11 +63,15 @@ const enum telemetryProperties { validationStatus = "validation-status", validationErrors = "validation-errors", validationWarnings = "validation-warnings", + validApisCount = "valid-apis-count", + allApisCount = "all-apis-count", + isFromAddingApi = "is-from-adding-api", } const enum telemetryEvents { validateApiSpec = "validate-api-spec", validateOpenAiPluginManifest = "validate-openai-plugin-manifest", + listApis = "spec-parser-list-apis-result", } enum OpenAIPluginManifestErrorType { @@ -75,6 +81,7 @@ enum OpenAIPluginManifestErrorType { export const copilotPluginParserOptions: ParseOptions = { allowAPIKeyAuth: true, + allowBearerTokenAuth: true, allowMultipleParameters: true, allowOauth2: true, projectType: ProjectType.Copilot, @@ -174,6 +181,8 @@ export async function listOperations( } const isPlugin = inputs[QuestionNames.Capabilities] === copilotPluginApiSpecOptionId; + const isCustomApi = + inputs[QuestionNames.CustomCopilotRag] === CustomCopilotRagOptions.customApi().id; try { const allowAPIKeyAuth = isPlugin || isApiKeyEnabled(); @@ -182,8 +191,12 @@ export async function listOperations( apiSpecUrl as string, isPlugin ? copilotPluginParserOptions + : isCustomApi + ? { + projectType: ProjectType.TeamsAi, + } : { - allowAPIKeyAuth, + allowBearerTokenAuth: allowAPIKeyAuth, // Currently, API key auth support is actually bearer token auth allowMultipleParameters, } ); @@ -203,7 +216,13 @@ export async function listOperations( return err(validationRes.errors); } - let operations: ListAPIResult[] = await specParser.list(); + const listResult: ListAPIResult = await specParser.list(); + let operations = listResult.APIs.filter((value) => value.isValid); + context.telemetryReporter.sendTelemetryEvent(telemetryEvents.listApis, { + [telemetryProperties.validApisCount]: listResult.validAPICount.toString(), + [telemetryProperties.allApisCount]: listResult.allAPICount.toString(), + [telemetryProperties.isFromAddingApi]: (!includeExistingAPIs).toString(), + }); // Filter out exsiting APIs if (!includeExistingAPIs) { @@ -228,7 +247,7 @@ export async function listOperations( } operations = operations.filter( - (operation: ListAPIResult) => !existingOperations.includes(operation.api) + (operation: ListAPIInfo) => !existingOperations.includes(operation.api) ); // No extra API can be added if (operations.length == 0) { @@ -257,7 +276,7 @@ export async function listOperations( } } -function sortOperations(operations: ListAPIResult[]): ApiOperation[] { +function sortOperations(operations: ListAPIInfo[]): ApiOperation[] { const operationsWithSeparator: ApiOperation[] = []; for (const operation of operations) { const arr = operation.api.toUpperCase().split(" "); @@ -270,7 +289,11 @@ function sortOperations(operations: ListAPIResult[]): ApiOperation[] { }, }; - if (operation.auth && operation.auth.type === "apiKey") { + if ( + operation.auth && + operation.auth.authScheme.type === "http" && + operation.auth.authScheme.scheme === "bearer" + ) { result.data.authName = operation.auth.name; } operationsWithSeparator.push(result); @@ -318,23 +341,8 @@ export async function listPluginExistingOperations( } const specParser = new SpecParser(apiSpecFilePath, copilotPluginParserOptions); - const validationRes = await specParser.validate(); - validationRes.errors = formatValidationErrors(validationRes.errors); - - if (validationRes.status === ValidationStatus.Error) { - const errorMessage = getLocalizedString( - "core.createProjectQuestion.apiSpec.multipleValidationErrors.message" - ); - throw new UserError( - "listPluginExistingOperations", - invalidApiSpecErrorName, - errorMessage, - errorMessage - ); - } - - const operations = await specParser.list(); - return operations.map((o) => o.api); + const listResult = await specParser.list(); + return listResult.APIs.map((o) => o.api); } export function logValidationResults( @@ -703,6 +711,8 @@ function formatValidationErrorContent(error: ApiSpecErrorResult): string { return getLocalizedString("core.common.CancelledMessage"); case ErrorType.SwaggerNotSupported: return getLocalizedString("core.common.SwaggerNotSupported"); + case ErrorType.SpecVersionNotSupported: + return getLocalizedString("core.common.SpecVersionNotSupported", error.data); default: return error.content; @@ -716,10 +726,12 @@ interface SpecObject { pathUrl: string; method: string; item: OpenAPIV3.OperationObject; + auth: boolean; } -function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { +function parseSpec(spec: OpenAPIV3.Document): [SpecObject[], boolean] { const res: SpecObject[] = []; + let needAuth = false; const paths = spec.paths; if (paths) { @@ -731,10 +743,16 @@ function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { if (method === "get" || method === "post") { const operationItem = (operations as any)[method] as OpenAPIV3.OperationObject; if (operationItem) { + const authResult = Utils.getAuthArray(operationItem.security, spec); + const hasAuth = authResult.length != 0; + if (hasAuth) { + needAuth = true; + } res.push({ item: operationItem, method: method, pathUrl: pathUrl, + auth: hasAuth, }); } } @@ -743,7 +761,7 @@ function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { } } - return res; + return [res, needAuth]; } async function updatePromptForCustomApi( @@ -799,10 +817,23 @@ async function updateActionForCustomApi( for (let i = 0; i < paramObject.length; i++) { const param = paramObject[i]; const schema = param.schema as OpenAPIV3.SchemaObject; - parameters.properties[param.name] = schema; - parameters.properties[param.name].description = param.description ?? ""; + const paramType = param.in; + + if (!parameters.properties[paramType]) { + parameters.properties[paramType] = { + type: "object", + properties: {}, + required: [], + }; + } + parameters.properties[paramType].properties[param.name] = schema; + parameters.properties[paramType].properties[param.name].description = + param.description ?? ""; if (param.required) { - parameters.required?.push(param.name); + parameters.properties[paramType].required.push(param.name); + if (!parameters.required.includes(paramType)) { + parameters.required.push(paramType); + } } } } @@ -822,6 +853,7 @@ const ActionCode = { javascript: ` app.ai.action("{{operationId}}", async (context, state, parameter) => { const client = await api.getClient(); + // Add authentication configuration for the client const path = client.paths["{{pathUrl}}"]; if (path && path.{{method}}) { const result = await path.{{method}}(parameter.path, parameter.body, { @@ -838,6 +870,7 @@ app.ai.action("{{operationId}}", async (context, state, parameter) => { typescript: ` app.ai.action("{{operationId}}", async (context: TurnContext, state: ApplicationTurnState, parameter: any) => { const client = await api.getClient(); + // Add authentication configuration for the client const path = client.paths["{{pathUrl}}"]; if (path && path.{{method}}) { const result = await path.{{method}}(parameter.path, parameter.body, { @@ -853,11 +886,23 @@ app.ai.action("{{operationId}}", async (context: TurnContext, state: Application `, }; +const AuthCode = { + javascript: { + actionCode: `addAuthConfig(client);`, + actionPlaceholder: `// Add authentication configuration for the client`, + }, + typescript: { + actionCode: `addAuthConfig(client);`, + actionPlaceholder: `// Add authentication configuration for the client`, + }, +}; + async function updateCodeForCustomApi( specItems: SpecObject[], language: string, destinationPath: string, - openapiSpecFileName: string + openapiSpecFileName: string, + needAuth: boolean ): Promise { if (language === ProgrammingLanguage.JS || language === ProgrammingLanguage.TS) { const codeTemplate = @@ -865,10 +910,14 @@ async function updateCodeForCustomApi( const appFolderPath = path.join(destinationPath, "src", "app"); const actionsCode = []; + const authCodeTemplate = + AuthCode[language === ProgrammingLanguage.JS ? "javascript" : "typescript"]; for (const item of specItems) { + const auth = item.auth; const code = codeTemplate + .replace(authCodeTemplate.actionPlaceholder, auth ? authCodeTemplate.actionCode : "") .replace(/{{operationId}}/g, item.item.operationId!) - .replace("{{pathUrl}}", item.pathUrl) + .replace(/{{pathUrl}}/g, item.pathUrl) .replace(/{{method}}/g, item.method); actionsCode.push(code); } @@ -898,7 +947,7 @@ export async function updateForCustomApi( // 1. update prompt folder await updatePromptForCustomApi(spec, language, chatFolder); - const specItems = parseSpec(spec); + const [specItems, needAuth] = parseSpec(spec); // 2. update adaptive card folder await updateAdaptiveCardForCustomApi(specItems, language, destinationPath); @@ -907,5 +956,5 @@ export async function updateForCustomApi( await updateActionForCustomApi(specItems, language, chatFolder); // 4. update code - await updateCodeForCustomApi(specItems, language, destinationPath, openapiSpecFileName); + await updateCodeForCustomApi(specItems, language, destinationPath, openapiSpecFileName, needAuth); } diff --git a/packages/fx-core/src/component/generator/generator.ts b/packages/fx-core/src/component/generator/generator.ts index 42edebeab6..2e69e510c5 100644 --- a/packages/fx-core/src/component/generator/generator.ts +++ b/packages/fx-core/src/component/generator/generator.ts @@ -35,7 +35,11 @@ import { renderTemplateFileData, renderTemplateFileName, } from "./utils"; -import { enableTestToolByDefault, isNewProjectTypeEnabled } from "../../common/featureFlags"; +import { + enableMETestToolByDefault, + enableTestToolByDefault, + isNewProjectTypeEnabled, +} from "../../common/featureFlags"; import { Utils } from "@microsoft/m365-spec-parser"; export class Generator { @@ -69,14 +73,15 @@ export class Generator { ApiSpecAuthRegistrationIdEnvName: safeRegistrationIdEnvName, ApiSpecPath: apiKeyAuthData?.openapiSpecPath ?? "", enableTestToolByDefault: enableTestToolByDefault() ? "true" : "", + enableMETestToolByDefault: enableMETestToolByDefault() ? "true" : "", useOpenAI: llmServiceData?.llmService === "llm-service-openai" ? "true" : "", useAzureOpenAI: llmServiceData?.llmService === "llm-service-azure-openai" ? "true" : "", openAIKey: llmServiceData?.openAIKey ?? "", azureOpenAIKey: llmServiceData?.azureOpenAIKey ?? "", azureOpenAIEndpoint: llmServiceData?.azureOpenAIEndpoint ?? "", isNewProjectTypeEnabled: isNewProjectTypeEnabled() ? "true" : "", - NewProjectTypeName: process.env.TEAMSFX_NEW_PROJECT_TYPE_NAME ?? "M365App", - NewProjectTypeExt: process.env.TEAMSFX_NEW_PROJECT_TYPE_EXTENSION ?? "maproj", + NewProjectTypeName: process.env.TEAMSFX_NEW_PROJECT_TYPE_NAME ?? "TeamsApp", + NewProjectTypeExt: process.env.TEAMSFX_NEW_PROJECT_TYPE_EXTENSION ?? "ttkproj", }; } @hooks([ diff --git a/packages/fx-core/src/component/generator/officeAddin/config/projectsJsonData.ts b/packages/fx-core/src/component/generator/officeAddin/config/projectsJsonData.ts deleted file mode 100644 index fc9bef71f8..0000000000 --- a/packages/fx-core/src/component/generator/officeAddin/config/projectsJsonData.ts +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import _ from "lodash"; -import { projectProperties } from "./projectProperties"; - -export default class projectsJsonData { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - projectJsonData: any; - - constructor() { - this.projectJsonData = projectProperties; - } - - getProjectDisplayName(projectType: string): string { - return this.projectJsonData.projectTypes[_.toLower(projectType)].displayname; - } - - getProjectDetails(projectType: string): string { - return this.projectJsonData.projectTypes[_.toLower(projectType)].detail; - } - - getParsedProjectJsonData(): unknown { - return this.projectJsonData; - } - - getProjectTemplateNames(): string[] { - const projectTemplates: string[] = []; - for (const key in this.projectJsonData.projectTypes) { - projectTemplates.push(key); - } - return projectTemplates; - } - - projectBothScriptTypes(projectType: string): boolean { - return ( - this.projectJsonData.projectTypes[_.toLower(projectType)].templates.javascript.archive != - undefined && - this.projectJsonData.projectTypes[_.toLower(projectType)].templates.typescript.archive != - undefined - ); - } - - projectBothScriptTypesNew(projectType: string): boolean { - return ( - this.projectJsonData.projectTypes[_.toLower(projectType)].templates.javascript != undefined && - this.projectJsonData.projectTypes[_.toLower(projectType)].templates.typescript != undefined - ); - } - - getManifestPath(projectType: string): string | undefined { - return this.projectJsonData.projectTypes[projectType].manifestPath; - } - - getHostTemplateNames(projectType: string): string[] { - let hosts: string[] = []; - if (projectType) { - for (const key in this.projectJsonData.projectTypes) { - if (key === projectType) { - hosts = this.projectJsonData.projectTypes[key].supportedHosts; - } - } - } - return hosts; - } - - getSupportedScriptTypes(projectType: string): string[] { - const scriptTypes: string[] = []; - if (projectType) { - for (const template in this.projectJsonData.projectTypes[projectType].templates) { - const archive = this.projectJsonData.projectTypes[projectType].templates[template].archive; - if (template === "javascript" && archive !== undefined) { - scriptTypes.push("JavaScript"); - } else if (template === "typescript" && archive !== undefined) { - scriptTypes.push("TypeScript"); - } - } - } - return scriptTypes; - } - - getSupportedScriptTypesNew(projectType: string): string[] { - const scriptTypes: string[] = []; - if (projectType) { - for (const template in this.projectJsonData.projectTypes[projectType].templates) { - let scriptType = ""; - if (template === "javascript") { - scriptType = "JavaScript"; - } else if (template === "typescript") { - scriptType = "TypeScript"; - } - - scriptTypes.push(scriptType); - } - } - return scriptTypes; - } - - getHostDisplayName(hostKey: string): string | undefined { - for (const key in this.projectJsonData.hostTypes) { - if (_.toLower(hostKey) == key) { - return this.projectJsonData.hostTypes[key].displayname; - } - } - return undefined; - } - - getProjectTemplateRepository(projectTypeKey: string, scriptType: string): string | undefined { - for (const key in this.projectJsonData.projectTypes) { - if (_.toLower(projectTypeKey) == key) { - if (projectTypeKey == "manifest") { - return this.projectJsonData.projectTypes[key].templates.manifestonly.repository; - } else { - return this.projectJsonData.projectTypes[key].templates[scriptType].repository; - } - } - } - return undefined; - } - - getProjectTemplateBranchName( - projectTypeKey: string, - scriptType: string, - prerelease: boolean - ): string | undefined { - for (const key in this.projectJsonData.projectTypes) { - if (_.toLower(projectTypeKey) == key) { - if (projectTypeKey == "manifest") { - return this.projectJsonData.projectTypes.manifest.templates.branch; - } else { - if (prerelease) { - return this.projectJsonData.projectTypes[key].templates[scriptType].prerelease; - } else { - return this.projectJsonData.projectTypes[key].templates[scriptType].branch; - } - } - } - } - return undefined; - } - - getProjectDownloadLink(projectTypeKey: string, scriptType: string): string { - scriptType = scriptType.toLowerCase(); - return this.projectJsonData.projectTypes[projectTypeKey].templates[scriptType] - .archive as string; - } - - getProjectDownloadLinkNew( - projectTypeKey: string, - scriptType: string, - frameworkType: string - ): string { - scriptType = scriptType.toLowerCase(); - return this.projectJsonData.projectTypes[projectTypeKey].templates[scriptType].frameworks[ - frameworkType - ].archive as string; - } - - getProjectRepoAndBranchNew( - projectTypeKey: string, - scriptType: string, - frameworkType: string, - prerelease: boolean - ): { repo: string | undefined; branch: string | undefined } { - const repoBranchInfo: { repo: string | undefined; branch: string | undefined } = { - repo: (null), - branch: (null), - }; - - repoBranchInfo.repo = this.getProjectTemplateRepositoryNew( - projectTypeKey, - scriptType, - frameworkType - ); - repoBranchInfo.branch = repoBranchInfo.repo - ? this.getProjectTemplateBranchNameNew(projectTypeKey, scriptType, frameworkType, prerelease) - : undefined; - - return repoBranchInfo; - } - - getProjectTemplateRepositoryNew( - projectTypeKey: string, - scriptType: string, - frameworkType: string - ): string | undefined { - for (const key in this.projectJsonData.projectTypes) { - if (_.toLower(projectTypeKey) == key) { - return this.projectJsonData.projectTypes[key].templates[scriptType].frameworks[ - frameworkType - ].repository; - } - } - return undefined; - } - - getProjectTemplateBranchNameNew( - projectTypeKey: string, - scriptType: string, - frameworkType: string, - prerelease: boolean - ): string | undefined { - for (const key in this.projectJsonData.projectTypes) { - if (_.toLower(projectTypeKey) == key) { - if (prerelease) { - return this.projectJsonData.projectTypes[key].templates[scriptType].frameworks[ - frameworkType - ].prerelease; - } else { - return this.projectJsonData.projectTypes[key].templates[scriptType].frameworks[ - frameworkType - ].branch; - } - } - } - return undefined; - } -} diff --git a/packages/fx-core/src/component/generator/officeAddin/generator.ts b/packages/fx-core/src/component/generator/officeAddin/generator.ts index a473913409..24936743bd 100644 --- a/packages/fx-core/src/component/generator/officeAddin/generator.ts +++ b/packages/fx-core/src/component/generator/officeAddin/generator.ts @@ -5,38 +5,48 @@ * @author yefuwang@microsoft.com */ +import { hooks } from "@feathersjs/hooks/lib"; import { + Context, FxError, Inputs, - Result, - ok, - err, ManifestUtil, + Result, devPreview, - Context, + err, + ok, } from "@microsoft/teamsfx-api"; -import { join } from "path"; -import { HelperMethods } from "./helperMethods"; -import { OfficeAddinManifest } from "office-addin-manifest"; -import projectsJsonData from "./config/projectsJsonData"; import * as childProcess from "child_process"; -import { promisify } from "util"; -import _ from "lodash"; -import { hooks } from "@feathersjs/hooks/lib"; -import { ActionExecutionMW } from "../../middleware/actionExecutionMW"; -import { Generator } from "../generator"; +import { OfficeAddinManifest } from "office-addin-manifest"; import { convertProject } from "office-addin-project"; -import { QuestionNames } from "../../../question/questionNames"; -import { ProjectTypeOptions, getTemplate } from "../../../question/create"; +import { join } from "path"; +import { promisify } from "util"; import { getLocalizedString } from "../../../common/localizeUtils"; import { assembleError } from "../../../error"; -import { isOfficeXMLAddinEnabled } from "../../../common/featureFlags"; +import { + CapabilityOptions, + OfficeAddinHostOptions, + ProjectTypeOptions, + getOfficeAddinFramework, +} from "../../../question/create"; +import { QuestionNames } from "../../../question/questionNames"; +import { ActionExecutionMW } from "../../middleware/actionExecutionMW"; +import { Generator } from "../generator"; +import { getOfficeAddinTemplateConfig } from "../officeXMLAddin/projectConfig"; +import { HelperMethods } from "./helperMethods"; +import { toLower } from "lodash"; +import { convertToLangKey } from "../utils"; const componentName = "office-addin"; const telemetryEvent = "generate"; const templateName = "office-addin"; const templateNameForWXPO = "office-json-addin"; +/** + * case 1: project-type=office-xml-addin-type AND addin-host=outlook + * case 2: project-type=office-addin-type (addin-host=undefined) + * case 3: project-type=outlook-addin-type (addin-host=undefined) + */ export class OfficeAddinGenerator { @hooks([ ActionExecutionMW({ @@ -57,9 +67,12 @@ export class OfficeAddinGenerator { } // If lang is undefined, it means the project is created from a folder. - const lang = inputs[QuestionNames.ProgrammingLanguage]; + const lang = toLower(inputs[QuestionNames.ProgrammingLanguage]) as "javascript" | "typescript"; const langKey = - lang != "No Options" ? (lang?.toLowerCase() === "typescript" ? "ts" : "js") : undefined; + inputs[QuestionNames.Capabilities] === CapabilityOptions.outlookAddinImport().id || + inputs[QuestionNames.Capabilities] === CapabilityOptions.officeAddinImport().id + ? "ts" + : convertToLangKey(lang); const templateRes = await Generator.generateTemplate( context, destinationPath, @@ -82,51 +95,58 @@ export class OfficeAddinGenerator { inputs: Inputs, destinationPath: string ): Promise> { - const template = getTemplate(inputs); const name = inputs[QuestionNames.AppName] as string; const addinRoot = destinationPath; const fromFolder = inputs[QuestionNames.OfficeAddinFolder]; - const language = inputs[QuestionNames.ProgrammingLanguage]; - const host = isOfficeXMLAddinEnabled() - ? inputs[QuestionNames.OfficeAddinCapability] === ProjectTypeOptions.outlookAddin().id - ? "Outlook" - : inputs[QuestionNames.OfficeAddinCapability] - : inputs[QuestionNames.OfficeAddinHost]; + const language = toLower(inputs[QuestionNames.ProgrammingLanguage]) as + | "javascript" + | "typescript"; + const projectType = inputs[QuestionNames.ProjectType]; + const capability = inputs[QuestionNames.Capabilities]; + const inputHost = inputs[QuestionNames.OfficeAddinHost]; + let host: string = inputHost; + if ( + projectType === ProjectTypeOptions.outlookAddin().id || + (projectType === ProjectTypeOptions.officeXMLAddin().id && + inputHost === OfficeAddinHostOptions.outlook().id) + ) { + host = "outlook"; + } else if (projectType === ProjectTypeOptions.officeAddin().id) { + if (capability === "json-taskpane") { + host = "wxpo"; // wxpo - support word, excel, powerpoint, outlook + } else if (capability === CapabilityOptions.officeContentAddin().id) { + host = "xp"; // content add-in support excel, powerpoint + } + } const workingDir = process.cwd(); - const importProgress = context.userInteraction.createProgressBar( - getLocalizedString("core.generator.officeAddin.importProject.title"), - 3 - ); + const importProgressStr = + projectType === ProjectTypeOptions.officeAddin().id + ? getLocalizedString("core.generator.officeAddin.importOfficeProject.title") + : getLocalizedString("core.generator.officeAddin.importProject.title"); + const importProgress = context.userInteraction.createProgressBar(importProgressStr, 3); process.chdir(addinRoot); try { if (!fromFolder) { // from template - const jsonData = new projectsJsonData(); - const isOfficeAddin = - inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id; - const framework = isOfficeAddin ? inputs[QuestionNames.OfficeAddinFramework] : undefined; - const projectLink = isOfficeAddin - ? jsonData.getProjectDownloadLinkNew(template, language, framework) - : jsonData.getProjectDownloadLink(template, language); + const framework = getOfficeAddinFramework(inputs); + const templateConfig = getOfficeAddinTemplateConfig( + projectType, + inputs[QuestionNames.OfficeAddinHost] + ); + const projectLink = templateConfig[capability].framework[framework][language]; // Copy project template files from project repository if (projectLink) { await HelperMethods.downloadProjectTemplateZipFile(addinRoot, projectLink); - + let cmdLine = ""; // Call 'convert-to-single-host' npm script in generated project, passing in host parameter if (inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id) { - // Call 'convert-to-single-host' npm script in generated project, passing in host parameter - const cmdLine = `npm run convert-to-single-host --if-present -- ${_.toLower( - "wxpo" // support word, excel, powerpoint, outlook - )} ${"json"}`; - await OfficeAddinGenerator.childProcessExec(cmdLine); + cmdLine = `npm run convert-to-single-host --if-present -- ${host} json`; } else { - // Call 'convert-to-single-host' npm script in generated project, passing in host parameter - const cmdLine = `npm run convert-to-single-host --if-present -- ${_.toLower(host)}`; - await OfficeAddinGenerator.childProcessExec(cmdLine); + cmdLine = `npm run convert-to-single-host --if-present -- ${host}`; } - - const manifestPath = jsonData.getManifestPath(template) as string; + await OfficeAddinGenerator.childProcessExec(cmdLine); + const manifestPath = templateConfig[capability].manifestPath as string; // modify manifest guid and DisplayName await OfficeAddinManifest.modifyManifestFile( `${join(addinRoot, manifestPath)}`, @@ -171,26 +191,30 @@ export class OfficeAddinGenerator { // TODO: update to handle different hosts when support for them is implemented // TODO: handle multiple scopes -type OfficeHost = "Outlook"; // | "Word" | "OneNote" | "PowerPoint" | "Project" | "Excel" -async function getHost(addinManifestPath: string): Promise { +type OfficeHost = "Outlook" | "Word" | "Excel" | "PowerPoint"; // | "OneNote" | "Project" +export async function getHost(addinManifestPath: string): Promise { // Read add-in manifest file const addinManifest: devPreview.DevPreviewSchema = await ManifestUtil.loadFromPath( addinManifestPath ); let host: OfficeHost = "Outlook"; switch (addinManifest.extensions?.[0].requirements?.scopes?.[0]) { - // case "document": - // host = "Word"; + case "document": + host = "Word"; + break; case "mail": host = "Outlook"; + break; // case "notebook": // host = "OneNote"; - // case "presentation": - // host = "PowerPoint"; + case "presentation": + host = "PowerPoint"; + break; // case "project": // host = "Project"; - // case "workbook": - // host = "Excel"; + case "workbook": + host = "Excel"; + break; } return host; } diff --git a/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts b/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts index deb1f1b39c..027511b8af 100644 --- a/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts +++ b/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts @@ -6,28 +6,36 @@ */ import { hooks } from "@feathersjs/hooks/lib"; -import { FxError, Inputs, Result, ok, err, Context } from "@microsoft/teamsfx-api"; +import { Context, FxError, Inputs, Result, err, ok } from "@microsoft/teamsfx-api"; import * as childProcess from "child_process"; -import _ from "lodash"; +import _, { merge } from "lodash"; import { OfficeAddinManifest } from "office-addin-manifest"; import { join } from "path"; import { promisify } from "util"; -import { Generator } from "../generator"; -import { HelperMethods } from "../officeAddin/helperMethods"; -import { ActionExecutionMW } from "../../middleware/actionExecutionMW"; +import { getLocalizedString } from "../../../common/localizeUtils"; import { assembleError } from "../../../error"; -import { ProgrammingLanguage } from "../../../question/create"; import { QuestionNames } from "../../../question/questionNames"; -import { - getOfficeXMLAddinHostProjectRepoInfo, - getOfficeXMLAddinHostProjectTemplateName, -} from "./projectConfig"; -import { getLocalizedString } from "../../../common/localizeUtils"; +import { ActionExecutionMW, ActionContext } from "../../middleware/actionExecutionMW"; +import { Generator } from "../generator"; +import { HelperMethods } from "../officeAddin/helperMethods"; +import { getOfficeAddinTemplateConfig } from "./projectConfig"; +import { convertToLangKey } from "../utils"; const COMPONENT_NAME = "office-xml-addin"; const TELEMETRY_EVENT = "generate"; const TEMPLATE_BASE = "office-xml-addin"; +const TEMPLATE_COMMON_NAME = "office-xml-addin-common"; +const TEMPLATE_COMMON_LANG = "common"; + +const enum OfficeXMLAddinTelemetryProperties { + host = "office-xml-addin-host", + project = "office-xml-addin-project", + lang = "office-xml-addin-lang", +} +/** + * project-type=office-xml-addin-type addin-host!==outlook + */ export class OfficeXMLAddinGenerator { @hooks([ ActionExecutionMW({ @@ -40,20 +48,32 @@ export class OfficeXMLAddinGenerator { static async generate( context: Context, inputs: Inputs, - destinationPath: string + destinationPath: string, + actionContext?: ActionContext ): Promise> { - const host = inputs[QuestionNames.OfficeAddinCapability] as string; - const project = inputs[QuestionNames.Capabilities]; - const lang = inputs[QuestionNames.ProgrammingLanguage] === ProgrammingLanguage.TS ? "ts" : "js"; + const host = inputs[QuestionNames.OfficeAddinHost] as string; + const capability = inputs[QuestionNames.Capabilities]; + const lang = _.toLower(inputs[QuestionNames.ProgrammingLanguage]) as + | "javascript" + | "typescript"; + const langKey = convertToLangKey(lang); const appName = inputs[QuestionNames.AppName] as string; - const templateName = getOfficeXMLAddinHostProjectTemplateName(host, project); - const repoInfo = getOfficeXMLAddinHostProjectRepoInfo(host, project, lang); + const projectType = inputs[QuestionNames.ProjectType]; + const templateConfig = getOfficeAddinTemplateConfig(projectType, host); + const templateName = templateConfig[capability].localTemplate; + const projectLink = templateConfig[capability].framework["default"][lang]; const workingDir = process.cwd(); const progressBar = context.userInteraction.createProgressBar( getLocalizedString("core.createProjectQuestion.officeXMLAddin.bar.title"), 1 ); + merge(actionContext?.telemetryProps, { + [OfficeXMLAddinTelemetryProperties.host]: host, + [OfficeXMLAddinTelemetryProperties.project]: capability, + [OfficeXMLAddinTelemetryProperties.lang]: lang, + }); + try { process.chdir(destinationPath); await progressBar.start(); @@ -61,11 +81,11 @@ export class OfficeXMLAddinGenerator { getLocalizedString("core.createProjectQuestion.officeXMLAddin.bar.detail") ); - if (!!repoInfo) { + if (!!projectLink) { // [Condition]: Project have remote repo (not manifest-only proj) // -> Step: Download the project from GitHub - await HelperMethods.downloadProjectTemplateZipFile(destinationPath, repoInfo); + await HelperMethods.downloadProjectTemplateZipFile(destinationPath, projectLink); // -> Step: Convert to single Host await OfficeXMLAddinGenerator.childProcessExec( @@ -79,10 +99,10 @@ export class OfficeXMLAddinGenerator { context, destinationPath, `${TEMPLATE_BASE}-manifest-only`, - lang + langKey ); if (getManifestOnlyProjectTemplateRes.isErr()) - return err(getManifestOnlyProjectTemplateRes.error); + throw err(getManifestOnlyProjectTemplateRes.error); } // -> Common Step: Copy the README (or with manifest for manifest-only proj) @@ -90,9 +110,9 @@ export class OfficeXMLAddinGenerator { context, destinationPath, `${TEMPLATE_BASE}-${templateName}`, - lang + langKey ); - if (getReadmeTemplateRes.isErr()) return err(getReadmeTemplateRes.error); + if (getReadmeTemplateRes.isErr()) throw err(getReadmeTemplateRes.error); // -> Common Step: Modify the Manifest await OfficeAddinManifest.modifyManifestFile( @@ -101,6 +121,15 @@ export class OfficeXMLAddinGenerator { `${appName}` ); + // -> Common Step: Generate OfficeXMLAddin specific `teamsapp.yml` + const generateOfficeYMLRes = await Generator.generateTemplate( + context, + destinationPath, + TEMPLATE_COMMON_NAME, + TEMPLATE_COMMON_LANG + ); + if (generateOfficeYMLRes.isErr()) throw err(generateOfficeYMLRes.error); + process.chdir(workingDir); await progressBar.end(true, true); return ok(undefined); diff --git a/packages/fx-core/src/component/generator/officeXMLAddin/projectConfig.ts b/packages/fx-core/src/component/generator/officeXMLAddin/projectConfig.ts index c84bffef04..3d0a36cd71 100644 --- a/packages/fx-core/src/component/generator/officeXMLAddin/projectConfig.ts +++ b/packages/fx-core/src/component/generator/officeXMLAddin/projectConfig.ts @@ -1,208 +1,201 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { OfficeAddinHostOptions, ProjectTypeOptions } from "../../../question"; + /** * @author zyun@microsoft.com */ -interface IOfficeXMLAddinHostConfig { +interface IOfficeAddinHostConfig { [property: string]: { title: string; detail: string; localTemplate: string; - lang: { - ts?: string; - js?: string; + manifestPath?: string; + framework: { + [property: string]: { + typescript?: string; + javascript?: string; + }; }; }; } -interface IOfficeXMLAddinProjectConfig { - [property: string]: IOfficeXMLAddinHostConfig; +interface IOfficeAddinProjectConfig { + [property: string]: IOfficeAddinHostConfig; } const CommonProjectConfig = { taskpane: { title: "core.createProjectQuestion.officeXMLAddin.taskpane.title", detail: "core.createProjectQuestion.officeXMLAddin.taskpane.detail", - lang: { - ts: "https://aka.ms/ccdevx-fx-taskpane-ts", - js: "https://aka.ms/ccdevx-fx-taskpane-js", + framework: { + default: { + typescript: "https://aka.ms/ccdevx-fx-taskpane-ts", + javascript: "https://aka.ms/ccdevx-fx-taskpane-js", + }, }, }, sso: { - lang: { - ts: "https://aka.ms/ccdevx-fx-sso-ts", - js: "https://aka.ms/ccdevx-fx-sso-js", + framework: { + default: { + typescript: "https://aka.ms/ccdevx-fx-sso-ts", + javascript: "https://aka.ms/ccdevx-fx-sso-js", + }, }, }, react: { - lang: { - ts: "https://aka.ms/ccdevx-fx-react-ts", - js: "https://aka.ms/ccdevx-fx-react-js", + framework: { + default: { + typescript: "https://aka.ms/ccdevx-fx-react-ts", + javascript: "https://aka.ms/ccdevx-fx-react-js", + }, }, }, manifest: { title: "core.createProjectQuestion.officeXMLAddin.manifestOnly.title", detail: "core.createProjectQuestion.officeXMLAddin.manifestOnly.detail", - lang: {}, + framework: { + default: {}, + }, }, }; -const OfficeXMLAddinProjectConfig: IOfficeXMLAddinProjectConfig = { +export const OfficeAddinProjectConfig: IOfficeAddinProjectConfig = { + json: { + "json-taskpane": { + title: "core.newTaskpaneAddin.label", + detail: "core.newTaskpaneAddin.detail", + localTemplate: "", + framework: { + default_old: { + typescript: "https://aka.ms/teams-toolkit/office-addin-taskpane", + }, + default: { + typescript: "https://aka.ms/teams-toolkit/office-addin-taskpane/ts-default", + javascript: "https://aka.ms/teams-toolkit/office-addin-taskpane/js-default", + }, + react: { + typescript: "https://aka.ms/teams-toolkit/office-addin-taskpane/ts-react", + javascript: "https://aka.ms/teams-toolkit/office-addin-taskpane/js-react", + }, + }, + manifestPath: "manifest.json", + }, + "office-content-addin": { + title: "core.newContentAddin.label", + detail: "core.newContentAddin.detail", + localTemplate: "", + framework: { + default: { + typescript: "https://aka.ms/teams-toolkit/office-addin-content/ts-default", + javascript: "https://aka.ms/teams-toolkit/office-addin-content/js-default", + }, + }, + manifestPath: "manifest.json", + }, + }, word: { - taskpane: { + "word-taskpane": { localTemplate: "word-taskpane", ...CommonProjectConfig.taskpane, }, - sso: { + "word-sso": { title: "core.createProjectQuestion.officeXMLAddin.word.sso.title", detail: "core.createProjectQuestion.officeXMLAddin.word.sso.detail", localTemplate: "word-sso", ...CommonProjectConfig.sso, }, - react: { + "word-react": { title: "core.createProjectQuestion.officeXMLAddin.word.react.title", detail: "core.createProjectQuestion.officeXMLAddin.word.react.detail", localTemplate: "word-react", ...CommonProjectConfig.react, }, - manifest: { + "word-manifest": { localTemplate: "word-manifest-only", ...CommonProjectConfig.manifest, }, }, excel: { - taskpane: { + "excel-taskpane": { localTemplate: "excel-taskpane", ...CommonProjectConfig.taskpane, }, - sso: { + "excel-sso": { title: "core.createProjectQuestion.officeXMLAddin.excel.sso.title", detail: "core.createProjectQuestion.officeXMLAddin.excel.sso.detail", localTemplate: "excel-sso", ...CommonProjectConfig.sso, }, - react: { + "excel-react": { title: "core.createProjectQuestion.officeXMLAddin.excel.react.title", detail: "core.createProjectQuestion.officeXMLAddin.excel.react.detail", localTemplate: "excel-react", ...CommonProjectConfig.react, }, - cfShared: { + "excel-custom-functions-shared": { title: "core.createProjectQuestion.officeXMLAddin.excel.cf.shared.title", detail: "core.createProjectQuestion.officeXMLAddin.excel.cf.shared.detail", localTemplate: "excel-cf", - lang: { - ts: "https://aka.ms/ccdevx-fx-cf-shared-ts", - js: "https://aka.ms/ccdevx-fx-cf-shared-js", + framework: { + default: { + typescript: "https://aka.ms/ccdevx-fx-cf-shared-ts", + javascript: "https://aka.ms/ccdevx-fx-cf-shared-js", + }, }, }, - cfJS: { + "excel-custom-functions-js": { title: "core.createProjectQuestion.officeXMLAddin.excel.cf.js.title", detail: "core.createProjectQuestion.officeXMLAddin.excel.cf.js.detail", localTemplate: "excel-cf", - lang: { - ts: "https://aka.ms/ccdevx-fx-cf-js-ts", - js: "https://aka.ms/ccdevx-fx-cf-js-js", + framework: { + default: { + typescript: "https://aka.ms/ccdevx-fx-cf-js-ts", + javascript: "https://aka.ms/ccdevx-fx-cf-js-js", + }, }, }, - manifest: { + "excel-manifest": { localTemplate: "excel-manifest-only", ...CommonProjectConfig.manifest, }, }, powerpoint: { - taskpane: { + "powerpoint-taskpane": { localTemplate: "powerpoint-taskpane", ...CommonProjectConfig.taskpane, }, - sso: { + "powerpoint-sso": { localTemplate: "powerpoint-sso", title: "core.createProjectQuestion.officeXMLAddin.powerpoint.sso.title", detail: "core.createProjectQuestion.officeXMLAddin.powerpoint.sso.detail", ...CommonProjectConfig.sso, }, - react: { + "powerpoint-react": { localTemplate: "powerpoint-react", title: "core.createProjectQuestion.officeXMLAddin.powerpoint.react.title", detail: "core.createProjectQuestion.officeXMLAddin.powerpoint.react.detail", ...CommonProjectConfig.react, }, - manifest: { + "powerpoint-manifest": { localTemplate: "powerpoint-manifest-only", ...CommonProjectConfig.manifest, }, }, }; -/** - * Get all available Office XML Addin Project Options of one host - * @param host Office host - * @returns the detail proj options[] of the host - */ -export function getOfficeXMLAddinHostProjectOptions(host: string): { - proj: string; - title: string; - detail: string; -}[] { - const result = []; - for (const proj in OfficeXMLAddinProjectConfig[host]) { - result.push({ - proj, - title: OfficeXMLAddinProjectConfig[host][proj].title, - detail: OfficeXMLAddinProjectConfig[host][proj].detail, - }); - } - return result; -} - -/** - * Get all available Lang Options of one host and proj - * @param host Office host - * @param proj proj name - * @returns the detail lang options[] of the proj - */ -export function getOfficeXMLAddinHostProjectLangOptions( - host: string, - proj: string -): { - id: string; - label: string; -}[] { - const result = []; - for (const lang in OfficeXMLAddinProjectConfig[host][proj].lang) { - result.push( - lang === "ts" - ? { id: "typescript", label: "TypeScript" } - : { id: "javascript", label: "JavaScript" } - ); +export function getOfficeAddinTemplateConfig( + projectType: string, + addinHost?: string +): IOfficeAddinHostConfig { + if ( + projectType === ProjectTypeOptions.officeXMLAddin().id && + addinHost && + addinHost !== OfficeAddinHostOptions.outlook().id + ) { + return OfficeAddinProjectConfig[addinHost]; } - return result; -} - -/** - * Get all available Lang Options of one host and proj - * @param host Office host - * @param proj proj name - * @returns the detail lang options[] of the proj - */ -export function getOfficeXMLAddinHostProjectTemplateName(host: string, proj: string): string { - return OfficeXMLAddinProjectConfig[host][proj].localTemplate; -} - -/** - * Get the Repo Info of the proj - * @param host wxp - * @param proj proj name - * @param lang ts or js - * @returns Repo Info - */ -export function getOfficeXMLAddinHostProjectRepoInfo( - host: string, - proj: string, - lang: "ts" | "js" -): string { - const result = OfficeXMLAddinProjectConfig[host][proj].lang?.[lang]; - return !!result ? result : ""; + return OfficeAddinProjectConfig["json"]; } diff --git a/packages/fx-core/src/component/utils/metadataGraphPermssion.ts b/packages/fx-core/src/component/utils/metadataGraphPermssion.ts index b93c0a6852..2ff2367ab7 100644 --- a/packages/fx-core/src/component/utils/metadataGraphPermssion.ts +++ b/packages/fx-core/src/component/utils/metadataGraphPermssion.ts @@ -10,6 +10,13 @@ import { AADManifest } from "../driver/aad/interface/AADManifest"; import { getDetailedGraphPermissionMap, graphAppId, graphAppName } from "../driver/aad/permissions"; import { TelemetryProperty } from "../../common/telemetry"; import { actionName } from "../driver/aad/update"; +interface summary { + hasGraphPermission: boolean; + hasRole: boolean; + hasAdminScope: boolean; + scopes: string[]; + roles: string[]; +} class MetadataGraphPermissionUtil { async parseAadManifest( ymlPath: string, @@ -35,7 +42,7 @@ class MetadataGraphPermissionUtil { try { const manifestString = await fs.readFile(aadManifestPath, "utf8"); const manifest = JSON.parse(manifestString); - const graphPermissionSummary = this.getPermissionSummary(manifest); + const graphPermissionSummary = this.summary(manifest); if (graphPermissionSummary) { props[TelemetryProperty.GraphPermission] = graphPermissionSummary.hasGraphPermission ? "true" @@ -47,17 +54,19 @@ class MetadataGraphPermissionUtil { ? "true" : "false"; props[TelemetryProperty.GraphPermissionScopes] = graphPermissionSummary.scopes.join(","); + props[TelemetryProperty.GraphPermissionRoles] = graphPermissionSummary.roles.join(","); } } catch (error) { return; } } - getPermissionSummary(manifest: AADManifest) { + summary(manifest: AADManifest): summary | undefined { let hasGraphPermission = false; let hasRole = false; let hasAdminScope = false; const scopes: string[] = []; + const roles: string[] = []; const graphPermissionMap = getDetailedGraphPermissionMap(); if (!graphPermissionMap) { return undefined; @@ -71,12 +80,17 @@ class MetadataGraphPermissionUtil { hasRole, hasAdminScope, scopes, + roles, }; } hasGraphPermission = true; graphPermission.resourceAccess?.forEach((access) => { if (access.type === "Role") { hasRole = true; + const id = isUUID(access.id) ? access.id : graphPermissionMap.roles[access.id]; + if (graphPermissionMap.roleIds[id]) { + roles.push(graphPermissionMap.roleIds[id].value); + } } else { const id = isUUID(access.id) ? access.id : graphPermissionMap.scopes[access.id]; if (graphPermissionMap.scopeIds[id]) { @@ -92,6 +106,7 @@ class MetadataGraphPermissionUtil { hasRole, hasAdminScope, scopes, + roles, }; } } diff --git a/packages/fx-core/src/component/utils/metadataRscPermission.ts b/packages/fx-core/src/component/utils/metadataRscPermission.ts new file mode 100644 index 0000000000..3d632634f2 --- /dev/null +++ b/packages/fx-core/src/component/utils/metadataRscPermission.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import path from "path"; +import fs from "fs-extra"; +import { MetadataV3 } from "../../common/versionMetadata"; +import { ProjectModel } from "../configManager/interface"; +import { ProjectTypeProps, TelemetryProperty } from "../../common/telemetry"; +import { manifestUtils } from "../driver/teamsApp/utils/ManifestUtils"; +import { TeamsAppManifest } from "../../../../manifest/build/manifest"; + +interface summary { + version: string; + rscApplication: string[]; + rscDelegated: string[]; +} + +class MetadataRscPermissionUtil { + async parseManifest( + ymlPath: string, + model: ProjectModel, + props: { [key: string]: string } + ): Promise { + let manifestName = path.join(MetadataV3.teamsManifestFolder, MetadataV3.teamsManifestFileName); + const action = model.provision?.driverDefs.find( + (def) => def.uses === "teamsApp/validateManifest" + ); + // if teamsApp/validateManifest action is defined, use the manifest file in the action + if (action) { + const parameters = action.with as { [key: string]: string }; + if (parameters && parameters["manifestPath"]) { + manifestName = parameters["manifestPath"]; + } + } + const manifestPath = path.join(path.dirname(ymlPath), manifestName); + if (!(await fs.pathExists(manifestPath))) { + return; + } + + try { + const result = await manifestUtils.readAppManifest(manifestPath); + if (result.isErr()) { + return; + } + const manifest = result.value; + const summary = this.summary(manifest); + if (summary) { + props[ProjectTypeProps.TeamsManifestVersion] = summary.version; + props[TelemetryProperty.RscApplication] = summary.rscApplication.join(","); + props[TelemetryProperty.RscDelegated] = summary.rscDelegated.join(","); + } + } catch (error) { + return; + } + } + + summary(manifest: TeamsAppManifest): summary | undefined { + const version = manifest.version; + const rscApplication: string[] = []; + const rscDelegated: string[] = []; + for (const permission of manifest.authorization?.permissions?.resourceSpecific || []) { + if (permission.type == "Application") { + rscApplication.push(permission.name); + } else { + rscDelegated.push(permission.name); + } + } + for (const permission of manifest.webApplicationInfo?.applicationPermissions || []) { + rscApplication.push(permission); + } + + return { + version, + rscApplication, + rscDelegated, + }; + } +} + +export const metadataRscPermissionUtil = new MetadataRscPermissionUtil(); diff --git a/packages/fx-core/src/component/utils/metadataUtil.ts b/packages/fx-core/src/component/utils/metadataUtil.ts index b7e1d874a7..d520617d29 100644 --- a/packages/fx-core/src/component/utils/metadataUtil.ts +++ b/packages/fx-core/src/component/utils/metadataUtil.ts @@ -9,6 +9,7 @@ import { LifecycleNames, ProjectModel } from "../configManager/interface"; import { yamlParser } from "../configManager/parser"; import { createHash } from "crypto"; import { metadataGraphPermissionUtil } from "./metadataGraphPermssion"; +import { metadataRscPermissionUtil } from "./metadataRscPermission"; class MetadataUtil { async parse(path: string, env: string | undefined): Promise> { @@ -33,6 +34,7 @@ class MetadataUtil { res.value.additionalMetadata ); await metadataGraphPermissionUtil.parseAadManifest(path, res.value, props); + await metadataRscPermissionUtil.parseManifest(path, res.value, props); TOOLS.telemetryReporter?.sendTelemetryEvent(TelemetryEvent.MetaData, props); } diff --git a/packages/fx-core/src/component/utils/pathUtils.ts b/packages/fx-core/src/component/utils/pathUtils.ts index 20d7fcac50..68c7e07baf 100644 --- a/packages/fx-core/src/component/utils/pathUtils.ts +++ b/packages/fx-core/src/component/utils/pathUtils.ts @@ -11,6 +11,7 @@ import { environmentNameManager } from "../../core/environmentName"; class PathUtils { getYmlFilePath(projectPath: string, env?: string): string { + if (process.env.TEAMSFX_CONFIG_FILE_PATH) return process.env.TEAMSFX_CONFIG_FILE_PATH; const envName = env || process.env.TEAMSFX_ENV || "dev"; if (!envName) throw new MissingRequiredInputError("env", "PathUtils"); const ymlPath = path.join( diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index 62c493d442..1732a1d043 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -25,6 +25,7 @@ import { Tools, err, ok, + UserError, } from "@microsoft/teamsfx-api"; import { DotenvParseOutput } from "dotenv"; import fs from "fs-extra"; @@ -66,6 +67,7 @@ import { CreateAppPackageDriver } from "../component/driver/teamsApp/createAppPa import { CreateAppPackageArgs } from "../component/driver/teamsApp/interfaces/CreateAppPackageArgs"; import { ValidateAppPackageArgs } from "../component/driver/teamsApp/interfaces/ValidateAppPackageArgs"; import { ValidateManifestArgs } from "../component/driver/teamsApp/interfaces/ValidateManifestArgs"; +import { ValidateWithTestCasesArgs } from "../component/driver/teamsApp/interfaces/ValidateWithTestCasesArgs"; import { teamsappMgr } from "../component/driver/teamsApp/teamsappMgr"; import { manifestUtils } from "../component/driver/teamsApp/utils/ManifestUtils"; import { @@ -74,6 +76,7 @@ import { } from "../component/driver/teamsApp/utils/utils"; import { ValidateManifestDriver } from "../component/driver/teamsApp/validate"; import { ValidateAppPackageDriver } from "../component/driver/teamsApp/validateAppPackage"; +import { ValidateWithTestCasesDriver } from "../component/driver/teamsApp/validateTestCases"; import { SSO } from "../component/feature/sso"; import { ErrorResult, @@ -108,7 +111,11 @@ import { NoNeedUpgradeError } from "../error/upgrade"; import { YamlFieldMissingError } from "../error/yml"; import { ValidateTeamsAppInputs } from "../question"; import { SPFxVersionOptionIds, ScratchOptions, createProjectCliHelpNode } from "../question/create"; -import { HubTypes, isAadMainifestContainsPlaceholder } from "../question/other"; +import { + HubTypes, + isAadMainifestContainsPlaceholder, + TeamsAppValidationOptions, +} from "../question/other"; import { QuestionNames } from "../question/questionNames"; import { copilotPluginApiSpecOptionId } from "../question/constants"; import { CallbackRegistry } from "./callback"; @@ -129,6 +136,7 @@ import { import { CoreTelemetryComponentName, CoreTelemetryEvent, CoreTelemetryProperty } from "./telemetry"; import { CoreHookContext, PreProvisionResForVS, VersionCheckRes } from "./types"; import "../component/feature/sso"; +import { pluginManifestUtils } from "../component/driver/teamsApp/utils/PluginManifestUtils"; export type CoreCallbackFunc = (name: string, err?: FxError, data?: any) => void | Promise; @@ -505,6 +513,8 @@ export class FxCore { async validateApplication(inputs: ValidateTeamsAppInputs): Promise> { if (inputs["manifest-path"]) { return await this.validateManifest(inputs); + } else if (inputs[QuestionNames.ValidateMethod] === TeamsAppValidationOptions.testCases().id) { + return await this.validateWithTestCases(inputs); } else { return await this.validateAppPackage(inputs); } @@ -549,6 +559,22 @@ export class FxCore { const driver: ValidateAppPackageDriver = Container.get("teamsApp/validateAppPackage"); return (await driver.execute(args, context)).result; } + + @hooks([ + ErrorContextMW({ component: "FxCore", stage: "validateWithTestCases", reset: true }), + ErrorHandlerMW, + ConcurrentLockerMW, + ]) + async validateWithTestCases(inputs: ValidateTeamsAppInputs): Promise> { + const context: DriverContext = createDriverContext(inputs); + const args: ValidateWithTestCasesArgs = { + appPackagePath: inputs["app-package-file-path"] as string, + showMessage: true, + showProgressBar: true, + }; + const driver: ValidateWithTestCasesDriver = Container.get("teamsApp/validateWithTestCases"); + return (await driver.execute(args, context)).result; + } /** * v3 only none lifecycle command */ @@ -959,6 +985,8 @@ export class FxCore { if (match) { if (match[1].startsWith("TEAMSFX_ENV=")) { writeStream.write(`TEAMSFX_ENV=${targetEnvName}${os.EOL}`); + } else if (match[1].startsWith("APP_NAME_SUFFIX=")) { + writeStream.write(`APP_NAME_SUFFIX=${targetEnvName}${os.EOL}`); } else { writeStream.write(`${match[1]}${os.EOL}`); } @@ -1225,6 +1253,7 @@ export class FxCore { const url = inputs[QuestionNames.ApiSpecLocation] ?? inputs.openAIPluginManifest?.api.url; const manifestPath = inputs[QuestionNames.ManifestPath]; const isPlugin = inputs[QuestionNames.Capabilities] === copilotPluginApiSpecOptionId; + const context = createContextV3(); // Get API spec file path from manifest const manifestRes = await manifestUtils._readAppManifest(manifestPath); @@ -1238,25 +1267,26 @@ export class FxCore { isPlugin ? copilotPluginParserOptions : { - allowAPIKeyAuth: isApiKeyEnabled(), + allowBearerTokenAuth: isApiKeyEnabled(), // Currently, API key auth support is actually bearer token auth allowMultipleParameters: isMultipleParametersEnabled(), } ); - const apiResultList = await specParser.list(); + const listResult = await specParser.list(); + const apiResultList = listResult.APIs.filter((value) => value.isValid); let existingOperations: string[]; let outputAPISpecPath: string; if (isPlugin) { + if (!inputs[QuestionNames.DestinationApiSpecFilePath]) { + return err(new MissingRequiredInputError(QuestionNames.DestinationApiSpecFilePath)); + } + outputAPISpecPath = inputs[QuestionNames.DestinationApiSpecFilePath]; existingOperations = await listPluginExistingOperations( manifestRes.value, manifestPath, inputs[QuestionNames.DestinationApiSpecFilePath] ); - if (!inputs[QuestionNames.DestinationApiSpecFilePath]) { - return err(new MissingRequiredInputError(QuestionNames.DestinationApiSpecFilePath)); - } - outputAPISpecPath = inputs[QuestionNames.DestinationApiSpecFilePath]; } else { const existingOperationIds = manifestUtils.getOperationIds(manifestRes.value); existingOperations = apiResultList @@ -1274,8 +1304,6 @@ export class FxCore { ResponseTemplatesFolderName ); - const context = createContextV3(); - try { if (isApiKeyEnabled()) { const authNames: Set = new Set(); @@ -1283,7 +1311,11 @@ export class FxCore { for (const api of operations) { const operation = apiResultList.find((op) => op.api === api); if (operation) { - if (operation.auth && operation.auth.type === "apiKey") { + if ( + operation.auth && + operation.auth.authScheme.type === "http" && + operation.auth.authScheme.scheme === "bearer" + ) { authNames.add(operation.auth.name); serverUrls.add(operation.server); } @@ -1314,12 +1346,29 @@ export class FxCore { } } - const generateResult = await specParser.generate( - manifestPath, - operations, - outputAPISpecPath, - adaptiveCardFolder - ); + let generateResult; + if (!isPlugin) { + generateResult = await specParser.generate( + manifestPath, + operations, + outputAPISpecPath, + adaptiveCardFolder + ); + } else { + const pluginPathRes = await manifestUtils.getPluginFilePath( + manifestRes.value, + manifestPath + ); + if (pluginPathRes.isErr()) { + return err(pluginPathRes.error); + } + generateResult = await specParser.generateForCopilot( + manifestPath, + operations, + outputAPISpecPath, + pluginPathRes.value + ); + } // Send SpecParser.generate() warnings context.telemetryReporter.sendTelemetryEvent(specParserGenerateResultTelemetryEvent, { @@ -1357,6 +1406,31 @@ export class FxCore { return ok(undefined); } + @hooks([ + ErrorContextMW({ component: "FxCore", stage: "copilotPluginListApiSpecs" }), + ErrorHandlerMW, + ]) + async listPluginApiSpecs(inputs: Inputs): Promise> { + try { + const manifestPath = inputs[QuestionNames.ManifestPath]; + const manifestRes = await manifestUtils._readAppManifest(manifestPath); + if (manifestRes.isErr()) { + return err(manifestRes.error); + } + const res = await pluginManifestUtils.getApiSpecFilePathFromTeamsManifest( + manifestRes.value, + manifestPath + ); + if (res.isOk()) { + return ok(res.value); + } else { + return err(res.error); + } + } catch (error) { + return err(error as FxError); + } + } + @hooks([ ErrorContextMW({ component: "FxCore", stage: "copilotPluginLoadOpenAIManifest" }), ErrorHandlerMW, @@ -1374,10 +1448,8 @@ export class FxCore { ErrorContextMW({ component: "FxCore", stage: "copilotPluginListOperations" }), ErrorHandlerMW, ]) - async copilotPluginListOperations( - inputs: Inputs - ): Promise> { - return await listOperations( + async copilotPluginListOperations(inputs: Inputs): Promise> { + const res = await listOperations( createContextV3(), inputs.manifest, inputs.apiSpecUrl, @@ -1385,6 +1457,12 @@ export class FxCore { inputs.includeExistingAPIs, inputs.shouldLogWarning ); + if (res.isErr()) { + const msg = res.error.map((e) => e.content).join("\n"); + return err(new UserError("FxCore", "ListOpenAPISpecOperationsError", msg, msg)); + } else { + return ok(res.value); + } } /** diff --git a/packages/fx-core/src/error/script.ts b/packages/fx-core/src/error/script.ts index 78563ed13b..5e221528ab 100644 --- a/packages/fx-core/src/error/script.ts +++ b/packages/fx-core/src/error/script.ts @@ -9,13 +9,13 @@ import { ErrorCategory } from "./types"; * Script execution timeout */ export class ScriptTimeoutError extends UserError { - constructor(cmd: string, error?: any) { + constructor(error?: Error) { const key = "error.script.ScriptTimeoutError"; const errorOptions: UserErrorOptions = { source: "script", name: "ScriptTimeoutError", - message: getDefaultString(key, cmd), - displayMessage: getLocalizedString(key, cmd), + message: getDefaultString(key), + displayMessage: getLocalizedString(key), error: error, categories: [ErrorCategory.External], }; @@ -27,13 +27,13 @@ export class ScriptTimeoutError extends UserError { * Script execution error */ export class ScriptExecutionError extends UserError { - constructor(script: string, message: string, error?: any) { + constructor(error?: Error) { const key = "error.script.ScriptExecutionError"; const errorOptions: UserErrorOptions = { source: "script", name: "ScriptExecutionError", - message: getDefaultString(key, script, message), - displayMessage: getLocalizedString(key, script, message), + message: getDefaultString(key), + displayMessage: getLocalizedString(key), error: error, categories: [ErrorCategory.External], }; diff --git a/packages/fx-core/src/question/constants.ts b/packages/fx-core/src/question/constants.ts index 03a14cf11d..2c141b9b81 100644 --- a/packages/fx-core/src/question/constants.ts +++ b/packages/fx-core/src/question/constants.ts @@ -13,4 +13,4 @@ export const copilotPluginOptionIds = [ copilotPluginApiSpecOptionId, copilotPluginOpenAIPluginOptionId, ]; -export const capabilitiesHavePythonOption = ["custom-copilot-basic"]; +export const capabilitiesHavePythonOption = ["custom-copilot-basic", "custom-copilot-rag"]; diff --git a/packages/fx-core/src/question/create.ts b/packages/fx-core/src/question/create.ts index 6867017fe8..7b35e5985e 100644 --- a/packages/fx-core/src/question/create.ts +++ b/packages/fx-core/src/question/create.ts @@ -22,14 +22,16 @@ import { cloneDeep } from "lodash"; import * as os from "os"; import * as path from "path"; import { ConstantString } from "../common/constants"; +import { Correlator } from "../common/correlator"; import { - isCLIDotNetEnabled, - isCopilotPluginEnabled, isApiCopilotPluginEnabled, isApiKeyEnabled, - isTdpTemplateCliTestEnabled, - isOfficeXMLAddinEnabled, + isCLIDotNetEnabled, + isCopilotPluginEnabled, isOfficeJSONAddinEnabled, + isOfficeXMLAddinEnabled, + isTdpTemplateCliTestEnabled, + isApiMeSSOEnabled, } from "../common/featureFlags"; import { getLocalizedString } from "../common/localizeUtils"; import { sampleProvider } from "../common/samples"; @@ -45,26 +47,24 @@ import { OpenAIPluginManifestHelper, listOperations, } from "../component/generator/copilotPlugin/helper"; -import projectsJsonData from "../component/generator/officeAddin/config/projectsJsonData"; +import { + OfficeAddinProjectConfig, + getOfficeAddinTemplateConfig, +} from "../component/generator/officeXMLAddin/projectConfig"; import { DevEnvironmentSetupError } from "../component/generator/spfx/error"; import { SPFxGenerator } from "../component/generator/spfx/spfxGenerator"; import { Constants } from "../component/generator/spfx/utils/constants"; import { Utils } from "../component/generator/spfx/utils/utils"; import { createContextV3 } from "../component/utils"; import { EmptyOptionError, assembleError } from "../error"; -import { CliQuestionName, QuestionNames } from "./questionNames"; -import { isValidHttpUrl } from "./util"; import { capabilitiesHavePythonOption, copilotPluginApiSpecOptionId, copilotPluginNewApiOptionId, copilotPluginOpenAIPluginOptionId, } from "./constants"; -import { Correlator } from "../common/correlator"; -import { - getOfficeXMLAddinHostProjectLangOptions, - getOfficeXMLAddinHostProjectOptions, -} from "../component/generator/officeXMLAddin/projectConfig"; +import { CliQuestionName, QuestionNames } from "./questionNames"; +import { isValidHttpUrl } from "./util"; export class ScratchOptions { static yes(): OptionItem { @@ -134,7 +134,7 @@ export class ProjectTypeOptions { static officeXMLAddin(platform?: Platform): OptionItem { return { id: "office-xml-addin-type", - label: `${platform === Platform.VSCode ? "$(inbox) " : ""}${getLocalizedString( + label: `${platform === Platform.VSCode ? "$(teamsfx-m365) " : ""}${getLocalizedString( "core.createProjectQuestion.officeXMLAddin.mainEntry.title" )}`, detail: getLocalizedString("core.createProjectQuestion.officeXMLAddin.mainEntry.detail"), @@ -151,6 +151,14 @@ export class ProjectTypeOptions { }; } + static officeAddinAllIds(platform?: Platform): string[] { + return [ + ProjectTypeOptions.officeAddin(platform).id, + ProjectTypeOptions.officeXMLAddin(platform).id, + ProjectTypeOptions.outlookAddin(platform).id, + ]; + } + static copilotPlugin(platform?: Platform): OptionItem { return { id: "copilot-plugin-type", @@ -177,10 +185,9 @@ function projectTypeQuestion(): SingleSelectQuestion { ProjectTypeOptions.bot(Platform.CLI), ProjectTypeOptions.tab(Platform.CLI), ProjectTypeOptions.me(Platform.CLI), - isOfficeXMLAddinEnabled() - ? ProjectTypeOptions.officeXMLAddin(Platform.CLI) - : ProjectTypeOptions.outlookAddin(Platform.CLI), + ProjectTypeOptions.officeXMLAddin(Platform.CLI), ProjectTypeOptions.officeAddin(Platform.CLI), + ProjectTypeOptions.outlookAddin(Platform.CLI), ]; return { name: QuestionNames.ProjectType, @@ -222,12 +229,31 @@ function projectTypeQuestion(): SingleSelectQuestion { }; } -export class OfficeAddinCapabilityOptions { +export class OfficeAddinHostOptions { + static all(platform?: Platform): OptionItem[] { + return [ + OfficeAddinHostOptions.outlook(platform), + OfficeAddinHostOptions.word(), + OfficeAddinHostOptions.excel(), + OfficeAddinHostOptions.powerpoint(), + ]; + } + static outlook(platform?: Platform): OptionItem { + return { + id: "outlook", + label: `${platform === Platform.VSCode ? "$(mail) " : ""}${getLocalizedString( + "core.createProjectQuestion.projectType.outlookAddin.label" + )}`, + detail: getLocalizedString("core.createProjectQuestion.projectType.outlookAddin.detail"), + data: "Outlook", + }; + } static word(): OptionItem { return { id: "word", label: getLocalizedString("core.createProjectQuestion.officeXMLAddin.word.title"), detail: getLocalizedString("core.createProjectQuestion.officeXMLAddin.word.detail"), + data: "Word", }; } @@ -236,6 +262,7 @@ export class OfficeAddinCapabilityOptions { id: "excel", label: getLocalizedString("core.createProjectQuestion.officeXMLAddin.excel.title"), detail: getLocalizedString("core.createProjectQuestion.officeXMLAddin.excel.detail"), + data: "Excel", }; } @@ -244,6 +271,7 @@ export class OfficeAddinCapabilityOptions { id: "powerpoint", label: getLocalizedString("core.createProjectQuestion.officeXMLAddin.powerpoint.title"), detail: getLocalizedString("core.createProjectQuestion.officeXMLAddin.powerpoint.detail"), + data: "PowerPoint", }; } } @@ -497,8 +525,62 @@ export class CapabilityOptions { ]; } - static officeAll(): OptionItem[] { - return [...CapabilityOptions.officeAddinItems(), CapabilityOptions.officeAddinImport()]; + static officeAddinStaticCapabilities(host?: string): OptionItem[] { + const items: OptionItem[] = []; + for (const h of Object.keys(OfficeAddinProjectConfig)) { + if (host && h !== host) continue; + const hostValue = OfficeAddinProjectConfig[h]; + for (const capability of Object.keys(hostValue)) { + const capabilityValue = hostValue[capability]; + items.push({ + id: capability, + label: getLocalizedString(capabilityValue.title), + detail: getLocalizedString(capabilityValue.detail), + }); + } + } + return items; + } + + static officeAddinDynamicCapabilities(projectType: string, host?: string): OptionItem[] { + const items: OptionItem[] = []; + const isOutlookAddin = projectType === ProjectTypeOptions.outlookAddin().id; + const isOfficeAddin = projectType === ProjectTypeOptions.officeAddin().id; + const isOfficeXMLAddinForOutlook = + projectType === ProjectTypeOptions.officeXMLAddin().id && + host === OfficeAddinHostOptions.outlook().id; + + const pushToItems = (option: any) => { + const capabilityValue = OfficeAddinProjectConfig.json[option]; + items.push({ + id: option, + label: getLocalizedString(capabilityValue.title), + detail: getLocalizedString(capabilityValue.detail), + }); + }; + + if (isOutlookAddin || isOfficeAddin || isOfficeXMLAddinForOutlook) { + pushToItems("json-taskpane"); + if (isOutlookAddin || isOfficeXMLAddinForOutlook) { + items.push(CapabilityOptions.outlookAddinImport()); + } else if (isOfficeAddin) { + items.push(CapabilityOptions.officeContentAddin()); + items.push(CapabilityOptions.officeAddinImport()); + } + } else { + if (host) { + const hostValue = OfficeAddinProjectConfig[host]; + for (const capability of Object.keys(hostValue)) { + const capabilityValue = hostValue[capability]; + items.push({ + id: capability, + label: getLocalizedString(capabilityValue.title), + detail: getLocalizedString(capabilityValue.detail), + }); + } + } + } + return items; } static copilotPlugins(): OptionItem[] { @@ -512,7 +594,7 @@ export class CapabilityOptions { static customCopilots(): OptionItem[] { return [ CapabilityOptions.customCopilotBasic(), - // CapabilityOptions.customCopilotRag(), + CapabilityOptions.customCopilotRag(), CapabilityOptions.customCopilotAssistant(), ]; } @@ -538,24 +620,7 @@ export class CapabilityOptions { ...CapabilityOptions.customCopilots(), ...CapabilityOptions.tdpIntegrationCapabilities(), ]; - if (isOfficeXMLAddinEnabled()) { - capabilityOptions.push( - ...[ - ...CapabilityOptions.officeXMLAddinHostOptionItems( - OfficeAddinCapabilityOptions.word().id - ), - ...CapabilityOptions.officeXMLAddinHostOptionItems( - OfficeAddinCapabilityOptions.excel().id - ), - ...CapabilityOptions.officeXMLAddinHostOptionItems( - OfficeAddinCapabilityOptions.powerpoint().id - ), - ] - ); - } else { - capabilityOptions.push(...CapabilityOptions.outlookAddinItems()); - } - + capabilityOptions.push(...CapabilityOptions.officeAddinStaticCapabilities()); return capabilityOptions; } @@ -567,7 +632,6 @@ export class CapabilityOptions { ...CapabilityOptions.bots(inputs), ...CapabilityOptions.tabs(), ...CapabilityOptions.collectMECaps(), - ...CapabilityOptions.outlookAddinItems(), ]; if (isApiCopilotPluginEnabled()) { capabilityOptions.push(...CapabilityOptions.copilotPlugins()); @@ -577,12 +641,26 @@ export class CapabilityOptions { // test templates that are used by TDP integration only capabilityOptions.push(...CapabilityOptions.tdpIntegrationCapabilities()); } + capabilityOptions.push( + ...CapabilityOptions.officeAddinDynamicCapabilities(inputs?.projectType, inputs?.host) + ); + // if (isOfficeXMLAddinEnabled()) { + // capabilityOptions.push( + // ...[ + // ...CapabilityOptions.officeAddinStaticCapabilities("word"), + // ...CapabilityOptions.officeAddinStaticCapabilities("excel"), + // ...CapabilityOptions.officeAddinStaticCapabilities("powerpoint"), + // ] + // ); + // } else { + // capabilityOptions.push(...CapabilityOptions.officeAddinStaticCapabilities("json")); + // } return capabilityOptions; } static outlookAddinImport(): OptionItem { return { - id: "import", + id: "outlook-addin-import", label: getLocalizedString("core.importAddin.label"), detail: getLocalizedString("core.importAddin.detail"), }; @@ -590,7 +668,7 @@ export class CapabilityOptions { static officeAddinImport(): OptionItem { return { - id: "import", + id: "office-addin-import", label: getLocalizedString("core.importOfficeAddin.label"), detail: getLocalizedString("core.importAddin.detail"), description: getLocalizedString( @@ -599,32 +677,40 @@ export class CapabilityOptions { }; } - static officeXMLAddinHostOptionItems(host: string): OptionItem[] { - return getOfficeXMLAddinHostProjectOptions(host).map((x) => ({ - id: x.proj, - label: getLocalizedString(x.title), - detail: getLocalizedString(x.detail), - })); - } - - static outlookAddinItems(): OptionItem[] { - return officeAddinJsonData.getProjectTemplateNames().map((template) => ({ - id: template, - label: getLocalizedString(officeAddinJsonData.getProjectDisplayName(template)), - detail: getLocalizedString(officeAddinJsonData.getProjectDetails(template)), - description: getLocalizedString( - "core.createProjectQuestion.option.description.previewOnWindow" - ), - })); + static officeContentAddin(): OptionItem { + return { + id: "office-content-addin", + label: getLocalizedString("core.officeContentAddin.label"), + detail: getLocalizedString("core.officeContentAddin.detail"), + }; } - static officeAddinItems(): OptionItem[] { - return officeAddinJsonData.getProjectTemplateNames().map((template) => ({ - id: template, - label: getLocalizedString(officeAddinJsonData.getProjectDisplayName(template)), - detail: getLocalizedString(officeAddinJsonData.getProjectDetails(template)), - })); - } + // static officeXMLAddinHostOptionItems(host: string): OptionItem[] { + // return getOfficeXMLAddinHostProjectOptions(host).map((x) => ({ + // id: x.proj, + // label: getLocalizedString(x.title), + // detail: getLocalizedString(x.detail), + // })); + // } + + // static jsonAddinTaskpane(): OptionItem { + // return { + // id: "json-taskpane", + // label: getLocalizedString("core.newTaskpaneAddin.label"), + // detail: getLocalizedString("core.newTaskpaneAddin.detail"), + // description: getLocalizedString( + // "core.createProjectQuestion.option.description.previewOnWindow" + // ), + // }; + // } + + // static officeAddinItems(): OptionItem[] { + // return officeAddinJsonData.getProjectTemplateNames().map((template) => ({ + // id: template, + // label: getLocalizedString(officeAddinJsonData.getProjectDisplayName(template)), + // detail: getLocalizedString(officeAddinJsonData.getProjectDetails(template)), + // })); + // } static nonSsoTabAndBot(): OptionItem { return { @@ -736,26 +822,6 @@ export function capabilityQuestion(): SingleSelectQuestion { return { name: QuestionNames.Capabilities, title: (inputs: Inputs) => { - // Office Add-in Capability - if (isOfficeXMLAddinEnabled()) { - switch (inputs[QuestionNames.OfficeAddinCapability]) { - case ProjectTypeOptions.outlookAddin().id: - return getLocalizedString("core.createProjectQuestion.projectType.outlookAddin.title"); - case OfficeAddinCapabilityOptions.word().id: - return getLocalizedString( - "core.createProjectQuestion.officeXMLAddin.word.create.title" - ); - case OfficeAddinCapabilityOptions.excel().id: - return getLocalizedString( - "core.createProjectQuestion.officeXMLAddin.excel.create.title" - ); - case OfficeAddinCapabilityOptions.powerpoint().id: - return getLocalizedString( - "core.createProjectQuestion.officeXMLAddin.powerpoint.create.title" - ); - } - } - const projectType = inputs[QuestionNames.ProjectType]; switch (projectType) { case ProjectTypeOptions.bot().id: @@ -767,9 +833,28 @@ export function capabilityQuestion(): SingleSelectQuestion { "core.createProjectQuestion.projectType.messageExtension.title" ); case ProjectTypeOptions.outlookAddin().id: - return getLocalizedString("core.createProjectQuestion.projectType.outlookAddin.title"); case ProjectTypeOptions.officeAddin().id: + case ProjectTypeOptions.officeXMLAddin().id: { + switch (inputs[QuestionNames.OfficeAddinHost]) { + case OfficeAddinHostOptions.outlook().id: + return getLocalizedString( + "core.createProjectQuestion.projectType.outlookAddin.title" + ); + case OfficeAddinHostOptions.word().id: + return getLocalizedString( + "core.createProjectQuestion.officeXMLAddin.word.create.title" + ); + case OfficeAddinHostOptions.excel().id: + return getLocalizedString( + "core.createProjectQuestion.officeXMLAddin.excel.create.title" + ); + case OfficeAddinHostOptions.powerpoint().id: + return getLocalizedString( + "core.createProjectQuestion.officeXMLAddin.powerpoint.create.title" + ); + } return getLocalizedString("core.createProjectQuestion.projectType.officeAddin.title"); + } case ProjectTypeOptions.copilotPlugin().id: return getLocalizedString("core.createProjectQuestion.projectType.copilotPlugin.title"); case ProjectTypeOptions.customCopilot().id: @@ -804,27 +889,17 @@ export function capabilityQuestion(): SingleSelectQuestion { // nodejs capabilities const projectType = inputs[QuestionNames.ProjectType]; - const officeHost = inputs[QuestionNames.OfficeAddinCapability]; if (projectType === ProjectTypeOptions.bot().id) { return CapabilityOptions.bots(inputs); } else if (projectType === ProjectTypeOptions.tab().id) { return CapabilityOptions.tabs(); } else if (projectType === ProjectTypeOptions.me().id) { return CapabilityOptions.mes(); - } else if ( - (!isOfficeXMLAddinEnabled() && projectType === ProjectTypeOptions.outlookAddin().id) || - (isOfficeXMLAddinEnabled() && - projectType === ProjectTypeOptions.officeXMLAddin().id && - officeHost === ProjectTypeOptions.outlookAddin().id) - ) { - return [...CapabilityOptions.outlookAddinItems(), CapabilityOptions.outlookAddinImport()]; - } else if ( - isOfficeXMLAddinEnabled() && - projectType === ProjectTypeOptions.officeXMLAddin().id - ) { - return CapabilityOptions.officeXMLAddinHostOptionItems(officeHost); - } else if (projectType === ProjectTypeOptions.officeAddin().id) { - return CapabilityOptions.officeAll(); + } else if (ProjectTypeOptions.officeAddinAllIds().includes(projectType)) { + return CapabilityOptions.officeAddinDynamicCapabilities( + projectType, + inputs[QuestionNames.OfficeAddinHost] + ); } else if (projectType === ProjectTypeOptions.copilotPlugin().id) { return CapabilityOptions.copilotPlugins(); } else if (projectType === ProjectTypeOptions.customCopilot().id) { @@ -1275,57 +1350,24 @@ export function SPFxImportFolderQuestion(hasDefaultFunc = false): FolderQuestion : undefined, }; } -export const getTemplate = (inputs: Inputs): string => { - const capabilities: string[] = inputs[QuestionNames.Capabilities]; - const templates: string[] = officeAddinJsonData.getProjectTemplateNames(); - const foundTemplate = templates.find((template) => { - return capabilities && capabilities.includes(template); - }); - - return foundTemplate ?? ""; -}; + export function officeAddinHostingQuestion(): SingleSelectQuestion { - const OfficeHostQuestion: SingleSelectQuestion = { - type: "singleSelect", + return { name: QuestionNames.OfficeAddinHost, - title: "Add-in Host", - staticOptions: [], - dynamicOptions: getAddinHostOptions, - default: (inputs: Inputs) => { - const template = getTemplate(inputs); - const options = officeAddinJsonData.getHostTemplateNames(template); - return options[0] || "No Options"; - }, - skipSingleOption: true, + title: getLocalizedString("core.createProjectQuestion.officeXMLAddin.create.title"), + type: "singleSelect", + staticOptions: OfficeAddinHostOptions.all(), }; - return OfficeHostQuestion; -} - -export function getAddinHostOptions(inputs: Inputs): OptionItem[] { - // office addin supports host defined in officeAddinJsonData - const projectType = inputs[QuestionNames.ProjectType]; - const template = getTemplate(inputs); - const hostTypes = officeAddinJsonData.getHostTemplateNames(template); - const options: OptionItem[] = []; - hostTypes.forEach((host) => { - options.push({ label: officeAddinJsonData.getHostDisplayName(host) as string, id: host }); - }); - // Outlook addin only supports outlook - if (projectType === ProjectTypeOptions.outlookAddin().id) { - return [options[0] || { label: "No Options", id: "No Options" }]; - } else if (projectType === ProjectTypeOptions.officeAddin().id) { - return options; - } - return options || "No Options"; } -export function OfficeAddinFrameworkQuestion(): SingleSelectQuestion { +export function officeAddinFrameworkQuestion(): SingleSelectQuestion { return { type: "singleSelect", name: QuestionNames.OfficeAddinFramework, cliShortName: "f", cliDescription: "Framework for WXP extension.", title: getLocalizedString("core.createProjectQuestion.projectType.officeAddin.framework.title"), + dynamicOptions: getAddinFrameworkOptions, staticOptions: [ { id: "default", label: "Default" }, { id: "react", label: "React" }, @@ -1333,68 +1375,109 @@ export function OfficeAddinFrameworkQuestion(): SingleSelectQuestion { placeholder: getLocalizedString( "core.createProjectQuestion.projectType.officeAddin.framework.placeholder" ), - default: "default", + skipSingleOption: true, }; } -const officeAddinJsonData = new projectsJsonData(); +export function getAddinFrameworkOptions(inputs: Inputs): OptionItem[] { + const projectType = inputs[QuestionNames.ProjectType]; + const capabilities = inputs[QuestionNames.Capabilities]; + const host = inputs[QuestionNames.OfficeAddinHost]; + if ( + projectType === ProjectTypeOptions.outlookAddin().id || + (projectType === ProjectTypeOptions.officeXMLAddin().id && + host === OfficeAddinHostOptions.outlook().id) + ) { + return [{ id: "default", label: "Default" }]; + } else if ( + (projectType === ProjectTypeOptions.officeAddin().id && + capabilities === CapabilityOptions.officeContentAddin().id) || + capabilities === CapabilityOptions.officeAddinImport().id + ) { + return [{ id: "default", label: "Default" }]; + } else { + return [ + { id: "default", label: "Default" }, + { id: "react", label: "React" }, + ]; + } +} + +/** + * when project-type=office-addin-type(office-addin-framework-type=default or react), use selected value; + * when project-type=outlook-addin-type, no framework to select, office-addin-framework-type=default_old + * when project-type=office-xml-addin-type, no framework to select, office-addin-framework-type=default_old + */ +export function getOfficeAddinFramework(inputs: Inputs): string { + const projectType = inputs[QuestionNames.ProjectType]; + if ( + projectType === ProjectTypeOptions.officeAddin().id && + inputs[QuestionNames.OfficeAddinFramework] + ) { + return inputs[QuestionNames.OfficeAddinFramework]; + } else if ( + (projectType === ProjectTypeOptions.officeXMLAddin().id && + inputs[QuestionNames.OfficeAddinHost] === OfficeAddinHostOptions.outlook().id) || + projectType === ProjectTypeOptions.outlookAddin().id + ) { + return "default_old"; + } else { + return "default"; + } +} export function getLanguageOptions(inputs: Inputs): OptionItem[] { const runtime = getRuntime(inputs); // dotnet runtime only supports C# if (runtime === RuntimeOptions.DotNet().id) { - return [{ id: "csharp", label: "C#" }]; + return [{ id: ProgrammingLanguage.CSharp, label: "C#" }]; } + const capabilities = inputs[QuestionNames.Capabilities] as string; + const host = inputs[QuestionNames.OfficeAddinHost] as string; + // office addin supports language defined in officeAddinJsonData const projectType = inputs[QuestionNames.ProjectType]; - const officeHost = inputs[QuestionNames.OfficeAddinCapability]; - if ( - (!isOfficeXMLAddinEnabled() && projectType === ProjectTypeOptions.outlookAddin().id) || - (isOfficeXMLAddinEnabled() && - projectType === ProjectTypeOptions.officeXMLAddin().id && - officeHost === ProjectTypeOptions.outlookAddin().id) - ) { - const template = getTemplate(inputs); - const supportedTypes = officeAddinJsonData.getSupportedScriptTypes(template); - const options = supportedTypes.map((language) => ({ label: language, id: language })); - return options.length > 0 ? options : [{ label: "No Options", id: "No Options" }]; - } - if (isOfficeXMLAddinEnabled() && projectType === ProjectTypeOptions.officeXMLAddin().id) { - const officeProject = inputs[QuestionNames.Capabilities]; - return officeProject !== "manifest" - ? getOfficeXMLAddinHostProjectLangOptions(officeHost, officeProject) - : [{ id: "javascript", label: "JavaScript" }]; - } - if (projectType === ProjectTypeOptions.officeAddin().id) { - const template = getTemplate(inputs); - const supportedTypes = officeAddinJsonData.getSupportedScriptTypesNew(template); - const options: OptionItem[] = []; - supportedTypes.forEach((language) => { - if (language === "TypeScript") { - options.push({ label: "TypeScript", id: "typescript" }); - } else if (language === "JavaScript") { - options.push({ label: "JavaScript", id: "javascript" }); - } - }); - return options.length > 0 ? options : [{ label: "No Options", id: "No Options" }]; + if (ProjectTypeOptions.officeAddinAllIds().includes(projectType)) { + if (capabilities.endsWith("-manifest")) { + return [{ id: ProgrammingLanguage.JS, label: "JavaScript" }]; + } + if ( + projectType === ProjectTypeOptions.outlookAddin().id || + (projectType === ProjectTypeOptions.officeXMLAddin().id && + host === OfficeAddinHostOptions.outlook().id) + ) { + return [{ id: ProgrammingLanguage.TS, label: "TypeScript" }]; + } + const officeXMLAddinLangConfig = getOfficeAddinTemplateConfig(projectType, host)[capabilities] + .framework["default"]; + const officeXMLAddinLangOptions = []; + if (!!officeXMLAddinLangConfig.typescript) + officeXMLAddinLangOptions.push({ id: ProgrammingLanguage.TS, label: "TypeScript" }); + if (!!officeXMLAddinLangConfig.javascript) + officeXMLAddinLangOptions.push({ id: ProgrammingLanguage.JS, label: "JavaScript" }); + return officeXMLAddinLangOptions; } - const capabilities = inputs[QuestionNames.Capabilities] as string; if (capabilities === CapabilityOptions.SPFxTab().id) { // SPFx only supports typescript - return [{ id: "typescript", label: "TypeScript" }]; + return [{ id: ProgrammingLanguage.TS, label: "TypeScript" }]; } else if (capabilitiesHavePythonOption.includes(capabilities)) { // support python language return [ - { id: "javascript", label: "JavaScript" }, - { id: "typescript", label: "TypeScript" }, - { id: "python", label: "Python" }, + { id: ProgrammingLanguage.JS, label: "JavaScript" }, + { id: ProgrammingLanguage.TS, label: "TypeScript" }, + { + id: ProgrammingLanguage.PY, + label: "Python", + detail: "", + description: getLocalizedString("core.createProjectQuestion.option.description.preview"), + }, ]; } else { // other cases return [ - { id: "javascript", label: "JavaScript" }, - { id: "typescript", label: "TypeScript" }, + { id: ProgrammingLanguage.JS, label: "JavaScript" }, + { id: ProgrammingLanguage.TS, label: "TypeScript" }, ]; } } @@ -1428,14 +1511,17 @@ export function programmingLanguageQuestion(): SingleSelectQuestion { if (runtime === RuntimeOptions.DotNet().id) { return ""; } - // office addin - const projectType = inputs[QuestionNames.ProjectType]; - if (projectType === ProjectTypeOptions.outlookAddin().id) { - const template = getTemplate(inputs); - const options = officeAddinJsonData.getSupportedScriptTypesNew(template); - return options[0] || "No Options"; - } + const capabilities = inputs[QuestionNames.Capabilities] as string; + + // // office addin + // const projectType = inputs[QuestionNames.ProjectType]; + // if (projectType === ProjectTypeOptions.outlookAddin().id) { + // const template = getTemplate(inputs); + // const options = officeAddinJsonData.getSupportedScriptTypesNew(template); + // return options[0] || "No Options"; + // } + // SPFx if (capabilities === CapabilityOptions.SPFxTab().id) { return getLocalizedString("core.ProgrammingLanguageQuestion.placeholder.spfx"); @@ -1751,8 +1837,19 @@ export class ApiMessageExtensionAuthOptions { }; } + static microsoftEntra(): OptionItem { + return { + id: "microsoft-entra", + label: "Microsoft Entra", + }; + } + static all(): OptionItem[] { - return [ApiMessageExtensionAuthOptions.none(), ApiMessageExtensionAuthOptions.apiKey()]; + return [ + ApiMessageExtensionAuthOptions.none(), + ApiMessageExtensionAuthOptions.apiKey(), + ApiMessageExtensionAuthOptions.microsoftEntra(), + ]; } } @@ -1948,6 +2045,16 @@ export function apiMessageExtensionAuthQuestion(): SingleSelectQuestion { ), cliDescription: "The authentication type for the API.", staticOptions: ApiMessageExtensionAuthOptions.all(), + dynamicOptions: () => { + const options: OptionItem[] = [ApiMessageExtensionAuthOptions.none()]; + if (isApiKeyEnabled()) { + options.push(ApiMessageExtensionAuthOptions.apiKey()); + } + if (isApiMeSSOEnabled()) { + options.push(ApiMessageExtensionAuthOptions.microsoftEntra()); + } + return options; + }, default: ApiMessageExtensionAuthOptions.none().id, }; } @@ -1997,7 +2104,11 @@ export function apiOperationQuestion(includeExistingAPIs = true): MultiSelectQue staticOptions: [], validation: { validFunc: (input: string[], inputs?: Inputs): string | undefined => { - if (input.length < 1 || input.length > 10) { + if ( + input.length < 1 || + (input.length > 10 && + inputs?.[QuestionNames.CustomCopilotRag] != CustomCopilotRagOptions.customApi().id) + ) { return getLocalizedString( "core.createProjectQuestion.apiSpec.operation.invalidMessage", input.length, @@ -2099,8 +2210,8 @@ export class CustomCopilotRagOptions { return [ CustomCopilotRagOptions.customize(), CustomCopilotRagOptions.azureAISearch(), - CustomCopilotRagOptions.customApi(), - CustomCopilotRagOptions.microsoft365(), + // CustomCopilotRagOptions.customApi(), + // CustomCopilotRagOptions.microsoft365(), ]; } } @@ -2269,8 +2380,13 @@ export function capabilitySubTree(): IQTreeNode { ], }, { - // office addin import sub-tree - condition: { equals: CapabilityOptions.outlookAddinImport().id }, + // office addin import sub-tree (capabilities=office-addin-import | outlook-addin-import) + condition: { + enum: [ + CapabilityOptions.outlookAddinImport().id, + CapabilityOptions.officeAddinImport().id, + ], + }, data: { type: "group", name: QuestionNames.OfficeAddinImport }, children: [ { @@ -2289,16 +2405,6 @@ export function capabilitySubTree(): IQTreeNode { }, ], }, - { - // office addin other items sub-tree - condition: (inputs: Inputs) => - isOfficeXMLAddinEnabled() - ? false - : CapabilityOptions.outlookAddinItems() - .map((i) => i.id) - .includes(inputs[QuestionNames.Capabilities]), - data: officeAddinHostingQuestion(), - }, { // Search ME sub-tree condition: { equals: CapabilityOptions.m365SearchMe().id }, @@ -2338,14 +2444,12 @@ export function capabilitySubTree(): IQTreeNode { { condition: (inputs: Inputs) => { return ( - isApiKeyEnabled() && - (inputs[QuestionNames.MeArchitectureType] == MeArchitectureOptions.newApi().id || - inputs[QuestionNames.Capabilities] == CapabilityOptions.copilotPluginNewApi().id) + (isApiKeyEnabled() || isApiMeSSOEnabled()) && + inputs[QuestionNames.MeArchitectureType] == MeArchitectureOptions.newApi().id ); }, data: apiMessageExtensionAuthQuestion(), }, - /* { condition: (inputs: Inputs) => { return inputs[QuestionNames.Capabilities] == CapabilityOptions.customCopilotRag().id; @@ -2370,7 +2474,6 @@ export function capabilitySubTree(): IQTreeNode { }, ], }, - */ { condition: (inputs: Inputs) => { return ( @@ -2388,7 +2491,9 @@ export function capabilitySubTree(): IQTreeNode { inputs[QuestionNames.Capabilities] !== CapabilityOptions.copilotPluginApiSpec().id && inputs[QuestionNames.Capabilities] !== CapabilityOptions.copilotPluginOpenAIPlugin().id && - inputs[QuestionNames.MeArchitectureType] !== MeArchitectureOptions.apiSpec().id + inputs[QuestionNames.MeArchitectureType] !== MeArchitectureOptions.apiSpec().id && + inputs[QuestionNames.Capabilities] !== CapabilityOptions.officeAddinImport().id && + inputs[QuestionNames.Capabilities] !== CapabilityOptions.outlookAddinImport().id ); }, }, @@ -2421,11 +2526,14 @@ export function capabilitySubTree(): IQTreeNode { ], }, { - // WXP addin framework + // Office addin framework for json manifest + data: officeAddinFrameworkQuestion(), condition: (inputs: Inputs) => { - return inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id; + return ( + inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id && + inputs[QuestionNames.Capabilities] !== CapabilityOptions.officeAddinImport().id + ); }, - data: OfficeAddinFrameworkQuestion(), }, { // root folder @@ -2457,19 +2565,8 @@ export function createProjectQuestionNode(): IQTreeNode { }, { condition: (inputs: Inputs) => - isOfficeXMLAddinEnabled() && inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeXMLAddin().id, - data: { - name: QuestionNames.OfficeAddinCapability, - title: getLocalizedString("core.createProjectQuestion.officeXMLAddin.create.title"), - type: "singleSelect", - staticOptions: [ - ProjectTypeOptions.outlookAddin(), - OfficeAddinCapabilityOptions.word(), - OfficeAddinCapabilityOptions.excel(), - OfficeAddinCapabilityOptions.powerpoint(), - ], - }, + data: officeAddinHostingQuestion(), }, capabilitySubTree(), { diff --git a/packages/fx-core/src/question/inputs/CreateProjectInputs.ts b/packages/fx-core/src/question/inputs/CreateProjectInputs.ts index 36e1a20484..9d1537205e 100644 --- a/packages/fx-core/src/question/inputs/CreateProjectInputs.ts +++ b/packages/fx-core/src/question/inputs/CreateProjectInputs.ts @@ -14,9 +14,15 @@ export interface CreateProjectInputs extends Inputs { /** @description Teams Toolkit: select runtime for your app */ runtime?: "node" | "dotnet"; /** @description New Project */ - "project-type"?: "bot-type" | "tab-type" | "me-type" | "outlook-addin-type" | "office-addin-type"; - /** @description Select to create an Outlook, Word, Excel, or PowerPoint Add-in */ - "addin-office-capability"?: "outlook-addin-type" | "word" | "excel" | "powerpoint"; + "project-type"?: + | "bot-type" + | "tab-type" + | "me-type" + | "office-xml-addin-type" + | "office-addin-type" + | "outlook-addin-type"; + /** @description Select to Create an Outlook, Word, Excel, or PowerPoint Add-in */ + "addin-host"?: "outlook" | "word" | "excel" | "powerpoint"; /** @description Capabilities */ capabilities?: | "bot" @@ -34,11 +40,27 @@ export interface CreateProjectInputs extends Inputs { | "copilot-plugin-new-api" | "copilot-plugin-existing-api" | "custom-copilot-basic" + | "custom-copilot-rag" | "custom-copilot-agent" | "message-extension" | "BotAndMessageExtension" | "TabNonSsoAndBot" - | "taskpane"; + | "json-taskpane" + | "office-content-addin" + | "word-taskpane" + | "word-sso" + | "word-react" + | "word-manifest" + | "excel-taskpane" + | "excel-sso" + | "excel-react" + | "excel-custom-functions-shared" + | "excel-custom-functions-js" + | "excel-manifest" + | "powerpoint-taskpane" + | "powerpoint-sso" + | "powerpoint-react" + | "powerpoint-manifest"; /** @description Select triggers */ "bot-host-type-trigger"?: | "http-restify" @@ -56,8 +78,6 @@ export interface CreateProjectInputs extends Inputs { "spfx-webpart-name"?: string; /** @description SPFx solution folder */ "spfx-folder"?: string; - /** @description Add-in Host */ - "addin-host"?: string; /** @description Architecture of Search Based Message Extension */ "me-architecture"?: "new-api" | "api-spec" | "bot-plugin" | "bot"; /** @description OpenAPI Description Document */ @@ -65,7 +85,9 @@ export interface CreateProjectInputs extends Inputs { /** @description Select Operation(s) Teams Can Interact with */ "api-operation"?: string[]; /** @description Authentication Type */ - "api-me-auth"?: "none" | "api-key"; + "api-me-auth"?: "none" | "api-key" | "microsoft-entra"; + /** @description Chat With Your Data */ + "custom-copilot-rag"?: "custom-copilot-rag-customize" | "custom-copilot-rag-azureAISearch"; /** @description AI Agent */ "custom-copilot-agent"?: "custom-copilot-agent-new" | "custom-copilot-agent-assistants-api"; /** @description Programming Language */ diff --git a/packages/fx-core/src/question/options/CreateProjectOptions.ts b/packages/fx-core/src/question/options/CreateProjectOptions.ts index afe143d506..63a141b85a 100644 --- a/packages/fx-core/src/question/options/CreateProjectOptions.ts +++ b/packages/fx-core/src/question/options/CreateProjectOptions.ts @@ -20,10 +20,10 @@ export const CreateProjectOptions: CLICommandOption[] = [ choices: ["node", "dotnet"], }, { - name: "addin-office-capability", + name: "addin-host", type: "string", - description: "Select to create an Outlook, Word, Excel, or PowerPoint Add-in", - choices: ["outlook-addin-type", "word", "excel", "powerpoint"], + description: "Select to Create an Outlook, Word, Excel, or PowerPoint Add-in", + choices: ["outlook", "word", "excel", "powerpoint"], }, { name: "capability", @@ -48,11 +48,27 @@ export const CreateProjectOptions: CLICommandOption[] = [ "copilot-plugin-new-api", "copilot-plugin-existing-api", "custom-copilot-basic", + "custom-copilot-rag", "custom-copilot-agent", "message-extension", "BotAndMessageExtension", "TabNonSsoAndBot", - "taskpane", + "json-taskpane", + "office-content-addin", + "word-taskpane", + "word-sso", + "word-react", + "word-manifest", + "excel-taskpane", + "excel-sso", + "excel-react", + "excel-custom-functions-shared", + "excel-custom-functions-js", + "excel-manifest", + "powerpoint-taskpane", + "powerpoint-sso", + "powerpoint-react", + "powerpoint-manifest", ], choiceListCommand: "teamsapp list templates", }, @@ -104,12 +120,6 @@ export const CreateProjectOptions: CLICommandOption[] = [ type: "string", description: "Directory or Path that contains the existing SharePoint Framework solution.", }, - { - name: "addin-host", - type: "string", - description: "Add-in Host", - default: "No Options", - }, { name: "me-architecture", type: "string", @@ -135,7 +145,14 @@ export const CreateProjectOptions: CLICommandOption[] = [ type: "string", description: "The authentication type for the API.", default: "none", - choices: ["none", "api-key"], + choices: ["none", "api-key", "microsoft-entra"], + }, + { + name: "custom-copilot-rag", + type: "string", + description: "Chat With Your Data", + default: "custom-copilot-rag-customize", + choices: ["custom-copilot-rag-customize", "custom-copilot-rag-azureAISearch"], }, { name: "custom-copilot-agent", @@ -179,7 +196,6 @@ export const CreateProjectOptions: CLICommandOption[] = [ type: "string", shortName: "f", description: "Framework for WXP extension.", - default: "default", choices: ["default", "react"], }, { diff --git a/packages/fx-core/src/question/other.ts b/packages/fx-core/src/question/other.ts index 43fa9dc7c4..0cad877cae 100644 --- a/packages/fx-core/src/question/other.ts +++ b/packages/fx-core/src/question/other.ts @@ -35,6 +35,7 @@ import { apiSpecLocationQuestion, } from "./create"; import { QuestionNames } from "./questionNames"; +import { isAsyncAppValidationEnabled } from "../common/featureFlags"; export function listCollaboratorQuestionNode(): IQTreeNode { const selectTeamsAppNode = selectTeamsAppManifestQuestionNode(); @@ -138,6 +139,10 @@ export function validateTeamsAppQuestionNode(): IQTreeNode { condition: { equals: TeamsAppValidationOptions.package().id }, data: selectTeamsAppPackageQuestion(), }, + { + condition: { equals: TeamsAppValidationOptions.testCases().id }, + data: selectTeamsAppPackageQuestion(), + }, ], }; } @@ -360,10 +365,16 @@ function confirmManifestQuestion(isTeamsApp = true, isLocal = false): SingleSele } function selectTeamsAppValidationMethodQuestion(): SingleSelectQuestion { + const options = [TeamsAppValidationOptions.schema(), TeamsAppValidationOptions.package()]; + + if (isAsyncAppValidationEnabled()) { + options.push(TeamsAppValidationOptions.testCases()); + } + return { name: QuestionNames.ValidateMethod, title: getLocalizedString("core.selectValidateMethodQuestion.validate.selectTitle"), - staticOptions: [TeamsAppValidationOptions.schema(), TeamsAppValidationOptions.package()], + staticOptions: options, type: "singleSelect", }; } @@ -398,6 +409,15 @@ export class TeamsAppValidationOptions { ), }; } + static testCases(): OptionItem { + return { + id: "validateWithTestCases", + label: getLocalizedString("core.selectValidateMethodQuestion.validate.testCasesOption"), + description: getLocalizedString( + "core.selectValidateMethodQuestion.validate.testCasesOptionDescription" + ), + }; + } } function selectTeamsAppPackageQuestion(): SingleFileQuestion { diff --git a/packages/fx-core/src/question/questionNames.ts b/packages/fx-core/src/question/questionNames.ts index 0d5d975a2b..2dfbff1d05 100644 --- a/packages/fx-core/src/question/questionNames.ts +++ b/packages/fx-core/src/question/questionNames.ts @@ -23,7 +23,6 @@ export enum QuestionNames { OfficeAddinTemplate = "addin-template-select", OfficeAddinHost = "addin-host", OfficeAddinImport = "addin-import", - OfficeAddinCapability = "addin-office-capability", OfficeAddinFramework = "office-addin-framework-type", Samples = "samples", ReplaceContentUrl = "replaceContentUrl", diff --git a/packages/fx-core/tests/common/officeAddInProjectSetting.test.ts b/packages/fx-core/tests/common/officeAddInProjectSetting.test.ts index f732a01b43..bb54537ac1 100644 --- a/packages/fx-core/tests/common/officeAddInProjectSetting.test.ts +++ b/packages/fx-core/tests/common/officeAddInProjectSetting.test.ts @@ -3,6 +3,7 @@ import * as fs from "fs-extra"; import mockFs from "mock-fs"; import * as sinon from "sinon"; import * as projectSettingsHelper from "../../src/common/projectSettingsHelper"; +import { OfficeManifestType } from "../../src/common/projectSettingsHelper"; describe("validateIsOfficeAddInProject", () => { const sandbox = sinon.createSandbox(); @@ -19,7 +20,13 @@ describe("validateIsOfficeAddInProject", () => { }); it("should return true if manifest list is not empty", () => { - fetchManifestListStub.returns(["manifest.xml"]); + fetchManifestListStub.callsFake((workspace: string, type: OfficeManifestType) => { + if (type == OfficeManifestType.XmlAddIn) { + return ["manifest.xml"]; + } else { + return []; + } + }); mockFs({ "/test/manifest.xml": "", }); @@ -38,32 +45,111 @@ describe("validateIsOfficeAddInProject", () => { fetchManifestListStub.throws(new Error("Error fetching manifest list")); chai.expect(projectSettingsHelper.isValidOfficeAddInProject("")).to.be.false; }); + + it("should return false if both manifest.xml and manifest.json exist", () => { + fetchManifestListStub.callsFake((workspace: string, type: OfficeManifestType) => { + if (type == OfficeManifestType.XmlAddIn) { + return ["manifest.xml"]; + } else if (type == OfficeManifestType.MetaOsAddIn) { + return ["manifest.json"]; + } else { + return []; + } + }); + mockFs({ + "/test/manifest.xml": "", + "/test/manifest.json": "", + }); + chai.expect(projectSettingsHelper.isValidOfficeAddInProject("/test")).to.be.false; + }); }); describe("fetchManifestList", () => { - let readdirSyncStub: any, isOfficeAddInManifestStub: any; + let readdirSyncStub: any, isOfficeXmlAddInManifestStub: any, isOfficeMetaOsAddInManifestStub: any; beforeEach(() => { readdirSyncStub = sinon.stub(fs, "readdirSync"); - isOfficeAddInManifestStub = sinon.stub(projectSettingsHelper, "isOfficeAddInManifest"); + isOfficeXmlAddInManifestStub = sinon.stub(projectSettingsHelper, "isOfficeXmlAddInManifest"); + isOfficeMetaOsAddInManifestStub = sinon.stub( + projectSettingsHelper, + "isOfficeMetaOsAddInManifest" + ); }); afterEach(() => { readdirSyncStub.restore(); - isOfficeAddInManifestStub.restore(); + isOfficeXmlAddInManifestStub.restore(); + isOfficeMetaOsAddInManifestStub.restore(); + mockFs.restore(); }); it("should return undefined if workspacePath is not provided", () => { chai.expect(projectSettingsHelper.fetchManifestList()).to.be.undefined; }); - it("should return manifest list if workspacePath is provided", () => { + it("should return manifest.xml if type is OfficeManifestType.XmlAddIn", () => { mockFs({ "/test/manifest.xml": "", }); readdirSyncStub.returns(["manifest.xml"]); - isOfficeAddInManifestStub.callsFake((fileName: string) => fileName === "manifest.xml"); - chai.expect(projectSettingsHelper.fetchManifestList("/test")).to.deep.equal(["manifest.xml"]); - mockFs.restore(); + isOfficeXmlAddInManifestStub.callsFake((fileName: string) => fileName === "manifest.xml"); + chai + .expect( + projectSettingsHelper.fetchManifestList( + "/test", + projectSettingsHelper.OfficeManifestType.XmlAddIn + ) + ) + .to.deep.equal(["manifest.xml"]); + }); + + it("should return manifest.json if type is OfficeManifestType.MetaOsAddIn", () => { + mockFs({ + "/test/manifest.json": "", + }); + readdirSyncStub.returns(["manifest.json"]); + isOfficeMetaOsAddInManifestStub.callsFake((fileName: string) => fileName === "manifest.json"); + chai + .expect( + projectSettingsHelper.fetchManifestList( + "/test", + projectSettingsHelper.OfficeManifestType.MetaOsAddIn + ) + ) + .to.deep.equal(["manifest.json"]); + }); + + it("should return false if both manifest.xml and manifest.json exist but type is OfficeManifestType.XmlAddIn", () => { + mockFs({ + "/test/manifest.xml": "", + "/test/manifest.json": "", + }); + readdirSyncStub.returns(["manifest.xml", "manifest.json"]); + isOfficeXmlAddInManifestStub.callsFake((fileName: string) => fileName === "manifest.xml"); + chai + .expect( + projectSettingsHelper.fetchManifestList( + "/test", + projectSettingsHelper.OfficeManifestType.XmlAddIn + ) + ) + .to.deep.equal(["manifest.xml"]); + }); + + it("should return true if manifest.json exist and type is OfficeManifestType.MetaOsAddIn", () => { + mockFs({ + "/test/manifest.xml": "", + "/test/manifest.json": "", + }); + readdirSyncStub.returns(["manifest.xml", "manifest.json"]); + isOfficeMetaOsAddInManifestStub.callsFake((fileName: string) => fileName === "manifest.json"); + chai + .expect( + projectSettingsHelper.fetchManifestList( + "/test", + projectSettingsHelper.OfficeManifestType.MetaOsAddIn + ) + ) + .to.deep.equal(["manifest.json"]); }); }); diff --git a/packages/fx-core/tests/common/samples.test.ts b/packages/fx-core/tests/common/samples.test.ts index b975fa92ca..29799b0c2f 100644 --- a/packages/fx-core/tests/common/samples.test.ts +++ b/packages/fx-core/tests/common/samples.test.ts @@ -5,9 +5,8 @@ import * as sinon from "sinon"; import { err } from "@microsoft/teamsfx-api"; import { - OfficeSampleConfigTag, SampleConfigBranchForPrerelease, - TeamsSampleConfigTag, + SampleConfigTag, sampleProvider, } from "../../src/common/samples"; import sampleConfigV3 from "./samples-config-v3.json"; @@ -39,15 +38,6 @@ describe("Samples", () => { }, ], }; - // Set office sample config empty to bypass ut - const fakedOfficeSampleConfig = { - filterOptions: { - capabilities: [], - languages: [], - technologies: [], - }, - samples: [], - }; afterEach(() => { sandbox.restore(); @@ -70,11 +60,6 @@ describe("Samples", () => { "https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/dev/.config/samples-config-v3.json" ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - "https://raw.githubusercontent.com/OfficeDev/Office-Samples/dev/.config/samples-config-v1.json" - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -100,11 +85,6 @@ describe("Samples", () => { `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${SampleConfigBranchForPrerelease}/.config/samples-config-v3.json` ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - `https://raw.githubusercontent.com/OfficeDev/Office-Samples/${SampleConfigBranchForPrerelease}/.config/samples-config-v1.json` - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -125,14 +105,9 @@ describe("Samples", () => { sandbox.stub(axios, "get").callsFake(async (url: string, config) => { if ( url === - `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${TeamsSampleConfigTag}/.config/samples-config-v3.json` + `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${SampleConfigTag}/.config/samples-config-v3.json` ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - `https://raw.githubusercontent.com/OfficeDev/Office-Samples/${OfficeSampleConfigTag}/.config/samples-config-v1.json` - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -142,7 +117,7 @@ describe("Samples", () => { chai.expect(samples[0].downloadUrlInfo).deep.equal({ owner: "OfficeDev", repository: "TeamsFx-Samples", - ref: TeamsSampleConfigTag, + ref: SampleConfigTag, dir: "hello-world-tab-with-backend", }); chai.expect(samples[0].gifUrl).equal(undefined); @@ -153,14 +128,9 @@ describe("Samples", () => { sandbox.stub(axios, "get").callsFake(async (url: string, config) => { if ( url === - `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${TeamsSampleConfigTag}/.config/samples-config-v3.json` + `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${SampleConfigTag}/.config/samples-config-v3.json` ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - `https://raw.githubusercontent.com/OfficeDev/Office-Samples/${OfficeSampleConfigTag}/.config/samples-config-v1.json` - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -170,7 +140,7 @@ describe("Samples", () => { chai.expect(samples[0].downloadUrlInfo).deep.equal({ owner: "OfficeDev", repository: "TeamsFx-Samples", - ref: TeamsSampleConfigTag, + ref: SampleConfigTag, dir: "hello-world-tab-with-backend", }); chai.expect(samples[0].gifUrl).equal(undefined); @@ -186,11 +156,6 @@ describe("Samples", () => { `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/v2.0.0/.config/samples-config-v3.json` ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - `https://raw.githubusercontent.com/OfficeDev/Office-Samples/v0.0.1/.config/samples-config-v1.json` - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -212,14 +177,9 @@ describe("Samples", () => { sandbox.stub(axios, "get").callsFake(async (url: string, config) => { if ( url === - `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${TeamsSampleConfigTag}/.config/samples-config-v3.json` + `https://raw.githubusercontent.com/OfficeDev/TeamsFx-Samples/${SampleConfigTag}/.config/samples-config-v3.json` ) { return { data: fakedSampleConfig, status: 200 }; - } else if ( - url === - `https://raw.githubusercontent.com/OfficeDev/Office-Samples/${OfficeSampleConfigTag}/.config/samples-config-v1.json` - ) { - return { data: fakedOfficeSampleConfig, status: 200 }; } else { throw err(undefined); } @@ -230,7 +190,7 @@ describe("Samples", () => { chai.expect(samples[0].downloadUrlInfo).deep.equal({ owner: "OfficeDev", repository: "TeamsFx-Samples", - ref: TeamsSampleConfigTag, + ref: SampleConfigTag, dir: "hello-world-tab-with-backend", }); chai.expect(samples[0].gifUrl).equal(undefined); diff --git a/packages/fx-core/tests/component/coordinator/coordinator.create.test.ts b/packages/fx-core/tests/component/coordinator/coordinator.create.test.ts index e003ed6664..85435ce1c8 100644 --- a/packages/fx-core/tests/component/coordinator/coordinator.create.test.ts +++ b/packages/fx-core/tests/component/coordinator/coordinator.create.test.ts @@ -26,6 +26,7 @@ import { CapabilityOptions, CustomCopilotRagOptions, MeArchitectureOptions, + OfficeAddinHostOptions, ProjectTypeOptions, ScratchOptions, } from "../../../src/question/create"; @@ -884,15 +885,20 @@ describe("coordinator create", () => { describe("Office Addin", async () => { const sandbox = sinon.createSandbox(); const tools = new MockTools(); + let mockedEnvRestore: RestoreFn = () => {}; tools.ui = new MockedUserInteraction(); setTools(tools); beforeEach(() => { sandbox.stub(fs, "ensureDir").resolves(); + mockedEnvRestore = mockedEnv({ + [FeatureFlagName.OfficeXMLAddin]: "false", + }); }); afterEach(() => { sandbox.restore(); + mockedEnvRestore(); }); it("should scaffold taskpane successfully", async () => { @@ -996,6 +1002,7 @@ describe("Office XML Addin", async () => { platform: Platform.VSCode, folder: ".", [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, [QuestionNames.AppName]: randomAppName(), [QuestionNames.Scratch]: ScratchOptions.yes().id, }; @@ -1042,6 +1049,7 @@ describe("Office XML Addin", async () => { [QuestionNames.Scratch]: ScratchOptions.yes().id, [QuestionNames.AppName]: randomAppName(), [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, }; const res = await coordinator.create(context, inputs); assert.isTrue(res.isErr() && res.error.name === "mockedError"); @@ -1051,15 +1059,20 @@ describe("Office XML Addin", async () => { describe("Office Addin", async () => { const sandbox = sinon.createSandbox(); const tools = new MockTools(); + let mockedEnvRestore: RestoreFn = () => {}; tools.ui = new MockedUserInteraction(); setTools(tools); beforeEach(() => { sandbox.stub(fs, "ensureDir").resolves(); + mockedEnvRestore = mockedEnv({ + [FeatureFlagName.OfficeXMLAddin]: "false", + }); }); afterEach(() => { sandbox.restore(); + mockedEnvRestore(); }); it("should scaffold taskpane successfully", async () => { diff --git a/packages/fx-core/tests/component/driver/aad/aadAppClient.test.ts b/packages/fx-core/tests/component/driver/aad/aadAppClient.test.ts index fcd233aac5..0dc5567465 100644 --- a/packages/fx-core/tests/component/driver/aad/aadAppClient.test.ts +++ b/packages/fx-core/tests/component/driver/aad/aadAppClient.test.ts @@ -17,6 +17,8 @@ import { DeleteOrUpdatePermissionFailedError, HostNameNotOnVerifiedDomainError, } from "../../../../src/component/driver/aad/error/aadManifestError"; +import { CredentialInvalidLifetimeError } from "../../../../src/component/driver/aad/error/credentialInvalidLifetimeError"; +import { ClientSecretNotAllowedError } from "../../../../src/component/driver/aad/error/clientSecretNotAllowedError"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -146,11 +148,18 @@ describe("AadAppClient", async () => { it("should use input signInAudience", async () => { const mock = new MockAdapter(axiosInstance); - mock.onPost(`https://graph.microsoft.com/v1.0/applications`).reply(201, { - id: expectedObjectId, - displayName: expectedDisplayName, - signInAudience: "AzureADMultipleOrgs", + mock.onPost(`https://graph.microsoft.com/v1.0/applications`).reply((config) => { + const data = JSON.parse(config.data); + return [ + 201, + { + id: expectedObjectId, + displayName: expectedDisplayName, + signInAudience: data.signInAudience, + }, + ]; }); + const createAadAppResult = await aadAppClient.createAadApp( expectedDisplayName, SignInAudience.AzureADMultipleOrgs @@ -161,6 +170,32 @@ describe("AadAppClient", async () => { expect(createAadAppResult.signInAudience).to.equal("AzureADMultipleOrgs"); }); + it("should use input serviceManagementReference", async () => { + const mock = new MockAdapter(axiosInstance); + mock.onPost(`https://graph.microsoft.com/v1.0/applications`).reply((config) => { + const data = JSON.parse(config.data); + expect(data.serviceManagementReference).to.equal("00000000-0000-0000-0000-000000000000"); + return [ + 201, + { + id: expectedObjectId, + displayName: data.displayName, + signInAudience: data.signInAudience, + }, + ]; + }); + + const createAadAppResult = await aadAppClient.createAadApp( + expectedDisplayName, + SignInAudience.AzureADMultipleOrgs, + "00000000-0000-0000-0000-000000000000" + ); + + expect(createAadAppResult.displayName).to.equal(expectedDisplayName); + expect(createAadAppResult.id).to.equal(expectedObjectId); + expect(createAadAppResult.signInAudience).to.equal("AzureADMultipleOrgs"); + }); + it("should send debug log when sending request and receiving response", async () => { const mock = new MockAdapter(axiosInstance); mock.onPost(`https://graph.microsoft.com/v1.0/applications`).reply(201, { @@ -230,7 +265,7 @@ describe("AadAppClient", async () => { expect(result).to.equal(expectedSecretText); }); - it("should set secret lifetime to 180 days", async () => { + it("should set secret lifetime and description based on user input", async () => { const mock = new MockAdapter(axiosInstance); mock .onPost(`https://graph.microsoft.com/v1.0/applications/${expectedObjectId}/addPassword`) @@ -238,6 +273,7 @@ describe("AadAppClient", async () => { const data = JSON.parse(config.data); expect(data.passwordCredential.endDateTime).to.not.be.undefined; expect(data.passwordCredential.startDateTime).to.not.be.undefined; + expect(data.passwordCredential.displayName).to.equal("test description"); const endDateTime = new Date(data.passwordCredential.endDateTime); const startDateTime = new Date(data.passwordCredential.startDateTime); @@ -246,12 +282,12 @@ describe("AadAppClient", async () => { expect(startDateTime.getTime()).to.be.closeTo(now.getTime(), 1000); // Allow a 1 second difference expect(endDateTime.getTime() - startDateTime.getTime()).to.equal( - 180 * 24 * 60 * 60 * 1000 + 90 * 24 * 60 * 60 * 1000 ); return [200, { secretText: expectedSecretText }]; }); - await aadAppClient.generateClientSecret(expectedObjectId); + await aadAppClient.generateClientSecret(expectedObjectId, 90, "test description"); }); it("should throw error when request fail", async () => { @@ -278,6 +314,56 @@ describe("AadAppClient", async () => { }); }); + it("should throw error when CredentialInvalidLifetimeAsPerAppPolicy error happens", async () => { + const expectedError = { + error: { + code: "CredentialInvalidLifetimeAsPerAppPolicy", + }, + }; + + const mock = new MockAdapter(axiosInstance); + mock + .onPost(`https://graph.microsoft.com/v1.0/applications/${expectedObjectId}/addPassword`) + .reply(400, expectedError); + + await expect( + aadAppClient.generateClientSecret(expectedObjectId) + ).to.eventually.be.rejected.then((err) => { + expect(err instanceof CredentialInvalidLifetimeError).to.be.true; + expect(err.source).equals("AadAppClient"); + expect(err.name).equals("CredentialInvalidLifetime"); + expect(err.message).equals( + "The client secret lifetime is too long for your tenant. Use a shorter value with the clientSecretExpireDays parameter." + ); + expect(err.helpLink).equals("https://aka.ms/teamsfx-actions/aadapp-create"); + }); + }); + + it("should throw error when CredentialTypeNotAllowedAsPerAppPolicy error happens", async () => { + const expectedError = { + error: { + code: "CredentialTypeNotAllowedAsPerAppPolicy", + }, + }; + + const mock = new MockAdapter(axiosInstance); + mock + .onPost(`https://graph.microsoft.com/v1.0/applications/${expectedObjectId}/addPassword`) + .reply(400, expectedError); + + await expect( + aadAppClient.generateClientSecret(expectedObjectId) + ).to.eventually.be.rejected.then((err) => { + expect(err instanceof ClientSecretNotAllowedError).to.be.true; + expect(err.source).equals("AadAppClient"); + expect(err.name).equals("ClientSecretNotAllowed"); + expect(err.message).equals( + "Your tenant doesn't allow creating a client secret for Microsoft Entra app. Create and configure the app manually." + ); + expect(err.helpLink).equals("https://aka.ms/teamsfx-actions/aadapp-create"); + }); + }); + it("should send debug log when sending request and receiving response", async () => { const mock = new MockAdapter(axiosInstance); mock diff --git a/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts b/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts index edc4e5bbc3..5f87ea368c 100644 --- a/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts +++ b/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts @@ -48,6 +48,13 @@ describe("Microsoft Entra manifest helper Test", () => { chai.expect(warning).contain(AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim.trimEnd()); }); + it("validateManifest with no accessToken property", async () => { + const invalidAadManifest = JSON.parse(JSON.stringify(fakeAadManifest)); + delete invalidAadManifest.optionalClaims.accessToken; + const warning = AadManifestHelper.validateManifest(invalidAadManifest); + chai.expect(warning).contain(AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim.trimEnd()); + }); + it("processRequiredResourceAccessInManifest with id", async () => { const manifestWithId: any = { requiredResourceAccess: [ @@ -260,6 +267,44 @@ describe("Microsoft Entra manifest helper Test", () => { "Unknown resourceAccess id: Sites.Read.All, if you're using permission as resourceAccess id, please try to use permission id instead." ); }); + + it("processRequiredResourceAccessInManifest with non-array required resource access/resource access", async () => { + let manifest: any = { + requiredResourceAccess: { + resourceAppId: "Microsoft Graph", + resourceAccess: [ + { + id: "User.Read", + type: "Scope", + }, + ], + }, + }; + + chai + .expect(() => { + AadManifestHelper.processRequiredResourceAccessInManifest(manifest); + }) + .to.throw("requiredResourceAccess should be an array."); + + manifest = { + requiredResourceAccess: [ + { + resourceAppId: "Microsoft Graph", + resourceAccess: { + id: "Sites.Read.All", + type: "Role", + }, + }, + ], + }; + + chai + .expect(() => { + AadManifestHelper.processRequiredResourceAccessInManifest(manifest); + }) + .to.throw("resourceAccess should be an array."); + }); }); const invalidAadManifest: AADManifest = { diff --git a/packages/fx-core/tests/component/driver/aad/create.test.ts b/packages/fx-core/tests/component/driver/aad/create.test.ts index 82e8bdc779..68db635b0b 100644 --- a/packages/fx-core/tests/component/driver/aad/create.test.ts +++ b/packages/fx-core/tests/component/driver/aad/create.test.ts @@ -23,6 +23,7 @@ import { import { UserError } from "@microsoft/teamsfx-api"; import { OutputEnvironmentVariableUndefinedError } from "../../../../src/component/driver/error/outputEnvironmentVariableUndefinedError"; import { AadAppNameTooLongError } from "../../../../src/component/driver/aad/error/aadAppNameTooLongError"; +import { SignInAudience } from "../../../../src/component/driver/aad/interface/signInAudience"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -165,6 +166,70 @@ describe("aadAppCreate", async () => { ); }); + it("shouldd set default values for client secret expire time, description, and service management reference", async () => { + sinon + .stub(AadAppClient.prototype, "createAadApp") + .callsFake(async (displayName, signInAudience, serviceManagementReference) => { + expect(serviceManagementReference).to.be.undefined; + return { + id: expectedObjectId, + displayName: expectedDisplayName, + appId: expectedClientId, + } as AADApplication; + }); + + sinon + .stub(AadAppClient.prototype, "generateClientSecret") + .callsFake(async (objectId, clientSecretExpireDays, clientSecretDescription) => { + expect(clientSecretExpireDays).to.equal(180); + expect(clientSecretDescription).to.equal("default"); + return expectedSecretText; + }); + + const args: any = { + name: "test", + generateClientSecret: true, + }; + + const result = await createAadAppDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isOk()).to.be.true; + }); + + it("should use user defined client secret expire time, description, and service management reference", async () => { + const expectedServiceManagementReference = "00000000-0000-0000-0000-000000000000"; + const expectedExpireTime = 90; + const expectedDescription = "custom"; + sinon + .stub(AadAppClient.prototype, "createAadApp") + .callsFake(async (displayName, signInAudience, serviceManagementReference) => { + expect(serviceManagementReference).to.equal(expectedServiceManagementReference); + return { + id: expectedObjectId, + displayName: expectedDisplayName, + appId: expectedClientId, + } as AADApplication; + }); + + sinon + .stub(AadAppClient.prototype, "generateClientSecret") + .callsFake(async (objectId, clientSecretExpireDays, clientSecretDescription) => { + expect(clientSecretExpireDays).to.equal(expectedExpireTime); + expect(clientSecretDescription).to.equal(expectedDescription); + return expectedSecretText; + }); + + const args: any = { + name: "test", + generateClientSecret: true, + clientSecretExpireDays: expectedExpireTime, + clientSecretDescription: expectedDescription, + serviceManagementReference: expectedServiceManagementReference, + }; + + const result = await createAadAppDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isOk()).to.be.true; + }); + it("should output to specific environment variable based on writeToEnvironmentFile declaration", async () => { sinon.stub(AadAppClient.prototype, "createAadApp").resolves({ id: expectedObjectId, @@ -421,6 +486,56 @@ describe("aadAppCreate", async () => { expect(endTelemetry.eventName).to.equal("aadApp/create"); expect(endTelemetry.properties.component).to.equal("aadAppcreate"); expect(endTelemetry.properties.success).to.equal("yes"); + expect(endTelemetry.properties["new-aad-app"]).to.equal("true"); + }); + + it("should set new-aad-app telemetry to false when reuse existing AAD app", async () => { + const mockedTelemetryReporter = new MockedTelemetryReporter(); + let startTelemetry: any, endTelemetry: any; + + sinon + .stub(mockedTelemetryReporter, "sendTelemetryEvent") + .onFirstCall() + .callsFake((eventName, properties, measurements) => { + startTelemetry = { + eventName, + properties, + measurements, + }; + }) + .onSecondCall() + .callsFake((eventName, properties, measurements) => { + endTelemetry = { + eventName, + properties, + measurements, + }; + }); + + envRestore = mockedEnv({ + [outputKeys.clientId]: "existing value", + [outputKeys.objectId]: "existing value", + [outputKeys.clientSecret]: "existing value", + }); + + const args: any = { + name: "test", + generateClientSecret: true, + }; + const driverContext: any = { + m365TokenProvider: new MockedM365Provider(), + telemetryReporter: mockedTelemetryReporter, + }; + + const result = await createAadAppDriver.execute(args, driverContext, outputEnvVarNames); + + expect(result.result.isOk()).to.be.true; + expect(startTelemetry.eventName).to.equal("aadApp/create-start"); + expect(startTelemetry.properties.component).to.equal("aadAppcreate"); + expect(endTelemetry.eventName).to.equal("aadApp/create"); + expect(endTelemetry.properties.component).to.equal("aadAppcreate"); + expect(endTelemetry.properties.success).to.equal("yes"); + expect(endTelemetry.properties["new-aad-app"]).to.equal("false"); }); it("should send telemetries when fail", async () => { @@ -482,9 +597,9 @@ describe("aadAppCreate", async () => { expect(endTelemetry.properties.success).to.equal("no"); expect(endTelemetry.properties["error-code"]).to.equal("aadAppCreate.HttpClientError"); expect(endTelemetry.properties["error-type"]).to.equal("user"); - expect(endTelemetry.properties["error-message"]).to.equal( - 'A http client error happened while performing the aadApp/create task. The error response is: {"error":{"code":"Request_BadRequest","message":"Invalid value specified for property \'displayName\' of resource \'Application\'."}}' - ); + // expect(endTelemetry.properties["error-message"]).to.equal( + // 'A http client error happened while performing the aadApp/create task. The error response is: {"error":{"code":"Request_BadRequest","message":"Invalid value specified for property \'displayName\' of resource \'Application\'."}}' + // ); }); it("should send telemetries with error stack", async () => { diff --git a/packages/fx-core/tests/component/driver/aad/update.test.ts b/packages/fx-core/tests/component/driver/aad/update.test.ts index b1f9ffcfe4..eb5c0a8a2c 100644 --- a/packages/fx-core/tests/component/driver/aad/update.test.ts +++ b/packages/fx-core/tests/component/driver/aad/update.test.ts @@ -619,9 +619,9 @@ describe("aadAppUpdate", async () => { expect(endTelemetry.properties.success).to.equal("no"); expect(endTelemetry.properties["error-code"]).to.equal("aadAppUpdate.HttpServerError"); expect(endTelemetry.properties["error-type"]).to.equal("system"); - expect(endTelemetry.properties["error-message"]).to.equal( - 'A http server error happened while performing the aadApp/update task. Please try again later. The error response is: {"error":{"code":"InternalServerError","message":"Internal server error"}}' - ); + // expect(endTelemetry.properties["error-message"]).to.equal( + // 'A http server error happened while performing the aadApp/update task. Please try again later. The error response is: {"error":{"code":"InternalServerError","message":"Internal server error"}}' + // ); }); it("should throw error when missing required environment variable in manifest", async () => { diff --git a/packages/fx-core/tests/component/driver/apiKey/create.test.ts b/packages/fx-core/tests/component/driver/apiKey/create.test.ts index cc13737379..148f4a762b 100644 --- a/packages/fx-core/tests/component/driver/apiKey/create.test.ts +++ b/packages/fx-core/tests/component/driver/apiKey/create.test.ts @@ -14,7 +14,10 @@ import { } from "../../../plugins/solution/util"; import { CreateApiKeyDriver } from "../../../../src/component/driver/apiKey/create"; import { AppStudioClient } from "../../../../src/component/driver/teamsApp/clients/appStudioClient"; -import { ApiSecretRegistrationAppType } from "../../../../src/component/driver/teamsApp/interfaces/ApiSecretRegistration"; +import { + ApiSecretRegistrationAppType, + ApiSecretRegistrationTargetAudience, +} from "../../../../src/component/driver/teamsApp/interfaces/ApiSecretRegistration"; import { SystemError, err } from "@microsoft/teamsfx-api"; import { setTools } from "../../../../src/core/globalVars"; import { SpecParser } from "@microsoft/m365-spec-parser"; @@ -63,18 +66,26 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", @@ -97,18 +108,27 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", @@ -132,18 +152,27 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); envRestore = mockedEnv({ ["api-key"]: "existingvalue", @@ -186,6 +215,56 @@ describe("CreateApiKeyDriver", () => { } }); + it("happy path: create registrationid, read applicableToApps and targetAudience from input", async () => { + sinon.stub(AppStudioClient, "createApiKeyRegistration").callsFake(async (token, apiKey) => { + expect(apiKey.targetAudience).equals(ApiSecretRegistrationTargetAudience.HomeTenant); + expect(apiKey.specificAppId).equals("mockedAppId"); + expect(apiKey.applicableToApps).equals(ApiSecretRegistrationAppType.SpecificApp); + return { + id: "mockedRegistrationId", + clientSecrets: [], + targetUrlsShouldStartWith: [], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }; + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedClientSecret", + apiSpecPath: "mockedPath", + applicableToApps: "SpecificApp", + targetAudience: "HomeTenant", + }; + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isOk()).to.be.true; + if (result.result.isOk()) { + expect(result.result.value.get(outputKeys.registrationId)).to.equal("mockedRegistrationId"); + expect(result.summaries.length).to.equal(1); + } + }); + it("should throw error when empty outputEnvVarNames", async () => { const args: any = { name: "test", @@ -327,28 +406,42 @@ describe("CreateApiKeyDriver", () => { primaryClientSecret: "mockedSecret", apiSpecPath: "mockedPath", }; - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - api: "api", - server: "https://test2", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + { + api: "api", + server: "https://test2", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 2, + validAPICount: 2, + }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); expect(result.result.isErr()).to.be.true; if (result.result.isErr()) { @@ -356,14 +449,106 @@ describe("CreateApiKeyDriver", () => { } }); - it("should throw error if domain = 0", async () => { + it("should throw error if list api is empty and domain = 0", async () => { const args: any = { name: "test", appId: "mockedAppId", primaryClientSecret: "mockedSecret", apiSpecPath: "mockedPath", }; - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ APIs: [], validAPICount: 0, allAPICount: 1 }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("ApiKeyFailedToGetDomain"); + } + }); + + it("should throw error if list api contains no auth and domain = 0", async () => { + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedSecret", + apiSpecPath: "mockedPath", + }; + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + isValid: true, + reason: [], + }, + ], + validAPICount: 1, + allAPICount: 1, + }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("ApiKeyFailedToGetDomain"); + } + }); + + it("should throw error if list api contains unsupported auth and domain = 0", async () => { + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedSecret", + apiSpecPath: "mockedPath", + }; + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api1", + server: "https://test", + operationId: "get1", + auth: { + name: "test1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api2", + server: "https://test", + operationId: "get2", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "basic", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api3", + server: "https://test", + operationId: "get3", + auth: { + name: "test1", + authScheme: { + type: "apiKey", + in: "header", + name: "test1", + }, + }, + isValid: true, + reason: [], + }, + ], + validAPICount: 3, + allAPICount: 3, + }); const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); expect(result.result.isErr()).to.be.true; if (result.result.isErr()) { @@ -375,18 +560,27 @@ describe("CreateApiKeyDriver", () => { sinon .stub(AppStudioClient, "createApiKeyRegistration") .throws(new SystemError("source", "name", "message")); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", @@ -415,4 +609,50 @@ describe("CreateApiKeyDriver", () => { expect(result.result.error.source).to.equal("apiKeyRegister"); } }); + + it("should throw error if invalid applicableToApps and targetAudience", async () => { + sinon.stub(AppStudioClient, "createApiKeyRegistration").resolves({ + id: "mockedRegistrationId", + clientSecrets: [], + targetUrlsShouldStartWith: [], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedClientSecret", + apiSpecPath: "mockedPath", + applicableToApps: "specificapp", + targetAudience: "hometenant", + }; + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + expect(result.result.error.message.includes("applicableToApps")).to.be.true; + expect(result.result.error.message.includes("targetAudience")).to.be.true; + } + }); }); diff --git a/packages/fx-core/tests/component/driver/apiKey/update.test.ts b/packages/fx-core/tests/component/driver/apiKey/update.test.ts new file mode 100644 index 0000000000..aefa1afdda --- /dev/null +++ b/packages/fx-core/tests/component/driver/apiKey/update.test.ts @@ -0,0 +1,449 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "mocha"; +import * as sinon from "sinon"; +import * as chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { RestoreFn } from "mocked-env"; +import { + MockedAzureAccountProvider, + MockedLogProvider, + MockedM365Provider, + MockedUserInteraction, +} from "../../../plugins/solution/util"; +import { UpdateApiKeyDriver } from "../../../../src/component/driver/apiKey/update"; +import { setTools } from "../../../../src/core/globalVars"; +import { AppStudioClient } from "../../../../src/component/driver/teamsApp/clients/appStudioClient"; +import { + ApiSecretRegistrationAppType, + ApiSecretRegistrationTargetAudience, +} from "../../../../src/component/driver/teamsApp/interfaces/ApiSecretRegistration"; +import { SpecParser } from "@microsoft/m365-spec-parser"; +import { UpdateApiKeyArgs } from "../../../../src/component/driver/apiKey/interface/updateApiKeyArgs"; +import { ConfirmConfig, UserError, err, ok } from "@microsoft/teamsfx-api"; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe("UpdateApiKeyDriver", () => { + const mockedDriverContext: any = { + m365TokenProvider: new MockedM365Provider(), + ui: new MockedUserInteraction(), + }; + const updateApiKeyDriver = new UpdateApiKeyDriver(); + + let envRestore: RestoreFn | undefined; + + beforeEach(() => { + setTools({ + ui: new MockedUserInteraction(), + logProvider: new MockedLogProvider(), + tokenProvider: { + azureAccountProvider: new MockedAzureAccountProvider(), + m365TokenProvider: new MockedM365Provider(), + }, + }); + }); + + afterEach(() => { + sinon.restore(); + if (envRestore) { + envRestore(); + envRestore = undefined; + } + }); + + it("happy path: update all fields", async () => { + sinon.stub(AppStudioClient, "updateApiKeyRegistration").resolves({ + description: "mockedDescription", + targetUrlsShouldStartWith: ["https://test2"], + applicableToApps: ApiSecretRegistrationAppType.SpecificApp, + targetAudience: ApiSecretRegistrationTargetAudience.HomeTenant, + specificAppId: "mockedAppId", + }); + sinon.stub(AppStudioClient, "getApiKeyRegistrationById").resolves({ + id: "mockedRegistrationId", + description: "mockedDescription", + clientSecrets: [], + targetUrlsShouldStartWith: ["https://test"], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api2", + server: "https://test", + operationId: "get", + auth: { + name: "test2", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + + sinon.stub(mockedDriverContext.ui, "confirm").callsFake(async (config) => { + expect((config as ConfirmConfig).title.includes("description")).to.be.true; + expect((config as ConfirmConfig).title.includes("applicableToApps")).to.be.true; + expect((config as ConfirmConfig).title.includes("specificAppId")).to.be.true; + expect((config as ConfirmConfig).title.includes("targetAudience")).to.be.true; + return ok({ type: "success", value: true }); + }); + + const args: UpdateApiKeyArgs = { + name: "test2", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + targetAudience: "HomeTenant", + applicableToApps: "SpecificApp", + registrationId: "mockedRegistrationId", + }; + + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isOk()).to.be.true; + if (result.result.isOk()) { + expect(result.result.value.size).to.equal(0); + expect(result.summaries.length).to.equal(1); + } + }); + + it("happy path: does not update when no changes", async () => { + sinon.stub(AppStudioClient, "getApiKeyRegistrationById").resolves({ + id: "test", + description: "test", + clientSecrets: [], + targetUrlsShouldStartWith: ["https://test"], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api2", + server: "https://test", + operationId: "get", + auth: { + name: "test2", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + const args: UpdateApiKeyArgs = { + name: "test", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + targetAudience: "AnyTenant", + applicableToApps: "AnyApp", + registrationId: "mockedRegistrationId", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isOk()).to.be.true; + if (result.result.isOk()) { + expect(result.result.value.size).to.equal(0); + expect(result.summaries.length).to.equal(1); + } + }); + + it("happy path: should not show confirm when only devtunnel url is different", async () => { + sinon.stub(AppStudioClient, "updateApiKeyRegistration").resolves({ + description: "test", + targetUrlsShouldStartWith: ["https://test2.asse.devtunnels.ms"], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(AppStudioClient, "getApiKeyRegistrationById").resolves({ + id: "test", + description: "test", + clientSecrets: [], + targetUrlsShouldStartWith: ["https://test.asse.devtunnels.ms"], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test2.asse.devtunnels.ms", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + + const confirmStub = sinon + .stub(mockedDriverContext.ui, "confirm") + .resolves(ok({ type: "success", value: true })); + + const args: UpdateApiKeyArgs = { + name: "test", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + targetAudience: "AnyTenant", + applicableToApps: "AnyApp", + registrationId: "mockedRegistrationId", + }; + + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isOk()).to.be.true; + if (result.result.isOk()) { + expect(result.result.value.size).to.equal(0); + expect(result.summaries.length).to.equal(1); + } + expect(confirmStub.notCalled).to.be.true; + }); + + it("should throw error when user canel", async () => { + sinon.stub(AppStudioClient, "getApiKeyRegistrationById").resolves({ + id: "mockedRegistrationId", + description: "mockedDescription", + clientSecrets: [], + targetUrlsShouldStartWith: ["https://test"], + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, + }); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api2", + server: "https://test", + operationId: "get", + auth: { + name: "test2", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + + sinon + .stub(mockedDriverContext.ui, "confirm") + .returns(err(new UserError("source", "userCancelled", "Cancel by user"))); + + const args: UpdateApiKeyArgs = { + name: "test2", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + targetAudience: "HomeTenant", + applicableToApps: "SpecificApp", + registrationId: "mockedRegistrationId", + }; + + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("userCancelled"); + } + }); + + it("should throw error if missing name", async () => { + const args: any = { + name: "", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + registrationId: "mockedRegistrationId", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + } + }); + + it("should throw error if name is too long", async () => { + const args: any = { + name: "a".repeat(129), + appId: "mockedAppId", + apiSpecPath: "mockedPath", + registrationId: "mockedRegistrationId", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("ApiKeyNameTooLong"); + } + }); + + it("should throw error if missing registrationId", async () => { + const args: any = { + name: "name", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + } + }); + + it("should throw error if missing apiSpecPath", async () => { + const args: any = { + name: "name", + appId: "mockedAppId", + regirstrationid: "mockedRegistrationId", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + } + }); + + it("should throw error if invalid applicableToApps", async () => { + const args: any = { + name: "name", + appId: "mockedAppId", + regirstrationid: "mockedRegistrationId", + apiSpecPath: "mockedPath", + applicableToApps: "test", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + } + }); + + it("should throw error if invalid targetAudience", async () => { + const args: any = { + name: "name", + appId: "mockedAppId", + regirstrationid: "mockedRegistrationId", + apiSpecPath: "mockedPath", + targetAudience: "test", + }; + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("InvalidActionInputError"); + } + }); + + it("should throw error when unhandled error", async () => { + sinon.stub(MockedM365Provider.prototype, "getAccessToken").throws(new Error("unhandled error")); + sinon.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + { + api: "api2", + server: "https://test", + operationId: "get", + auth: { + name: "test2", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + const args: UpdateApiKeyArgs = { + name: "test2", + appId: "mockedAppId", + apiSpecPath: "mockedPath", + targetAudience: "HomeTenant", + applicableToApps: "SpecificApp", + registrationId: "mockedRegistrationId", + }; + + const result = await updateApiKeyDriver.execute(args, mockedDriverContext); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.source).to.equal("apiKeyUpdate"); + } + }); +}); diff --git a/packages/fx-core/tests/component/driver/teamsApp/appstudioclient.test.ts b/packages/fx-core/tests/component/driver/teamsApp/appstudioclient.test.ts index f74f3f7f82..c7bfe011b3 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/appstudioclient.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/appstudioclient.test.ts @@ -21,6 +21,7 @@ import { DeveloperPortalAPIFailedError } from "../../../../src/error/teamsApp"; import { ApiSecretRegistration, ApiSecretRegistrationAppType, + ApiSecretRegistrationUpdate, } from "../../../../src/component/driver/teamsApp/interfaces/ApiSecretRegistration"; import { AsyncAppValidationStatus } from "../../../../src/component/driver/teamsApp/interfaces/AsyncAppValidationResponse"; @@ -908,6 +909,51 @@ describe("App Studio API Test", () => { }); }); + describe("updateApiKeyRegistration", () => { + const appApiRegistration: ApiSecretRegistrationUpdate = { + description: "fake description", + applicableToApps: ApiSecretRegistrationAppType.AnyApp, + targetUrlsShouldStartWith: ["https://www.example.com"], + }; + it("404 not found", async () => { + const fakeAxiosInstance = axios.create(); + sinon.stub(axios, "create").returns(fakeAxiosInstance); + + const error = { + name: "404", + message: "fake message", + }; + sinon.stub(fakeAxiosInstance, "patch").throws(error); + + try { + await AppStudioClient.updateApiKeyRegistration( + appStudioToken, + appApiRegistration, + "fakeId" + ); + } catch (error) { + chai.assert.equal(error.name, DeveloperPortalAPIFailedError.name); + } + }); + + it("Happy path", async () => { + const fakeAxiosInstance = axios.create(); + sinon.stub(axios, "create").returns(fakeAxiosInstance); + + const response = { + data: appApiRegistration, + }; + sinon.stub(fakeAxiosInstance, "patch").resolves(response); + + const res = await AppStudioClient.updateApiKeyRegistration( + appStudioToken, + appApiRegistration, + "fakeId" + ); + chai.assert.equal(res, appApiRegistration); + }); + }); + describe("list Teams app", () => { it("Happy path", async () => { const fakeAxiosInstance = axios.create(); @@ -1053,7 +1099,7 @@ describe("App Studio API Test", () => { }; sinon.stub(fakeAxiosInstance, "get").resolves(response); const res = await AppStudioClient.getAppValidationRequestList("fakeId", appStudioToken); - chai.assert.equal(res.appValidations.length, 0); + chai.assert.equal(res.appValidations!.length, 0); }); it("404 not found", async () => { diff --git a/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts b/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts index 8ea5044ef3..867b14dd0e 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts @@ -16,7 +16,7 @@ import { import { FileNotFoundError, JSONSyntaxError } from "../../../../src/error/common"; import { FeatureFlagName } from "../../../../src/common/constants"; import { manifestUtils } from "../../../../src/component/driver/teamsApp/utils/ManifestUtils"; -import { ok, Platform, TeamsAppManifest } from "@microsoft/teamsfx-api"; +import { ok, Platform, PluginManifestSchema, TeamsAppManifest } from "@microsoft/teamsfx-api"; import AdmZip from "adm-zip"; import { InvalidFileOutsideOfTheDirectotryError } from "../../../../src/error/teamsApp"; @@ -37,6 +37,7 @@ describe("teamsApp/createAppPackage", async () => { [FeatureFlagName.CopilotPlugin]: "true", ["CONFIG_TEAMS_APP_NAME"]: "fakeName", [openapiServerPlaceholder]: fakeUrl, + ["APP_NAME_SUFFIX"]: "test", }); }); @@ -229,6 +230,94 @@ describe("teamsApp/createAppPackage", async () => { } }); + it("should return error when placeholder is not resolved in ai-plugin.json - case 1", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputJsonPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/manifest.dev.json", + }; + sinon.stub(fs, "pathExists").callsFake((filePath) => { + return true; + }); + + const manifest = new TeamsAppManifest(); + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + manifest.plugins = [ + { + pluginFile: "resources/ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + sinon.stub(fs, "chmod").callsFake(async () => {}); + sinon.stub(fs, "writeFile").callsFake(async () => {}); + + delete process.env["APP_NAME_SUFFIX"]; + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + + chai.assert( + result.isErr() && + result.error.name === "MissingEnvironmentVariablesError" && + result.error.message.includes("APP_NAME_SUFFIX") + ); + }); + + it("should return error when placeholder is not resolved in ai-plugin.json- case 2", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputJsonPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/manifest.dev.json", + }; + sinon.stub(fs, "pathExists").callsFake((filePath) => { + return true; + }); + + const pluginJson: PluginManifestSchema = { + name_for_human: "test", + schema_version: "v2", + description_for_human: "test", + runtimes: [ + { + type: "OpenApi", + auth: { type: "none" }, + spec: { url: "test\\openai.yml" }, + }, + ], + }; + sinon.stub(fs, "readJSON").resolves(pluginJson); + + const manifest = new TeamsAppManifest(); + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + manifest.plugins = [ + { + pluginFile: "resources/ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + sinon.stub(fs, "chmod").callsFake(async () => {}); + sinon.stub(fs, "writeFile").callsFake(async () => {}); + + delete process.env["APP_NAME_SUFFIX"]; + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + + chai.assert( + result.isErr() && + result.error.name === "MissingEnvironmentVariablesError" && + result.error.message.includes("APP_NAME_SUFFIX") + ); + }); + it("should throw error if api spec not exists for API plugin", async () => { const args: CreateAppPackageArgs = { manifestPath: @@ -357,7 +446,20 @@ describe("teamsApp/createAppPackage", async () => { chai.assert(result.isOk()); if (await fs.pathExists(args.outputZipPath)) { const zip = new AdmZip(args.outputZipPath); - const openapiContent = zip.getEntry("resources/openai.yml")?.getData().toString("utf8"); + + let openapiContent = ""; + + const entries = zip.getEntries(); + for (const e of entries) { + const name = e.entryName; + + if (name.endsWith("openai.yml")) { + const data = e.getData(); + openapiContent = data.toString("utf8"); + break; + } + } + chai.assert( openapiContent != undefined && openapiContent.length > 0 && @@ -612,11 +714,29 @@ describe("teamsApp/createAppPackage", async () => { chai.assert.isTrue(outputExist); if (outputExist) { const zip = new AdmZip(args.outputZipPath); + let aiPluginContent = ""; + let openapiContent = ""; + + const entries = zip.getEntries(); + entries.forEach((e) => { + const name = e.entryName; + if (name.endsWith("ai-plugin.json")) { + const data = e.getData(); + aiPluginContent = data.toString("utf8"); + } - const aiPluginContent = zip.getEntry("resources/ai-plugin.json")?.getData(); - const openapiContent = zip.getEntry("resources/openai.yml")?.getData(); + if (name.endsWith("openai.yml")) { + const data = e.getData(); + openapiContent = data.toString("utf8"); + } + }); - chai.assert(openapiContent != undefined && aiPluginContent != undefined); + chai.assert( + openapiContent && + aiPluginContent && + openapiContent.search("APP_NAME_SUFFIX") < 0 && + aiPluginContent.search(openapiServerPlaceholder) < 0 + ); await fs.remove(args.outputZipPath); } }); diff --git a/packages/fx-core/tests/component/driver/teamsApp/utils.test.ts b/packages/fx-core/tests/component/driver/teamsApp/utils.test.ts new file mode 100644 index 0000000000..59fbc21af9 --- /dev/null +++ b/packages/fx-core/tests/component/driver/teamsApp/utils.test.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "mocha"; +import { expect } from "chai"; +import { normalizePath } from "../../../../src/component/driver/teamsApp/utils/utils"; + +describe("utils", async () => { + it("normalizePath: should use forward slash", () => { + const res = normalizePath("resources\\test.yaml", true); + expect(res).equal("resources/test.yaml"); + }); + + it("normalizePath: no need to convert", () => { + const res = normalizePath("resources\\test.yaml", false); + expect(res).equal("resources\\test.yaml"); + }); +}); diff --git a/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts b/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts index 2c519b45a2..5e3cdd8c4e 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts @@ -5,12 +5,27 @@ import "mocha"; import * as sinon from "sinon"; import chai from "chai"; import fs from "fs-extra"; -import { ManifestUtil } from "@microsoft/teamsfx-api"; +import { + ManifestUtil, + SystemError, + err, + ok, + Platform, + TeamsAppManifest, +} from "@microsoft/teamsfx-api"; +import * as commonTools from "../../../../src/common/tools"; import { ValidateManifestDriver } from "../../../../src/component/driver/teamsApp/validate"; import { ValidateManifestArgs } from "../../../../src/component/driver/teamsApp/interfaces/ValidateManifestArgs"; import { IAppValidationNote } from "../../../../src/component/driver/teamsApp/interfaces/appdefinitions/IValidationResult"; +import { AsyncAppValidationResultsResponse } from "../../../../src/component/driver/teamsApp/interfaces/AsyncAppValidationResultsResponse"; +import { + AsyncAppValidationResponse, + AsyncAppValidationStatus, +} from "../../../../src/component/driver/teamsApp/interfaces/AsyncAppValidationResponse"; import { ValidateAppPackageDriver } from "../../../../src/component/driver/teamsApp/validateAppPackage"; import { ValidateAppPackageArgs } from "../../../../src/component/driver/teamsApp/interfaces/ValidateAppPackageArgs"; +import { ValidateWithTestCasesDriver } from "../../../../src/component/driver/teamsApp/validateTestCases"; +import { ValidateWithTestCasesArgs } from "../../../../src/component/driver/teamsApp/interfaces/ValidateWithTestCasesArgs"; import { AppStudioError } from "../../../../src/component/driver/teamsApp/errors"; import { AppStudioClient } from "../../../../src/component/driver/teamsApp/clients/appStudioClient"; import { @@ -18,11 +33,13 @@ import { MockedM365Provider, MockedUserInteraction, } from "../../../plugins/solution/util"; -import { Platform, TeamsAppManifest } from "@microsoft/teamsfx-api"; import AdmZip from "adm-zip"; import { Constants } from "../../../../src/component/driver/teamsApp/constants"; import { metadataUtil } from "../../../../src/component/utils/metadataUtil"; -import { InvalidActionInputError } from "../../../../src/error/common"; +import { InvalidActionInputError, UserCancelError } from "../../../../src/error/common"; +import { teamsappMgr } from "../../../../src/component/driver/teamsApp/teamsappMgr"; +import { setTools } from "../../../../src/core/globalVars"; +import { MockTools } from "../../../core/utils"; describe("teamsApp/validateManifest", async () => { const teamsAppDriver = new ValidateManifestDriver(); @@ -651,3 +668,738 @@ describe("teamsApp/validateAppPackage", async () => { chai.assert(result.isOk()); }); }); + +describe("teamsApp/validateWithTestCases", async () => { + const tools = new MockTools(); + setTools(tools); + + const teamsAppDriver = new ValidateWithTestCasesDriver(); + + const mockedDriverContext: any = { + m365TokenProvider: new MockedM365Provider(), + logProvider: new MockedLogProvider(), + ui: new MockedUserInteraction(), + projectPath: "./", + }; + + beforeEach(() => { + sinon.stub(commonTools, "waitSeconds").resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("file not found - app package", async () => { + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + if (result.isErr()) { + chai.assert.equal(AppStudioError.FileNotFoundError.name, result.error.name); + } + }); + + it("file not found - manifest.json", async () => { + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + }; + + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + if (result.isErr()) { + chai.assert.equal(AppStudioError.FileNotFoundError.name, result.error.name); + } + }); + + it("invalid param error", async () => { + const args: ValidateWithTestCasesArgs = { + appPackagePath: "", + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + if (result.isErr()) { + chai.assert.isTrue(result.error instanceof InvalidActionInputError); + } + }); + + it("Failed to get token", async () => { + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakePath", + }; + + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + sinon + .stub(mockedDriverContext.m365TokenProvider, "getAccessToken") + .resolves(err(new SystemError({}))); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + }); + + it("Invalid validation result response - Null details", async () => { + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves(undefined); + const mockSubmitValidationResponse: AsyncAppValidationResponse = { + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }; + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const invalidValidationResultResponseJson: any = { + appValidationId: "appValidationId123", + appId: "appId123", + status: "Completed", + appVersion: "1.0.0", + manifestVersion: "1.0.0", + createdAt: "2024-03-27T12:00:00.000Z", + updatedAt: "2024-03-27T12:00:00.000Z", + validationResults: { + successes: null, + warnings: null, + failures: null, + skipped: null, + }, + }; + const invalidValidationResultResponse: AsyncAppValidationResultsResponse = < + AsyncAppValidationResultsResponse + >invalidValidationResultResponseJson; + sinon.stub(AppStudioClient, "getAppValidationById").resolves(invalidValidationResultResponse); + await teamsAppDriver.runningBackgroundJob( + args, + mockedDriverContext, + "test_token", + mockSubmitValidationResponse, + "test_id" + ); + chai.assert( + mockedDriverContext.logProvider.msg.includes("Validation request completed, status:") + ); + }); + + it("Invalid validation result response - Null validation results", async () => { + const mockSubmitValidationResponse: AsyncAppValidationResponse = { + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }; + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const invalidValidationResultResponseJson: any = { + appValidationId: "appValidationId123", + appId: "appId123", + status: "Completed", + appVersion: "1.0.0", + manifestVersion: "1.0.0", + createdAt: "2024-03-27T12:00:00.000Z", + updatedAt: "2024-03-27T12:00:00.000Z", + validationResults: null, + }; + const invalidValidationResultResponse: AsyncAppValidationResultsResponse = < + AsyncAppValidationResultsResponse + >invalidValidationResultResponseJson; + sinon.stub(AppStudioClient, "getAppValidationById").resolves(invalidValidationResultResponse); + await teamsAppDriver.runningBackgroundJob( + args, + mockedDriverContext, + "test_token", + mockSubmitValidationResponse, + "test_id" + ); + chai.assert( + mockedDriverContext.logProvider.msg.includes("Validation request completed, status:") + ); + }); + + it("Valid validation result response", async () => { + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Aborted, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + const mockSubmitValidationResponse: AsyncAppValidationResponse = { + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }; + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + sinon.stub(AppStudioClient, "getAppValidationById").resolves({ + status: AsyncAppValidationStatus.Completed, + appValidationId: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + validationResults: { + successes: [ + { + title: "Validation_Success_Example", + message: "Success validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + warnings: [ + { + title: "Validation_Warning_Example", + message: "Warning validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + failures: [ + { + title: "Validation_Failure_Example", + message: "Failure validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + skipped: [ + { + title: "Validation_Skipped_Example", + message: "Skipped validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + await teamsAppDriver.runningBackgroundJob( + args, + mockedDriverContext, + "test_token", + mockSubmitValidationResponse, + "test_id" + ); + chai.assert( + mockedDriverContext.logProvider.msg.includes("Validation request completed, status:") + ); + chai.assert( + mockedDriverContext.logProvider.msg.includes("1 failed, 1 warning, 1 skipped, 1 passed") + ); + }); + + it("Duplicate validations - InProgress", async () => { + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.InProgress, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").throws("should not be called"); + sinon.stub(AppStudioClient, "getAppValidationById").throws("should not be called"); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Duplicate validations - Created", async () => { + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Created, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").throws("should not be called"); + sinon.stub(AppStudioClient, "getAppValidationById").throws("should not be called"); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Duplicate validations - CLI", async () => { + const mockedCliDriverContext = { + ...mockedDriverContext, + platform: Platform.CLI, + }; + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.InProgress, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").throws("should not be called"); + sinon.stub(AppStudioClient, "getAppValidationById").throws("should not be called"); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedCliDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Invalid list validation response", async () => { + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({}); + sinon.stub(AppStudioClient, "submitAppValidationRequest").resolves({ + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }); + + sinon.stub(AppStudioClient, "getAppValidationById").resolves({ + status: AsyncAppValidationStatus.Completed, + appValidationId: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + validationResults: { + successes: [ + { + title: "Validation_Success_Example", + message: "Success validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + warnings: [], + failures: [], + skipped: [], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Happy path", async () => { + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Aborted, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").resolves({ + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }); + + sinon.stub(AppStudioClient, "getAppValidationById").resolves({ + status: AsyncAppValidationStatus.Completed, + appValidationId: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + validationResults: { + successes: [ + { + title: "Validation_Success_Example", + message: "Success validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + warnings: [ + { + title: "Validation_Warning_Example", + message: "Warning validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + failures: [ + { + title: "Validation_Failure_Example", + message: "Failure validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + skipped: [ + { + title: "Validation_Skipped_Example", + message: "Skipped validation example message.", + artifacts: { + filePath: "fakePath", + docsUrl: "https://docs.microsoft.com", + policyNumber: "123", + policyLinkUrl: "https://docs.microsoft.com", + recommendation: "fakeRecommendation", + }, + }, + ], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Aborted", async () => { + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Aborted, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").resolves({ + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }); + + sinon.stub(AppStudioClient, "getAppValidationById").resolves({ + status: AsyncAppValidationStatus.Aborted, + appValidationId: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + validationResults: { + failures: [], + warnings: [], + successes: [], + skipped: [], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: false, + }; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("Happy path - CLI", async () => { + const mockedCliDriverContext = { + ...mockedDriverContext, + platform: Platform.CLI, + }; + + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "readFile").callsFake(async () => { + const zip = new AdmZip(); + zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(new TeamsAppManifest()))); + const archivedFile = zip.toBuffer(); + return archivedFile; + }); + sinon.stub(metadataUtil, "parseManifest"); + + sinon.stub(AppStudioClient, "getAppValidationRequestList").resolves({ + appValidations: [ + { + id: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Completed, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "fakeId2", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + status: AsyncAppValidationStatus.Aborted, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }); + sinon.stub(AppStudioClient, "submitAppValidationRequest").resolves({ + status: AsyncAppValidationStatus.Created, + appValidationId: "fakeId", + }); + + sinon.stub(AppStudioClient, "getAppValidationById").resolves({ + status: AsyncAppValidationStatus.Completed, + appValidationId: "fakeId", + appId: "fakeAppId", + appVersion: "1.0.0", + manifestVersion: "1.16", + validationResults: { + failures: [], + warnings: [], + successes: [], + skipped: [], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const args: ValidateWithTestCasesArgs = { + appPackagePath: "fakepath", + showMessage: true, + showProgressBar: true, + }; + + const result = (await teamsAppDriver.execute(args, mockedCliDriverContext)).result; + chai.assert(result.isOk()); + }); + + it("CLI - succeed", async () => { + sinon.stub(ValidateWithTestCasesDriver.prototype, "validate").resolves(ok(new Map())); + const result = await teamsappMgr.validateTeamsApp({ + projectPath: "xxx", + platform: Platform.CLI, + "package-file": "xxx", + "validate-method": "test-cases", + }); + chai.assert(result.isOk()); + }); + + it("CLI - failed", async () => { + sinon + .stub(ValidateWithTestCasesDriver.prototype, "validate") + .resolves(err(new UserCancelError())); + const result = await teamsappMgr.validateTeamsApp({ + projectPath: "xxx", + platform: Platform.CLI, + "package-file": "xxx", + "validate-method": "test-cases", + }); + chai.assert(result.isErr()); + }); +}); diff --git a/packages/fx-core/tests/component/envUtil.test.ts b/packages/fx-core/tests/component/envUtil.test.ts index 7ac0501c38..fbab1e4903 100644 --- a/packages/fx-core/tests/component/envUtil.test.ts +++ b/packages/fx-core/tests/component/envUtil.test.ts @@ -83,6 +83,11 @@ describe("envUtils", () => { assert.isTrue(e instanceof MissingRequiredFileError); } }); + it("happy path for customized yaml path", async () => { + mockedEnvRestore = mockedEnv({ TEAMSFX_CONFIG_FILE_PATH: "./customized.yml" }); + const res1 = pathUtils.getYmlFilePath(".", "dev"); + assert.equal(res1, "./customized.yml"); + }); }); describe("pathUtils.getEnvFolderPath", () => { diff --git a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts index 6b84ff8705..e499ba3719 100644 --- a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @@ -30,6 +30,7 @@ import { WarningType, SpecParserError, AdaptiveCardGenerator, + ProjectType, } from "@microsoft/m365-spec-parser"; import { CopilotPluginGenerator } from "../../../src/component/generator/copilotPlugin/generator"; import { assert, expect } from "chai"; @@ -50,6 +51,7 @@ import { ErrorResult } from "@microsoft/m365-spec-parser"; import { PluginManifestUtils } from "../../../src/component/driver/teamsApp/utils/PluginManifestUtils"; import path from "path"; import { OpenAPIV3 } from "openapi-types"; +import { format } from "util"; const openAIPluginManifest = { schema_version: "v1", @@ -972,6 +974,11 @@ describe("formatValidationErrors", () => { type: ErrorType.SwaggerNotSupported, content: "test", }, + { + type: ErrorType.SpecVersionNotSupported, + content: "test", + data: "3.1.0", + }, { type: ErrorType.Unknown, content: "unknown", @@ -993,7 +1000,10 @@ describe("formatValidationErrors", () => { expect(res[8].content).equals("resolveurl"); expect(res[9].content).equals(getLocalizedString("core.common.CancelledMessage")); expect(res[10].content).equals(getLocalizedString("core.common.SwaggerNotSupported")); - expect(res[11].content).equals("unknown"); + expect(res[11].content).equals( + format(getLocalizedString("core.common.SpecVersionNotSupported"), res[11].data) + ); + expect(res[12].content).equals("unknown"); }); }); @@ -1020,18 +1030,26 @@ describe("listPluginExistingOperations", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "api1", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "api1", + server: "https://test", + operationId: "get", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const res = await listPluginExistingOperations( teamsManifestWithPlugin, "manifestPath", @@ -1071,34 +1089,6 @@ describe("listPluginExistingOperations", () => { } expect(hasException).to.be.true; }); - - it("invalid openapi spec", async () => { - sandbox - .stub(PluginManifestUtils.prototype, "getApiSpecFilePathFromTeamsManifest") - .resolves(ok(["openapi.yaml"])); - - sandbox.stub(SpecParser.prototype, "validate").resolves({ - status: ValidationStatus.Error, - warnings: [], - errors: [ - { - type: ErrorType.NoServerInformation, - content: "content", - }, - ], - }); - - let hasException = false; - - try { - await listPluginExistingOperations(teamsManifestWithPlugin, "manifestPath", "openapi.yaml"); - } catch (e) { - hasException = true; - expect(e.source).equal("listPluginExistingOperations"); - expect(e.name).equal("invalid-api-spec"); - } - expect(hasException).to.be.true; - }); }); describe("updateForCustomApi", async () => { @@ -1314,4 +1304,293 @@ describe("updateForCustomApi", async () => { await CopilotPluginHelper.updateForCustomApi(limitedSpec, "javascript", "path", "openapi.yaml"); expect(mockWriteFile.calledThrice).to.be.true; }); + + it("happy path with spec with required and multiple parameter", async () => { + const newSpec = { + openapi: "3.0.0", + info: { + title: "My API", + version: "1.0.0", + }, + description: "test", + paths: { + "/hello": { + get: { + operationId: "getHello", + summary: "Returns a greeting", + parameters: [ + { + name: "query", + in: "query", + schema: { type: "string" }, + required: true, + }, + { + name: "query2", + in: "query", + schema: { type: "string" }, + requried: false, + }, + { + name: "query3", + in: "query", + schema: { type: "string" }, + requried: true, + description: "test", + }, + ], + responses: { + "200": { + description: "", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + post: { + operationId: "createPet", + summary: "Create a pet", + description: "", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as OpenAPIV3.Document; + sandbox.stub(fs, "ensureDir").resolves(); + sandbox.stub(fs, "writeFile").callsFake((file, data) => { + if (file === path.join("path", "src", "prompts", "chat", "skprompt.txt")) { + expect(data).to.contains("The following is a conversation with an AI assistant."); + } else if (file === path.join("path", "src", "adaptiveCard", "hello.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "prompts", "chat", "actions.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "app", "app.ts")) { + expect(data).to.contains(`app.ai.action("getHello"`); + expect(data).not.to.contains("{{"); + expect(data).not.to.contains("// Replace with action code"); + } + }); + sandbox + .stub(fs, "readFile") + .resolves(Buffer.from("test code // Replace with action code {{OPENAPI_SPEC_PATH}}")); + await CopilotPluginHelper.updateForCustomApi(newSpec, "typescript", "path", "openapi.yaml"); + }); + + it("happy path with spec with auth", async () => { + const authSpec = { + openapi: "3.0.0", + info: { + title: "My API", + version: "1.0.0", + }, + description: "test", + paths: { + "/hello": { + get: { + operationId: "getHello", + summary: "Returns a greeting", + parameters: [ + { + name: "query", + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "A greeting message", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + security: [ + { + api_key: [], + }, + ], + }, + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, + } as OpenAPIV3.Document; + sandbox.stub(fs, "ensureDir").resolves(); + sandbox.stub(fs, "writeFile").callsFake((file, data) => { + if (file === path.join("path", "src", "prompts", "chat", "skprompt.txt")) { + expect(data).to.contains("The following is a conversation with an AI assistant."); + } else if (file === path.join("path", "src", "adaptiveCard", "hello.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "prompts", "chat", "actions.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "app", "app.ts")) { + expect(data).to.contains(`app.ai.action("getHello"`); + expect(data).not.to.contains("{{"); + expect(data).not.to.contains("// Replace with action code"); + } + }); + sandbox + .stub(fs, "readFile") + .resolves(Buffer.from("test code // Replace with action code {{OPENAPI_SPEC_PATH}}")); + await CopilotPluginHelper.updateForCustomApi(authSpec, "typescript", "path", "openapi.yaml"); + }); +}); + +describe("listOperations", async () => { + const context = createContextV3(); + const sandbox = sinon.createSandbox(); + const inputs = { + "custom-copilot-rag": "custom-copilot-rag-customApi", + platform: Platform.VSCode, + }; + const spec = { + openapi: "3.0.0", + info: { + title: "My API", + version: "1.0.0", + }, + description: "test", + paths: { + "/hello": { + get: { + operationId: "getHello", + summary: "Returns a greeting", + parameters: [ + { + name: "query", + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "A greeting message", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + security: [ + { + api_key: [], + }, + ], + }, + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, + } as OpenAPIV3.Document; + + afterEach(async () => { + sandbox.restore(); + }); + + it("allow auth for teams ai project", async () => { + sandbox.stub(CopilotPluginHelper, "formatValidationErrors").resolves([]); + sandbox.stub(CopilotPluginHelper, "logValidationResults").resolves(); + sandbox.stub(SpecParser.prototype, "validate").resolves({ + status: ValidationStatus.Valid, + warnings: [], + errors: [], + }); + sandbox + .stub(SpecParser.prototype, "list") + .resolves({ APIs: [], allAPICount: 1, validAPICount: 0 }); + + const res = await CopilotPluginHelper.listOperations( + context, + undefined, + "", + inputs, + true, + false, + "" + ); + expect(res.isOk()).to.be.true; + }); }); diff --git a/packages/fx-core/tests/component/generator/generator.test.ts b/packages/fx-core/tests/component/generator/generator.test.ts index b386dc8ac7..d336339dcb 100644 --- a/packages/fx-core/tests/component/generator/generator.test.ts +++ b/packages/fx-core/tests/component/generator/generator.test.ts @@ -799,6 +799,18 @@ describe("Generator happy path", async () => { assert.equal(vars.enableTestToolByDefault, ""); }); + it("template variables when ME test tool enabled", async () => { + sandbox.stub(process, "env").value({ TEAMSFX_ME_TEST_TOOL: "true" }); + const vars = Generator.getDefaultVariables("test"); + assert.equal(vars.enableMETestToolByDefault, "true"); + }); + + it("template variables when ME test tool disabled", async () => { + sandbox.stub(process, "env").value({ TEAMSFX_ME_TEST_TOOL: "false" }); + const vars = Generator.getDefaultVariables("test"); + assert.equal(vars.enableMETestToolByDefault, ""); + }); + it("template variables when new project enabled", async () => { sandbox.stub(process, "env").value({ TEAMSFX_NEW_PROJECT_TYPE: "true", diff --git a/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts b/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts index 67913a4d8a..1ac33838e8 100644 --- a/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts @@ -1,3 +1,4 @@ +import { Capability } from "./../../../../tests/src/utils/constants"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -23,6 +24,7 @@ import fse from "fs-extra"; import "mocha"; import mockfs from "mock-fs"; import mockedEnv, { RestoreFn } from "mocked-env"; +import * as fetch from "node-fetch"; import { OfficeAddinManifest } from "office-addin-manifest"; import * as path from "path"; import proxyquire from "proxyquire"; @@ -32,19 +34,24 @@ import * as uuid from "uuid"; import { cpUtils } from "../../../src/common/deps-checker"; import { manifestUtils } from "../../../src/component/driver/teamsApp/utils/ManifestUtils"; import { Generator } from "../../../src/component/generator/generator"; -import projectsJsonData from "../../../src/component/generator/officeAddin/config/projectsJsonData"; -import { OfficeAddinGenerator } from "../../../src/component/generator/officeAddin/generator"; +import { + OfficeAddinGenerator, + getHost, +} from "../../../src/component/generator/officeAddin/generator"; import { HelperMethods, unzipErrorHandler, } from "../../../src/component/generator/officeAddin/helperMethods"; import { createContextV3 } from "../../../src/component/utils"; import { setTools } from "../../../src/core/globalVars"; -import { ProjectTypeOptions, QuestionNames } from "../../../src/question"; +import { AccessGithubError, UserCancelError } from "../../../src/error"; +import { + CapabilityOptions, + OfficeAddinHostOptions, + ProjectTypeOptions, + QuestionNames, +} from "../../../src/question"; import { MockTools } from "../../core/utils"; -import * as fetch from "node-fetch"; -import { AccessGithubError, ReadFileError, UserCancelError } from "../../../src/error"; -import { Readable } from "stream"; describe("OfficeAddinGenerator for Outlook Addin", function () { const testFolder = path.resolve("./tmp"); @@ -91,6 +98,47 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + const doScaffoldStub = sinon + .stub(OfficeAddinGenerator, "doScaffolding") + .resolves(ok(undefined)); + const generateTemplateStub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); + + const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); + + chai.expect(result.isOk()).to.eq(true); + chai.expect(doScaffoldStub.calledOnce).to.be.true; + chai.expect(generateTemplateStub.calledOnce).to.be.true; + }); + + it("should call both doScaffolding and template generator if Capabilities is outlookAddinImport", async function () { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "outlook-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.outlookAddinImport().id; + const doScaffoldStub = sinon + .stub(OfficeAddinGenerator, "doScaffolding") + .resolves(ok(undefined)); + const generateTemplateStub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); + + const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); + + chai.expect(result.isOk()).to.eq(true); + chai.expect(doScaffoldStub.calledOnce).to.be.true; + chai.expect(generateTemplateStub.calledOnce).to.be.true; + }); + + it("should call both doScaffolding and template generator if Capabilities is json-taskpane", async function () { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "outlook-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; const doScaffoldStub = sinon .stub(OfficeAddinGenerator, "doScaffolding") .resolves(ok(undefined)); @@ -109,6 +157,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(err(mockedError)); sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -123,6 +172,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); sinon.stub(Generator, "generateTemplate").resolves(err(mockedError)); @@ -131,15 +181,36 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { chai.assert.isTrue(result.isErr() && result.error.name === "mockedError"); }); - it("should scaffold taskpane successfully on happy path", async () => { + it("should scaffold taskpane successfully on happy path if project-type is outlookAddin", async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, "app-name": "outlook-addin-test", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.OfficeAddinFolder] = undefined; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + + sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); + sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); + const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); + + chai.expect(result.isOk()).to.eq(true); + }); + + it("should scaffold taskpane successfully on happy path if project-type is officeXMLAddin and host is outlook", async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "outlook-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeXMLAddin().id; + inputs[QuestionNames.OfficeAddinHost] = OfficeAddinHostOptions.outlook().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.OfficeAddinFolder] = undefined; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); @@ -155,9 +226,10 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.OfficeAddinFolder] = undefined; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(new UserCancelError()); @@ -173,9 +245,10 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.OfficeAddinFolder] = "somepath"; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; inputs[QuestionNames.OfficeAddinManifest] = "manifest.json"; const copyAddinFilesStub = sinon @@ -205,6 +278,9 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { chai.expect(copyAddinFilesStub.calledOnce).to.be.true; chai.expect(updateManifestStub.calledOnce).to.be.true; chai.expect(inputs[QuestionNames.OfficeAddinHost]).to.eq("Outlook"); + + const hostResult = await getHost(inputs[QuestionNames.OfficeAddinFolder]); + chai.expect(hostResult).to.equal("Outlook"); }); it("should copy addin files and convert manifest if addin folder is specified with xml manifest", async () => { @@ -213,9 +289,10 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { projectPath: testFolder, "app-name": "outlook-addin-test", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.OfficeAddinFolder] = "somepath"; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; inputs[QuestionNames.OfficeAddinManifest] = "manifest.xml"; let progressBarStartCalled = 0; @@ -275,6 +352,9 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { chai.expect(progressBarStartCalled).to.eq(1); chai.expect(progressBarNextCalled).to.eq(3); chai.expect(progessBarEndCalled).to.eq(1); + + const hostResult = await getHost(inputs[QuestionNames.OfficeAddinFolder]); + chai.expect(hostResult).to.equal("Outlook"); }); afterEach(async () => { @@ -285,31 +365,29 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { } }); - it(`should generate common template if language is "No Options"`, async () => { + it(`should generate common template if language is undefined`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, + ProjectType: ProjectTypeOptions.outlookAddin().id, "app-name": "outlook-addin-test", - "programming-language": "No Options", + "programming-language": undefined, }; sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); - - chai.assert.isTrue( - // The forth parameter is the language parameter, which should be undefined so that - // common template will be scaffolded. - result.isOk() && stub.calledWith(context, testFolder, "office-addin", undefined) - ); + chai.assert.isTrue(result.isOk()); + // chai.assert.isTrue(stub.calledWith(context, testFolder, "office-addin", undefined)); }); - it(`should generate ts template if language is "TypeScript"`, async () => { + it(`should generate ts template if language is "typescript"`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, + ProjectType: ProjectTypeOptions.outlookAddin().id, "app-name": "outlook-addin-test", - "programming-language": "TypeScript", + "programming-language": "typescript", }; sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -319,12 +397,13 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { chai.assert.isTrue(result.isOk() && stub.calledWith(context, testFolder, "office-addin", "ts")); }); - it(`should generate js template if language is "JavaScript"`, async () => { + it(`should generate js template if language is "javascript"`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, + ProjectType: ProjectTypeOptions.outlookAddin().id, "app-name": "outlook-addin-test", - "programming-language": "JavaScript", + "programming-language": "javascript", }; sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -670,92 +749,6 @@ describe("helperMethods", async () => { }); }); -describe("projectsJsonData for Outlook Addin", () => { - it("should contain desired values", () => { - const data = new projectsJsonData(); - chai.assert.equal(data.getHostDisplayName("outlook"), "Outlook"); - chai.assert.isUndefined(data.getHostDisplayName("xxx")); - chai.assert.deepEqual(data.getHostTemplateNames("taskpane"), [ - "Outlook", - "Word", - "Excel", - "PowerPoint", - ]); - chai.assert.isEmpty(data.getHostTemplateNames("xxx")); - chai.assert.deepEqual(data.getSupportedScriptTypes("taskpane"), ["TypeScript"]); - chai.assert.equal( - data.getProjectTemplateRepository("taskpane", "typescript"), - "https://github.com/OfficeDev/Office-Addin-TaskPane" - ); - chai.assert.equal( - data.getProjectTemplateBranchName("taskpane", "typescript", false), - "json-preview-yo-office" - ); - - chai.assert.deepEqual( - data.getProjectDownloadLink("taskpane", "TypeScript"), - "https://aka.ms/teams-toolkit/office-addin-taskpane" - ); - - chai.assert.isDefined(data.getParsedProjectJsonData()); - chai.assert.isFalse(data.projectBothScriptTypes("taskpane")); - }); -}); - -describe("projectsJsonData for Office Addin", () => { - it("should contain desired values", () => { - const data = new projectsJsonData(); - chai.assert.equal(data.getHostDisplayName("outlook"), "Outlook"); - chai.assert.isUndefined(data.getHostDisplayName("xxx")); - chai.assert.deepEqual(data.getHostTemplateNames("taskpane"), [ - "Outlook", - "Word", - "Excel", - "PowerPoint", - ]); - chai.assert.isEmpty(data.getHostTemplateNames("xxx")); - chai.assert.deepEqual(data.getSupportedScriptTypesNew("taskpane"), [ - "TypeScript", - "JavaScript", - ]); - chai.assert.equal( - data.getProjectTemplateRepositoryNew("taskpane", "typescript", "default"), - "https://github.com/OfficeDev/Office-Addin-TaskPane" - ); - chai.assert.isUndefined(data.getProjectTemplateRepositoryNew("xxx", "typescript", "default")); - chai.assert.equal( - data.getProjectTemplateBranchNameNew("taskpane", "typescript", "default", false), - "json-wxpo-preview" - ); - chai.assert.isUndefined( - data.getProjectTemplateBranchNameNew("xxx", "typescript", "default", false) - ); - chai.assert.equal( - data.getProjectTemplateBranchNameNew("taskpane", "typescript", "default", true), - "json-wxpo-preview" - ); - chai.assert.deepEqual( - data.getProjectRepoAndBranchNew("taskpane", "typescript", "default", false), - { - repo: "https://github.com/OfficeDev/Office-Addin-TaskPane", - branch: "json-wxpo-preview", - } - ); - chai.assert.deepEqual(data.getProjectRepoAndBranchNew("xxx", "typescript", "default", false), { - repo: undefined, - branch: undefined, - }); - - chai.assert.deepEqual( - data.getProjectDownloadLinkNew("taskpane", "TypeScript", "default"), - "https://aka.ms/teams-toolkit/office-addin-taskpane/ts-default" - ); - - chai.assert.isDefined(data.getParsedProjectJsonData()); - chai.assert.isTrue(data.projectBothScriptTypesNew("taskpane")); - }); -}); - describe("OfficeAddinGenerator for Office Addin", function () { const testFolder = path.resolve("./tmp"); let context: Context; @@ -847,15 +840,15 @@ describe("OfficeAddinGenerator for Office Addin", function () { chai.assert.isTrue(result.isErr() && result.error.name === "mockedError"); }); - it("should scaffold taskpane successfully on happy path", async () => { + it("should scaffold taskpane successfully on happy path if project-type is officeAddin and capability is json-taskpane", async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", "office-addin-framework-type": "default", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.OfficeAddinFolder] = undefined; inputs[QuestionNames.ProgrammingLanguage] = "typescript"; @@ -867,138 +860,178 @@ describe("OfficeAddinGenerator for Office Addin", function () { chai.expect(result.isOk()).to.eq(true); }); - it("should scaffold taskpane failed, throw error", async () => { + it("should scaffold taskpane successfully on happy path if project-type is officeAddin and capability is office-content-addin", async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", - "office-addin-framework-type": "default", }; - inputs["capabilities"] = ["taskpane"]; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; inputs[QuestionNames.OfficeAddinFolder] = undefined; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(new UserCancelError()); + sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); - chai.expect(result.isErr()).to.eq(true); + chai.expect(result.isOk()).to.eq(true); }); - it("should copy addin files and updateManifest if addin folder is specified with json manifest", async () => { + it("should scaffold taskpane failed, throw error", async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", - "office-addin-framework-type": "default", }; - inputs["capabilities"] = ["taskpane"]; - inputs[QuestionNames.OfficeAddinFolder] = "somepath"; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; - inputs[QuestionNames.OfficeAddinManifest] = "manifest.json"; - - const copyAddinFilesStub = sinon - .stub(HelperMethods, "copyAddinFiles") - .callsFake((from: string, to: string) => { - return; - }); - const updateManifestStub = sinon - .stub(HelperMethods, "updateManifest") - .callsFake(async (destination: string, manifestPath: string) => { - return; - }); - - sinon.stub(ManifestUtil, "loadFromPath").resolves({ - extensions: [ - { - requirements: { - scopes: ["mail"], - }, - }, - ], - }); + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.OfficeAddinFolder] = undefined; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); + sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(new UserCancelError()); + sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); - chai.expect(result.isOk()).to.eq(true); - chai.expect(copyAddinFilesStub.calledOnce).to.be.true; - chai.expect(updateManifestStub.calledOnce).to.be.true; - chai.expect(inputs[QuestionNames.OfficeAddinHost]).to.eq("Outlook"); + chai.expect(result.isErr()).to.eq(true); }); - it("should copy addin files and convert manifest if addin folder is specified with xml manifest", async () => { - const inputs: Inputs = { - platform: Platform.CLI, - projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, - "app-name": "office-addin-test", - "office-addin-framework-type": "default", - }; - inputs["capabilities"] = ["taskpane"]; - inputs[QuestionNames.OfficeAddinFolder] = "somepath"; - inputs[QuestionNames.ProgrammingLanguage] = "TypeScript"; - inputs[QuestionNames.OfficeAddinManifest] = "manifest.xml"; - - let progressBarStartCalled = 0; - let progressBarNextCalled = 0; - let progessBarEndCalled = 0; - const createProgressBarStub = sinon.stub(context.userInteraction, "createProgressBar").returns({ - start: async () => { - progressBarStartCalled++; - }, - next: async () => { - progressBarNextCalled++; - }, - end: async () => { - progessBarEndCalled++; - }, - }); - - const copyAddinFilesStub = sinon - .stub(HelperMethods, "copyAddinFiles") - .callsFake((from: string, to: string) => { - return; - }); - const updateManifestStub = sinon - .stub(HelperMethods, "updateManifest") - .callsFake(async (destination: string, manifestPath: string) => { - return; - }); - const convertProjectStub = sinon - .stub() - .callsFake(async (manifestPath?: string, backupPath?: string) => { - return; + const testCases = [ + { scope: "document", host: "Word" }, + { scope: "workbook", host: "Excel" }, + { scope: "presentation", host: "PowerPoint" }, + ]; + + testCases.forEach((testCase) => { + it(`should copy addin files and updateManifest if addin folder is specified with json manifest for ${testCase.host}`, async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "office-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.OfficeAddinFolder] = "somepath"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + inputs[QuestionNames.OfficeAddinManifest] = "manifest.json"; + + const copyAddinFilesStub = sinon + .stub(HelperMethods, "copyAddinFiles") + .callsFake((from: string, to: string) => { + return; + }); + const updateManifestStub = sinon + .stub(HelperMethods, "updateManifest") + .callsFake(async (destination: string, manifestPath: string) => { + return; + }); + + sinon.stub(ManifestUtil, "loadFromPath").resolves({ + extensions: [ + { + requirements: { + scopes: [testCase.scope], + }, + }, + ], }); - const generator = proxyquire("../../../src/component/generator/officeAddin/generator", { - "office-addin-project": { - convertProject: convertProjectStub, - }, + const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); + + chai.expect(result.isOk()).to.eq(true); + chai.expect(copyAddinFilesStub.calledOnce).to.be.true; + chai.expect(updateManifestStub.calledOnce).to.be.true; + chai.expect(inputs[QuestionNames.OfficeAddinHost]).to.equal(testCase.host); + const hostResult = await getHost(inputs[QuestionNames.OfficeAddinFolder]); + chai.expect(hostResult).to.equal(testCase.host); }); + }); - sinon.stub(ManifestUtil, "loadFromPath").resolves({ - extensions: [ - { - requirements: { - scopes: ["mail"], + testCases.forEach((testCase) => { + it(`should copy addin files and convert manifest if addin folder is specified with xml manifest for ${testCase.host}`, async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "office-addin-test", + [QuestionNames.ProjectType]: ProjectTypeOptions.officeAddin().id, + [QuestionNames.Capabilities]: "json-taskpane", + [QuestionNames.OfficeAddinFolder]: "somepath", + [QuestionNames.ProgrammingLanguage]: "typescript", + [QuestionNames.OfficeAddinFramework]: "default", + [QuestionNames.OfficeAddinManifest]: "manifest.xml", + }; + + let progressBarStartCalled = 0; + let progressBarNextCalled = 0; + let progessBarEndCalled = 0; + const createProgressBarStub = sinon + .stub(context.userInteraction, "createProgressBar") + .returns({ + start: async () => { + progressBarStartCalled++; + }, + next: async () => { + progressBarNextCalled++; }, + end: async () => { + progessBarEndCalled++; + }, + }); + + const copyAddinFilesStub = sinon + .stub(HelperMethods, "copyAddinFiles") + .callsFake((from: string, to: string) => { + return; + }); + const updateManifestStub = sinon + .stub(HelperMethods, "updateManifest") + .callsFake(async (destination: string, manifestPath: string) => { + return; + }); + const convertProjectStub = sinon + .stub() + .callsFake(async (manifestPath?: string, backupPath?: string) => { + return; + }); + + const generator = proxyquire("../../../src/component/generator/officeAddin/generator", { + "office-addin-project": { + convertProject: convertProjectStub, }, - ], - }); + }); - const result = await generator.OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); + sinon.stub(ManifestUtil, "loadFromPath").resolves({ + extensions: [ + { + requirements: { + scopes: [testCase.scope], + }, + }, + ], + }); - chai.expect(result.isOk()).to.eq(true); - chai.expect(copyAddinFilesStub.calledOnce).to.be.true; - chai.expect(updateManifestStub.calledOnce).to.be.true; - chai.expect(convertProjectStub.calledOnce).to.be.true; - chai.expect(inputs[QuestionNames.OfficeAddinHost]).to.eq("Outlook"); - chai.expect(progressBarStartCalled).to.eq(1); - chai.expect(progressBarNextCalled).to.eq(3); - chai.expect(progessBarEndCalled).to.eq(1); + const result = await generator.OfficeAddinGenerator.doScaffolding( + context, + inputs, + testFolder + ); + + chai.expect(result.isOk()).to.eq(true); + chai.expect(copyAddinFilesStub.calledOnce).to.be.true; + chai.expect(updateManifestStub.calledOnce).to.be.true; + chai.expect(convertProjectStub.calledOnce).to.be.true; + chai.expect(inputs[QuestionNames.OfficeAddinHost]).to.equal(testCase.host); + chai.expect(progressBarStartCalled).to.eq(1); + chai.expect(progressBarNextCalled).to.eq(3); + chai.expect(progessBarEndCalled).to.eq(1); + + const resultHost = await getHost(inputs[QuestionNames.OfficeAddinFolder]); + chai.expect(resultHost).to.equal(testCase.host); + }); }); afterEach(async () => { @@ -1009,55 +1042,97 @@ describe("OfficeAddinGenerator for Office Addin", function () { } }); - it(`should generate common template if language is "No Options"`, async () => { + it(`should generate common template if language is undefined`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", - "programming-language": "No Options", - "office-addin-framework-type": "default", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.ProgrammingLanguage] = undefined; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); + chai.assert.isTrue(result.isOk()); + // chai.assert.isTrue(stub.calledWith(context, testFolder, "office-json-addin", undefined)); + }); - chai.assert.isTrue( - // The forth parameter is the language parameter, which should be undefined so that - // common template will be scaffolded. - result.isOk() && stub.calledWith(context, testFolder, "office-json-addin", undefined) - ); + it(`should generate taskpane ts template if language is "typescript"`, async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "office-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + + sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); + const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); + + const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); + + chai.assert.isTrue(result.isOk()); + chai.assert.isTrue(stub.calledWith(context, testFolder, "office-json-addin", "ts")); }); - it(`should generate ts template if language is "TypeScript"`, async () => { + it(`should generate taskpane js template if language is "JavaScript"`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", - "programming-language": "TypeScript", - "office-addin-framework-type": "default", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.ProgrammingLanguage] = "JavaScript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); chai.assert.isTrue( - result.isOk() && stub.calledWith(context, testFolder, "office-json-addin", "ts") + result.isOk() && stub.calledWith(context, testFolder, "office-json-addin", "js") ); }); - it(`should generate js template if language is "JavaScript"`, async () => { + it(`should generate content ts template if language is "typescript"`, async () => { const inputs: Inputs = { platform: Platform.CLI, projectPath: testFolder, - "project-type": ProjectTypeOptions.officeAddin().id, "app-name": "office-addin-test", - "programming-language": "JavaScript", - "office-addin-framework-type": "default", }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + + sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); + const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); + + const result = await OfficeAddinGenerator.generate(context, inputs, testFolder); + + chai.assert.isTrue(result.isOk()); + chai.assert.isTrue(stub.calledWith(context, testFolder, "office-json-addin", "ts")); + }); + + it(`should generate content js template if language is "JavaScript"`, async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + "app-name": "office-addin-test", + }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; + inputs[QuestionNames.ProgrammingLanguage] = "JavaScript"; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + sinon.stub(OfficeAddinGenerator, "doScaffolding").resolves(ok(undefined)); const stub = sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -1067,4 +1142,24 @@ describe("OfficeAddinGenerator for Office Addin", function () { result.isOk() && stub.calledWith(context, testFolder, "office-json-addin", "js") ); }); + + // it("should scaffold taskpane successfully on happy path if capability is office-content-addin", async () => { + // const inputs: Inputs = { + // platform: Platform.CLI, + // projectPath: testFolder, + // "project-type": ProjectTypeOptions.officeAddin().id, + // "app-name": "office-addin-test", + // "office-addin-framework-type": "default", + // }; + // inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; + // inputs[QuestionNames.OfficeAddinFolder] = undefined; + // inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + + // sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); + // sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + // sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); + // const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); + + // chai.expect(result.isOk()).to.eq(true); + // }); }); diff --git a/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts b/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts index e90167b12a..1c3875ab33 100644 --- a/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts @@ -5,7 +5,7 @@ * @author zyun@microsoft.com */ -import { Context, Inputs, ok, Platform } from "@microsoft/teamsfx-api"; +import { Context, Inputs, ok, Platform, err, SystemError } from "@microsoft/teamsfx-api"; import * as chai from "chai"; import * as childProcess from "child_process"; import fs from "fs"; @@ -22,19 +22,16 @@ import { OfficeXMLAddinGenerator } from "../../../src/component/generator/office import { HelperMethods } from "../../../src/component/generator/officeAddin/helperMethods"; import { createContextV3 } from "../../../src/component/utils"; import { setTools } from "../../../src/core/globalVars"; -import { - OfficeAddinCapabilityOptions, - ProjectTypeOptions, - QuestionNames, -} from "../../../src/question"; +import { OfficeAddinHostOptions, ProjectTypeOptions, QuestionNames } from "../../../src/question"; import { MockTools } from "../../core/utils"; import { FeatureFlagName } from "../../../src/common/constants"; -import { getOfficeXMLAddinHostProjectRepoInfo } from "../../../src/component/generator/officeXMLAddin/projectConfig"; +import { getOfficeAddinTemplateConfig } from "../../../src/component/generator/officeXMLAddin/projectConfig"; describe("OfficeXMLAddinGenerator", function () { const testFolder = path.resolve("./tmp"); let context: Context; let mockedEnvRestore: RestoreFn; + const mockedError = new SystemError("mockedSource", "mockedError", "mockedMessage"); beforeEach(async () => { mockedEnvRestore = mockedEnv( @@ -85,11 +82,11 @@ describe("OfficeXMLAddinGenerator", function () { platform: Platform.CLI, projectPath: testFolder, [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, - [QuestionNames.OfficeAddinCapability]: OfficeAddinCapabilityOptions.word().id, - [QuestionNames.Capabilities]: ["taskpane"], + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: "word-taskpane", [QuestionNames.AppName]: "office-addin-test", [QuestionNames.OfficeAddinFolder]: undefined, - [QuestionNames.ProgrammingLanguage]: "TypeScript", + [QuestionNames.ProgrammingLanguage]: "typescript", }; sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); @@ -106,11 +103,11 @@ describe("OfficeXMLAddinGenerator", function () { platform: Platform.CLI, projectPath: testFolder, [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, - [QuestionNames.OfficeAddinCapability]: OfficeAddinCapabilityOptions.word().id, - [QuestionNames.Capabilities]: ["manifest"], + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: "word-manifest", [QuestionNames.AppName]: "office-addin-test", [QuestionNames.OfficeAddinFolder]: undefined, - [QuestionNames.ProgrammingLanguage]: "TypeScript", + [QuestionNames.ProgrammingLanguage]: "javascript", }; sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -125,11 +122,11 @@ describe("OfficeXMLAddinGenerator", function () { platform: Platform.CLI, projectPath: testFolder, [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, - [QuestionNames.OfficeAddinCapability]: OfficeAddinCapabilityOptions.word().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, [QuestionNames.Capabilities]: ["react"], [QuestionNames.AppName]: "office-addin-test", [QuestionNames.OfficeAddinFolder]: undefined, - [QuestionNames.ProgrammingLanguage]: "TypeScript", + [QuestionNames.ProgrammingLanguage]: "typescript", }; sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(undefined); @@ -138,16 +135,74 @@ describe("OfficeXMLAddinGenerator", function () { chai.assert.isTrue(result.isErr()); }); -}); -describe("projectConfig", () => { - it("should return empty repo info if manifest-only project", () => { - chai.assert.equal(getOfficeXMLAddinHostProjectRepoInfo("excel", "manifest", "ts"), ""); + it("should failed when get manifest-only failed", async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: ["word-manifest"], + [QuestionNames.AppName]: "office-addin-test", + [QuestionNames.OfficeAddinFolder]: undefined, + [QuestionNames.ProgrammingLanguage]: "javascript", + }; + + sinon.stub(Generator, "generateTemplate").onCall(0).resolves(err(mockedError)); + const result = await OfficeXMLAddinGenerator.generate(context, inputs, testFolder); + + chai.assert.isTrue(result.isErr()); + }); + + it("should failed when get readme failed", async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: ["word-manifest"], + [QuestionNames.AppName]: "office-addin-test", + [QuestionNames.OfficeAddinFolder]: undefined, + [QuestionNames.ProgrammingLanguage]: "javascript", + }; + + const generatorStub = sinon.stub(Generator, "generateTemplate"); + generatorStub.onCall(0).resolves(ok(undefined)); + generatorStub.onCall(1).resolves(err(mockedError)); + const result = await OfficeXMLAddinGenerator.generate(context, inputs, testFolder); + + chai.assert.isTrue(result.isErr()); }); - it("should success return repo info if not manifest-only project", () => { + it("should failed when gen yml failed", async () => { + const inputs: Inputs = { + platform: Platform.CLI, + projectPath: testFolder, + [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: ["word-manifest"], + [QuestionNames.AppName]: "office-addin-test", + [QuestionNames.OfficeAddinFolder]: undefined, + [QuestionNames.ProgrammingLanguage]: "javascript", + }; + + const generatorStub = sinon.stub(Generator, "generateTemplate"); + generatorStub.onCall(0).resolves(ok(undefined)); + generatorStub.onCall(1).resolves(ok(undefined)); + generatorStub.onCall(2).resolves(err(mockedError)); + sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); + const result = await OfficeXMLAddinGenerator.generate(context, inputs, testFolder); + + chai.assert.isTrue(result.isErr()); + }); +}); + +describe("getOfficeAddinTemplateConfig", () => { + it("should return empty repo info if manifest-only project", () => { + const config = getOfficeAddinTemplateConfig(ProjectTypeOptions.officeXMLAddin().id, "excel"); + chai.assert.equal(config["excel-manifest"].framework?.default?.typescript, undefined); chai.assert.equal( - getOfficeXMLAddinHostProjectRepoInfo("excel", "react", "ts"), + config["excel-react"].framework?.default?.typescript, "https://aka.ms/ccdevx-fx-react-ts" ); }); diff --git a/packages/fx-core/tests/component/util/metadataGraphPermissionUtil.test.ts b/packages/fx-core/tests/component/util/metadataGraphPermissionUtil.test.ts index 3e4fcdecda..9ab5139161 100644 --- a/packages/fx-core/tests/component/util/metadataGraphPermissionUtil.test.ts +++ b/packages/fx-core/tests/component/util/metadataGraphPermissionUtil.test.ts @@ -1,35 +1,17 @@ -import { err, FxError, LogProvider, ok, Result } from "@microsoft/teamsfx-api"; +import { ok } from "@microsoft/teamsfx-api"; import { assert } from "chai"; import "mocha"; import sinon from "sinon"; import fs from "fs-extra"; -import { - DriverInstance, - ExecutionResult, - ProjectModel, -} from "../../../src/component/configManager/interface"; +import { ExecutionResult, ProjectModel } from "../../../src/component/configManager/interface"; import { DriverContext } from "../../../src/component/driver/interface/commonArgs"; import { setTools } from "../../../src/core/globalVars"; import { MockTools } from "../../core/utils"; -import { ExecutionResult as DriverResult } from "../../../src/component/driver/interface/stepDriver"; import { metadataGraphPermissionUtil } from "../../../src/component/utils/metadataGraphPermssion"; import { TelemetryProperty } from "../../../src/common/telemetry"; import { graphAppId } from "../../../src/component/driver/aad/permissions"; import * as permission from "../../../src/component/driver/aad/permissions"; - -function mockedResolveDriverInstances(log: LogProvider): Result { - return ok([ - { - uses: "arm/deploy", - with: undefined, - instance: { - execute: async (args: unknown, context: DriverContext): Promise => { - return { result: ok(new Map()), summaries: [] }; - }, - }, - }, - ]); -} +import { mockedResolveDriverInstances } from "../coordinator/coordinator.test"; describe("metadata graph permission util", () => { const manifestContent = ` @@ -44,7 +26,11 @@ describe("metadata graph permission util", () => { { "id": "User.Read", "type": "Scope" - } + }, + { + "id": "User.Read.All", + "type": "Role" + } ] } ] @@ -91,9 +77,10 @@ describe("metadata graph permission util", () => { let props: any = {}; await metadataGraphPermissionUtil.parseAadManifest(ymlPath, mockProjectModel, props); assert(props[TelemetryProperty.GraphPermission] === "true"); - assert(props[TelemetryProperty.GraphPermissionHasRole] === "false"); + assert(props[TelemetryProperty.GraphPermissionHasRole] === "true"); assert(props[TelemetryProperty.GraphPermissionHasAdminScope] === "false"); assert(props[TelemetryProperty.GraphPermissionScopes] === "User.Read"); + assert(props[TelemetryProperty.GraphPermissionRoles] === "User.Read.All"); assert(props[TelemetryProperty.AadManifest] === "true"); // no aad manifest path in aad/update action @@ -102,9 +89,10 @@ describe("metadata graph permission util", () => { props = {}; await metadataGraphPermissionUtil.parseAadManifest(ymlPath, model, props); assert(props[TelemetryProperty.GraphPermission] === "true"); - assert(props[TelemetryProperty.GraphPermissionHasRole] === "false"); + assert(props[TelemetryProperty.GraphPermissionHasRole] === "true"); assert(props[TelemetryProperty.GraphPermissionHasAdminScope] === "false"); assert(props[TelemetryProperty.GraphPermissionScopes] === "User.Read"); + assert(props[TelemetryProperty.GraphPermissionRoles] === "User.Read.All"); assert(props[TelemetryProperty.AadManifest] === "true"); }); @@ -122,21 +110,21 @@ describe("metadata graph permission util", () => { it("getPermissionSummary no graph permission map", async () => { sandbox.stub(permission, "getDetailedGraphPermissionMap").returns(null); const manifest = JSON.parse(manifestContent); - const res = metadataGraphPermissionUtil.getPermissionSummary(manifest); + const res = metadataGraphPermissionUtil.summary(manifest); assert(res === undefined); }); it("getPermissionSummary no graph permission", async () => { const manifest = JSON.parse(manifestContent); manifest.requiredResourceAccess = []; - const res: any = metadataGraphPermissionUtil.getPermissionSummary(manifest); + const res: any = metadataGraphPermissionUtil.summary(manifest); assert(res["hasGraphPermission"] === false); }); it("getPermissionSummary graph permission is uuid", async () => { const manifest = JSON.parse(manifestContent); manifest.requiredResourceAccess[0].resourceAppId = graphAppId; - const res = metadataGraphPermissionUtil.getPermissionSummary(manifest); + const res = metadataGraphPermissionUtil.summary(manifest); assert(res !== undefined); }); @@ -152,7 +140,7 @@ describe("metadata graph permission util", () => { type: "Scope", } ); - const res: any = metadataGraphPermissionUtil.getPermissionSummary(manifest); + const res: any = metadataGraphPermissionUtil.summary(manifest); assert(res["hasRole"] === true); assert(res["hasAdminScope"] === true); assert(res["hasGraphPermission"] === true); diff --git a/packages/fx-core/tests/component/util/metadataRscPermissionUtil.test.ts b/packages/fx-core/tests/component/util/metadataRscPermissionUtil.test.ts new file mode 100644 index 0000000000..e1634ff34a --- /dev/null +++ b/packages/fx-core/tests/component/util/metadataRscPermissionUtil.test.ts @@ -0,0 +1,201 @@ +import { err, FxError, LogProvider, ok, Result } from "@microsoft/teamsfx-api"; +import { assert } from "chai"; +import "mocha"; +import sinon from "sinon"; +import fs from "fs-extra"; +import { + DriverInstance, + ExecutionResult, + ProjectModel, +} from "../../../src/component/configManager/interface"; +import { DriverContext } from "../../../src/component/driver/interface/commonArgs"; +import { setTools } from "../../../src/core/globalVars"; +import { MockTools } from "../../core/utils"; +import { ExecutionResult as DriverResult } from "../../../src/component/driver/interface/stepDriver"; +import { ProjectTypeProps, TelemetryProperty } from "../../../src/common/telemetry"; +import { metadataRscPermissionUtil } from "../../../src/component/utils/metadataRscPermission"; +import { manifestUtils } from "../../../src/component/driver/teamsApp/utils/ManifestUtils"; + +function mockedResolveDriverInstances(log: LogProvider): Result { + return ok([ + { + uses: "arm/deploy", + with: undefined, + instance: { + execute: async (args: unknown, context: DriverContext): Promise => { + return { result: ok(new Map()), summaries: [] }; + }, + }, + }, + ]); +} + +describe("metadata rsc permission util", () => { + const manifestContent = ` + { + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", + "manifestVersion": "1.16", + "version": "1.0.0", + "id": "TEAMS_APP_ID", + "packageName": "com.microsoft.teams.extension", + "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": "sso-botAPP_NAME_SUFFIX", + "full": "full name for sso-bot" + }, + "description": { + "short": "short description for sso-bot", + "full": "full description for sso-bot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "BOT_ID", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false, + "commandLists": [ + { + "scopes": [ + "personal", + "team", + "groupchat" + ], + "commands": [ + { + "title": "show", + "description": "Show user profile using Single Sign On feature" + } + ] + } + ] + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "BOT_DOMAIN" + ], + "webApplicationInfo": { + "id": "AAD_APP_CLIENT_ID", + "resource": "api://botid-BOT_ID", + "applicationPermissions": [ + "ChatSettings.Read.Chat" + ] + }, + "authorization": { + "permissions": { + "resourceSpecific": [ + { + "name": "TeamSettings.Read.Group", + "type": "Application" + }, + { + "name": "ChannelMeetingStage.Write.Group", + "type": "Delegated" + } + ] + } + } +} + `; + const version = "1.16"; + const readAppManifestRes = { + version: version, + authorization: { + permissions: { + resourceSpecific: [ + { + name: "TeamSettings.Read.Group", + type: "Application", + }, + { + name: "ChannelMeetingStage.Write.Group", + type: "Delegated", + }, + ], + }, + }, + webApplicationInfo: { + applicationPermissions: ["ChatSettings.Read.Chat"], + }, + }; + const sandbox = sinon.createSandbox(); + const mockProjectModel: ProjectModel = { + version: "1.0.0", + provision: { + name: "provision", + driverDefs: [ + { + uses: "teamsApp/validateManifest", + with: { + manifestPath: "./appPackage/manifest.json", + }, + }, + ], + resolvePlaceholders: () => { + return ["AZURE_SUBSCRIPTION_ID", "AZURE_RESOURCE_GROUP_NAME"]; + }, + execute: async (ctx: DriverContext): Promise => { + return { result: ok(new Map()), summaries: [] }; + }, + resolveDriverInstances: mockedResolveDriverInstances, + }, + environmentFolderPath: "./envs", + }; + let tools: MockTools; + const ymlPath = "teamsapp.yml"; + + beforeEach(() => { + tools = new MockTools(); + setTools(tools); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("parseManifest happy path", async () => { + sandbox.stub(fs, "pathExists").resolves(true); + sandbox.stub(manifestUtils, "readAppManifest").resolves(ok(readAppManifestRes as any)); + let props: any = {}; + await metadataRscPermissionUtil.parseManifest(ymlPath, mockProjectModel, props); + assert(props[ProjectTypeProps.TeamsManifestVersion] === "1.16"); + assert(props[TelemetryProperty.RscDelegated] === "ChannelMeetingStage.Write.Group"); + assert( + props[TelemetryProperty.RscApplication] === "TeamSettings.Read.Group,ChatSettings.Read.Chat" + ); + + // no manifest path in teamsApp/validateManifest action + const model = Object.assign({}, mockProjectModel); + model.provision!.driverDefs[0].with = undefined; + props = {}; + await metadataRscPermissionUtil.parseManifest(ymlPath, model, props); + assert(props[ProjectTypeProps.TeamsManifestVersion] === "1.16"); + }); + + it("parseManifest no manifest", async () => { + sandbox.stub(fs, "pathExists").resolves(false); + const props: any = {}; + await metadataRscPermissionUtil.parseManifest(ymlPath, mockProjectModel, props); + assert(props[ProjectTypeProps.TeamsManifestVersion] === undefined); + }); +}); diff --git a/packages/fx-core/tests/component/utils.test.ts b/packages/fx-core/tests/component/utils.test.ts index 1141d80f92..610937f597 100644 --- a/packages/fx-core/tests/component/utils.test.ts +++ b/packages/fx-core/tests/component/utils.test.ts @@ -277,7 +277,6 @@ describe("TeamsFxTelemetryReporter", () => { success: "no", "error-code": "source.name", "error-type": "user", - "error-message": "message", }); reporterCalled = true; }); @@ -299,7 +298,6 @@ describe("TeamsFxTelemetryReporter", () => { success: "no", "error-code": "my error code", "error-type": "user", - "error-message": "message", "my-property": "value", }); reporterCalled = true; @@ -322,7 +320,6 @@ describe("TeamsFxTelemetryReporter", () => { .stub(mockedTelemetryReporter, "sendTelemetryErrorEvent") .callsFake((eventName, properties, measurements, errorProps) => { expect(errorProps).include("test"); - expect(errorProps).include("error-message"); reporterCalled = true; }); diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index a1b075320d..0d70bf20ac 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -35,6 +35,7 @@ import { } from "../../src/common/projectTypeChecker"; import { ErrorType, + ListAPIResult, SpecParser, SpecParserError, ValidationStatus, @@ -87,6 +88,8 @@ import { import { HubOptions } from "../../src/question/other"; import { validationUtils } from "../../src/ui/validationUtils"; import { MockTools, randomAppName } from "./utils"; +import { ValidateWithTestCasesDriver } from "../../src/component/driver/teamsApp/validateTestCases"; +import { pluginManifestUtils } from "../../src/component/driver/teamsApp/utils/PluginManifestUtils"; const tools = new MockTools(); @@ -240,7 +243,7 @@ describe("Core basic APIs", () => { it("deploy aad manifest happy path", async () => { const promtionOnVSC = - 'Your Microsoft Entra app has been deployed successfully. To view that, click "Learn more"'; + 'Your Microsoft Entra app has been deployed successfully. To view that, click "More info"'; const core = new FxCore(tools); const showMessage = sandbox.spy(tools.ui, "showMessage") as unknown as sinon.SinonSpy< @@ -272,12 +275,12 @@ describe("Core basic APIs", () => { assert.equal(showMessage.getCall(0).args[0], "info"); assert.equal(showMessage.getCall(0).args[1], promtionOnVSC); assert.isFalse(showMessage.getCall(0).args[2]); - assert.equal(showMessage.getCall(0).args[3], "Learn more"); + assert.equal(showMessage.getCall(0).args[3], "More info"); assert.isFalse(openUrl.called); }); - it("deploy aad manifest happy path with click learn more", async () => { + it("deploy aad manifest happy path with click more info", async () => { const core = new FxCore(tools); - sandbox.stub(tools.ui, "showMessage").resolves(ok("Learn more")); + sandbox.stub(tools.ui, "showMessage").resolves(ok("More info")); sandbox.stub(tools.ui, "openUrl").resolves(ok(true)); const appName = await mockV3Project(); sandbox @@ -897,6 +900,7 @@ describe("createEnvCopyV3", async () => { const sourceEnvContent = [ "# this is a comment", "TEAMSFX_ENV=dev", + "APP_NAME_SUFFIX=dev", "", "_KEY1=value1", "KEY2=value2", @@ -939,17 +943,21 @@ describe("createEnvCopyV3", async () => { writeStreamContent[1] === `TEAMSFX_ENV=newEnv${os.EOL}`, "TEAMSFX_ENV's value should be new env name" ); - assert(writeStreamContent[2] === `${os.EOL}`, "empty line should be coped"); assert( - writeStreamContent[3] === `_KEY1=${os.EOL}`, + writeStreamContent[2] === `APP_NAME_SUFFIX=newEnv${os.EOL}`, + "APP_NAME_SUFFIX's value should be new env name" + ); + assert(writeStreamContent[3] === `${os.EOL}`, "empty line should be coped"); + assert( + writeStreamContent[4] === `_KEY1=${os.EOL}`, "key starts with _ should be copied with empty value" ); assert( - writeStreamContent[4] === `KEY2=${os.EOL}`, + writeStreamContent[5] === `KEY2=${os.EOL}`, "key not starts with _ should be copied with empty value" ); assert( - writeStreamContent[5] === `SECRET_KEY3=${os.EOL}`, + writeStreamContent[6] === `SECRET_KEY3=${os.EOL}`, "key not starts with SECRET_ should be copied with empty value" ); }); @@ -1047,6 +1055,28 @@ describe("Teams app APIs", async () => { sinon.assert.calledOnce(runSpy); }); + it("validate with test cases", async () => { + const appName = await mockV3Project(); + + const mockedEnvRestore = mockedEnv({ + [FeatureFlagName.AsyncAppValidation]: "true", + }); + + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.TeamsAppPackageFilePath]: ".\\build\\appPackage\\appPackage.dev.zip", + [QuestionNames.ValidateMethod]: "validateWithTestCases", + projectPath: path.join(os.tmpdir(), appName), + }; + + const runSpy = sinon.spy(ValidateWithTestCasesDriver.prototype, "execute"); + await core.validateApplication(inputs); + sinon.assert.calledOnce(runSpy); + + mockedEnvRestore(); + }); + it("create app package", async () => { setTools(tools); const appName = await mockV3Project(); @@ -1430,193 +1460,187 @@ describe("isEnvFile", async () => { assert.isTrue(res.value); } }); - - describe("getQuestions", async () => { - const sandbox = sinon.createSandbox(); - let mockedEnvRestore: RestoreFn = () => {}; - afterEach(() => { - sandbox.restore(); - mockedEnvRestore(); - }); - it("happy path", async () => { - mockedEnvRestore = mockedEnv({ - TEAMSFX_CLI_DOTNET: "false", - [FeatureFlagName.CopilotPlugin]: "false", - }); - const core = new FxCore(tools); - const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); - assert.isTrue(res.isOk()); - if (res.isOk()) { - const node = res.value; - const names: string[] = []; - collectNodeNames(node!, names); - assert.deepEqual(names, [ - "addin-office-capability", - "capabilities", - "bot-host-type-trigger", - "spfx-solution", - "spfx-install-latest-package", - "spfx-framework-type", - "spfx-webpart-name", - "spfx-folder", - "me-architecture", - "openapi-spec-location", - "api-operation", - "api-me-auth", - // "custom-copilot-rag", - // "openapi-spec-location", - // "api-operation", - "custom-copilot-agent", - "programming-language", - "llm-service", - "azure-openai-key", - "azure-openai-endpoint", - "openai-key", - "office-addin-framework-type", - "folder", - "app-name", - ]); - } +}); +describe("getQuestions", async () => { + const sandbox = sinon.createSandbox(); + let mockedEnvRestore: RestoreFn = () => {}; + afterEach(() => { + sandbox.restore(); + mockedEnvRestore(); + }); + it("happy path", async () => { + mockedEnvRestore = mockedEnv({ + TEAMSFX_CLI_DOTNET: "false", + [FeatureFlagName.CopilotPlugin]: "false", }); - it("happy path with runtime", async () => { - mockedEnvRestore = mockedEnv({ - TEAMSFX_CLI_DOTNET: "true", - [FeatureFlagName.CopilotPlugin]: "false", - }); - const core = new FxCore(tools); - const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); - assert.isTrue(res.isOk()); - if (res.isOk()) { - const node = res.value; - const names: string[] = []; - collectNodeNames(node!, names); - assert.deepEqual(names, [ - "runtime", - "addin-office-capability", - "capabilities", - "bot-host-type-trigger", - "spfx-solution", - "spfx-install-latest-package", - "spfx-framework-type", - "spfx-webpart-name", - "spfx-folder", - "me-architecture", - "openapi-spec-location", - "api-operation", - "api-me-auth", - // "custom-copilot-rag", - // "openapi-spec-location", - // "api-operation", - "custom-copilot-agent", - "programming-language", - "llm-service", - "azure-openai-key", - "azure-openai-endpoint", - "openai-key", - "office-addin-framework-type", - "folder", - "app-name", - ]); - } + const core = new FxCore(tools); + const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); + assert.isTrue(res.isOk()); + if (res.isOk()) { + const node = res.value; + const names: string[] = []; + collectNodeNames(node!, names); + assert.deepEqual(names, [ + "capabilities", + "bot-host-type-trigger", + "spfx-solution", + "spfx-install-latest-package", + "spfx-framework-type", + "spfx-webpart-name", + "spfx-folder", + "me-architecture", + "openapi-spec-location", + "api-operation", + "api-me-auth", + "custom-copilot-rag", + "openapi-spec-location", + "api-operation", + "custom-copilot-agent", + "programming-language", + "llm-service", + "azure-openai-key", + "azure-openai-endpoint", + "openai-key", + "office-addin-framework-type", + "folder", + "app-name", + ]); + } + }); + it("happy path with runtime", async () => { + mockedEnvRestore = mockedEnv({ + TEAMSFX_CLI_DOTNET: "true", + [FeatureFlagName.CopilotPlugin]: "false", }); + const core = new FxCore(tools); + const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); + assert.isTrue(res.isOk()); + if (res.isOk()) { + const node = res.value; + const names: string[] = []; + collectNodeNames(node!, names); + assert.deepEqual(names, [ + "runtime", + "capabilities", + "bot-host-type-trigger", + "spfx-solution", + "spfx-install-latest-package", + "spfx-framework-type", + "spfx-webpart-name", + "spfx-folder", + "me-architecture", + "openapi-spec-location", + "api-operation", + "api-me-auth", + "custom-copilot-rag", + "openapi-spec-location", + "api-operation", + "custom-copilot-agent", + "programming-language", + "llm-service", + "azure-openai-key", + "azure-openai-endpoint", + "openai-key", + "office-addin-framework-type", + "folder", + "app-name", + ]); + } + }); - it("happy path: API Copilot plugin enabled", async () => { - const restore = mockedEnv({ - [FeatureFlagName.CopilotPlugin]: "true", - [FeatureFlagName.ApiCopilotPlugin]: "true", - }); - const core = new FxCore(tools); - const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); - assert.isTrue(res.isOk()); - if (res.isOk()) { - const node = res.value; - const names: string[] = []; - collectNodeNames(node!, names); - assert.deepEqual(names, [ - "addin-office-capability", - "capabilities", - "bot-host-type-trigger", - "spfx-solution", - "spfx-install-latest-package", - "spfx-framework-type", - "spfx-webpart-name", - "spfx-folder", - "me-architecture", - "openapi-spec-location", - "api-operation", - "api-me-auth", - // "custom-copilot-rag", - // "openapi-spec-location", - // "api-operation", - "custom-copilot-agent", - "programming-language", - "llm-service", - "azure-openai-key", - "azure-openai-endpoint", - "openai-key", - "office-addin-framework-type", - "folder", - "app-name", - ]); - } - restore(); + it("happy path: API Copilot plugin enabled", async () => { + const restore = mockedEnv({ + [FeatureFlagName.CopilotPlugin]: "true", + [FeatureFlagName.ApiCopilotPlugin]: "true", }); + const core = new FxCore(tools); + const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); + assert.isTrue(res.isOk()); + if (res.isOk()) { + const node = res.value; + const names: string[] = []; + collectNodeNames(node!, names); + assert.deepEqual(names, [ + "capabilities", + "bot-host-type-trigger", + "spfx-solution", + "spfx-install-latest-package", + "spfx-framework-type", + "spfx-webpart-name", + "spfx-folder", + "me-architecture", + "openapi-spec-location", + "api-operation", + "api-me-auth", + "custom-copilot-rag", + "openapi-spec-location", + "api-operation", + "custom-copilot-agent", + "programming-language", + "llm-service", + "azure-openai-key", + "azure-openai-endpoint", + "openai-key", + "office-addin-framework-type", + "folder", + "app-name", + ]); + } + restore(); + }); - it("happy path: copilot feature enabled but not API Copilot plugin", async () => { - const restore = mockedEnv({ - [FeatureFlagName.CopilotPlugin]: "true", - [FeatureFlagName.ApiCopilotPlugin]: "false", - }); - const core = new FxCore(tools); - const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); - assert.isTrue(res.isOk()); - if (res.isOk()) { - const node = res.value; - const names: string[] = []; - collectNodeNames(node!, names); - assert.deepEqual(names, [ - "addin-office-capability", - "capabilities", - "bot-host-type-trigger", - "spfx-solution", - "spfx-install-latest-package", - "spfx-framework-type", - "spfx-webpart-name", - "spfx-folder", - "me-architecture", - "openapi-spec-location", - "api-operation", - "api-me-auth", - // "custom-copilot-rag", - // "openapi-spec-location", - // "api-operation", - "custom-copilot-agent", - "programming-language", - "llm-service", - "azure-openai-key", - "azure-openai-endpoint", - "openai-key", - "office-addin-framework-type", - "folder", - "app-name", - ]); - } - restore(); + it("happy path: copilot feature enabled but not API Copilot plugin", async () => { + const restore = mockedEnv({ + [FeatureFlagName.CopilotPlugin]: "true", + [FeatureFlagName.ApiCopilotPlugin]: "false", }); + const core = new FxCore(tools); + const res = await core.getQuestions(Stage.create, { platform: Platform.CLI_HELP }); + assert.isTrue(res.isOk()); + if (res.isOk()) { + const node = res.value; + const names: string[] = []; + collectNodeNames(node!, names); + assert.deepEqual(names, [ + "capabilities", + "bot-host-type-trigger", + "spfx-solution", + "spfx-install-latest-package", + "spfx-framework-type", + "spfx-webpart-name", + "spfx-folder", + "me-architecture", + "openapi-spec-location", + "api-operation", + "api-me-auth", + "custom-copilot-rag", + "openapi-spec-location", + "api-operation", + "custom-copilot-agent", + "programming-language", + "llm-service", + "azure-openai-key", + "azure-openai-endpoint", + "openai-key", + "office-addin-framework-type", + "folder", + "app-name", + ]); + } + restore(); + }); - function collectNodeNames(node: IQTreeNode, names: string[]) { - if (node.data.type !== "group") { - names.push(node.data.name); - } - if (node.children) { - for (const child of node.children) { - collectNodeNames(child, names); - } + function collectNodeNames(node: IQTreeNode, names: string[]) { + if (node.data.type !== "group") { + names.push(node.data.name); + } + if (node.children) { + for (const child of node.children) { + collectNodeNames(child, names); } } - }); + } }); - describe("copilotPlugin", async () => { let mockedEnvRestore: RestoreFn = () => {}; @@ -1643,10 +1667,26 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + isValid: true, + reason: [], + }, + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + isValid: true, + reason: [], + }, + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1677,17 +1717,35 @@ describe("copilotPlugin", async () => { pluginFile: "ai-plugin.json", }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + isValid: true, + reason: [], + }, + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + isValid: true, + reason: [], + }, + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); - sinon.stub(SpecParser.prototype, "generate").resolves({ + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ warnings: [], allSuccess: true, }); sinon.stub(SpecParser.prototype, "list").resolves(listResult); sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon.stub(manifestUtils, "getPluginFilePath").resolves(ok("ai-plugin.json")); sinon.stub(validationUtils, "validateInputs").resolves(undefined); sinon.stub(CopilotPluginHelper, "listPluginExistingOperations").resolves([]); const result = await core.copilotPluginAddAPI(inputs); @@ -1714,12 +1772,29 @@ describe("copilotPlugin", async () => { pluginFile: "ai-plugin.json", }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + isValid: true, + reason: [], + }, + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + isValid: true, + reason: [], + }, + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); - sinon.stub(SpecParser.prototype, "generate").resolves({ + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ warnings: [], allSuccess: true, }); @@ -1734,6 +1809,65 @@ describe("copilotPlugin", async () => { } }); + it("add API error when getting plugin path - Copilot plugin", async () => { + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.ApiSpecLocation]: "test.json", + [QuestionNames.ApiOperation]: ["GET /user/{userId}"], + [QuestionNames.ManifestPath]: "manifest.json", + [QuestionNames.Capabilities]: CapabilityOptions.copilotPluginApiSpec().id, + [QuestionNames.DestinationApiSpecFilePath]: "destination.json", + projectPath: path.join(os.tmpdir(), appName), + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + isValid: true, + reason: [], + }, + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + isValid: true, + reason: [], + }, + ], + validAPICount: 2, + allAPICount: 2, + }; + + const core = new FxCore(tools); + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ + warnings: [], + allSuccess: true, + }); + sinon.stub(SpecParser.prototype, "list").resolves(listResult); + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(manifestUtils, "getPluginFilePath") + .resolves(err(new SystemError("testError", "testError", "", ""))); + sinon.stub(validationUtils, "validateInputs").resolves(undefined); + sinon.stub(CopilotPluginHelper, "listPluginExistingOperations").resolves([]); + const result = await core.copilotPluginAddAPI(inputs); + + assert.isTrue(result.isErr()); + if (result.isErr()) { + assert.equal(result.error.name, "testError"); + } + }); + it("add API - return multiple auth error", async () => { const appName = await mockV3Project(); mockedEnvRestore = mockedEnv({ @@ -1756,28 +1890,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key2", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + auth: { + name: "bearerAuth2", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1815,28 +1963,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server2", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server2", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1874,28 +2036,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1939,28 +2115,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2013,28 +2203,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2096,28 +2300,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2172,12 +2390,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2219,28 +2437,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2263,12 +2495,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2313,28 +2545,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2400,12 +2646,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2447,28 +2693,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2491,7 +2751,7 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2529,12 +2789,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2576,28 +2836,41 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2662,12 +2935,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2709,28 +2982,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2782,12 +3069,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2830,28 +3117,42 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + isValid: true, + reason: [], }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2914,12 +3215,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2967,10 +3268,26 @@ describe("copilotPlugin", async () => { }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + APIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + isValid: true, + reason: [], + }, + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + isValid: true, + reason: [], + }, + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ @@ -3057,6 +3374,73 @@ describe("copilotPlugin", async () => { assert.isTrue(result.isErr()); }); + describe("listPluginApiSpecs", async () => { + it("success", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(ok(["apispec.json"])); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isOk()); + }); + + it("read manifest error", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + sinon + .stub(manifestUtils, "_readAppManifest") + .resolves(err(new SystemError("read manifest error", "read manifest error", "", ""))); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.equal(res.error.name, "read manifest error"); + } + }); + + it("get api spec error", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(err(new SystemError("get plugin error", "get plugin error", "", ""))); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.equal(res.error.name, "get plugin error"); + } + }); + }); + it("load OpenAI manifest - should run successful", async () => { const core = new FxCore(tools); const inputs = { domain: "mydomain.com" }; @@ -3129,7 +3513,9 @@ describe("copilotPlugin", async () => { sinon .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ APIs: [], allAPICount: 0, validAPICount: 0 }); try { await core.copilotPluginListOperations(inputs as any); @@ -3153,7 +3539,9 @@ describe("copilotPlugin", async () => { sinon .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ APIs: [], allAPICount: 0, validAPICount: 0 }); try { await core.copilotPluginListOperations(inputs as any); diff --git a/packages/fx-core/tests/core/other.test.ts b/packages/fx-core/tests/core/other.test.ts index 9e3ce133c1..e5e2ae3bae 100644 --- a/packages/fx-core/tests/core/other.test.ts +++ b/packages/fx-core/tests/core/other.test.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ResourceManagementClient } from "@azure/arm-resources"; import { TokenCredential } from "@azure/identity"; import { AzureAccountProvider, Settings, SubscriptionInfo } from "@microsoft/teamsfx-api"; import { assert } from "chai"; @@ -14,8 +13,11 @@ import sinon from "sinon"; import { isFeatureFlagEnabled } from "../../src/common/featureFlags"; import { execPowerShell, execShell } from "../../src/common/local/process"; import { TaskDefinition } from "../../src/common/local/taskDefinition"; -import { isValidProject } from "../../src/common/projectSettingsHelper"; -import { resourceGroupHelper } from "../../src/component/utils/ResourceGroupHelper"; +import { + isValidOfficeAddInProject, + isValidProject, + isValidProjectV3, +} from "../../src/common/projectSettingsHelper"; import { cpUtils } from "../../src/component/utils/depsChecker/cpUtils"; import { MyTokenCredential } from "../plugins/solution/util"; import { randomAppName } from "./utils"; @@ -229,6 +231,7 @@ describe("Other test case", () => { }; sandbox.stub(fs, "readJsonSync").returns(projectSettings); sandbox.stub(fs, "existsSync").returns(true); + sandbox.stub(fs, "readdirSync").returns([]); const isValid = isValidProject("aaa"); assert.isTrue(isValid); }); @@ -243,6 +246,7 @@ describe("Other test case", () => { }; sandbox.stub(fs, "readJsonSync").returns(settings); sandbox.stub(fs, "existsSync").returns(true); + sandbox.stub(fs, "readdirSync").returns([]); const isValid = isValidProject("aaa"); assert.isTrue(isValid); } finally { @@ -279,4 +283,12 @@ describe("Other test case", () => { mockedEnvRestore(); } }); + it("projectSettingsHelper - isValidProjectV3 - office add-in", () => { + sandbox.stub(fs, "readdirSync").returns(["manifest.xml"] as any); + assert.equal(isValidProjectV3("test"), false); + }); + it("projectSettingsHelper - isValidOfficeAddInProject - metaos add-in", () => { + sandbox.stub(fs, "readdirSync").returns(["manifest.json", "manifest.xml"] as any); + assert.equal(isValidOfficeAddInProject("test"), false); + }); }); diff --git a/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/resources/ai-plugin.json b/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/resources/ai-plugin.json index a5e4726cdb..355c9bb2b2 100644 --- a/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/resources/ai-plugin.json +++ b/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/resources/ai-plugin.json @@ -1,6 +1,6 @@ { "schema_version": "v2", - "name_for_human": "Plugin", + "name_for_human": "Plugin${{APP_NAME_SUFFIX}}", "description_for_model": "Plugin", "runtimes": [ diff --git a/packages/fx-core/tests/question/create.test.ts b/packages/fx-core/tests/question/create.test.ts index ccc6571719..a2312231c3 100644 --- a/packages/fx-core/tests/question/create.test.ts +++ b/packages/fx-core/tests/question/create.test.ts @@ -1,5 +1,7 @@ +import { AppYmlGenerator } from "./../../src/core/middleware/utils/appYmlGenerator"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { ErrorType, SpecParser, ValidationStatus, WarningType } from "@microsoft/m365-spec-parser"; import { Context, FuncValidation, @@ -12,27 +14,34 @@ import { Platform, Question, SingleSelectQuestion, - StaticOptions, UserInteraction, ok, } from "@microsoft/teamsfx-api"; import axios from "axios"; -import { assert } from "chai"; +import { assert, expect } from "chai"; import fs from "fs-extra"; import "mocha"; import mockedEnv, { RestoreFn } from "mocked-env"; import * as path from "path"; import sinon from "sinon"; import { FeatureFlagName } from "../../src/common/constants"; +import { isApiCopilotPluginEnabled } from "../../src/common/featureFlags"; import { getLocalizedString } from "../../src/common/localizeUtils"; -import { ErrorType, ValidationStatus, WarningType, SpecParser } from "@microsoft/m365-spec-parser"; import { AppDefinition } from "../../src/component/driver/teamsApp/interfaces/appdefinitions/appDefinition"; import { manifestUtils } from "../../src/component/driver/teamsApp/utils/ManifestUtils"; +import { pluginManifestUtils } from "../../src/component/driver/teamsApp/utils/PluginManifestUtils"; +import { convertToLangKey } from "../../src/component/generator/utils"; +import * as utils from "../../src/component/utils"; import { setTools } from "../../src/core/globalVars"; import { - MeArchitectureOptions, + ApiMessageExtensionAuthOptions, CapabilityOptions, + CustomCopilotAssistantOptions, + CustomCopilotRagOptions, + MeArchitectureOptions, NotificationTriggerOptions, + OfficeAddinHostOptions, + ProgrammingLanguage, ProjectTypeOptions, RuntimeOptions, SPFxVersionOptionIds, @@ -44,45 +53,40 @@ import { createSampleProjectQuestionNode, folderQuestion, getLanguageOptions, - getAddinHostOptions, - getTemplate, officeAddinHostingQuestion, openAIPluginManifestLocationQuestion, programmingLanguageQuestion, - ApiMessageExtensionAuthOptions, - CustomCopilotRagOptions, - CustomCopilotAssistantOptions, - OfficeAddinCapabilityOptions, - ProgrammingLanguage, + officeAddinFrameworkQuestion, + getAddinFrameworkOptions, } from "../../src/question/create"; import { QuestionNames } from "../../src/question/questionNames"; import { QuestionTreeVisitor, traverse } from "../../src/ui/visitor"; import { MockTools, MockUserInteraction, randomAppName } from "../core/utils"; -import { isApiCopilotPluginEnabled } from "../../src/common/featureFlags"; import { MockedLogProvider, MockedUserInteraction } from "../plugins/solution/util"; -import * as utils from "../../src/component/utils"; -import { pluginManifestUtils } from "../../src/component/driver/teamsApp/utils/PluginManifestUtils"; -import { convertToLangKey } from "../../src/component/generator/utils"; +import { sampleProvider } from "../../src/common/samples"; +import { OfficeAddinProjectConfig } from "../../src/component/generator/officeXMLAddin/projectConfig"; export async function callFuncs(question: Question, inputs: Inputs, answer?: string) { - if (question.default && typeof question.default !== "string") { - await (question.default as LocalFunc)(inputs); - } - - if ( - (question.type === "singleSelect" || question.type === "multiSelect") && - typeof question.dynamicOptions !== "object" && - question.dynamicOptions - ) { - await question.dynamicOptions(inputs); - } - if (answer && (question as any).validation?.validFunc) { - await (question as any).validation.validFunc(answer, inputs); - } - - if ((question as any).placeholder && typeof (question as any).placeholder !== "string") { - await (question as any).placeholder(inputs); - } + try { + if (question.default && typeof question.default !== "string") { + await (question.default as LocalFunc)(inputs); + } + + if ( + (question.type === "singleSelect" || question.type === "multiSelect") && + typeof question.dynamicOptions !== "object" && + question.dynamicOptions + ) { + await question.dynamicOptions(inputs); + } + if (answer && (question as any).validation?.validFunc) { + await (question as any).validation.validFunc(answer, inputs); + } + + if ((question as any).placeholder && typeof (question as any).placeholder !== "string") { + await (question as any).placeholder(inputs); + } + } catch (e) {} } describe("scaffold question", () => { @@ -99,7 +103,8 @@ describe("scaffold question", () => { beforeEach(() => { mockedEnvRestore = mockedEnv({ [FeatureFlagName.CopilotPlugin]: "false", - [FeatureFlagName.TeamsSampleConfigBranch]: "dev", + [FeatureFlagName.SampleConfigBranch]: "dev", + [FeatureFlagName.OfficeXMLAddin]: "false", }); }); afterEach(() => { @@ -129,6 +134,7 @@ describe("scaffold question", () => { } return ok({ type: "success", result: undefined }); }; + sandbox.stub(sampleProvider, "SampleCollection").resolves({ samples: ["1"] }); await traverse(createSampleProjectQuestionNode(), inputs, ui, undefined, visitor); assert.deepEqual(questions, [QuestionNames.Samples, QuestionNames.Folder]); }); @@ -284,6 +290,11 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 3); return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 2); + return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -362,6 +373,11 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 3); return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 2); + return ok({ type: "success", result: ApiMessageExtensionAuthOptions.apiKey().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -400,6 +416,76 @@ describe("scaffold question", () => { ]); }); + it("traverse in vscode me from new api (sso auth)", async () => { + mockedEnvRestore = mockedEnv({ + [FeatureFlagName.ApiKey]: "true", + [FeatureFlagName.ApiMeSSO]: "true", + }); + const inputs: Inputs = { + platform: Platform.VSCode, + }; + const questions: string[] = []; + const visitor: QuestionTreeVisitor = async ( + question: Question, + ui: UserInteraction, + inputs: Inputs, + step?: number, + totalSteps?: number + ) => { + questions.push(question.name); + + await callFuncs(question, inputs); + + if (question.name === QuestionNames.ProjectType) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 5); + return ok({ type: "success", result: ProjectTypeOptions.me().id }); + } else if (question.name === QuestionNames.Capabilities) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 3); + const title = + typeof question.title === "function" ? await question.title(inputs) : question.title; + assert.equal( + title, + getLocalizedString("core.createProjectQuestion.projectType.messageExtension.title") + ); + return ok({ type: "success", result: CapabilityOptions.m365SearchMe().id }); + } else if (question.name === QuestionNames.MeArchitectureType) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 3); + return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 3); + return ok({ + type: "success", + result: ApiMessageExtensionAuthOptions.microsoftEntra().id, + }); + } else if (question.name === QuestionNames.ProgrammingLanguage) { + return ok({ type: "success", result: "javascript" }); + } else if (question.name === QuestionNames.AppName) { + return ok({ type: "success", result: "test001" }); + } else if (question.name === QuestionNames.Folder) { + return ok({ type: "success", result: "./" }); + } + return ok({ type: "success", result: undefined }); + }; + await traverse(createProjectQuestionNode(), inputs, ui, undefined, visitor); + assert.deepEqual(questions, [ + QuestionNames.ProjectType, + QuestionNames.Capabilities, + QuestionNames.MeArchitectureType, + QuestionNames.ApiMEAuth, + QuestionNames.ProgrammingLanguage, + QuestionNames.Folder, + QuestionNames.AppName, + ]); + }); + it("traverse in vscode api me from existing api", async () => { const inputs: Inputs = { platform: Platform.VSCode, @@ -464,7 +550,7 @@ describe("scaffold question", () => { ]); }); - it("traverse in vscode Outlook addin", async () => { + it("traverse in vscode Outlook addin import", async () => { const inputs: Inputs = { platform: Platform.VSCode, }; @@ -486,27 +572,16 @@ describe("scaffold question", () => { return ok({ type: "success", result: ProjectTypeOptions.outlookAddin().id }); } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.deepEqual(options, [ - ...CapabilityOptions.outlookAddinItems(), - CapabilityOptions.outlookAddinImport(), - ]); - const title = - typeof question.title === "function" ? await question.title(inputs) : question.title; - assert.equal( - title, - getLocalizedString("core.createProjectQuestion.projectType.outlookAddin.title") + const options = (await select.dynamicOptions!(inputs)) as OptionItem[]; + assert.deepEqual( + options.map((o) => o.id), + ["json-taskpane", CapabilityOptions.outlookAddinImport().id] ); return ok({ type: "success", result: CapabilityOptions.outlookAddinImport().id }); } else if (question.name === QuestionNames.OfficeAddinFolder) { return ok({ type: "success", result: "./" }); } else if (question.name === QuestionNames.OfficeAddinManifest) { return ok({ type: "success", result: "./manifest.json" }); - } else if (question.name === QuestionNames.ProgrammingLanguage) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 1); - return ok({ type: "success", result: "typescript" }); } else if (question.name === QuestionNames.Folder) { return ok({ type: "success", result: "./" }); } else if (question.name === QuestionNames.AppName) { @@ -520,7 +595,6 @@ describe("scaffold question", () => { QuestionNames.Capabilities, QuestionNames.OfficeAddinFolder, QuestionNames.OfficeAddinManifest, - QuestionNames.ProgrammingLanguage, QuestionNames.Folder, QuestionNames.AppName, ]); @@ -546,14 +620,14 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 5); return ok({ type: "success", result: ProjectTypeOptions.officeXMLAddin().id }); - } else if (question.name === QuestionNames.OfficeAddinCapability) { + } else if (question.name === QuestionNames.OfficeAddinHost) { const select = question as SingleSelectQuestion; const options = await select.staticOptions; assert.deepEqual(options, [ - ProjectTypeOptions.outlookAddin(), - OfficeAddinCapabilityOptions.word(), - OfficeAddinCapabilityOptions.excel(), - OfficeAddinCapabilityOptions.powerpoint(), + OfficeAddinHostOptions.outlook(), + OfficeAddinHostOptions.word(), + OfficeAddinHostOptions.excel(), + OfficeAddinHostOptions.powerpoint(), ]); const title = typeof question.title === "function" ? await question.title(inputs) : question.title; @@ -561,18 +635,22 @@ describe("scaffold question", () => { title, getLocalizedString("core.createProjectQuestion.officeXMLAddin.create.title") ); - return ok({ type: "success", result: OfficeAddinCapabilityOptions.excel().id }); + return ok({ type: "success", result: OfficeAddinHostOptions.excel().id }); } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.deepEqual(options, CapabilityOptions.officeXMLAddinHostOptionItems("excel")); + const items = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.officeXMLAddin().id, + OfficeAddinHostOptions.excel().id + ); + assert.deepEqual(options, items); const title = typeof question.title === "function" ? await question.title(inputs) : question.title; assert.equal( title, getLocalizedString("core.createProjectQuestion.officeXMLAddin.excel.create.title") ); - return ok({ type: "success", result: "react" }); + return ok({ type: "success", result: "excel-react" }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); @@ -588,7 +666,7 @@ describe("scaffold question", () => { await traverse(createProjectQuestionNode(), inputs, ui, undefined, visitor); assert.deepEqual(questions, [ QuestionNames.ProjectType, - QuestionNames.OfficeAddinCapability, + QuestionNames.OfficeAddinHost, QuestionNames.Capabilities, QuestionNames.ProgrammingLanguage, QuestionNames.Folder, @@ -619,16 +697,10 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.deepEqual(options, [ - ...CapabilityOptions.officeAddinItems(), - CapabilityOptions.officeAddinImport(), - ]); - const title = - typeof question.title === "function" ? await question.title(inputs) : question.title; - assert.equal( - title, - getLocalizedString("core.createProjectQuestion.projectType.officeAddin.title") + const items = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.officeAddin().id ); + assert.deepEqual(options, items); return ok({ type: "success", result: CapabilityOptions.officeAddinImport().id }); } else if (question.name === QuestionNames.OfficeAddinFolder) { return ok({ type: "success", result: "./" }); @@ -654,8 +726,6 @@ describe("scaffold question", () => { QuestionNames.Capabilities, QuestionNames.OfficeAddinFolder, QuestionNames.OfficeAddinManifest, - QuestionNames.ProgrammingLanguage, - QuestionNames.OfficeAddinFramework, QuestionNames.Folder, QuestionNames.AppName, ]); @@ -1065,7 +1135,7 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotBasic().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; @@ -1120,18 +1190,18 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotRag().id }); } else if (question.name === QuestionNames.CustomCopilotRag) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 4); + assert.isTrue(options.length === 2); return ok({ type: "success", result: CustomCopilotRagOptions.customize().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: "typescript" }); + assert.isTrue(options.length === 3); + return ok({ type: "success", result: "python" }); } else if (question.name === QuestionNames.LLMService) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); @@ -1152,7 +1222,7 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - // QuestionNames.CustomCopilotRag, + QuestionNames.CustomCopilotRag, QuestionNames.ProgrammingLanguage, QuestionNames.LLMService, QuestionNames.AzureOpenAIKey, @@ -1184,18 +1254,18 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotRag().id }); } else if (question.name === QuestionNames.CustomCopilotRag) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 4); + assert.isTrue(options.length === 2); return ok({ type: "success", result: CustomCopilotRagOptions.customize().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: "typescript" }); + assert.isTrue(options.length === 3); + return ok({ type: "success", result: "python" }); } else if (question.name === QuestionNames.LLMService) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); @@ -1216,7 +1286,7 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - // QuestionNames.CustomCopilotRag, + QuestionNames.CustomCopilotRag, QuestionNames.ProgrammingLanguage, QuestionNames.LLMService, QuestionNames.AzureOpenAIKey, @@ -1247,7 +1317,7 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotRag().id }); } else if (question.name === QuestionNames.CustomCopilotRag) { const select = question as SingleSelectQuestion; @@ -1287,15 +1357,15 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - // QuestionNames.CustomCopilotRag, + QuestionNames.CustomCopilotRag, // QuestionNames.ApiSpecLocation, // QuestionNames.ApiOperation, - QuestionNames.ProgrammingLanguage, - QuestionNames.LLMService, - QuestionNames.AzureOpenAIKey, - QuestionNames.AzureOpenAIEndpoint, - QuestionNames.Folder, - QuestionNames.AppName, + // QuestionNames.ProgrammingLanguage, + // QuestionNames.LLMService, + // QuestionNames.AzureOpenAIKey, + // QuestionNames.AzureOpenAIEndpoint, + // QuestionNames.Folder, + // QuestionNames.AppName, ]); }); @@ -1321,7 +1391,7 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotRag().id }); } else if (question.name === QuestionNames.CustomCopilotRag) { const select = question as SingleSelectQuestion; @@ -1353,13 +1423,13 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - // QuestionNames.CustomCopilotRag, - QuestionNames.ProgrammingLanguage, - QuestionNames.LLMService, - QuestionNames.AzureOpenAIKey, - QuestionNames.AzureOpenAIEndpoint, - QuestionNames.Folder, - QuestionNames.AppName, + QuestionNames.CustomCopilotRag, + // QuestionNames.ProgrammingLanguage, + // QuestionNames.LLMService, + // QuestionNames.AzureOpenAIKey, + // QuestionNames.AzureOpenAIEndpoint, + // QuestionNames.Folder, + // QuestionNames.AppName, ]); }); @@ -1385,7 +1455,7 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotAssistant().id }); } else if (question.name === QuestionNames.CustomCopilotAssistant) { const select = question as SingleSelectQuestion; @@ -1446,7 +1516,7 @@ describe("scaffold question", () => { } else if (question.name === QuestionNames.Capabilities) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); + assert.isTrue(options.length === 3); return ok({ type: "success", result: CapabilityOptions.customCopilotAssistant().id }); } else if (question.name === QuestionNames.CustomCopilotAssistant) { const select = question as SingleSelectQuestion; @@ -1506,64 +1576,6 @@ describe("scaffold question", () => { } }); it("traverse in vscode Copilot Plugin from new API (no auth)", async () => { - mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", - }); - const inputs: Inputs = { - platform: Platform.VSCode, - }; - const questions: string[] = []; - const visitor: QuestionTreeVisitor = async ( - question: Question, - ui: UserInteraction, - inputs: Inputs, - step?: number, - totalSteps?: number - ) => { - questions.push(question.name); - await callFuncs(question, inputs); - if (question.name === QuestionNames.ProjectType) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 6); - return ok({ type: "success", result: "copilot-plugin-type" }); - } else if (question.name === QuestionNames.Capabilities) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - const select = question as SingleSelectQuestion; - const options = await select.staticOptions; - assert.isTrue(options.length === 2); - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); - } else if (question.name === QuestionNames.ProgrammingLanguage) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: "typescript" }); - } else if (question.name === QuestionNames.Folder) { - return ok({ type: "success", result: "./" }); - } else if (question.name === QuestionNames.AppName) { - return ok({ type: "success", result: "test001" }); - } - return ok({ type: "success", result: undefined }); - }; - await traverse(createProjectQuestionNode(), inputs, ui, undefined, visitor); - assert.deepEqual(questions, [ - QuestionNames.ProjectType, - QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, - QuestionNames.ProgrammingLanguage, - QuestionNames.Folder, - QuestionNames.AppName, - ]); - }); - - it("traverse in vscode Copilot Plugin from new API (key auth)", async () => { - mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", - }); const inputs: Inputs = { platform: Platform.VSCode, }; @@ -1587,11 +1599,6 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 2); return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - const select = question as SingleSelectQuestion; - const options = await select.staticOptions; - assert.isTrue(options.length === 2); - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.apiKey().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); @@ -1608,7 +1615,6 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, QuestionNames.ProgrammingLanguage, QuestionNames.Folder, QuestionNames.AppName, @@ -1678,7 +1684,6 @@ describe("scaffold question", () => { it("traverse in cli", async () => { mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", TEAMSFX_CLI_DOTNET: "false", }); @@ -1697,8 +1702,6 @@ describe("scaffold question", () => { await callFuncs(question, inputs); if (question.name === QuestionNames.Capabilities) { return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -1712,7 +1715,6 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, QuestionNames.ProgrammingLanguage, QuestionNames.Folder, QuestionNames.AppName, @@ -1841,13 +1843,11 @@ describe("scaffold question", () => { assert.isUndefined(res); }); - it(" validate operations with auth successfully", async () => { - mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", - }); + it(" validate operations successfully with Teams AI project", async () => { const question = apiOperationQuestion(); const inputs: Inputs = { platform: Platform.VSCode, + "custom-copilot-rag": "custom-copilot-rag-customApi", [QuestionNames.ApiSpecLocation]: "apispec", supportedApisFromApiSpec: [ { @@ -1855,7 +1855,6 @@ describe("scaffold question", () => { label: "operation1", groupName: "1", data: { - authName: "auth1", serverUrl: "https://server1", }, }, @@ -1864,60 +1863,302 @@ describe("scaffold question", () => { label: "operation2", groupName: "2", data: { - authName: "auth1", serverUrl: "https://server1", }, }, - ], - }; - - const validationSchema = question.validation as FuncValidation; - const placeholder = (question as any).placeholder(inputs) as string; - const res = await validationSchema.validFunc!(["operation1", "operation2"], inputs); - - assert.isTrue(placeholder.includes("API key")); - assert.isUndefined(res); - }); - - it(" validate operations should return error message when selected APIs with multiple server url", async () => { - const question = apiOperationQuestion(); - const inputs: Inputs = { - platform: Platform.VSCode, - [QuestionNames.ApiSpecLocation]: "apispec", - supportedApisFromApiSpec: [ { - id: "operation1", - label: "operation1", - groupName: "1", + id: "operation3", + label: "operation2", + groupName: "2", data: { - authName: "auth1", serverUrl: "https://server1", }, }, { - id: "operation2", + id: "operation4", label: "operation2", groupName: "2", data: { - authName: "auth1", - serverUrl: "https://server2", + serverUrl: "https://server1", }, }, - ], - }; - - const validationSchema = question.validation as FuncValidation; - const res = await validationSchema.validFunc!(["operation1", "operation2"], inputs); - - assert.equal( - res, - getLocalizedString( - "core.createProjectQuestion.apiSpec.operation.multipleServer", - ["https://server1", "https://server2"].join(", ") - ) - ); - }); - + { + id: "operation5", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation6", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation7", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation8", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation9", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation10", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation11", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const res = await validationSchema.validFunc!( + [ + "operation1", + "operation2", + "operation3", + "operation4", + "operation5", + "operation6", + "operation7", + "operation8", + "operation9", + "operation10", + "operation11", + ], + inputs + ); + + assert.isUndefined(res); + }); + + it(" validate operations successfully due to length limitation", async () => { + const question = apiOperationQuestion(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.ApiSpecLocation]: "apispec", + supportedApisFromApiSpec: [ + { + id: "operation1", + label: "operation1", + groupName: "1", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation2", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation3", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation4", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation5", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation6", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation7", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation8", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation9", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation10", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation11", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const res = await validationSchema.validFunc!( + [ + "operation1", + "operation2", + "operation3", + "operation4", + "operation5", + "operation6", + "operation7", + "operation8", + "operation9", + "operation10", + "operation11", + ], + inputs + ); + + expect(res).to.equal( + "11 API(s) selected. You can select at least one and at most 10 APIs." + ); + }); + + it(" validate operations with auth successfully", async () => { + mockedEnvRestore = mockedEnv({ + [FeatureFlagName.ApiKey]: "true", + }); + const question = apiOperationQuestion(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.ApiSpecLocation]: "apispec", + supportedApisFromApiSpec: [ + { + id: "operation1", + label: "operation1", + groupName: "1", + data: { + authName: "auth1", + serverUrl: "https://server1", + }, + }, + { + id: "operation2", + label: "operation2", + groupName: "2", + data: { + authName: "auth1", + serverUrl: "https://server1", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const placeholder = (question as any).placeholder(inputs) as string; + const res = await validationSchema.validFunc!(["operation1", "operation2"], inputs); + + assert.isTrue(placeholder.includes("API key")); + assert.isUndefined(res); + }); + + it(" validate operations should return error message when selected APIs with multiple server url", async () => { + const question = apiOperationQuestion(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.ApiSpecLocation]: "apispec", + supportedApisFromApiSpec: [ + { + id: "operation1", + label: "operation1", + groupName: "1", + data: { + authName: "auth1", + serverUrl: "https://server1", + }, + }, + { + id: "operation2", + label: "operation2", + groupName: "2", + data: { + authName: "auth1", + serverUrl: "https://server2", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const res = await validationSchema.validFunc!(["operation1", "operation2"], inputs); + + assert.equal( + res, + getLocalizedString( + "core.createProjectQuestion.apiSpec.operation.multipleServer", + ["https://server1", "https://server2"].join(", ") + ) + ); + }); + it(" validate operations should success when selected APIs with multiple server url but only one contains auth", async () => { const question = apiOperationQuestion(); const inputs: Inputs = { @@ -2041,19 +2282,33 @@ describe("scaffold question", () => { errors: [], warnings: [{ content: "warn", type: WarningType.Unknown }], }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "bearerAuth", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + operationId: "getOperation1", + isValid: true, + reason: [], + }, + { + api: "get operation2", + server: "https://server2", + operationId: "getOperation2", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); sandbox.stub(fs, "pathExists").resolves(true); const validationSchema = question.validation as FuncValidation; @@ -2064,7 +2319,7 @@ describe("scaffold question", () => { label: "get operation1", groupName: "GET", data: { - authName: "api_key", + authName: "bearerAuth", serverUrl: "https://server", }, }, @@ -2089,20 +2344,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "bearerAuth", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + operationId: "getOperation1", + isValid: true, + reason: [], + }, + + { + api: "get operation2", + server: "https://server2", + operationId: "getOperation2", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationSchema = question.validation as FuncValidation; const res = await validationSchema.validFunc!("https://www.test.com", inputs); @@ -2112,7 +2382,7 @@ describe("scaffold question", () => { label: "get operation1", groupName: "GET", data: { - authName: "api_key", + authName: "bearerAuth", serverUrl: "https://server", }, }, @@ -2135,19 +2405,35 @@ describe("scaffold question", () => { .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); sandbox.stub(fs, "pathExists").resolves(true); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getOperation1", + isValid: true, + reason: [], + }, + { + api: "get operation2", + server: "https://server2", + operationId: "getOperation2", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); let err: Error | undefined = undefined; try { @@ -2259,19 +2545,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox.stub(manifestUtils, "getOperationIds").returns(["getUserById"]); sandbox.stub(fs, "pathExists").resolves(true); @@ -2301,19 +2603,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox.stub(manifestUtils, "getOperationIds").returns(["getUserById", "getStoreOrder"]); sandbox.stub(fs, "pathExists").resolves(true); @@ -2338,18 +2656,41 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "list") .onFirstCall() - .resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]) + .resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }) .onSecondCall() - .resolves([ - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + .resolves({ + APIs: [ + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") @@ -2390,19 +2731,34 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationRes = await (question.validation as any).validFunc!("test.com", inputs); const additionalValidationRes = await ( @@ -2431,19 +2787,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationRes = await (question.validation as any).validFunc!("test.com", inputs); const additionalValidationRes = await ( @@ -2472,19 +2844,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", - }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + + sandbox.stub(SpecParser.prototype, "list").resolves({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); const res = await (question.additionalValidationOnAccept as any).validFunc( "https://test.com/", @@ -2699,26 +3087,6 @@ describe("scaffold question", () => { }); }); - describe("getAddinHostOptions", () => { - it("should return outlook host", async () => { - const options = getAddinHostOptions({ - platform: Platform.VSCode, - [QuestionNames.ProjectType]: ProjectTypeOptions.outlookAddin().id, - [QuestionNames.Capabilities]: "taskpane", - }); - assert.isTrue(options.length === 1 || options[0].id === "Outlook"); - }); - - it("should return office host", async () => { - const options = getAddinHostOptions({ - platform: Platform.VSCode, - [QuestionNames.ProjectType]: ProjectTypeOptions.officeAddin().id, - [QuestionNames.Capabilities]: "taskpane", - }); - assert.isTrue(options.length === 4); - }); - }); - describe("getLanguageOptions", () => { let mockedEnvRestore: RestoreFn = () => {}; @@ -2747,18 +3115,21 @@ describe("scaffold question", () => { const options = getLanguageOptions({ platform: Platform.VSCode, [QuestionNames.ProjectType]: ProjectTypeOptions.outlookAddin().id, - [QuestionNames.Capabilities]: "taskpane", + [QuestionNames.Capabilities]: "json-taskpane", }); - assert.isTrue(options.length === 1 && options[0].id === "TypeScript"); + assert.deepEqual(options, [{ id: "typescript", label: "TypeScript" }]); }); it("office addin", async () => { const options = getLanguageOptions({ platform: Platform.VSCode, [QuestionNames.ProjectType]: ProjectTypeOptions.officeAddin().id, - [QuestionNames.Capabilities]: "taskpane", + [QuestionNames.Capabilities]: "json-taskpane", [QuestionNames.OfficeAddinFramework]: "default", }); - assert.isTrue(options.length === 2 && options[0].id === "typescript"); + assert.deepEqual(options, [ + { id: "typescript", label: "TypeScript" }, + { id: "javascript", label: "JavaScript" }, + ]); }); it("SPFx", async () => { @@ -2791,17 +3162,6 @@ describe("scaffold question", () => { }); }); - describe("getTemplate", () => { - it("should find taskpane template", () => { - const inputs: Inputs = { - platform: Platform.CLI, - }; - inputs["capabilities"] = ["taskpane"]; - const template = getTemplate(inputs); - assert.equal(template, "taskpane"); - }); - }); - describe("appNameQuestion", () => { const question = appNameQuestion(); const validFunc = (question.validation as FuncValidation).validFunc; @@ -3023,6 +3383,48 @@ describe("scaffold question", () => { options.findIndex((o: OptionItem) => o.id === CapabilityOptions.nonSsoTabAndBot().id) < 0 ); }); + + describe("officeAddinStaticCapabilities()", () => { + it("should return correct capabilities for specific host", () => { + const capabilities = CapabilityOptions.officeAddinStaticCapabilities( + OfficeAddinHostOptions.word().id + ); + assert.equal(capabilities.length, 4); + }); + it("should return correct capabilities without specific host", () => { + const capabilities = CapabilityOptions.officeAddinStaticCapabilities(); + assert.equal(capabilities.length, 16); + }); + }); + + describe("officeAddinDynamicCapabilities()", () => { + it("should return correct capabilities for outlook addin", () => { + const capabilities = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.outlookAddin().id + ); + assert.equal(capabilities.length, 2); + }); + it("should return correct capabilities for office addin", () => { + const capabilities = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.officeAddin().id + ); + assert.equal(capabilities.length, 3); + }); + it("should return correct capabilities for office xml addin with outlook host", () => { + const capabilities = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.officeXMLAddin().id, + OfficeAddinHostOptions.outlook().id + ); + assert.equal(capabilities.length, 2); + }); + it("should return correct capabilities for office xml addin with word host", () => { + const capabilities = CapabilityOptions.officeAddinDynamicCapabilities( + ProjectTypeOptions.officeXMLAddin().id, + OfficeAddinHostOptions.word().id + ); + assert.equal(capabilities.length, 4); + }); + }); }); describe("ME copilot plugin template only", () => { @@ -3103,22 +3505,22 @@ describe("scaffold question", () => { const question = programmingLanguageQuestion(); it("outlook addin: should have typescript as options", async () => { const inputs: Inputs = { platform: Platform.CLI }; - inputs[QuestionNames.Capabilities] = ["taskpane"]; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; assert.isDefined(question.dynamicOptions); if (question.dynamicOptions) { const options = await question.dynamicOptions(inputs); - assert.deepEqual(options, [{ label: "TypeScript", id: "TypeScript" }]); + assert.deepEqual(options, [{ label: "TypeScript", id: "typescript" }]); } }); it("outlook addin: should default to TypeScript for taskpane projects", async () => { const inputs: Inputs = { platform: Platform.CLI }; - inputs[QuestionNames.Capabilities] = ["taskpane"]; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; assert.isDefined(question.default); const lang = await (question.default as LocalFunc)(inputs); - assert.equal(lang, "TypeScript"); + assert.equal(lang, "typescript"); }); it("office xml addin: normal project have ts and js", async () => { @@ -3128,8 +3530,8 @@ describe("scaffold question", () => { const inputs: Inputs = { platform: Platform.CLI, [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, - [QuestionNames.OfficeAddinCapability]: OfficeAddinCapabilityOptions.word().id, - [QuestionNames.Capabilities]: "react", + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: "word-react", }; assert.isDefined(question.dynamicOptions); if (question.dynamicOptions) { @@ -3149,8 +3551,8 @@ describe("scaffold question", () => { const inputs: Inputs = { platform: Platform.CLI, [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, - [QuestionNames.OfficeAddinCapability]: OfficeAddinCapabilityOptions.word().id, - [QuestionNames.Capabilities]: "manifest", + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: "word-manifest", }; assert.isDefined(question.dynamicOptions); if (question.dynamicOptions) { @@ -3162,7 +3564,7 @@ describe("scaffold question", () => { it("office addin: should have typescript as options", async () => { const inputs: Inputs = { platform: Platform.CLI }; - inputs[QuestionNames.Capabilities] = ["taskpane"]; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; inputs[QuestionNames.OfficeAddinFramework] = "default"; assert.isDefined(question.dynamicOptions); @@ -3177,7 +3579,7 @@ describe("scaffold question", () => { it("office addin: should default to TypeScript for taskpane projects", async () => { const inputs: Inputs = { platform: Platform.CLI }; - inputs[QuestionNames.Capabilities] = ["taskpane"]; + inputs[QuestionNames.Capabilities] = "json-taskpane"; inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; inputs[QuestionNames.OfficeAddinFramework] = "default"; assert.isDefined(question.default); @@ -3185,6 +3587,21 @@ describe("scaffold question", () => { assert.equal(lang, "typescript"); }); + it("office content addin: should have typescript as options", async () => { + const inputs: Inputs = { platform: Platform.CLI }; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.OfficeAddinFramework] = "default"; + assert.isDefined(question.dynamicOptions); + if (question.dynamicOptions) { + const options = await question.dynamicOptions(inputs); + assert.deepEqual(options, [ + { label: "TypeScript", id: "typescript" }, + { label: "JavaScript", id: "javascript" }, + ]); + } + }); + it("SPFxTab", async () => { const inputs: Inputs = { platform: Platform.VSCode, @@ -3233,16 +3650,25 @@ describe("scaffold question", () => { } } }); - }); - describe("getTemplate", () => { - it("should find taskpane template", () => { + it("office xml addin: patch coverage getLanguageOptions", async () => { + sandbox.stub(OfficeAddinProjectConfig, "word").value({ + "word-taskpane": { + localTemplate: "word-taskpane", + title: "core.createProjectQuestion.officeXMLAddin.taskpane.title", + detail: "core.createProjectQuestion.officeXMLAddin.taskpane.detail", + framework: { + default: {}, + }, + }, + }); const inputs: Inputs = { platform: Platform.CLI, + [QuestionNames.ProjectType]: ProjectTypeOptions.officeXMLAddin().id, + [QuestionNames.OfficeAddinHost]: OfficeAddinHostOptions.word().id, + [QuestionNames.Capabilities]: "word-taskpane", }; - inputs[QuestionNames.Capabilities] = ["taskpane"]; - const template = getTemplate(inputs); - assert.equal(template, "taskpane"); + assert.deepEqual(getLanguageOptions(inputs), []); }); }); @@ -3265,10 +3691,64 @@ describe("scaffold question", () => { describe("officeAddinHostingQuestion", async () => { const q = officeAddinHostingQuestion(); const options = await q.dynamicOptions!({ platform: Platform.VSCode }); - assert.isTrue(options.length > 0); + assert.equal(options.length, 4); if (typeof q.default === "function") { const defaultV = await q.default({ platform: Platform.VSCode }); assert.isDefined(defaultV); } }); + + describe("officeAddinFrameworkQuestion", () => { + const question = officeAddinFrameworkQuestion(); + it("office taskpane addin: should have default as options", async () => { + const inputs: Inputs = { platform: Platform.CLI }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + assert.isDefined(question.dynamicOptions); + if (question.dynamicOptions) { + const options = await question.dynamicOptions(inputs); + assert.deepEqual(options, [ + { id: "default", label: "Default" }, + { id: "react", label: "React" }, + ]); + } + }); + + it("office addin import: should have default as options", async () => { + const inputs: Inputs = { platform: Platform.CLI }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeAddinImport().id; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + assert.isDefined(question.dynamicOptions); + if (question.dynamicOptions) { + const options = await question.dynamicOptions(inputs); + assert.deepEqual(options, [{ id: "default", label: "Default" }]); + } + }); + + it("office content addin: should have default as options", async () => { + const inputs: Inputs = { platform: Platform.CLI }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.officeAddin().id; + inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + assert.isDefined(question.dynamicOptions); + if (question.dynamicOptions) { + const options = await question.dynamicOptions(inputs); + assert.deepEqual(options, [{ id: "default", label: "Default" }]); + } + }); + + it("outlook addin: should have default as options", async () => { + const inputs: Inputs = { platform: Platform.CLI }; + inputs[QuestionNames.ProjectType] = ProjectTypeOptions.outlookAddin().id; + inputs[QuestionNames.Capabilities] = "json-taskpane"; + inputs[QuestionNames.ProgrammingLanguage] = "typescript"; + assert.isDefined(question.dynamicOptions); + if (question.dynamicOptions) { + const options = await question.dynamicOptions(inputs); + assert.deepEqual(options, [{ id: "default", label: "Default" }]); + } + }); + }); }); diff --git a/packages/manifest/devPreviewSchema.json b/packages/manifest/devPreviewSchema.json index 5b7909d85f..5762441fbc 100644 --- a/packages/manifest/devPreviewSchema.json +++ b/packages/manifest/devPreviewSchema.json @@ -1297,7 +1297,7 @@ "items": { "type": "string", "enum": [ - "mail" + "mail", "document", "workbook", "presentation" ] } }, diff --git a/packages/manifest/src/manifest.ts b/packages/manifest/src/manifest.ts index f505657099..c04ee1226c 100644 --- a/packages/manifest/src/manifest.ts +++ b/packages/manifest/src/manifest.ts @@ -280,6 +280,10 @@ export interface IParameter { * Type of the parameter */ inputType?: "text" | "textarea" | "number" | "date" | "time" | "toggle" | "choiceset"; + /** + * Indicates whether this parameter is required or not. By default, it is not. + */ + isRequired?: boolean; /** * Title of the parameter. */ diff --git a/packages/sdk-react/package.json b/packages/sdk-react/package.json index a9d4cd93ac..35c46e49d2 100644 --- a/packages/sdk-react/package.json +++ b/packages/sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx-react", - "version": "3.1.0", + "version": "3.1.1", "description": "React helper functions for Microsoft TeamsFx", "main": "build/cjs/index.js", "module": "build/esm/index.js", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d75895c589..dda6177306 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx", - "version": "2.3.0", + "version": "2.3.1", "description": "Microsoft Teams Framework for Node.js and browser.", "main": "dist/index.node.cjs.js", "browser": "dist/index.esm2017.js", diff --git a/packages/sdk/pnpm-lock.yaml b/packages/sdk/pnpm-lock.yaml index 2ef97e2d40..c305ced9e2 100644 --- a/packages/sdk/pnpm-lock.yaml +++ b/packages/sdk/pnpm-lock.yaml @@ -299,12 +299,6 @@ packages: dependencies: tslib: 2.3.1 - /@azure/abort-controller@2.0.0: - resolution: {integrity: sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==} - engines: {node: '>=18.0.0'} - dependencies: - tslib: 2.3.1 - /@azure/core-auth@1.4.0: resolution: {integrity: sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==} engines: {node: '>=12.0.0'} @@ -344,7 +338,7 @@ packages: '@azure/abort-controller': 1.1.0 '@azure/core-auth': 1.4.0 '@azure/core-tracing': 1.0.0-preview.13 - '@azure/core-util': 1.7.0 + '@azure/core-util': 1.6.1 '@azure/logger': 1.0.4 '@types/node-fetch': 2.6.11 '@types/tunnel': 0.0.3 @@ -410,13 +404,6 @@ packages: '@azure/abort-controller': 1.1.0 tslib: 2.3.1 - /@azure/core-util@1.7.0: - resolution: {integrity: sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==} - engines: {node: '>=18.0.0'} - dependencies: - '@azure/abort-controller': 2.0.0 - tslib: 2.3.1 - /@azure/identity@2.0.1(supports-color@9.4.0): resolution: {integrity: sha512-gdGGuLKlKIQaf2RefA84keoBfmWfiAntbW2SzcdKvwLSGzsio/qkyY3sYUpXRz/sqLDxguuimgZukp7TPgwIlg==} engines: {node: '>=12.0.0'} @@ -451,7 +438,7 @@ packages: '@azure/core-client': 1.7.3(supports-color@9.4.0) '@azure/core-rest-pipeline': 1.13.0(supports-color@9.4.0) '@azure/core-tracing': 1.0.1 - '@azure/core-util': 1.7.0 + '@azure/core-util': 1.6.1 '@azure/logger': 1.0.4 '@azure/msal-browser': 2.38.3 '@azure/msal-common': 7.6.0 @@ -1689,7 +1676,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.2(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -2526,6 +2513,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 9.4.0 + dev: true /debug@4.3.3(supports-color@8.1.1): resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} @@ -3209,7 +3197,7 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.3.2(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -3783,7 +3771,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2(supports-color@9.4.0) - debug: 4.3.2(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color dev: true diff --git a/packages/sdk/src/conversationWithCloudAdapter/conversation.ts b/packages/sdk/src/conversationWithCloudAdapter/conversation.ts index 3d8f85d001..c492690496 100644 --- a/packages/sdk/src/conversationWithCloudAdapter/conversation.ts +++ b/packages/sdk/src/conversationWithCloudAdapter/conversation.ts @@ -158,17 +158,20 @@ export class ConversationBot { // This check writes out errors to console. console.error(`[onTurnError] unhandled error`, error); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - error instanceof Error ? error.message : error, - "https://www.botframework.com/schemas/error", - "TurnError" - ); - - // Send a message to the user - await context.sendActivity(`The bot encountered unhandled error: ${error.message}`); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + error instanceof Error ? error.message : error, + "https://www.botframework.com/schemas/error", + "TurnError" + ); + + // Send a message to the user + await context.sendActivity(`The bot encountered unhandled error: ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; return adapter; diff --git a/packages/sdk/test/unit/node/conversationWithCloudAdapter/conversation.spec.ts b/packages/sdk/test/unit/node/conversationWithCloudAdapter/conversation.spec.ts index 1cb9d49f03..0148b728ef 100644 --- a/packages/sdk/test/unit/node/conversationWithCloudAdapter/conversation.spec.ts +++ b/packages/sdk/test/unit/node/conversationWithCloudAdapter/conversation.spec.ts @@ -125,8 +125,9 @@ describe("ConversationBot Tests - Node", () => { assert.isTrue(called); }); - it("onTurnError correctly handles error", async () => { + it("onTurnError correctly handles error when it's message activity", async () => { const context = sandbox.createStubInstance(TurnContext); + sandbox.stub(TurnContext.prototype, "activity").value({ type: "message" }); const conversationBot = new ConversationBot({}); const error = new Error("test error"); await conversationBot.adapter.onTurnError(context, error); @@ -140,4 +141,30 @@ describe("ConversationBot Tests - Node", () => { ) ); }); + + it("onTurnError correctly handles error with error string", async () => { + const context = sandbox.createStubInstance(TurnContext); + sandbox.stub(TurnContext.prototype, "activity").value({ type: "message" }); + const conversationBot = new ConversationBot({}); + const error = "test error"; + await conversationBot.adapter.onTurnError(context, error as any); + assert.isTrue(context.sendActivity.calledTwice); + assert.isTrue( + context.sendActivity.calledWith(`The bot encountered unhandled error: undefined`) + ); + assert.isTrue( + context.sendActivity.calledWith( + "To continue to run this bot, please fix the bot source code." + ) + ); + }); + + it("onTurnError skip to handle error when it's not message activity", async () => { + const context = sandbox.createStubInstance(TurnContext); + sandbox.stub(TurnContext.prototype, "activity").value({ type: "invoke" }); + const conversationBot = new ConversationBot({}); + const error = new Error("test error"); + await conversationBot.adapter.onTurnError(context, error); + assert.isFalse(context.sendActivity.called); + }); }); diff --git a/packages/server/package.json b/packages/server/package.json index 5b45858ab5..cdbc53aad3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx-server", - "version": "2.0.5", + "version": "2.0.6", "author": "Microsoft Corporation", "description": "", "license": "MIT", diff --git a/packages/server/src/apis.ts b/packages/server/src/apis.ts index 981ecbcd42..48207500b5 100644 --- a/packages/server/src/apis.ts +++ b/packages/server/src/apis.ts @@ -183,6 +183,10 @@ export interface IServerConnection { options: TestToolInstallOptions & { correlationId: string }, token: CancellationToken ) => Promise>; + listPluginApiSpecs: ( + inputs: Inputs, + token: CancellationToken + ) => Promise>; } /** diff --git a/packages/server/src/serverConnection.ts b/packages/server/src/serverConnection.ts index 2303d7ef81..61e84a11d5 100644 --- a/packages/server/src/serverConnection.ts +++ b/packages/server/src/serverConnection.ts @@ -96,6 +96,7 @@ export default class ServerConnection implements IServerConnection { this.loadOpenAIPluginManifestRequest.bind(this), this.listOpenAPISpecOperationsRequest.bind(this), this.checkAndInstallTestTool.bind(this), + this.listPluginApiSpecs.bind(this), ].forEach((fn) => { /// fn.name = `bound ${functionName}` connection.onRequest(`${ServerConnection.namespace}/${fn.name.split(" ")[1]}`, fn); @@ -442,6 +443,19 @@ export default class ServerConnection implements IServerConnection { return standardizeResult(res); } + public async listPluginApiSpecs( + inputs: Inputs, + token: CancellationToken + ): Promise> { + const corrId = inputs.correlationId ? inputs.correlationId : ""; + const res = await Correlator.runWithId( + corrId, + (inputs) => this.core.listPluginApiSpecs(inputs), + inputs + ); + return standardizeResult(res); + } + public async loadOpenAIPluginManifestRequest( inputs: Inputs, token: CancellationToken @@ -465,13 +479,8 @@ export default class ServerConnection implements IServerConnection { (inputs) => this.core.copilotPluginListOperations(inputs), inputs ); - if (res.isErr()) { - const msg = res.error.map((e) => e.content).join("\n"); - return standardizeResult( - err(new UserError("Fx-VS", "ListOpenAPISpecOperationsError", msg, msg)) - ); - } - return standardizeResult(ok(res.value)); + + return standardizeResult(res); } public async checkAndInstallTestTool( diff --git a/packages/server/tests/serverConnection.test.ts b/packages/server/tests/serverConnection.test.ts index 8f6fbe6ea2..727e524adb 100644 --- a/packages/server/tests/serverConnection.test.ts +++ b/packages/server/tests/serverConnection.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { err, Inputs, ok, Platform, Stage, Void } from "@microsoft/teamsfx-api"; +import { err, Inputs, ok, Platform, Stage, UserError, Void } from "@microsoft/teamsfx-api"; import * as tools from "@microsoft/teamsfx-core/build/common/tools"; import { assert } from "chai"; import "mocha"; @@ -452,7 +452,7 @@ describe("serverConnections", () => { it("copilotPluginListOperations fail", async () => { const connection = new ServerConnection(msgConn); - const fake = sandbox.fake.resolves(err([{ content: "error1" }, { content: "error2" }])); + const fake = sandbox.fake.resolves(err(new UserError("source", "name", "", ""))); sandbox.replace(connection["core"], "copilotPluginListOperations", fake); const res = await connection.listOpenAPISpecOperationsRequest( {} as Inputs, @@ -460,7 +460,7 @@ describe("serverConnections", () => { ); assert.isTrue(res.isErr()); if (res.isErr()) { - assert.equal(res.error.message, "error1\nerror2"); + assert.equal(res.error.source, "source"); } }); @@ -523,4 +523,20 @@ describe("serverConnections", () => { assert.isFalse(res.isOk()); assert.match(res._unsafeUnwrapErr().message, /MockError/); }); + + it("listPluginApiSpecs fail", async () => { + const connection = new ServerConnection(msgConn); + const fake = sandbox.fake.resolves(err("error")); + sandbox.replace(connection["core"], "listPluginApiSpecs", fake); + const res = await connection.listPluginApiSpecs({} as Inputs, {} as CancellationToken); + assert.isTrue(res.isErr()); + }); + + it("listPluginApiSpecsRequest", async () => { + const connection = new ServerConnection(msgConn); + const fake = sandbox.fake.resolves(ok(undefined)); + sandbox.replace(connection["core"], "listPluginApiSpecs", fake); + const res = await connection.listPluginApiSpecs({} as Inputs, {} as CancellationToken); + assert.isTrue(res.isOk()); + }); }); diff --git a/packages/spec-parser/src/adaptiveCardGenerator.ts b/packages/spec-parser/src/adaptiveCardGenerator.ts index 6277fa508d..07140df021 100644 --- a/packages/spec-parser/src/adaptiveCardGenerator.ts +++ b/packages/spec-parser/src/adaptiveCardGenerator.ts @@ -17,7 +17,7 @@ import { SpecParserError } from "./specParserError"; export class AdaptiveCardGenerator { static generateAdaptiveCard(operationItem: OpenAPIV3.OperationObject): [AdaptiveCard, string] { try { - const json = Utils.getResponseJson(operationItem); + const { json } = Utils.getResponseJson(operationItem); let cardBody: Array = []; diff --git a/packages/spec-parser/src/constants.ts b/packages/spec-parser/src/constants.ts index c596f62c01..8798d8e04d 100644 --- a/packages/spec-parser/src/constants.ts +++ b/packages/spec-parser/src/constants.ts @@ -30,6 +30,9 @@ export class ConstantString { static readonly SwaggerNotSupported = "Swagger 2.0 is not supported. Please convert to OpenAPI 3.0 manually before proceeding."; + static readonly SpecVersionNotSupported = + "Unsupported OpenAPI version %s. Please use version 3.0.x."; + static readonly MultipleAuthNotSupported = "Multiple authentication methods are unsupported. Ensure all selected APIs use identical authentication."; diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index 92101da86b..3e9d5c88e6 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. "use strict"; +import { IParameter } from "@microsoft/teams-manifest"; import { OpenAPIV3 } from "openapi-types"; /** @@ -24,6 +25,18 @@ export interface ValidateResult { errors: ErrorResult[]; } +export interface SpecValidationResult { + /** + * An array of warning results generated during validation. + */ + warnings: WarningResult[]; + + /** + * An array of error results generated during validation. + */ + errors: ErrorResult[]; +} + /** * An interface that represents a warning result generated during validation. */ @@ -83,6 +96,7 @@ export enum ErrorType { ResolveServerUrlFailed = "resolve-server-url-failed", SwaggerNotSupported = "swagger-not-supported", MultipleAuthNotSupported = "multiple-auth-not-supported", + SpecVersionNotSupported = "spec-version-not-supported", ListFailed = "list-failed", listSupportedAPIInfoFailed = "list-supported-api-info-failed", @@ -93,6 +107,22 @@ export enum ErrorType { ValidateFailed = "validate-failed", GetSpecFailed = "get-spec-failed", + AuthTypeIsNotSupported = "auth-type-is-not-supported", + MissingOperationId = "missing-operation-id", + PostBodyContainMultipleMediaTypes = "post-body-contain-multiple-media-types", + ResponseContainMultipleMediaTypes = "response-contain-multiple-media-types", + ResponseJsonIsEmpty = "response-json-is-empty", + PostBodySchemaIsNotJson = "post-body-schema-is-not-json", + PostBodyContainsRequiredUnsupportedSchema = "post-body-contains-required-unsupported-schema", + ParamsContainRequiredUnsupportedSchema = "params-contain-required-unsupported-schema", + ParamsContainsNestedObject = "params-contains-nested-object", + RequestBodyContainsNestedObject = "request-body-contains-nested-object", + ExceededRequiredParamsLimit = "exceeded-required-params-limit", + NoParameter = "no-parameter", + NoAPIInfo = "no-api-info", + MethodNotAllowed = "method-not-allowed", + UrlPathNotExist = "url-path-not-exist", + Cancelled = "cancelled", Unknown = "unknown", } @@ -161,24 +191,11 @@ export interface WrappedAdaptiveCard { previewCardTemplate: PreviewCardTemplate; } -export interface ChoicesItem { - title: string; - value: string; -} - -export interface Parameter { - name: string; - title: string; - description: string; - inputType?: "text" | "textarea" | "number" | "date" | "time" | "toggle" | "choiceset"; - value?: string; - choices?: ChoicesItem[]; -} - export interface CheckParamResult { requiredNum: number; optionalNum: number; isValid: boolean; + reason: ErrorType[]; } export interface ParseOptions { @@ -197,6 +214,11 @@ export interface ParseOptions { */ allowAPIKeyAuth?: boolean; + /** + * If true, the parser will allow Bearer Token authentication in the spec file. + */ + allowBearerTokenAuth?: boolean; + /** * If true, the parser will allow multiple parameters in the spec file. Teams AI project would ignore this parameters and always true */ @@ -230,19 +252,40 @@ export interface APIInfo { path: string; title: string; id: string; - parameters: Parameter[]; + parameters: IParameter[]; description: string; warning?: WarningResult; } -export interface ListAPIResult { +export interface ListAPIInfo { api: string; server: string; operationId: string; - auth?: OpenAPIV3.SecuritySchemeObject; + isValid: boolean; + reason: ErrorType[]; + auth?: AuthInfo; +} + +export interface APIMap { + [key: string]: { + operation: OpenAPIV3.OperationObject; + isValid: boolean; + reason: ErrorType[]; + }; +} + +export interface APIValidationResult { + isValid: boolean; + reason: ErrorType[]; +} + +export interface ListAPIResult { + allAPICount: number; + validAPICount: number; + APIs: ListAPIInfo[]; } export interface AuthInfo { - authSchema: OpenAPIV3.SecuritySchemeObject; + authScheme: OpenAPIV3.SecuritySchemeObject; name: string; } diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index d3cabba943..738d3edc31 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -5,7 +5,14 @@ import { OpenAPIV3 } from "openapi-types"; import fs from "fs-extra"; import path from "path"; -import { AuthInfo, ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces"; +import { + AuthInfo, + ErrorType, + ParseOptions, + ProjectType, + WarningResult, + WarningType, +} from "./interfaces"; import { Utils } from "./utils"; import { SpecParserError } from "./specParserError"; import { ConstantString } from "./constants"; @@ -35,10 +42,17 @@ export class ManifestUpdater { }, ]; + const appName = this.removeEnvs(manifest.name.short); + ManifestUpdater.updateManifestDescription(manifest, spec); const specRelativePath = ManifestUpdater.getRelativePath(manifestPath, outputSpecPath); - const apiPlugin = ManifestUpdater.generatePluginManifestSchema(spec, specRelativePath, options); + const apiPlugin = ManifestUpdater.generatePluginManifestSchema( + spec, + specRelativePath, + appName, + options + ); return [manifest, apiPlugin]; } @@ -80,6 +94,7 @@ export class ManifestUpdater { static generatePluginManifestSchema( spec: OpenAPIV3.Document, specRelativePath: string, + appName: string, options: ParseOptions ): PluginManifestSchema { const functions: FunctionObject[] = []; @@ -174,7 +189,7 @@ export class ManifestUpdater { const apiPlugin: PluginManifestSchema = { schema_version: "v2", - name_for_human: spec.info.title, + name_for_human: appName, description_for_human: spec.info.description ?? "", functions: functions, runtimes: [ @@ -225,9 +240,8 @@ export class ManifestUpdater { }; if (authInfo) { - let auth = authInfo.authSchema; - if (Utils.isAPIKeyAuth(auth)) { - auth = auth as OpenAPIV3.ApiKeySecurityScheme; + const auth = authInfo.authScheme; + if (Utils.isAPIKeyAuth(auth) || Utils.isBearerTokenAuth(auth)) { const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName( `${authInfo.name}_${ConstantString.RegistrationIdPostfix}` ); @@ -290,7 +304,25 @@ export class ManifestUpdater { if (options.allowMethods?.includes(method)) { const operationItem = (operations as any)[method]; if (operationItem) { - const [command, warning] = Utils.parseApiInfo(operationItem, options); + const command = Utils.parseApiInfo(operationItem, options); + + if ( + command.parameters && + command.parameters.length >= 1 && + command.parameters.some((param) => param.isRequired) + ) { + command.parameters = command.parameters.filter((param) => param.isRequired); + } else if (command.parameters && command.parameters.length > 0) { + command.parameters = [command.parameters[0]]; + warnings.push({ + type: WarningType.OperationOnlyContainsOptionalParam, + content: Utils.format( + ConstantString.OperationOnlyContainsOptionalParam, + command.id + ), + data: command.id, + }); + } if (adaptiveCardFolder) { const adaptiveCardPath = path.join(adaptiveCardFolder, command.id + ".json"); @@ -299,10 +331,6 @@ export class ManifestUpdater { : ""; } - if (warning) { - warnings.push(warning); - } - commands.push(command); } } @@ -318,4 +346,14 @@ export class ManifestUpdater { const relativePath = path.relative(path.dirname(from), to); return path.normalize(relativePath).replace(/\\/g, "/"); } + + static removeEnvs(str: string): string { + const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g; + const matches = placeHolderReg.exec(str); + let newStr = str; + if (matches != null) { + newStr = newStr.replace(matches[0], ""); + } + return newStr; + } } diff --git a/packages/spec-parser/src/specFilter.ts b/packages/spec-parser/src/specFilter.ts index d7e2da3b58..ce9e095007 100644 --- a/packages/spec-parser/src/specFilter.ts +++ b/packages/spec-parser/src/specFilter.ts @@ -7,6 +7,7 @@ import { Utils } from "./utils"; import { SpecParserError } from "./specParserError"; import { ErrorType, ParseOptions } from "./interfaces"; import { ConstantString } from "./constants"; +import { ValidatorFactory } from "./validators/validatorFactory"; export class SpecFilter { static specFilter( @@ -22,24 +23,34 @@ export class SpecFilter { const [method, path] = filterItem.split(" "); const methodName = method.toLowerCase(); - if (!Utils.isSupportedApi(methodName, path, resolvedSpec, options)) { - continue; - } + const pathObj = resolvedSpec.paths?.[path] as any; + if ( + ConstantString.AllOperationMethods.includes(methodName) && + pathObj && + pathObj[methodName] + ) { + const validator = ValidatorFactory.create(resolvedSpec, options); + const validateResult = validator.validateAPI(methodName, path); - if (!newPaths[path]) { - newPaths[path] = { ...unResolveSpec.paths[path] }; - for (const m of ConstantString.AllOperationMethods) { - delete (newPaths[path] as any)[m]; + if (!validateResult.isValid) { + continue; } - } - (newPaths[path] as any)[methodName] = (unResolveSpec.paths[path] as any)[methodName]; + if (!newPaths[path]) { + newPaths[path] = { ...unResolveSpec.paths[path] }; + for (const m of ConstantString.AllOperationMethods) { + delete (newPaths[path] as any)[m]; + } + } - // Add the operationId if missing - if (!(newPaths[path] as any)[methodName].operationId) { - (newPaths[path] as any)[ - methodName - ].operationId = `${methodName}${Utils.convertPathToCamelCase(path)}`; + (newPaths[path] as any)[methodName] = (unResolveSpec.paths[path] as any)[methodName]; + + // Add the operationId if missing + if (!(newPaths[path] as any)[methodName].operationId) { + (newPaths[path] as any)[ + methodName + ].operationId = `${methodName}${Utils.convertPathToCamelCase(path)}`; + } } } diff --git a/packages/spec-parser/src/specParser.browser.ts b/packages/spec-parser/src/specParser.browser.ts index d96106d198..a6d424ec2c 100644 --- a/packages/spec-parser/src/specParser.browser.ts +++ b/packages/spec-parser/src/specParser.browser.ts @@ -11,13 +11,18 @@ import { ParseOptions, ValidateResult, ValidationStatus, - Parameter, ListAPIResult, ProjectType, + APIMap, + WarningType, + ErrorResult, + WarningResult, } from "./interfaces"; import { SpecParserError } from "./specParserError"; import { Utils } from "./utils"; import { ConstantString } from "./constants"; +import { ValidatorFactory } from "./validators/validatorFactory"; +import { Validator } from "./validators/validator"; /** * A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts. @@ -27,8 +32,9 @@ export class SpecParser { public readonly parser: SwaggerParser; public readonly options: Required; - private apiMap: { [key: string]: OpenAPIV3.PathItemObject } | undefined; + private apiMap: APIMap | undefined; private spec: OpenAPIV3.Document | undefined; + private validator: Validator | undefined; private unResolveSpec: OpenAPIV3.Document | undefined; private isSwaggerFile: boolean | undefined; @@ -37,6 +43,7 @@ export class SpecParser { allowSwagger: false, allowAPIKeyAuth: false, allowMultipleParameters: false, + allowBearerTokenAuth: false, allowOauth2: false, allowMethods: ["get", "post"], projectType: ProjectType.SME, @@ -65,11 +72,7 @@ export class SpecParser { try { try { await this.loadSpec(); - await this.parser.validate(this.spec!, { - validate: { - schema: false, - }, - }); + await this.parser.validate(this.spec!); } catch (e) { return { status: ValidationStatus.Error, @@ -78,17 +81,51 @@ export class SpecParser { }; } + const errors: ErrorResult[] = []; + const warnings: WarningResult[] = []; + if (!this.options.allowSwagger && this.isSwaggerFile) { return { status: ValidationStatus.Error, warnings: [], errors: [ - { type: ErrorType.SwaggerNotSupported, content: ConstantString.SwaggerNotSupported }, + { + type: ErrorType.SwaggerNotSupported, + content: ConstantString.SwaggerNotSupported, + }, ], }; } - return Utils.validateSpec(this.spec!, this.parser, !!this.isSwaggerFile, this.options); + // Remote reference not supported + const refPaths = this.parser.$refs.paths(); + // refPaths [0] is the current spec file path + if (refPaths.length > 1) { + errors.push({ + type: ErrorType.RemoteRefNotSupported, + content: Utils.format(ConstantString.RemoteRefNotSupported, refPaths.join(", ")), + data: refPaths, + }); + } + + const validator = this.getValidator(this.spec!); + const validationResult = validator.validateSpec(); + + warnings.push(...validationResult.warnings); + errors.push(...validationResult.errors); + + let status = ValidationStatus.Valid; + if (warnings.length > 0 && errors.length === 0) { + status = ValidationStatus.Warning; + } else if (errors.length > 0) { + status = ValidationStatus.Error; + } + + return { + status: status, + warnings: warnings, + errors: errors, + }; } catch (err) { throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed); } @@ -97,33 +134,34 @@ export class SpecParser { async listSupportedAPIInfo(): Promise { try { await this.loadSpec(); - const apiMap = this.getAllSupportedAPIs(this.spec!); + const apiMap = this.getAPIs(this.spec!); const apiInfos: APIInfo[] = []; for (const key in apiMap) { - const pathObjectItem = apiMap[key]; + const { operation, isValid } = apiMap[key]; + + if (!isValid) { + continue; + } + const [method, path] = key.split(" "); - const operationId = pathObjectItem.operationId; + const operationId = operation.operationId; // In Browser environment, this api is by default not support api without operationId if (!operationId) { continue; } - const [command, warning] = Utils.parseApiInfo(pathObjectItem, this.options); + const command = Utils.parseApiInfo(operation, this.options); const apiInfo: APIInfo = { method: method, path: path, title: command.title, id: operationId, - parameters: command.parameters! as Parameter[], + parameters: command.parameters!, description: command.description!, }; - if (warning) { - apiInfo.warning = warning; - } - apiInfos.push(apiInfo); } @@ -203,14 +241,22 @@ export class SpecParser { } } - private getAllSupportedAPIs(spec: OpenAPIV3.Document): { - [key: string]: OpenAPIV3.OperationObject; - } { + private getAPIs(spec: OpenAPIV3.Document): APIMap { if (this.apiMap !== undefined) { return this.apiMap; } - const result = Utils.listSupportedAPIs(spec, this.options); - this.apiMap = result; - return result; + const validator = this.getValidator(spec); + const apiMap = validator.listAPIs(); + this.apiMap = apiMap; + return apiMap; + } + + private getValidator(spec: OpenAPIV3.Document): Validator { + if (this.validator) { + return this.validator; + } + const validator = ValidatorFactory.create(spec, this.options); + this.validator = validator; + return validator; } } diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index f59313e557..1abd4274cc 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -10,14 +10,18 @@ import fs from "fs-extra"; import path from "path"; import { APIInfo, + APIMap, AuthInfo, + ErrorResult, ErrorType, GenerateResult, + ListAPIInfo, ListAPIResult, ParseOptions, ProjectType, ValidateResult, ValidationStatus, + WarningResult, WarningType, } from "./interfaces"; import { ConstantString } from "./constants"; @@ -27,6 +31,8 @@ import { Utils } from "./utils"; import { ManifestUpdater } from "./manifestUpdater"; import { AdaptiveCardGenerator } from "./adaptiveCardGenerator"; import { wrapAdaptiveCard } from "./adaptiveCardWrapper"; +import { ValidatorFactory } from "./validators/validatorFactory"; +import { Validator } from "./validators/validator"; /** * A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts. @@ -36,7 +42,8 @@ export class SpecParser { public readonly parser: SwaggerParser; public readonly options: Required; - private apiMap: { [key: string]: OpenAPIV3.PathItemObject } | undefined; + private apiMap: APIMap | undefined; + private validator: Validator | undefined; private spec: OpenAPIV3.Document | undefined; private unResolveSpec: OpenAPIV3.Document | undefined; private isSwaggerFile: boolean | undefined; @@ -45,6 +52,7 @@ export class SpecParser { allowMissingId: true, allowSwagger: true, allowAPIKeyAuth: false, + allowBearerTokenAuth: false, allowMultipleParameters: false, allowOauth2: false, allowMethods: ["get", "post"], @@ -83,6 +91,9 @@ export class SpecParser { }; } + const errors: ErrorResult[] = []; + const warnings: WarningResult[] = []; + if (!this.options.allowSwagger && this.isSwaggerFile) { return { status: ValidationStatus.Error, @@ -93,7 +104,42 @@ export class SpecParser { }; } - return Utils.validateSpec(this.spec!, this.parser, !!this.isSwaggerFile, this.options); + // Remote reference not supported + const refPaths = this.parser.$refs.paths(); + // refPaths [0] is the current spec file path + if (refPaths.length > 1) { + errors.push({ + type: ErrorType.RemoteRefNotSupported, + content: Utils.format(ConstantString.RemoteRefNotSupported, refPaths.join(", ")), + data: refPaths, + }); + } + + if (!!this.isSwaggerFile && this.options.allowSwagger) { + warnings.push({ + type: WarningType.ConvertSwaggerToOpenAPI, + content: ConstantString.ConvertSwaggerToOpenAPI, + }); + } + + const validator = this.getValidator(this.spec!); + const validationResult = validator.validateSpec(); + + warnings.push(...validationResult.warnings); + errors.push(...validationResult.errors); + + let status = ValidationStatus.Valid; + if (warnings.length > 0 && errors.length === 0) { + status = ValidationStatus.Warning; + } else if (errors.length > 0) { + status = ValidationStatus.Error; + } + + return { + status: status, + warnings: warnings, + errors: errors, + }; } catch (err) { throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed); } @@ -109,53 +155,52 @@ export class SpecParser { * @returns A string array that represents the HTTP method and path of each operation, such as ['GET /pets/{petId}', 'GET /user/{userId}'] * according to copilot plugin spec, only list get and post method without auth */ - async list(): Promise { + async list(): Promise { try { await this.loadSpec(); const spec = this.spec!; - const apiMap = this.getAllSupportedAPIs(spec); - const result: ListAPIResult[] = []; + const apiMap = this.getAPIs(spec); + const result: ListAPIResult = { + APIs: [], + allAPICount: 0, + validAPICount: 0, + }; for (const apiKey in apiMap) { - const apiResult: ListAPIResult = { - api: "", - server: "", - operationId: "", - }; + const { operation, isValid, reason } = apiMap[apiKey]; const [method, path] = apiKey.split(" "); - const operation = apiMap[apiKey]; - const rootServer = spec.servers && spec.servers[0]; - const methodServer = spec.paths[path]!.servers && spec.paths[path]?.servers![0]; - const operationServer = operation.servers && operation.servers[0]; - - const serverUrl = operationServer || methodServer || rootServer; - if (!serverUrl) { - throw new SpecParserError( - ConstantString.NoServerInformation, - ErrorType.NoServerInformation - ); - } - apiResult.server = Utils.resolveServerUrl(serverUrl.url); + const operationId = + operation.operationId ?? `${method.toLowerCase()}${Utils.convertPathToCamelCase(path)}`; - let operationId = operation.operationId; - if (!operationId) { - operationId = `${method.toLowerCase()}${Utils.convertPathToCamelCase(path)}`; - } - apiResult.operationId = operationId; + const apiResult: ListAPIInfo = { + api: apiKey, + server: "", + operationId: operationId, + isValid: isValid, + reason: reason, + }; - const authArray = Utils.getAuthArray(operation.security, spec); + if (isValid) { + const serverObj = Utils.getServerObject(spec, method.toLocaleLowerCase(), path); + if (serverObj) { + apiResult.server = Utils.resolveEnv(serverObj.url); + } - for (const auths of authArray) { - if (auths.length === 1) { - apiResult.auth = auths[0].authSchema; - break; + const authArray = Utils.getAuthArray(operation.security, spec); + for (const auths of authArray) { + if (auths.length === 1) { + apiResult.auth = auths[0]; + break; + } } } - apiResult.api = apiKey; - result.push(apiResult); + result.APIs.push(apiResult); } + result.allAPICount = result.APIs.length; + result.validAPICount = result.APIs.filter((api) => api.isValid).length; + return result; } catch (err) { if (err instanceof SpecParserError) { @@ -283,8 +328,8 @@ export class SpecParser { const newUnResolvedSpec = newSpecs[0]; const newSpec = newSpecs[1]; - const authSet: Set = new Set(); let hasMultipleAuth = false; + let authInfo: AuthInfo | undefined = undefined; for (const url in newSpec.paths) { for (const method in newSpec.paths[url]) { @@ -293,8 +338,10 @@ export class SpecParser { const authArray = Utils.getAuthArray(operation.security, newSpec); if (authArray && authArray.length > 0) { - authSet.add(authArray[0][0]); - if (authSet.size > 1) { + const currentAuth = authArray[0][0]; + if (!authInfo) { + authInfo = authArray[0][0]; + } else if (authInfo.name !== currentAuth.name) { hasMultipleAuth = true; break; } @@ -350,7 +397,6 @@ export class SpecParser { throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled); } - const authInfo = Array.from(authSet)[0]; const [updatedManifest, warnings] = await ManifestUpdater.updateManifest( manifestPath, outputSpecPath, @@ -388,14 +434,19 @@ export class SpecParser { } } - private getAllSupportedAPIs(spec: OpenAPIV3.Document): { - [key: string]: OpenAPIV3.OperationObject; - } { - if (this.apiMap !== undefined) { - return this.apiMap; + private getAPIs(spec: OpenAPIV3.Document): APIMap { + const validator = this.getValidator(spec); + const apiMap = validator.listAPIs(); + this.apiMap = apiMap; + return apiMap; + } + + private getValidator(spec: OpenAPIV3.Document): Validator { + if (this.validator) { + return this.validator; } - const result = Utils.listSupportedAPIs(spec, this.options); - this.apiMap = result; - return result; + const validator = ValidatorFactory.create(spec, this.options); + this.validator = validator; + return validator; } } diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 6eb3bafdcc..d521b08805 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -6,11 +6,10 @@ import { OpenAPIV3 } from "openapi-types"; import SwaggerParser from "@apidevtools/swagger-parser"; import { ConstantString } from "./constants"; import { + APIMap, AuthInfo, - CheckParamResult, ErrorResult, ErrorType, - Parameter, ParseOptions, ProjectType, ValidateResult, @@ -18,7 +17,7 @@ import { WarningResult, WarningType, } from "./interfaces"; -import { IMessagingExtensionCommand } from "@microsoft/teams-manifest"; +import { IMessagingExtensionCommand, IParameter } from "@microsoft/teams-manifest"; export class Utils { static hasNestedObjectInSchema(schema: OpenAPIV3.SchemaObject): boolean { @@ -33,307 +32,26 @@ export class Utils { return false; } - static checkParameters( - paramObject: OpenAPIV3.ParameterObject[], - isCopilot: boolean - ): CheckParamResult { - const paramResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - }; - - if (!paramObject) { - return paramResult; - } - - for (let i = 0; i < paramObject.length; i++) { - const param = paramObject[i]; - const schema = param.schema as OpenAPIV3.SchemaObject; - - if (isCopilot && this.hasNestedObjectInSchema(schema)) { - paramResult.isValid = false; - continue; - } - - const isRequiredWithoutDefault = param.required && schema.default === undefined; - - if (isCopilot) { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - continue; - } - - if (param.in === "header" || param.in === "cookie") { - if (isRequiredWithoutDefault) { - paramResult.isValid = false; - } - continue; - } - - if ( - schema.type !== "boolean" && - schema.type !== "string" && - schema.type !== "number" && - schema.type !== "integer" - ) { - if (isRequiredWithoutDefault) { - paramResult.isValid = false; - } - continue; - } - - if (param.in === "query" || param.in === "path") { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - } - } - - return paramResult; - } - - static checkPostBody( - schema: OpenAPIV3.SchemaObject, - isRequired = false, - isCopilot = false - ): CheckParamResult { - const paramResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - }; - - if (Object.keys(schema).length === 0) { - return paramResult; - } - - const isRequiredWithoutDefault = isRequired && schema.default === undefined; - - if (isCopilot && this.hasNestedObjectInSchema(schema)) { - paramResult.isValid = false; - return paramResult; - } - - if ( - schema.type === "string" || - schema.type === "integer" || - schema.type === "boolean" || - schema.type === "number" - ) { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - } else if (schema.type === "object") { - const { properties } = schema; - for (const property in properties) { - let isRequired = false; - if (schema.required && schema.required?.indexOf(property) >= 0) { - isRequired = true; - } - const result = Utils.checkPostBody( - properties[property] as OpenAPIV3.SchemaObject, - isRequired, - isCopilot - ); - paramResult.requiredNum += result.requiredNum; - paramResult.optionalNum += result.optionalNum; - paramResult.isValid = paramResult.isValid && result.isValid; - } - } else { - if (isRequiredWithoutDefault && !isCopilot) { - paramResult.isValid = false; - } - } - return paramResult; - } - static containMultipleMediaTypes( bodyObject: OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject ): boolean { return Object.keys(bodyObject?.content || {}).length > 1; } - /** - * Checks if the given API is supported. - * @param {string} method - The HTTP method of the API. - * @param {string} path - The path of the API. - * @param {OpenAPIV3.Document} spec - The OpenAPI specification document. - * @returns {boolean} - Returns true if the API is supported, false otherwise. - * @description The following APIs are supported: - * 1. only support Get/Post operation without auth property - * 2. parameter inside query or path only support string, number, boolean and integer - * 3. parameter inside post body only support string, number, boolean, integer and object - * 4. request body + required parameters <= 1 - * 5. response body should be “application/json” and not empty, and response code should be 20X - * 6. only support request body with “application/json” content type - */ - static isSupportedApi( - method: string, - path: string, - spec: OpenAPIV3.Document, - options: ParseOptions - ): boolean { - const pathObj = spec.paths[path] as any; - method = method.toLocaleLowerCase(); - if (pathObj) { - if (options.allowMethods?.includes(method) && pathObj[method]) { - const securities = pathObj[method].security; - - const isTeamsAi = options.projectType === ProjectType.TeamsAi; - const isCopilot = options.projectType === ProjectType.Copilot; - - // Teams AI project doesn't care about auth, it will use authProvider for user to implement - if (!isTeamsAi) { - const authArray = Utils.getAuthArray(securities, spec); - - if (!Utils.isSupportedAuth(authArray, options)) { - return false; - } - } - - const operationObject = pathObj[method] as OpenAPIV3.OperationObject; - if (!options.allowMissingId && !operationObject.operationId) { - return false; - } - const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; - - const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; - const requestJsonBody = requestBody?.content["application/json"]; - - if (!isTeamsAi && Utils.containMultipleMediaTypes(requestBody)) { - return false; - } - - const responseJson = Utils.getResponseJson(operationObject, isTeamsAi); - - if (Object.keys(responseJson).length === 0) { - return false; - } - - // Teams AI project doesn't care about request parameters/body - if (isTeamsAi) { - return true; - } - - let requestBodyParamResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - }; - - if (requestJsonBody) { - const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; - - if (isCopilot && requestBodySchema.type !== "object") { - return false; - } - - requestBodyParamResult = Utils.checkPostBody( - requestBodySchema, - requestBody.required, - isCopilot - ); - } - - if (!requestBodyParamResult.isValid) { - return false; - } - - const paramResult = Utils.checkParameters(paramObject, isCopilot); - - if (!paramResult.isValid) { - return false; - } - - // Copilot support arbitrary parameters - if (isCopilot) { - return true; - } - - if (requestBodyParamResult.requiredNum + paramResult.requiredNum > 1) { - if ( - options.allowMultipleParameters && - requestBodyParamResult.requiredNum + paramResult.requiredNum <= - ConstantString.SMERequiredParamsMaxNum - ) { - return true; - } - return false; - } else if ( - requestBodyParamResult.requiredNum + - requestBodyParamResult.optionalNum + - paramResult.requiredNum + - paramResult.optionalNum === - 0 - ) { - return false; - } else { - return true; - } - } - } - - return false; - } - - static isSupportedAuth(authSchemaArray: AuthInfo[][], options: ParseOptions): boolean { - if (authSchemaArray.length === 0) { - return true; - } - - if (options.allowAPIKeyAuth || options.allowOauth2) { - // Currently we don't support multiple auth in one operation - if (authSchemaArray.length > 0 && authSchemaArray.every((auths) => auths.length > 1)) { - return false; - } - - for (const auths of authSchemaArray) { - if (auths.length === 1) { - if ( - !options.allowOauth2 && - options.allowAPIKeyAuth && - Utils.isAPIKeyAuth(auths[0].authSchema) - ) { - return true; - } else if ( - !options.allowAPIKeyAuth && - options.allowOauth2 && - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema) - ) { - return true; - } else if ( - options.allowAPIKeyAuth && - options.allowOauth2 && - (Utils.isAPIKeyAuth(auths[0].authSchema) || - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema)) - ) { - return true; - } - } - } - } - - return false; + static isBearerTokenAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "http" && authScheme.scheme === "bearer"; } - static isAPIKeyAuth(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - return authSchema.type === "apiKey"; + static isAPIKeyAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "apiKey"; } - static isOAuthWithAuthCodeFlow(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - if (authSchema.type === "oauth2" && authSchema.flows && authSchema.flows.authorizationCode) { - return true; - } - - return false; + static isOAuthWithAuthCodeFlow(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return !!( + authScheme.type === "oauth2" && + authScheme.flows && + authScheme.flows.authorizationCode + ); } static getAuthArray( @@ -350,7 +68,7 @@ export class Utils { for (const name in security) { const auth = securitySchemas[name] as OpenAPIV3.SecuritySchemeObject; authArray.push({ - authSchema: auth, + authScheme: auth, name: name, }); } @@ -370,18 +88,21 @@ export class Utils { return str.charAt(0).toUpperCase() + str.slice(1); } - static getResponseJson( - operationObject: OpenAPIV3.OperationObject | undefined, - isTeamsAiProject = false - ): OpenAPIV3.MediaTypeObject { + static getResponseJson(operationObject: OpenAPIV3.OperationObject | undefined): { + json: OpenAPIV3.MediaTypeObject; + multipleMediaType: boolean; + } { let json: OpenAPIV3.MediaTypeObject = {}; + let multipleMediaType = false; for (const code of ConstantString.ResponseCodeFor20X) { const responseObject = operationObject?.responses?.[code] as OpenAPIV3.ResponseObject; if (responseObject?.content?.["application/json"]) { + multipleMediaType = false; json = responseObject.content["application/json"]; - if (!isTeamsAiProject && Utils.containMultipleMediaTypes(responseObject)) { + if (Utils.containMultipleMediaTypes(responseObject)) { + multipleMediaType = true; json = {}; } else { break; @@ -389,7 +110,7 @@ export class Utils { } } - return json; + return { json, multipleMediaType }; } static convertPathToCamelCase(path: string): string { @@ -411,21 +132,21 @@ export class Utils { } } - static resolveServerUrl(url: string): string { + static resolveEnv(str: string): string { const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g; - let matches = placeHolderReg.exec(url); - let newUrl = url; + let matches = placeHolderReg.exec(str); + let newStr = str; while (matches != null) { const envVar = matches[1]; const envVal = process.env[envVar]; if (!envVal) { throw new Error(Utils.format(ConstantString.ResolveServerUrlFailed, envVar)); } else { - newUrl = newUrl.replace(matches[0], envVal); + newStr = newStr.replace(matches[0], envVal); } - matches = placeHolderReg.exec(url); + matches = placeHolderReg.exec(str); } - return newUrl; + return newStr; } static checkServerUrl(servers: OpenAPIV3.ServerObject[]): ErrorResult[] { @@ -433,7 +154,7 @@ export class Utils { let serverUrl; try { - serverUrl = Utils.resolveServerUrl(servers[0].url); + serverUrl = Utils.resolveEnv(servers[0].url); } catch (err) { errors.push({ type: ErrorType.ResolveServerUrlFailed, @@ -486,12 +207,13 @@ export class Utils { if (methods?.servers && methods.servers.length >= 1) { hasPathLevelServers = true; const serverErrors = Utils.checkServerUrl(methods.servers); + errors.push(...serverErrors); } for (const method in methods) { const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; - if (Utils.isSupportedApi(method, path, spec, options)) { + if (options.allowMethods?.includes(method) && operationObject) { if (operationObject?.servers && operationObject.servers.length >= 1) { hasOperationLevelServers = true; const serverErrors = Utils.checkServerUrl(operationObject.servers); @@ -500,12 +222,14 @@ export class Utils { } } } + if (!hasTopLevelServers && !hasPathLevelServers && !hasOperationLevelServers) { errors.push({ type: ErrorType.NoServerInformation, content: ConstantString.NoServerInformation, }); } + return errors; } @@ -524,9 +248,9 @@ export class Utils { name: string, allowMultipleParameters: boolean, isRequired = false - ): [Parameter[], Parameter[]] { - const requiredParams: Parameter[] = []; - const optionalParams: Parameter[] = []; + ): [IParameter[], IParameter[]] { + const requiredParams: IParameter[] = []; + const optionalParams: IParameter[] = []; if ( schema.type === "string" || @@ -534,7 +258,7 @@ export class Utils { schema.type === "boolean" || schema.type === "number" ) { - const parameter = { + const parameter: IParameter = { name: name, title: Utils.updateFirstLetter(name).slice(0, ConstantString.ParameterTitleMaxLens), description: (schema.description ?? "").slice( @@ -548,6 +272,7 @@ export class Utils { } if (isRequired && schema.default === undefined) { + parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); @@ -574,7 +299,7 @@ export class Utils { return [requiredParams, optionalParams]; } - static updateParameterWithInputType(schema: OpenAPIV3.SchemaObject, param: Parameter): void { + static updateParameterWithInputType(schema: OpenAPIV3.SchemaObject, param: IParameter): void { if (schema.enum) { param.inputType = "choiceset"; param.choices = []; @@ -600,14 +325,14 @@ export class Utils { static parseApiInfo( operationItem: OpenAPIV3.OperationObject, options: ParseOptions - ): [IMessagingExtensionCommand, WarningResult | undefined] { - const requiredParams: Parameter[] = []; - const optionalParams: Parameter[] = []; + ): IMessagingExtensionCommand { + const requiredParams: IParameter[] = []; + const optionalParams: IParameter[] = []; const paramObject = operationItem.parameters as OpenAPIV3.ParameterObject[]; if (paramObject) { paramObject.forEach((param: OpenAPIV3.ParameterObject) => { - const parameter: Parameter = { + const parameter: IParameter = { name: param.name, title: Utils.updateFirstLetter(param.name).slice(0, ConstantString.ParameterTitleMaxLens), description: (param.description ?? "").slice( @@ -623,6 +348,7 @@ export class Utils { if (param.in !== "header" && param.in !== "cookie") { if (param.required && schema?.default === undefined) { + parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); @@ -649,13 +375,7 @@ export class Utils { const operationId = operationItem.operationId!; - const parameters = []; - - if (requiredParams.length !== 0) { - parameters.push(...requiredParams); - } else { - parameters.push(optionalParams[0]); - } + const parameters = [...requiredParams, ...optionalParams]; const command: IMessagingExtensionCommand = { context: ["compose"], @@ -668,108 +388,7 @@ export class Utils { ConstantString.CommandDescriptionMaxLens ), }; - let warning: WarningResult | undefined = undefined; - - if (requiredParams.length === 0 && optionalParams.length > 1) { - warning = { - type: WarningType.OperationOnlyContainsOptionalParam, - content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, operationId), - data: operationId, - }; - } - return [command, warning]; - } - - static listSupportedAPIs( - spec: OpenAPIV3.Document, - options: ParseOptions - ): { - [key: string]: OpenAPIV3.OperationObject; - } { - const paths = spec.paths; - const result: { [key: string]: OpenAPIV3.OperationObject } = {}; - for (const path in paths) { - const methods = paths[path]; - for (const method in methods) { - if (Utils.isSupportedApi(method, path, spec, options)) { - const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; - result[`${method.toUpperCase()} ${path}`] = operationObject; - } - } - } - return result; - } - - static validateSpec( - spec: OpenAPIV3.Document, - parser: SwaggerParser, - isSwaggerFile: boolean, - options: ParseOptions - ): ValidateResult { - const errors: ErrorResult[] = []; - const warnings: WarningResult[] = []; - - if (isSwaggerFile) { - warnings.push({ - type: WarningType.ConvertSwaggerToOpenAPI, - content: ConstantString.ConvertSwaggerToOpenAPI, - }); - } - - // Server validation - const serverErrors = Utils.validateServer(spec, options); - errors.push(...serverErrors); - - // Remote reference not supported - const refPaths = parser.$refs.paths(); - - // refPaths [0] is the current spec file path - if (refPaths.length > 1) { - errors.push({ - type: ErrorType.RemoteRefNotSupported, - content: Utils.format(ConstantString.RemoteRefNotSupported, refPaths.join(", ")), - data: refPaths, - }); - } - - // No supported API - const apiMap = Utils.listSupportedAPIs(spec, options); - if (Object.keys(apiMap).length === 0) { - errors.push({ - type: ErrorType.NoSupportedApi, - content: ConstantString.NoSupportedApi, - }); - } - - // OperationId missing - const apisMissingOperationId: string[] = []; - for (const key in apiMap) { - const pathObjectItem = apiMap[key]; - if (!pathObjectItem.operationId) { - apisMissingOperationId.push(key); - } - } - - if (apisMissingOperationId.length > 0) { - warnings.push({ - type: WarningType.OperationIdMissing, - content: Utils.format(ConstantString.MissingOperationId, apisMissingOperationId.join(", ")), - data: apisMissingOperationId, - }); - } - - let status = ValidationStatus.Valid; - if (warnings.length > 0 && errors.length === 0) { - status = ValidationStatus.Warning; - } else if (errors.length > 0) { - status = ValidationStatus.Error; - } - - return { - status, - warnings, - errors, - }; + return command; } static format(str: string, ...args: string[]): string { @@ -793,4 +412,22 @@ export class Utils { return safeRegistrationIdEnvName; } + + static getServerObject( + spec: OpenAPIV3.Document, + method: string, + path: string + ): OpenAPIV3.ServerObject | undefined { + const pathObj = spec.paths[path] as any; + + const operationObject = pathObj[method] as OpenAPIV3.OperationObject; + + const rootServer = spec.servers && spec.servers[0]; + const methodServer = spec.paths[path]!.servers && spec.paths[path]!.servers![0]; + const operationServer = operationObject.servers && operationObject.servers[0]; + + const serverUrl = operationServer || methodServer || rootServer; + + return serverUrl; + } } diff --git a/packages/spec-parser/src/validators/copilotValidator.ts b/packages/spec-parser/src/validators/copilotValidator.ts new file mode 100644 index 0000000000..b06ccbb800 --- /dev/null +++ b/packages/spec-parser/src/validators/copilotValidator.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + ProjectType, + SpecValidationResult, +} from "../interfaces"; +import { Validator } from "./validator"; +import { Utils } from "../utils"; + +export class CopilotValidator extends Validator { + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.Copilot; + this.options = options; + this.spec = spec; + } + + validateSpec(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + + // validate spec version + let validationResult = this.validateSpecVersion(); + result.errors.push(...validationResult.errors); + + // validate spec server + validationResult = this.validateSpecServer(); + result.errors.push(...validationResult.errors); + + // validate no supported API + validationResult = this.validateSpecNoSupportAPI(); + result.errors.push(...validationResult.errors); + + // validate operationId missing + validationResult = this.validateSpecOperationId(); + result.warnings.push(...validationResult.warnings); + + return result; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate auth + const authCheckResult = this.validateAuth(method, path); + result.reason.push(...authCheckResult.reason); + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + // validate response + const validateResponseResult = this.validateResponse(method, path); + result.reason.push(...validateResponseResult.reason); + + // validate requestBody + const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; + const requestJsonBody = requestBody?.content["application/json"]; + + if (Utils.containMultipleMediaTypes(requestBody)) { + result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); + } + + if (requestJsonBody) { + const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; + + if (requestBodySchema.type !== "object") { + result.reason.push(ErrorType.PostBodySchemaIsNotJson); + } + + const requestBodyParamResult = this.checkPostBodySchema( + requestBodySchema, + requestBody.required + ); + result.reason.push(...requestBodyParamResult.reason); + } + + // validate parameters + const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; + const paramResult = this.checkParamSchema(paramObject); + result.reason.push(...paramResult.reason); + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/smeValidator.ts b/packages/spec-parser/src/validators/smeValidator.ts new file mode 100644 index 0000000000..df679007b8 --- /dev/null +++ b/packages/spec-parser/src/validators/smeValidator.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + ProjectType, + CheckParamResult, + SpecValidationResult, +} from "../interfaces"; +import { Validator } from "./validator"; +import { Utils } from "../utils"; + +export class SMEValidator extends Validator { + private static readonly SMERequiredParamsMaxNum = 5; + + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.SME; + this.options = options; + this.spec = spec; + } + + validateSpec(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + + // validate spec version + let validationResult = this.validateSpecVersion(); + result.errors.push(...validationResult.errors); + + // validate spec server + validationResult = this.validateSpecServer(); + result.errors.push(...validationResult.errors); + + // validate no supported API + validationResult = this.validateSpecNoSupportAPI(); + result.errors.push(...validationResult.errors); + + // validate operationId missing + validationResult = this.validateSpecOperationId(); + result.warnings.push(...validationResult.warnings); + + return result; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate auth + const authCheckResult = this.validateAuth(method, path); + result.reason.push(...authCheckResult.reason); + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + // validate response + const validateResponseResult = this.validateResponse(method, path); + result.reason.push(...validateResponseResult.reason); + + let postBodyResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + // validate requestBody + const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; + const requestJsonBody = requestBody?.content["application/json"]; + + if (Utils.containMultipleMediaTypes(requestBody)) { + result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); + } + + if (requestJsonBody) { + const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; + + postBodyResult = this.checkPostBodySchema(requestBodySchema, requestBody.required); + result.reason.push(...postBodyResult.reason); + } + + // validate parameters + const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; + const paramResult = this.checkParamSchema(paramObject); + result.reason.push(...paramResult.reason); + + // validate total parameters count + if (paramResult.isValid && postBodyResult.isValid) { + const paramCountResult = this.validateParamCount(postBodyResult, paramResult); + result.reason.push(...paramCountResult.reason); + } + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } + + private validateParamCount( + postBodyResult: CheckParamResult, + paramResult: CheckParamResult + ): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + const totalRequiredParams = postBodyResult.requiredNum + paramResult.requiredNum; + const totalParams = totalRequiredParams + postBodyResult.optionalNum + paramResult.optionalNum; + + if (totalRequiredParams > 1) { + if ( + !this.options.allowMultipleParameters || + totalRequiredParams > SMEValidator.SMERequiredParamsMaxNum + ) { + result.reason.push(ErrorType.ExceededRequiredParamsLimit); + } + } else if (totalParams === 0) { + result.reason.push(ErrorType.NoParameter); + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/teamsAIValidator.ts b/packages/spec-parser/src/validators/teamsAIValidator.ts new file mode 100644 index 0000000000..6b188de944 --- /dev/null +++ b/packages/spec-parser/src/validators/teamsAIValidator.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + ProjectType, + SpecValidationResult, +} from "../interfaces"; +import { Validator } from "./validator"; + +export class TeamsAIValidator extends Validator { + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.TeamsAi; + this.options = options; + this.spec = spec; + } + + validateSpec(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + + // validate spec server + let validationResult = this.validateSpecServer(); + result.errors.push(...validationResult.errors); + + // validate no supported API + validationResult = this.validateSpecNoSupportAPI(); + result.errors.push(...validationResult.errors); + + return result; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/validator.ts b/packages/spec-parser/src/validators/validator.ts new file mode 100644 index 0000000000..67df382180 --- /dev/null +++ b/packages/spec-parser/src/validators/validator.ts @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + CheckParamResult, + ProjectType, + APIMap, + SpecValidationResult, + WarningType, +} from "../interfaces"; +import { Utils } from "../utils"; +import { ConstantString } from "../constants"; + +export abstract class Validator { + projectType!: ProjectType; + spec!: OpenAPIV3.Document; + options!: ParseOptions; + + private apiMap: APIMap | undefined; + + abstract validateAPI(method: string, path: string): APIValidationResult; + abstract validateSpec(): SpecValidationResult; + + listAPIs(): APIMap { + if (this.apiMap) { + return this.apiMap; + } + + const paths = this.spec.paths; + const result: APIMap = {}; + for (const path in paths) { + const methods = paths[path]; + for (const method in methods) { + const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; + if (this.options.allowMethods?.includes(method) && operationObject) { + const validateResult = this.validateAPI(method, path); + result[`${method.toUpperCase()} ${path}`] = { + operation: operationObject, + isValid: validateResult.isValid, + reason: validateResult.reason, + }; + } + } + } + + this.apiMap = result; + return result; + } + + protected validateSpecVersion(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + + if (this.spec.openapi >= "3.1.0") { + result.errors.push({ + type: ErrorType.SpecVersionNotSupported, + content: Utils.format(ConstantString.SpecVersionNotSupported, this.spec.openapi), + data: this.spec.openapi, + }); + } + + return result; + } + + protected validateSpecServer(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + const serverErrors = Utils.validateServer(this.spec, this.options); + result.errors.push(...serverErrors); + return result; + } + + protected validateSpecNoSupportAPI(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + + const apiMap = this.listAPIs(); + + const validAPIs = Object.entries(apiMap).filter(([, value]) => value.isValid); + if (validAPIs.length === 0) { + result.errors.push({ + type: ErrorType.NoSupportedApi, + content: ConstantString.NoSupportedApi, + }); + } + + return result; + } + + protected validateSpecOperationId(): SpecValidationResult { + const result: SpecValidationResult = { errors: [], warnings: [] }; + const apiMap = this.listAPIs(); + + // OperationId missing + const apisMissingOperationId: string[] = []; + for (const key in apiMap) { + const { operation } = apiMap[key]; + if (!operation.operationId) { + apisMissingOperationId.push(key); + } + } + + if (apisMissingOperationId.length > 0) { + result.warnings.push({ + type: WarningType.OperationIdMissing, + content: Utils.format(ConstantString.MissingOperationId, apisMissingOperationId.join(", ")), + data: apisMissingOperationId, + }); + } + + return result; + } + + protected validateMethodAndPath(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + + if (this.options.allowMethods && !this.options.allowMethods.includes(method)) { + result.isValid = false; + result.reason.push(ErrorType.MethodNotAllowed); + return result; + } + + const pathObj = this.spec.paths[path] as any; + + if (!pathObj || !pathObj[method]) { + result.isValid = false; + result.reason.push(ErrorType.UrlPathNotExist); + return result; + } + + return result; + } + + protected validateResponse(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); + + // only support response body only contains “application/json” content type + if (multipleMediaType) { + result.reason.push(ErrorType.ResponseContainMultipleMediaTypes); + } else if (Object.keys(json).length === 0) { + // response body should not be empty + result.reason.push(ErrorType.ResponseJsonIsEmpty); + } + + return result; + } + + protected validateServer(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + const serverObj = Utils.getServerObject(this.spec, method, path); + if (!serverObj) { + // should contain server URL + result.reason.push(ErrorType.NoServerInformation); + } else { + // server url should be absolute url with https protocol + const serverValidateResult = Utils.checkServerUrl([serverObj]); + result.reason.push(...serverValidateResult.map((item) => item.type)); + } + + return result; + } + + protected validateAuth(method: string, path: string): APIValidationResult { + const pathObj = this.spec.paths[path] as any; + const operationObject = pathObj[method] as OpenAPIV3.OperationObject; + + const securities = operationObject.security; + const authSchemeArray = Utils.getAuthArray(securities, this.spec); + + if (authSchemeArray.length === 0) { + return { isValid: true, reason: [] }; + } + + if ( + this.options.allowAPIKeyAuth || + this.options.allowOauth2 || + this.options.allowBearerTokenAuth + ) { + // Currently we don't support multiple auth in one operation + if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { + return { + isValid: false, + reason: [ErrorType.MultipleAuthNotSupported], + }; + } + + for (const auths of authSchemeArray) { + if (auths.length === 1) { + if ( + (this.options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || + (this.options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || + (this.options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme)) + ) { + return { isValid: true, reason: [] }; + } + } + } + } + + return { isValid: false, reason: [ErrorType.AuthTypeIsNotSupported] }; + } + + protected checkPostBodySchema( + schema: OpenAPIV3.SchemaObject, + isRequired = false + ): CheckParamResult { + const paramResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + if (Object.keys(schema).length === 0) { + return paramResult; + } + + const isRequiredWithoutDefault = isRequired && schema.default === undefined; + const isCopilot = this.projectType === ProjectType.Copilot; + + if (isCopilot && this.hasNestedObjectInSchema(schema)) { + paramResult.isValid = false; + paramResult.reason = [ErrorType.RequestBodyContainsNestedObject]; + return paramResult; + } + + if ( + schema.type === "string" || + schema.type === "integer" || + schema.type === "boolean" || + schema.type === "number" + ) { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + } else if (schema.type === "object") { + const { properties } = schema; + for (const property in properties) { + let isRequired = false; + if (schema.required && schema.required?.indexOf(property) >= 0) { + isRequired = true; + } + const result = this.checkPostBodySchema( + properties[property] as OpenAPIV3.SchemaObject, + isRequired + ); + paramResult.requiredNum += result.requiredNum; + paramResult.optionalNum += result.optionalNum; + paramResult.isValid = paramResult.isValid && result.isValid; + paramResult.reason.push(...result.reason); + } + } else { + if (isRequiredWithoutDefault && !isCopilot) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.PostBodyContainsRequiredUnsupportedSchema); + } + } + return paramResult; + } + + protected checkParamSchema(paramObject: OpenAPIV3.ParameterObject[]): CheckParamResult { + const paramResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + if (!paramObject) { + return paramResult; + } + + const isCopilot = this.projectType === ProjectType.Copilot; + + for (let i = 0; i < paramObject.length; i++) { + const param = paramObject[i]; + const schema = param.schema as OpenAPIV3.SchemaObject; + + if (isCopilot && this.hasNestedObjectInSchema(schema)) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainsNestedObject); + continue; + } + + const isRequiredWithoutDefault = param.required && schema.default === undefined; + + if (isCopilot) { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + continue; + } + + if (param.in === "header" || param.in === "cookie") { + if (isRequiredWithoutDefault) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); + } + continue; + } + + if ( + schema.type !== "boolean" && + schema.type !== "string" && + schema.type !== "number" && + schema.type !== "integer" + ) { + if (isRequiredWithoutDefault) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); + } + continue; + } + + if (param.in === "query" || param.in === "path") { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + } + } + + return paramResult; + } + + private hasNestedObjectInSchema(schema: OpenAPIV3.SchemaObject): boolean { + if (schema.type === "object") { + for (const property in schema.properties) { + const nestedSchema = schema.properties[property] as OpenAPIV3.SchemaObject; + if (nestedSchema.type === "object") { + return true; + } + } + } + return false; + } +} diff --git a/packages/spec-parser/src/validators/validatorFactory.ts b/packages/spec-parser/src/validators/validatorFactory.ts new file mode 100644 index 0000000000..ad8ee858e7 --- /dev/null +++ b/packages/spec-parser/src/validators/validatorFactory.ts @@ -0,0 +1,23 @@ +import { OpenAPIV3 } from "openapi-types"; +import { ParseOptions, ProjectType } from "../interfaces"; +import { CopilotValidator } from "./copilotValidator"; +import { SMEValidator } from "./smeValidator"; +import { TeamsAIValidator } from "./teamsAIValidator"; +import { Validator } from "./validator"; + +export class ValidatorFactory { + static create(spec: OpenAPIV3.Document, options: ParseOptions): Validator { + const type = options.projectType ?? ProjectType.SME; + + switch (type) { + case ProjectType.SME: + return new SMEValidator(spec, options); + case ProjectType.Copilot: + return new CopilotValidator(spec, options); + case ProjectType.TeamsAi: + return new TeamsAIValidator(spec, options); + default: + throw new Error(`Invalid project type: ${type}`); + } + } +} diff --git a/packages/spec-parser/test/browser/specParser.browser.test.ts b/packages/spec-parser/test/browser/specParser.browser.test.ts index 7ab964ab3b..7efc82761e 100644 --- a/packages/spec-parser/test/browser/specParser.browser.test.ts +++ b/packages/spec-parser/test/browser/specParser.browser.test.ts @@ -11,6 +11,7 @@ import { ConstantString } from "../../src/constants"; import { OpenAPIV3 } from "openapi-types"; import { Utils } from "../../src/utils"; import SwaggerParser from "@apidevtools/swagger-parser"; +import { SMEValidator } from "../../src/validators/smeValidator"; describe("SpecParser in Browser", () => { afterEach(() => { @@ -22,6 +23,11 @@ describe("SpecParser in Browser", () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowMissingId: false }); const spec = { + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/pets": { get: { @@ -100,6 +106,11 @@ describe("SpecParser in Browser", () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowMissingId: false }); const spec = { + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/user/{userId}": { get: { @@ -170,13 +181,13 @@ describe("SpecParser in Browser", () => { title: "UserId", description: "User Id", }, + { + name: "name", + title: "Name", + description: "User Name", + }, ], description: "Get user by user id, balabala", - warning: { - type: WarningType.OperationOnlyContainsOptionalParam, - content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, "getUserById"), - data: "getUserById", - }, }, ]); }); @@ -185,6 +196,11 @@ describe("SpecParser in Browser", () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowMissingId: false }); const spec = { + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/user/{userId}": { get: { @@ -233,7 +249,7 @@ describe("SpecParser in Browser", () => { const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); - const listSupportedAPIsSyp = sinon.spy(Utils, "listSupportedAPIs"); + const listAPIsSyp = sinon.spy(SMEValidator.prototype, "listAPIs"); let result = await specParser.listSupportedAPIInfo(); result = await specParser.listSupportedAPIInfo(); expect(result).to.deep.equal([ @@ -252,13 +268,18 @@ describe("SpecParser in Browser", () => { description: "Get user by user id, balabala", }, ]); - expect(listSupportedAPIsSyp.callCount).to.equal(1); + expect(listAPIsSyp.callCount).to.equal(1); }); it("should not list api without operationId with allowMissingId is true", async () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowMissingId: true }); const spec = { + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/pets": { get: { @@ -740,7 +761,7 @@ describe("SpecParser in Browser", () => { const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); - sinon.stub(Utils, "validateSpec").throws(new Error("validateSpec error")); + sinon.stub(SMEValidator.prototype, "validateSpec").throws(new Error("validateSpec error")); const result = await specParser.validate(); expect.fail("Expected SpecParserError to be thrown"); diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index e26a5979f3..60d35dbd1c 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -92,7 +92,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedPlugins: PluginManifestSchema = { schema_version: "v2", - name_for_human: "My API", + name_for_human: "Original Name", description_for_human: "My API description", functions: [ { @@ -153,7 +153,7 @@ describe("updateManifestWithAiPlugin", () => { expect(apiPlugin).to.deep.equal(expectedPlugins); }); - it("should update the manifest with the correct manifest and apiPlugin files with optional parameters", async () => { + it("should update the plugin json correctly when contains env in name and description", async () => { const spec: any = { openapi: "3.0.2", info: { @@ -180,12 +180,6 @@ describe("updateManifestWithAiPlugin", () => { type: "integer", }, }, - { - name: "id", - schema: { - type: "string", - }, - }, ], }, post: { @@ -218,11 +212,14 @@ describe("updateManifestWithAiPlugin", () => { sinon.stub(fs, "pathExists").resolves(true); const originalManifest = { - name: { short: "Original Name", full: "Original Full Name" }, - description: { short: "Original Short Description", full: "Original Full Description" }, + name: { short: "Original Name${{TestEnv}}", full: "Original Full Name" }, + description: { + short: "Original Short Description", + full: "Original Full Description${{TestEnv}}", + }, }; const expectedManifest = { - name: { short: "Original Name", full: "Original Full Name" }, + name: { short: "Original Name${{TestEnv}}", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, plugins: [ { @@ -233,7 +230,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedPlugins: PluginManifestSchema = { schema_version: "v2", - name_for_human: "My API", + name_for_human: "Original Name", description_for_human: "My API description", functions: [ { @@ -246,10 +243,6 @@ describe("updateManifestWithAiPlugin", () => { type: "integer", description: "Maximum number of pets to return", }, - id: { - type: "string", - description: "", - }, }, required: ["limit"], }, @@ -283,7 +276,6 @@ describe("updateManifestWithAiPlugin", () => { ], }; sinon.stub(fs, "readJSON").resolves(originalManifest); - const options: ParseOptions = { allowMethods: ["get", "post"], }; @@ -299,7 +291,7 @@ describe("updateManifestWithAiPlugin", () => { expect(apiPlugin).to.deep.equal(expectedPlugins); }); - it("should generate default ai plugin file if no api", async () => { + it("should update the manifest with the correct manifest and apiPlugin files with optional parameters", async () => { const spec: any = { openapi: "3.0.2", info: { @@ -311,74 +303,6 @@ describe("updateManifestWithAiPlugin", () => { url: "/v3", }, ], - paths: {}, - }; - const manifestPath = "/path/to/your/manifest.json"; - const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; - const pluginFilePath = "/path/to/your/ai-plugin.json"; - - sinon.stub(fs, "pathExists").resolves(true); - const originalManifest = { - name: { short: "Original Name", full: "Original Full Name" }, - description: { short: "Original Short Description", full: "Original Full Description" }, - }; - const expectedManifest = { - name: { short: "Original Name", full: "Original Full Name" }, - description: { short: "My API", full: "My API description" }, - plugins: [ - { - pluginFile: "ai-plugin.json", - }, - ], - }; - - const expectedPlugins: PluginManifestSchema = { - schema_version: "v2", - name_for_human: "My API", - description_for_human: "My API description", - functions: [], - runtimes: [ - { - type: "OpenApi", - auth: { - type: "none", - }, - spec: { - url: "spec/outputSpec.yaml", - }, - run_for_functions: [], - }, - ], - }; - sinon.stub(fs, "readJSON").resolves(originalManifest); - const options: ParseOptions = { - allowMethods: ["get", "post"], - }; - const [manifest, apiPlugin] = await ManifestUpdater.updateManifestWithAiPlugin( - manifestPath, - outputSpecPath, - pluginFilePath, - spec, - options - ); - - expect(manifest).to.deep.equal(expectedManifest); - expect(apiPlugin).to.deep.equal(expectedPlugins); - }); - - it("should truncate if title is long", async () => { - const spec: any = { - openapi: "3.0.2", - info: { - title: - "long title long title long title long title long title long title long title long title long title long title long title long title", - description: "This is the description", - }, - servers: [ - { - url: "/v3", - }, - ], paths: { "/pets": { get: { @@ -394,6 +318,12 @@ describe("updateManifestWithAiPlugin", () => { type: "integer", }, }, + { + name: "id", + schema: { + type: "string", + }, + }, ], }, post: { @@ -431,10 +361,7 @@ describe("updateManifestWithAiPlugin", () => { }; const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, - description: { - short: "long title long title long title long title long title long title long title lon", - full: "This is the description", - }, + description: { short: "My API", full: "My API description" }, plugins: [ { pluginFile: "ai-plugin.json", @@ -444,9 +371,8 @@ describe("updateManifestWithAiPlugin", () => { const expectedPlugins: PluginManifestSchema = { schema_version: "v2", - name_for_human: - "long title long title long title long title long title long title long title long title long title long title long title long title", - description_for_human: "This is the description", + name_for_human: "Original Name", + description_for_human: "My API description", functions: [ { name: "getPets", @@ -458,6 +384,10 @@ describe("updateManifestWithAiPlugin", () => { type: "integer", description: "Maximum number of pets to return", }, + id: { + type: "string", + description: "", + }, }, required: ["limit"], }, @@ -491,6 +421,74 @@ describe("updateManifestWithAiPlugin", () => { ], }; sinon.stub(fs, "readJSON").resolves(originalManifest); + + const options: ParseOptions = { + allowMethods: ["get", "post"], + }; + const [manifest, apiPlugin] = await ManifestUpdater.updateManifestWithAiPlugin( + manifestPath, + outputSpecPath, + pluginFilePath, + spec, + options + ); + + expect(manifest).to.deep.equal(expectedManifest); + expect(apiPlugin).to.deep.equal(expectedPlugins); + }); + + it("should generate default ai plugin file if no api", async () => { + const spec: any = { + openapi: "3.0.2", + info: { + title: "My API", + description: "My API description", + }, + servers: [ + { + url: "/v3", + }, + ], + paths: {}, + }; + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const pluginFilePath = "/path/to/your/ai-plugin.json"; + + sinon.stub(fs, "pathExists").resolves(true); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "My API", full: "My API description" }, + plugins: [ + { + pluginFile: "ai-plugin.json", + }, + ], + }; + + const expectedPlugins: PluginManifestSchema = { + schema_version: "v2", + name_for_human: "Original Name", + description_for_human: "My API description", + functions: [], + runtimes: [ + { + type: "OpenApi", + auth: { + type: "none", + }, + spec: { + url: "spec/outputSpec.yaml", + }, + run_for_functions: [], + }, + ], + }; + sinon.stub(fs, "readJSON").resolves(originalManifest); const options: ParseOptions = { allowMethods: ["get", "post"], }; @@ -733,7 +731,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -743,7 +746,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -864,25 +869,35 @@ describe("manifestUpdater", () => { title: "Limit", description: "Maximum number of pets to return", inputType: "number", + isRequired: true, + }, + { + name: "name", + title: "Name", + description: "Pet Name", + inputType: "text", + isRequired: true, }, - { name: "name", title: "Name", description: "Pet Name", inputType: "text" }, { name: "id", title: "Id", description: "Pet Id", inputType: "number", + isRequired: true, }, { name: "other1", title: "Other1", description: "Other Property1", inputType: "toggle", + isRequired: true, }, { name: "other2", title: "Other2", description: "Other Property2", inputType: "choiceset", + isRequired: true, choices: [ { title: "enum1", @@ -1005,8 +1020,15 @@ describe("manifestUpdater", () => { title: "Id", description: "Pet Id", inputType: "number", + isRequired: true, + }, + { + name: "name", + title: "Name", + description: "Pet Name", + inputType: "text", + isRequired: true, }, - { name: "name", title: "Name", description: "Pet Name", inputType: "text" }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, @@ -1120,7 +1142,13 @@ describe("manifestUpdater", () => { ); expect(result).to.deep.equal(expectedManifest); - expect(warnings).to.deep.equal([]); + expect(warnings).to.deep.equal([ + { + type: WarningType.OperationOnlyContainsOptionalParam, + content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, "getPets"), + data: "getPets", + }, + ]); }); it("should contain auth property in manifest if pass the api key auth", async () => { @@ -1154,7 +1182,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1164,7 +1197,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1173,7 +1208,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { + authScheme: { type: "apiKey" as const, name: "api_key_name", in: "header", @@ -1199,6 +1234,88 @@ describe("manifestUpdater", () => { expect(warnings).to.deep.equal([]); }); + it("should contain auth property in manifest if pass the bearer token auth", async () => { + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const adaptiveCardFolder = "/path/to/your/adaptiveCards"; + sinon.stub(fs, "pathExists").resolves(true); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + composeExtensions: [], + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: spec.info.title, full: spec.info.description }, + composeExtensions: [ + { + composeExtensionType: "apiBased", + apiSpecificationFile: "spec/outputSpec.yaml", + authorization: { + authType: "apiSecretServiceAuth", + apiSecretServiceAuthConfiguration: { + apiSecretRegistrationId: "${{BEARER_TOKEN_AUTH_REGISTRATION_ID}}", + }, + }, + commands: [ + { + context: ["compose"], + type: "query", + title: "Get all pets", + description: "Returns all pets from the system that the user has access to", + id: "getPets", + parameters: [ + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, + ], + apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", + }, + { + context: ["compose"], + type: "query", + title: "Create a pet", + description: "Create a new pet in the store", + id: "createPet", + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], + apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", + }, + ], + }, + ], + }; + const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); + const bearerTokenAuth: AuthInfo = { + authScheme: { + type: "http" as const, + scheme: "bearer", + }, + name: "bearer_token_auth", + }; + const options: ParseOptions = { + allowMultipleParameters: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const [result, warnings] = await ManifestUpdater.updateManifest( + manifestPath, + outputSpecPath, + spec, + options, + adaptiveCardFolder, + bearerTokenAuth + ); + + expect(result).to.deep.equal(expectedManifest); + expect(warnings).to.deep.equal([]); + }); + it("should contain auth property in manifest if pass the oauth2 with auth code flow", async () => { const manifestPath = "/path/to/your/manifest.json"; const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; @@ -1230,7 +1347,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1240,7 +1362,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1253,7 +1377,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const oauth2: AuthInfo = { - authSchema: { + authScheme: { type: "oauth2", flows: { authorizationCode: { @@ -1312,7 +1436,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1322,7 +1451,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1331,7 +1462,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const basicAuth: AuthInfo = { - authSchema: { + authScheme: { type: "http" as const, scheme: "basic", }, @@ -1386,7 +1517,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1396,7 +1532,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1405,10 +1543,10 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { - type: "apiKey" as const, - name: "key_name", - in: "header", + authScheme: { + type: "http" as const, + scheme: "bearer", + bearerFormat: "JWT", }, name: "*api-key_auth", }; @@ -1588,6 +1726,7 @@ describe("manifestUpdater", () => { description: "Maximum number of pets to return", name: "limit", title: "Limit", + isRequired: true, }, ], title: "Get all pets", @@ -1603,6 +1742,7 @@ describe("manifestUpdater", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], title: "Create a pet", @@ -1662,7 +1802,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1676,6 +1821,7 @@ describe("manifestUpdater", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], title: "Create a pet", @@ -1833,7 +1979,12 @@ describe("generateCommands", () => { id: "getPets", description: "", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1848,6 +1999,7 @@ describe("generateCommands", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", @@ -1858,7 +2010,9 @@ describe("generateCommands", () => { title: "Get a pet by ID", description: "", id: "getPetById", - parameters: [{ name: "id", title: "Id", description: "ID of the pet to retrieve" }], + parameters: [ + { name: "id", title: "Id", description: "ID of the pet to retrieve", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/getPetById.json", }, { @@ -1867,7 +2021,9 @@ describe("generateCommands", () => { description: "", title: "Get all pets owned by an owner", id: "getOwnerPets", - parameters: [{ name: "ownerId", title: "OwnerId", description: "ID of the owner" }], + parameters: [ + { name: "ownerId", title: "OwnerId", description: "ID of the owner", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/getOwnerPets.json", }, ]; @@ -1924,6 +2080,7 @@ describe("generateCommands", () => { title: "LongLimitlongLimitlongLimitlongL", description: "Long maximum number of pets to return. Long maximum number of pets to return. Long maximum number of pets to return. Long maximu", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", @@ -2092,7 +2249,13 @@ describe("generateCommands", () => { type: "query", }, ]); - expect(warnings).to.deep.equal([]); + expect(warnings).to.deep.equal([ + { + type: WarningType.OperationOnlyContainsOptionalParam, + content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, "createPet"), + data: "createPet", + }, + ]); }); it("should not show warning for each GET/POST operation in the spec if only contains 1 optional parameters", async () => { @@ -2169,7 +2332,18 @@ describe("generateCommands", () => { type: "query", }, ]); - expect(warnings).to.deep.equal([]); + expect(warnings).to.deep.equal([ + { + type: WarningType.OperationOnlyContainsOptionalParam, + content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, "getPets"), + data: "getPets", + }, + { + type: WarningType.OperationOnlyContainsOptionalParam, + content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, "createPet"), + data: "createPet", + }, + ]); }); it("should only generate commands for GET operation with required parameter", async () => { @@ -2196,7 +2370,7 @@ describe("generateCommands", () => { title: "Get all pets", description: "", id: "getPets", - parameters: [{ name: "id", title: "Id", description: "ID of the pet" }], + parameters: [{ name: "id", title: "Id", description: "ID of the pet", isRequired: true }], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, ]; @@ -2316,6 +2490,7 @@ describe("generateCommands", () => { description: "Name of the pet", name: "requestBody", title: "RequestBody", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", diff --git a/packages/spec-parser/test/specFilter.test.ts b/packages/spec-parser/test/specFilter.test.ts index 545c5701ef..491d21cbdd 100644 --- a/packages/spec-parser/test/specFilter.test.ts +++ b/packages/spec-parser/test/specFilter.test.ts @@ -9,6 +9,7 @@ import sinon from "sinon"; import { SpecParserError } from "../src/specParserError"; import { ErrorType, ParseOptions, ProjectType } from "../src/interfaces"; import { Utils } from "../src/utils"; +import { ValidatorFactory } from "../src/validators/validatorFactory"; describe("specFilter", () => { afterEach(() => { @@ -20,6 +21,11 @@ describe("specFilter", () => { title: "My API", version: "1.0.0", }, + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello": { get: { @@ -96,6 +102,11 @@ describe("specFilter", () => { title: "My API", version: "1.0.0", }, + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello": { get: { @@ -169,6 +180,11 @@ describe("specFilter", () => { title: "My API", version: "1.0.0", }, + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello": { get: { @@ -215,6 +231,11 @@ describe("specFilter", () => { const filter = ["get /hello/{id}"]; const unResolvedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello/{id}": { get: { @@ -248,6 +269,11 @@ describe("specFilter", () => { }; const expectedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: {}, }; @@ -274,6 +300,11 @@ describe("specFilter", () => { const filter = ["get /hello/{id}"]; const unResolvedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello/{id}": { get: { @@ -307,6 +338,11 @@ describe("specFilter", () => { }; const expectedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello/{id}": { get: { @@ -380,6 +416,11 @@ describe("specFilter", () => { const filter = ["get /nonexistent"]; const unResolvedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: { "/hello": { get: { @@ -395,6 +436,11 @@ describe("specFilter", () => { const expectedSpec = { openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], paths: {}, }; @@ -434,12 +480,29 @@ describe("specFilter", () => { expect(clonedSpec).to.deep.equal(unResolveSpec); }); - it("should throw a SpecParserError if isSupportedApi throws an error", () => { - const filter = ["GET /path"]; - const unResolveSpec = {} as any; - const isSupportedApiStub = sinon - .stub(Utils, "isSupportedApi") - .throws(new Error("isSupportedApi error")); + it("should throw a SpecParserError if ValidatorFactory throws an error", () => { + const filter = ["GET /hello"]; + const unResolveSpec = { + openapi: "3.0.0", + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/hello": { + get: { + responses: { + "200": { + description: "OK", + }, + }, + }, + }, + }, + } as any; + + sinon.stub(ValidatorFactory, "create").throws(new Error("ValidatorFactory create error")); try { const options: ParseOptions = { @@ -456,7 +519,7 @@ describe("specFilter", () => { } catch (err: any) { expect(err).to.be.instanceOf(SpecParserError); expect(err.errorType).to.equal(ErrorType.FilterSpecFailed); - expect(err.message).to.equal("Error: isSupportedApi error"); + expect(err.message).to.equal("Error: ValidatorFactory create error"); } }); }); diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index a4e14ca72e..4f6d53adac 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -18,6 +18,9 @@ import { AdaptiveCardGenerator } from "../src/adaptiveCardGenerator"; import { Utils } from "../src/utils"; import jsyaml from "js-yaml"; import mockedEnv, { RestoreFn } from "mocked-env"; +import { Validator } from "../src/validators/validator"; +import { SMEValidator } from "../src/validators/smeValidator"; +import { ValidatorFactory } from "../src/validators/validatorFactory"; describe("SpecParser", () => { afterEach(() => { @@ -491,6 +494,217 @@ describe("SpecParser", () => { sinon.assert.calledOnce(dereferenceStub); }); + it("should return a valid result when the spec is valid for copilot", async () => { + const specPath = "path/to/spec"; + const spec = { + openapi: "3.0.2", + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/pet": { + get: { + tags: ["pet"], + operationId: "getPet", + summary: "Get pet information from the store", + parameters: [ + { + name: "tags", + in: "query", + description: "Tags to filter by", + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Pet", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const specParser = new SpecParser(specPath, { projectType: ProjectType.Copilot }); + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); + const result = await specParser.validate(); + expect(result.status).to.equal(ValidationStatus.Valid); + expect(result.warnings).to.be.an("array").that.is.empty; + expect(result.errors).to.be.an("array").that.is.empty; + sinon.assert.calledOnce(dereferenceStub); + }); + + it("should only create validator once if already created", async () => { + const specPath = "path/to/spec"; + const spec = { + openapi: "3.0.2", + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/pet": { + get: { + tags: ["pet"], + operationId: "getPet", + summary: "Get pet information from the store", + parameters: [ + { + name: "tags", + in: "query", + description: "Tags to filter by", + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Pet", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const specParser = new SpecParser(specPath, { projectType: ProjectType.Copilot }); + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); + const createValidatorSpy = sinon.spy(ValidatorFactory, "create"); + const result1 = await specParser.validate(); + const result2 = await specParser.validate(); + sinon.assert.calledOnce(createValidatorSpy); + }); + + it("should return error result is project type is SME/Copilot, and OpenAPI spec version >= 3.1.0", async () => { + const specPath = "path/to/spec"; + const spec = { + openapi: "3.1.0", + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/pet": { + get: { + tags: ["pet"], + operationId: "getPet", + summary: "Get pet information from the store", + parameters: [ + { + name: "tags", + in: "query", + description: "Tags to filter by", + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Pet", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const specParser = new SpecParser(specPath); + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); + const result = await specParser.validate(); + expect(result.errors[0].type).equal(ErrorType.SpecVersionNotSupported); + expect(result.errors[0].content).equal( + Utils.format(ConstantString.SpecVersionNotSupported, "3.1.0") + ); + expect(result.errors[0].data).equal("3.1.0"); + expect(result.status).equal(ValidationStatus.Error); + + sinon.assert.calledOnce(dereferenceStub); + }); + + it("should return valid result is project type is Teams Ai, and OpenAPI spec version >= 3.1.0", async () => { + const specPath = "path/to/spec"; + const spec = { + openapi: "3.1.0", + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/pet": { + get: { + tags: ["pet"], + operationId: "getPet", + summary: "Get pet information from the store", + parameters: [ + { + name: "tags", + in: "query", + description: "Tags to filter by", + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Pet", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const specParser = new SpecParser(specPath, { projectType: ProjectType.TeamsAi }); + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); + const result = await specParser.validate(); + expect(result.status).to.equal(ValidationStatus.Valid); + expect(result.warnings).to.be.an("array").that.is.empty; + expect(result.errors).to.be.an("array").that.is.empty; + sinon.assert.calledOnce(dereferenceStub); + }); + it("should throw a SpecParserError when an error occurs", async () => { const specPath = "path/to/spec"; const spec = { @@ -537,7 +751,7 @@ describe("SpecParser", () => { const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); const validateStub = sinon.stub(specParser.parser, "validate").resolves(spec as any); - sinon.stub(Utils, "validateSpec").throws(new Error("validateSpec error")); + sinon.stub(SMEValidator.prototype, "validateSpec").throws(new Error("validateSpec error")); const result = await specParser.validate(); expect.fail("Expected SpecParserError to be thrown"); @@ -1270,6 +1484,114 @@ describe("SpecParser", () => { } }); + it("should work if two api contains same auth", async () => { + const specParser = new SpecParser("path/to/spec.yaml", { allowAPIKeyAuth: true }); + const spec = { + openapi: "3.0.0", + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, + paths: { + "/hello": { + get: { + operationId: "getHello", + security: [ + { + api_key: [], + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + post: { + security: [ + { + api_key: [], + }, + ], + operationId: "postHello", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const specFilterStub = sinon.stub(SpecFilter, "specFilter").returns({} as any); + const outputFileStub = sinon.stub(fs, "outputFile").resolves(); + const outputJSONStub = sinon.stub(fs, "outputJSON").resolves(); + const JsyamlSpy = sinon.spy(jsyaml, "dump"); + + const manifestUpdaterStub = sinon + .stub(ManifestUpdater, "updateManifest") + .resolves([{}, []] as any); + const generateAdaptiveCardStub = sinon + .stub(AdaptiveCardGenerator, "generateAdaptiveCard") + .returns([ + { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.5", + body: [ + { + type: "TextBlock", + text: "id: ${id}", + wrap: true, + }, + ], + }, + "$", + ]); + + const filter = ["get /hello", "post /hello"]; + + const outputSpecPath = "path/to/output.yaml"; + const result = await specParser.generate("path/to/manifest.json", filter, outputSpecPath); + expect(result.allSuccess).to.be.true; + expect(JsyamlSpy.calledOnce).to.be.true; + expect(specFilterStub.calledOnce).to.be.true; + expect(outputFileStub.calledOnce).to.be.true; + expect(manifestUpdaterStub.calledOnce).to.be.true; + expect(outputFileStub.firstCall.args[0]).to.equal(outputSpecPath); + expect(outputJSONStub.calledOnce).to.be.true; + expect(generateAdaptiveCardStub.notCalled).to.be.true; + }); + it("should work if contain multiple API key in spec when project Type is teams ai", async () => { const specParser = new SpecParser("path/to/spec.yaml", { allowAPIKeyAuth: true, @@ -1547,6 +1869,15 @@ describe("SpecParser", () => { url: "https://server1", }, ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, paths: { "/pets": { get: { @@ -1601,13 +1932,40 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /pets", + server: "", + operationId: "getPetById", + reason: ["auth-type-is-not-supported", "response-json-is-empty", "no-parameter"], + isValid: false, + }, + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "POST /user/{userId}", + server: "", + operationId: "createUser", + reason: ["auth-type-is-not-supported", "response-json-is-empty", "no-parameter"], + isValid: false, + }, + { + api: "POST /store/order", + server: "", + operationId: "placeOrder", + reason: ["response-json-is-empty", "no-parameter"], + isValid: false, + }, + ], + allAPICount: 4, + validAPICount: 1, + }); }); it("should generate an operationId if not exist", async () => { @@ -1648,15 +2006,6 @@ describe("SpecParser", () => { }, }, }, - post: { - operationId: "createUser", - security: [{ api_key: [] }], - }, - }, - "/store/order": { - post: { - operationId: "placeOrder", - }, }, }, }; @@ -1666,13 +2015,19 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserUserId", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserUserId", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return correct server information", async () => { @@ -1742,13 +2097,19 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server5", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server5", + operationId: "getUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return a list of HTTP methods and paths for all GET with 1 parameter and api key auth security", async () => { @@ -1809,14 +2170,101 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, - operationId: "getUserById", + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { + authScheme: { type: "apiKey", name: "api_key", in: "header" }, + name: "api_key", + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); + }); + + it("should return a list of HTTP methods and paths for all GET with 1 parameter and bearer token auth security", async () => { + const specPath = "valid-spec.yaml"; + const specParser = new SpecParser(specPath, { allowBearerTokenAuth: true }); + const spec = { + components: { + securitySchemes: { + bearerTokenAuth: { + type: "http", + scheme: "bearer", + }, + }, }, - ]); + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/user/{userId}": { + get: { + security: [{ bearerTokenAuth: [] }], + operationId: "getUserById", + parameters: [ + { + name: "userId", + in: "path", + schema: { + type: "string", + }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + + const result = await specParser.list(); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { + authScheme: { + type: "http", + scheme: "bearer", + }, + name: "bearerTokenAuth", + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return correct auth information", async () => { @@ -1933,20 +2381,42 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, - operationId: "getUserById", - }, - { - api: "POST /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, - operationId: "postUserById", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { + authScheme: { + type: "apiKey", + name: "api_key1", + in: "header", + }, + name: "api_key1", + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + { + api: "POST /user/{userId}", + server: "https://server1", + auth: { + authScheme: { + type: "apiKey", + name: "api_key1", + in: "header", + }, + name: "api_key1", + }, + operationId: "postUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 2, + validAPICount: 2, + }); }); it("should allow multiple parameters if allowMultipleParameters is true", async () => { @@ -2006,26 +2476,31 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should not list api without operationId with allowMissingId is false", async () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowMissingId: false }); const spec = { - paths: { - "/pets": { - get: { - operationId: "getPetById", - security: [{ api_key: [] }], - }, + servers: [ + { + url: "https://server1", }, + ], + paths: { "/user/{userId}": { get: { parameters: [ @@ -2054,15 +2529,6 @@ describe("SpecParser", () => { }, }, }, - post: { - operationId: "createUser", - security: [{ api_key: [] }], - }, - }, - "/store/order": { - post: { - operationId: "placeOrder", - }, }, }, }; @@ -2072,7 +2538,19 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "", + operationId: "getUserUserId", + isValid: false, + reason: ["missing-operation-id"], + }, + ], + allAPICount: 1, + validAPICount: 0, + }); }); it("should throw an error when the SwaggerParser library throws an error", async () => { @@ -2129,15 +2607,6 @@ describe("SpecParser", () => { }, }, }, - post: { - operationId: "createUser", - security: [{ api_key: [] }], - }, - }, - "/store/order": { - post: { - operationId: "placeOrder", - }, }, }, }; @@ -2145,13 +2614,28 @@ describe("SpecParser", () => { const specParser = new SpecParser(specPath); const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); - try { - await specParser.list(); - expect.fail("Expected an error to be thrown"); - } catch (err) { - expect((err as SpecParserError).message).contain(ConstantString.NoServerInformation); - expect((err as SpecParserError).errorType).to.equal(ErrorType.NoServerInformation); - } + const result = await specParser.list(); + + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /pets", + server: "", + operationId: "getPetById", + isValid: false, + reason: ["no-server-information", "response-json-is-empty", "no-parameter"], + }, + { + api: "GET /user/{userId}", + server: "", + operationId: "getUserUserId", + isValid: false, + reason: ["no-server-information"], + }, + ], + allAPICount: 2, + validAPICount: 0, + }); }); it("should return correct domain when domain contains placeholder", async () => { @@ -2215,14 +2699,27 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + APIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { + authScheme: { + type: "apiKey", + name: "api_key", + in: "header", + }, + name: "api_key", + }, + operationId: "getUserById", + isValid: true, + reason: [], + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); }); diff --git a/packages/spec-parser/test/utils.test.ts b/packages/spec-parser/test/utils.test.ts index 91b6273657..add04359a9 100644 --- a/packages/spec-parser/test/utils.test.ts +++ b/packages/spec-parser/test/utils.test.ts @@ -65,2349 +65,6 @@ describe("utils", () => { }); }); - describe("isSupportedApi", () => { - it("should return true if method is GET, path is valid, and parameter is supported", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if have no operationId with allowMissingId is false", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: false, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is POST, path is valid, and no required parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is POST, path is valid, parameter is supported and only one required param in parameters but contains auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if allowAPIKeyAuth is true but contains multiple apiKey auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - api_key: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if allowOauth2 is true and contains aad auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - oauth: { - type: "oauth2", - flows: { - authorizationCode: { - authorizationUrl: "https://example.com/api/oauth/dialog", - tokenUrl: "https://example.com/api/oauth/token", - refreshUrl: "https://example.com/api/outh/refresh", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contain oauth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - authorizationCode: { - authorizationUrl: "https://example.com/api/oauth/dialog", - tokenUrl: "https://example.com/api/oauth/token", - refreshUrl: "https://example.com/api/outh/refresh", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - implicit: { - authorizationUrl: "https://example.com/api/oauth/dialog", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - implicit: { - authorizationUrl: "https://example.com/api/oauth/dialog", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return true if method is POST, path is valid, parameter is supported and only one required param in parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is POST, path is valid, parameter is supported and both postBody and parameters contains required param", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is POST, path is valid, parameter is supported and both postBody and parameters contains multiple required param for copilot", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should support multiple required parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should not support multiple required parameters count larger than 5", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should support multiple required parameters count larger than 5 for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should not support multiple required parameters count larger than 5 for copilot", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is POST, but requestBody contains unsupported parameter and required", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - items: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is POST, but requestBody contains unsupported parameter and required but has default value", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - default: ["item"], - items: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is POST, but parameters contain nested object", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "object", - properties: { - id: { - type: "string", - }, - }, - }, - }, - }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if method is POST, but requestBody contain nested object", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "object", - properties: { - id: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is POST, path is valid, parameter is supported and only one required param in postBody", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is GET, path is valid, parameter is supported, but response is empty", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 400: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if method is not GET or POST", () => { - const method = "PUT"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if path is not valid", () => { - const method = "GET"; - const path = "/invalid"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if parameter is not supported and required", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "object" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should ignore unsupported schema type with default value", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "object", default: { name: "test" } }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if parameter is in header and required", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "header", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if parameter is in header and required for copilot", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "header", - required: true, - schema: { type: "string" }, - }, - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if there is no parameters", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if there is no parameters for copilot", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if parameters is null", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if has parameters but no 20X response", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "object" }, - }, - ], - responses: { - 404: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if method is POST, but request body contains media type other than application/json", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is POST, and request body contains media type other than application/json for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - - it("should return false if method is POST, and request body schema is not object", () => { - const method = "POST"; - const path = "/users"; - const spec = { - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return false if method is GET, but response body contains media type other than application/json", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, false); - }); - - it("should return true if method is GET, and response body contains media type other than application/json for teams ai project", () => { - const method = "GET"; - const path = "/users"; - const spec = { - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const result = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(result, true); - }); - }); - describe("getUrlProtocol", () => { it("should return the protocol of a valid URL", () => { const url = "https://example.com/path/to/file"; @@ -2434,238 +91,6 @@ describe("utils", () => { }); }); - describe("checkRequiredParameters", () => { - it("should valid if there is only one required parameter", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - }); - - it("should valid if there are multiple required parameters", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 2); - assert.strictEqual(result.optionalNum, 0); - }); - - it("should not valid if any required parameter is in header or cookie and is required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should valid if parameter in header or cookie is required but have default value", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: true, schema: { type: "string", default: "value" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - // header param is ignored - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should treat required param with default value as optional param", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string", default: "value" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "query", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 2); - }); - - it("should ignore required query param with default value and array type", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "query", required: true, schema: { type: "array", default: ["item"] } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should ignore in header or cookie if is not required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: false, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should return false if any schema is array and required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "array" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should return false if any schema is object and required", () => { - const paramObject = [ - { in: "query", required: false, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "object" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should return valid if any schema is object but optional", () => { - const paramObject = [ - { in: "query", required: false, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "object" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - }); - }); - - describe("checkPostBodyRequiredParameters", () => { - it("should return 0 for an empty schema", () => { - const schema = {}; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 0); - }); - - it("should treat required schema with default value as optional param", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - default: "value", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return 1 if the schema has a required string property", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 0); - assert.strictEqual(result.isValid, true); - }); - - it("should return 0 if the schema has an optional string property", () => { - const schema = { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return the correct count for a nested schema", () => { - const schema = { - type: "object", - required: ["name", "address"], - properties: { - name: { - type: "string", - }, - address: { - type: "object", - required: ["street"], - properties: { - street: { - type: "string", - }, - city: { - type: "string", - }, - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 2); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return not valid for an unsupported schema type", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - items: { - type: "string", - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.isValid, false); - }); - - it("should return valid for an unsupported schema type but it is required with default value", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - default: ["item"], - items: { - type: "string", - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 0); - }); - }); - describe("checkServerUrl", () => { it("should return an empty array if the server URL is valid", () => { const servers = [{ url: "https://example.com" }]; @@ -2720,7 +145,7 @@ describe("utils", () => { ]); }); - it("should return an error if there is no server information in supported apis", () => { + it("should return an error if protocol is not supported ", () => { const spec = { paths: { "/api": { @@ -2743,8 +168,9 @@ describe("utils", () => { const errors = Utils.validateServer(spec as any, options); assert.deepStrictEqual(errors, [ { - type: ErrorType.NoServerInformation, - content: ConstantString.NoServerInformation, + type: ErrorType.UrlProtocolNotSupported, + content: Utils.format(ConstantString.UrlProtocolNotSupported, "ftp"), + data: "ftp", }, ]); }); @@ -2968,8 +394,9 @@ describe("utils", () => { describe("getResponseJson", () => { it("should return an empty object if no JSON response is defined", () => { const operationObject = {}; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({}); + expect(multipleMediaType).to.be.false; }); it("should return the JSON response for status code 200", () => { @@ -2989,7 +416,7 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({ schema: { type: "object", @@ -2998,6 +425,7 @@ describe("utils", () => { }, }, }); + expect(multipleMediaType).to.be.false; }); it("should return empty JSON response for status code 200 with multiple media type", () => { @@ -3025,44 +453,9 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({}); - }); - - it("should return JSON response for status code 200 with multiple media type when it is teams ai project", () => { - const operationObject = { - responses: { - "200": { - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }, - }, - }, - }, - } as any; - const json = Utils.getResponseJson(operationObject, true); - expect(json).to.deep.equal({ - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }); + expect(multipleMediaType).to.be.true; }); it("should return the JSON response for status code 201", () => { @@ -3082,7 +475,7 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({ schema: { type: "object", @@ -3091,6 +484,8 @@ describe("utils", () => { }, }, }); + + expect(multipleMediaType).to.be.false; }); it("should return the JSON response for the default status code", () => { @@ -3110,7 +505,7 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({ schema: { type: "object", @@ -3119,6 +514,7 @@ describe("utils", () => { }, }, }); + expect(multipleMediaType).to.be.false; }); it("should return the JSON response for the 200 status code", () => { @@ -3150,7 +546,7 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({ schema: { type: "object", @@ -3159,6 +555,7 @@ describe("utils", () => { }, }, }); + expect(multipleMediaType).to.be.false; }); it("should return an empty object if all JSON responses are undefined", () => { @@ -3190,8 +587,9 @@ describe("utils", () => { }, }, } as any; - const json = Utils.getResponseJson(operationObject); + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); expect(json).to.deep.equal({}); + expect(multipleMediaType).to.be.false; }); }); @@ -3200,7 +598,7 @@ describe("utils", () => { process.env.OPENAPI_SERVER_URL = "https://localhost:3000/api"; const url = "${{OPENAPI_SERVER_URL}}"; const expectedUrl = "https://localhost:3000/api"; - const resolvedUrl = Utils.resolveServerUrl(url); + const resolvedUrl = Utils.resolveEnv(url); assert.strictEqual(resolvedUrl, expectedUrl); }); @@ -3209,7 +607,7 @@ describe("utils", () => { const url = "${{OPENAPI_SERVER_URL}}"; const expectedUrl = "https://localhost:3000/api"; assert.throws( - () => Utils.resolveServerUrl(url), + () => Utils.resolveEnv(url), Error, Utils.format(ConstantString.ResolveServerUrlFailed, "OPENAPI_SERVER_URL") ); @@ -3220,7 +618,7 @@ describe("utils", () => { process.env.API_PORT = "3000"; const url = "http://${{API_HOST}}:${{API_PORT}}/api"; const expectedUrl = "http://localhost:3000/api"; - const resolvedUrl = Utils.resolveServerUrl(url); + const resolvedUrl = Utils.resolveEnv(url); assert.strictEqual(resolvedUrl, expectedUrl); }); @@ -3229,7 +627,7 @@ describe("utils", () => { process.env.API_HOST = "localhost"; const url = "http://${{API_HOST}}:${{API_PORT}}/api"; assert.throws( - () => Utils.resolveServerUrl(url), + () => Utils.resolveEnv(url), Error, Utils.format(ConstantString.ResolveServerUrlFailed, "API_PORT") ); diff --git a/packages/spec-parser/test/validator.test.ts b/packages/spec-parser/test/validator.test.ts new file mode 100644 index 0000000000..051e855ae7 --- /dev/null +++ b/packages/spec-parser/test/validator.test.ts @@ -0,0 +1,2742 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert, expect } from "chai"; +import "mocha"; +import { ErrorType, ProjectType, ParseOptions } from "../src/interfaces"; +import { ValidatorFactory } from "../src/validators/validatorFactory"; +import { SMEValidator } from "../src/validators/smeValidator"; +import { CopilotValidator } from "../src/validators/copilotValidator"; +import { TeamsAIValidator } from "../src/validators/teamsAIValidator"; + +describe("Validator", () => { + describe("ValidatorFactory", () => { + it("should create validator correctly", () => { + const options: ParseOptions = { + projectType: undefined, + }; + + let validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, SMEValidator); + + options.projectType = ProjectType.SME; + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, SMEValidator); + + options.projectType = ProjectType.Copilot; + + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, CopilotValidator); + + options.projectType = ProjectType.TeamsAi; + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, TeamsAIValidator); + }); + + it("should throw error if project type is unknown", () => { + const options: ParseOptions = { + projectType: "test" as any, + }; + + assert.throws( + () => { + ValidatorFactory.create({} as any, options); + }, + Error, + "Invalid project type: test" + ); + }); + }); + describe("SMEValidator", () => { + it("should return true if method is GET, path is valid, and parameter is supported", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if have no operationId with allowMissingId is false", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: false, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MissingOperationId]); + }); + + it("should return true if method is POST, path is valid, and no required parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, path is valid, parameter is supported and only one required param in parameters but contains auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return true if allowBearerTokenAuth is true and contains bearer token auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + bearer_token1: { + type: "http", + scheme: "bearer", + }, + bearer_token2: { + type: "http", + scheme: "bearer", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + bearer_token2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowBearerTokenAuth: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if allowAPIKeyAuth is true but contains multiple apiKey auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + api_key: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MultipleAuthNotSupported]); + }); + + it("should return true if allowOauth2 is true and contains aad auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + oauth: { + type: "oauth2", + flows: { + authorizationCode: { + authorizationUrl: "https://example.com/api/oauth/dialog", + tokenUrl: "https://example.com/api/oauth/token", + refreshUrl: "https://example.com/api/outh/refresh", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contain oauth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + authorizationCode: { + authorizationUrl: "https://example.com/api/oauth/dialog", + tokenUrl: "https://example.com/api/oauth/token", + refreshUrl: "https://example.com/api/outh/refresh", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return false if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + implicit: { + authorizationUrl: "https://example.com/api/oauth/dialog", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return true if method is POST, path is valid, parameter is supported and only one required param in parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, path is valid, parameter is supported and both postBody and parameters contains required param", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); + }); + + it("should support multiple required parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should not support multiple required parameters count larger than 5", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); + }); + + it("should return false if method is POST, but requestBody contains unsupported parameter and required", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "array", + items: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.PostBodyContainsRequiredUnsupportedSchema]); + }); + + it("should return true if method is POST, but requestBody contains unsupported parameter and required but has default value", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "array", + default: ["item"], + items: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is POST, path is valid, parameter is supported and only one required param in postBody", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is GET, path is valid, parameter is supported, but response is empty", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 400: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ResponseJsonIsEmpty]); + }); + + it("should return false if method is not GET or POST", () => { + const method = "PUT"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MethodNotAllowed]); + }); + + it("should return false if path is not valid", () => { + const method = "GET"; + const path = "/invalid"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.UrlPathNotExist]); + }); + + it("should return false if parameter is not supported and required", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "object" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); + }); + + it("should return false due to ignore unsupported schema type with default value", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "object", default: { name: "test" } }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if parameter is in header and required", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "header", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); + }); + + it("should return false if there is no parameters", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if parameters is null", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if has parameters but no 20X response", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "object" }, + }, + ], + responses: { + 404: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + + // NoParameter because object is not supported and there is no required parameters + expect(reason).to.have.members([ErrorType.NoParameter, ErrorType.ResponseJsonIsEmpty]); + expect(reason.length).equals(2); + }); + + it("should return false if method is POST, but request body contains media type other than application/json", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ + ErrorType.PostBodyContainMultipleMediaTypes, + ErrorType.ExceededRequiredParamsLimit, + ]); + }); + + it("should return false if method is GET, but response body contains media type other than application/json", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ResponseContainMultipleMediaTypes]); + }); + }); + + describe("CopilotValidator", () => { + it("should return true if method is POST, path is valid, parameter is supported and both postBody and parameters contains multiple required param for copilot", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, and request body schema is not object", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.PostBodySchemaIsNotJson]); + }); + + it("should return true if there is no parameters for copilot", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if parameter is in header and required for copilot", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "header", + required: true, + schema: { type: "string" }, + }, + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should not support multiple required parameters count larger than 5 for copilot", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, parameters contain nested object, and request body is not json", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "object", + properties: { + id: { + type: "string", + }, + }, + }, + }, + }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + expect(reason).to.have.members([ + ErrorType.ParamsContainsNestedObject, + ErrorType.PostBodySchemaIsNotJson, + ]); + expect(reason.length).equals(2); + }); + + it("should return false if method is POST, but requestBody contain nested object", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "object", + properties: { + id: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.RequestBodyContainsNestedObject]); + }); + }); + + describe("TeamsAIValidator", () => { + it("should return true if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + implicit: { + authorizationUrl: "https://example.com/api/oauth/dialog", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should support multiple required parameters count larger than 5 for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is POST, and request body contains media type other than application/json for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is GET, and response body contains media type other than application/json for teams ai project", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + }); +}); diff --git a/packages/tests/package.json b/packages/tests/package.json index 0876c4204a..ac3c600363 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/teamsfx-test", - "version": "0.0.3", + "version": "0.0.4", "description": "A UI Test Project of Teams Toolkit Extension", "private": true, "author": "Microsoft Corporation", diff --git a/packages/tests/src/e2e/bot/ProvisionAiBot.tests.ts b/packages/tests/src/e2e/bot/ProvisionAiBot.tests.ts index 240e75451c..c2c705cf5c 100644 --- a/packages/tests/src/e2e/bot/ProvisionAiBot.tests.ts +++ b/packages/tests/src/e2e/bot/ProvisionAiBot.tests.ts @@ -32,7 +32,7 @@ class AiBotTestCase extends CaseFactory { new AiBotTestCase( Capability.AiBot, 24808531, - "v-ivanchen@microsoft.com", + "qidon@microsoft.com", ["bot"], {} ).test(); diff --git a/packages/tests/src/e2e/caseFactory.ts b/packages/tests/src/e2e/caseFactory.ts index 02d0b9361d..116336ff47 100644 --- a/packages/tests/src/e2e/caseFactory.ts +++ b/packages/tests/src/e2e/caseFactory.ts @@ -128,7 +128,7 @@ export abstract class CaseFactory { onBeforeProvision, onCreate, } = this; - describe("Sample Tests", function () { + describe(`template Test: ${capability}`, function () { const testFolder = getTestFolder(); const appName = getUniqueAppName(); const projectPath = path.resolve(testFolder, appName); diff --git a/packages/tests/src/e2e/samples/ProvisionAdaptiveCard.tests.ts b/packages/tests/src/e2e/samples/ProvisionAdaptiveCard.tests.ts index c1eca0a59b..1f3661ce62 100644 --- a/packages/tests/src/e2e/samples/ProvisionAdaptiveCard.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionAdaptiveCard.tests.ts @@ -13,5 +13,5 @@ class AdaptiveCardTestCase extends CaseFactory {} new AdaptiveCardTestCase( TemplateProjectFolder.AdaptiveCard, 15277474, - "v-ivanchen@microsoft.com" + "qidon@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionBotSSO.tests.ts b/packages/tests/src/e2e/samples/ProvisionBotSSO.tests.ts index 854fdba541..2507457f33 100644 --- a/packages/tests/src/e2e/samples/ProvisionBotSSO.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionBotSSO.tests.ts @@ -13,6 +13,6 @@ class HelloWorldBotSSOTestCase extends CaseFactory {} new HelloWorldBotSSOTestCase( TemplateProjectFolder.HelloWorldBotSSO, 15277464, - "v-ivanchen@microsoft.com", + "yukundong@microsoft.com", ["bot"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionChefBot.tests.ts b/packages/tests/src/e2e/samples/ProvisionChefBot.tests.ts index 21b19a1e78..efda386548 100644 --- a/packages/tests/src/e2e/samples/ProvisionChefBot.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionChefBot.tests.ts @@ -7,25 +7,37 @@ import { TemplateProjectFolder } from "../../utils/constants"; import { CaseFactory } from "./sampleCaseFactory"; +import { Executor } from "../../utils/executor"; import * as fs from "fs-extra"; import * as path from "path"; import { expect } from "chai"; class ChefBotTestCase extends CaseFactory { + public override async onCreate( + appName: string, + testFolder: string, + sampleName: TemplateProjectFolder + ): Promise { + await Executor.openTemplateProject( + appName, + testFolder, + sampleName, + undefined, + "js/samples" + ); + } public override async onAfterCreate(projectPath: string): Promise { expect(fs.pathExistsSync(path.resolve(projectPath, "infra"))).to.be.true; - const userFile = path.resolve(projectPath, "env", `.env.dev.user`); - const KEY = "SECRET_OPENAI_API_KEY=MY_OPENAI_API_KEY"; + const userFile = path.resolve(projectPath, ".env"); + const KEY = "OPENAI_KEY=MY_OPENAI_API_KEY"; fs.writeFileSync(userFile, KEY); - console.log(`add key ${KEY} to .env.dev.user file`); + console.log(`add key ${KEY} to .env file`); } } new ChefBotTestCase( TemplateProjectFolder.ChefBot, 25227103, - "v-ivanchen@microsoft.com", - [], - { skipValidate: true } + "ning.tang@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionContactExporter.tests.ts b/packages/tests/src/e2e/samples/ProvisionContactExporter.tests.ts index 123888fecc..fe730a8e09 100644 --- a/packages/tests/src/e2e/samples/ProvisionContactExporter.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionContactExporter.tests.ts @@ -13,6 +13,6 @@ class ContactExporterTestCase extends CaseFactory {} new ContactExporterTestCase( TemplateProjectFolder.ContactExporter, 15277462, - "v-ivanchen@microsoft.com", + "rentu@microsoft.com", ["tab", "aad"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionDeveloperDashboard.tests.ts b/packages/tests/src/e2e/samples/ProvisionDeveloperDashboard.tests.ts index 17e11ebfdb..882ff29b7b 100644 --- a/packages/tests/src/e2e/samples/ProvisionDeveloperDashboard.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionDeveloperDashboard.tests.ts @@ -28,6 +28,6 @@ class AssistDashboardTestCase extends CaseFactory { new AssistDashboardTestCase( TemplateProjectFolder.AssistDashboard, 24121324, - "v-ivanchen@microsoft.com", + "huimiao@microsoft.com", ["dashboard"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionDiceRoller.tests.ts b/packages/tests/src/e2e/samples/ProvisionDiceRoller.tests.ts index ea6c8993d2..b8aeb079b6 100644 --- a/packages/tests/src/e2e/samples/ProvisionDiceRoller.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionDiceRoller.tests.ts @@ -13,5 +13,5 @@ class DiceRollerTestCase extends CaseFactory {} new DiceRollerTestCase( TemplateProjectFolder.DiceRoller, 24132156, - "v-ivanchen@microsoft.com" + "ning.tang@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionGraphConnector.tests.ts b/packages/tests/src/e2e/samples/ProvisionGraphConnector.tests.ts index ee53611b9d..4d063fda7f 100644 --- a/packages/tests/src/e2e/samples/ProvisionGraphConnector.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionGraphConnector.tests.ts @@ -13,6 +13,6 @@ class GraphConnectorTestCase extends CaseFactory {} new GraphConnectorTestCase( TemplateProjectFolder.GraphConnector, 15277460, - "v-ivanchen@microsoft.com", + "junhan@microsoft.com", ["tab", "aad"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionGraphConnectorBot.tests.ts b/packages/tests/src/e2e/samples/ProvisionGraphConnectorBot.tests.ts index ac3fd06261..80497417b7 100644 --- a/packages/tests/src/e2e/samples/ProvisionGraphConnectorBot.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionGraphConnectorBot.tests.ts @@ -13,6 +13,6 @@ class GraphConnectorBotTestCase extends CaseFactory {} new GraphConnectorBotTestCase( TemplateProjectFolder.GraphConnectorBot, 25178480, - "v-ivanchen@microsoft.com", + "junhan@microsoft.com", ["bot", "aad"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionHelloWorldTabBackEnd.tests.ts b/packages/tests/src/e2e/samples/ProvisionHelloWorldTabBackEnd.tests.ts index 9d35d45613..ef91061956 100644 --- a/packages/tests/src/e2e/samples/ProvisionHelloWorldTabBackEnd.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionHelloWorldTabBackEnd.tests.ts @@ -13,6 +13,6 @@ class HelloWorldTabBackEndTestCase extends CaseFactory {} new HelloWorldTabBackEndTestCase( TemplateProjectFolder.HelloWorldTabBackEnd, 15277459, - "v-ivanchen@microsoft.com", + "rentu@microsoft.com", ["tab"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionIncomingWebhook.tests.ts b/packages/tests/src/e2e/samples/ProvisionIncomingWebhook.tests.ts index 3fe517f64d..f6f0f519e9 100644 --- a/packages/tests/src/e2e/samples/ProvisionIncomingWebhook.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionIncomingWebhook.tests.ts @@ -19,7 +19,7 @@ class IncomingWebhookTestCase extends CaseFactory { new IncomingWebhookTestCase( TemplateProjectFolder.IncomingWebhook, 15277475, - "v-ivanchen@microsoft.com", + "qidon@microsoft.com", [], { skipProvision: true } ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionLargeScaleNotiBot.tests.ts b/packages/tests/src/e2e/samples/ProvisionLargeScaleNotiBot.tests.ts index 8ce368d8a6..8122abe5ed 100644 --- a/packages/tests/src/e2e/samples/ProvisionLargeScaleNotiBot.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionLargeScaleNotiBot.tests.ts @@ -26,6 +26,6 @@ class LargeScaleBotTestCase extends CaseFactory { new LargeScaleBotTestCase( TemplateProjectFolder.LargeScaleBot, 25929126, - "v-ivanchen@microsoft.com", + "yiqingzhao@microsoft.com", ["bot"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionMyFirstMetting.tests.ts b/packages/tests/src/e2e/samples/ProvisionMyFirstMetting.tests.ts index e42cfb4902..81a49c7156 100644 --- a/packages/tests/src/e2e/samples/ProvisionMyFirstMetting.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionMyFirstMetting.tests.ts @@ -13,6 +13,6 @@ class MyFirstMettingTestCase extends CaseFactory {} new MyFirstMettingTestCase( TemplateProjectFolder.MyFirstMetting, 15277468, - "v-ivanchen@microsoft.com", + "kaiyan@microsoft.com", ["tab"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionNpmSearch.tests.ts b/packages/tests/src/e2e/samples/ProvisionNpmSearch.tests.ts index 4f4a1f6ec7..b9eadf7329 100644 --- a/packages/tests/src/e2e/samples/ProvisionNpmSearch.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionNpmSearch.tests.ts @@ -13,6 +13,6 @@ class NpmSearchTestCase extends CaseFactory {} new NpmSearchTestCase( TemplateProjectFolder.NpmSearch, 15277471, - "v-ivanchen@microsoft.com", + "qidon@microsoft.com", ["bot"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionOneProductivityHub.tests.ts b/packages/tests/src/e2e/samples/ProvisionOneProductivityHub.tests.ts index 1375de52e3..0eb864bab2 100644 --- a/packages/tests/src/e2e/samples/ProvisionOneProductivityHub.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionOneProductivityHub.tests.ts @@ -13,6 +13,6 @@ class OneProductivityHubTestCase extends CaseFactory {} new OneProductivityHubTestCase( TemplateProjectFolder.OneProductivityHub, 15277463, - "v-ivanchen@microsoft.com", + "rentu@microsoft.com", ["aad", "tab"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionProactiveMessage.tests.ts b/packages/tests/src/e2e/samples/ProvisionProactiveMessage.tests.ts index 3d3bf59f5e..925c8b9c2e 100644 --- a/packages/tests/src/e2e/samples/ProvisionProactiveMessage.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionProactiveMessage.tests.ts @@ -40,7 +40,7 @@ class ProactiveMessagingTestCase extends CaseFactory { new ProactiveMessagingTestCase( TemplateProjectFolder.ProactiveMessaging, 15277473, - "v-ivanchen@microsoft.com", + "ning.tang@microsoft.com", [], { manifestFolderName: "appManifest" } ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionQueryOrg.tests.ts b/packages/tests/src/e2e/samples/ProvisionQueryOrg.tests.ts index 03020aebd6..5ecbcceea8 100644 --- a/packages/tests/src/e2e/samples/ProvisionQueryOrg.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionQueryOrg.tests.ts @@ -13,5 +13,5 @@ class QueryOrgTestCase extends CaseFactory {} new QueryOrgTestCase( TemplateProjectFolder.QueryOrg, 24132148, - "v-ivanchen@microsoft.com" + "wenyutang@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionReactRetailDashboard.tests.ts b/packages/tests/src/e2e/samples/ProvisionReactRetailDashboard.tests.ts index 9005ff86b8..16c7b303b1 100644 --- a/packages/tests/src/e2e/samples/ProvisionReactRetailDashboard.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionReactRetailDashboard.tests.ts @@ -21,5 +21,5 @@ class RetailDashboardTestCase extends CaseFactory { new RetailDashboardTestCase( TemplateProjectFolder.RetailDashboard, 25051144, - "v-ivanchen@microsoft.com" + "ning.tang@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionShareNow.tests.ts b/packages/tests/src/e2e/samples/ProvisionShareNow.tests.ts index 90fbd7e347..0225a5cdec 100644 --- a/packages/tests/src/e2e/samples/ProvisionShareNow.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionShareNow.tests.ts @@ -28,6 +28,6 @@ class ShareNowTestCase extends CaseFactory { new ShareNowTestCase( TemplateProjectFolder.ShareNow, 15277467, - "v-ivanchen@microsoft.com", + "zhaofengxu@microsoft.com", ["sql", "tab & bot"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionSignatureOutlook.tests.ts b/packages/tests/src/e2e/samples/ProvisionSignatureOutlook.tests.ts index abdacf7702..4aef3ec90e 100644 --- a/packages/tests/src/e2e/samples/ProvisionSignatureOutlook.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionSignatureOutlook.tests.ts @@ -35,7 +35,7 @@ class OutlookSignatureTestCase extends CaseFactory { new OutlookSignatureTestCase( TemplateProjectFolder.OutlookSignature, 24132154, - "v-ivanchen@microsoft.com", + "huajiezhang@microsoft.com", [], { skipDeploy: true } ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionSpfxProductivity.tests.ts b/packages/tests/src/e2e/samples/ProvisionSpfxProductivity.tests.ts index 487e17e9bd..3089d648f1 100644 --- a/packages/tests/src/e2e/samples/ProvisionSpfxProductivity.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionSpfxProductivity.tests.ts @@ -21,6 +21,6 @@ class SpfxProductivityTestCase extends CaseFactory { new SpfxProductivityTestCase( TemplateProjectFolder.SpfxProductivity, 24753056, - "v-ivanchen@microsoft.com", + "ning.tang@microsoft.com", ["spfx"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionStockUpdate.tests.ts b/packages/tests/src/e2e/samples/ProvisionStockUpdate.tests.ts index 50feb21b3f..e6cf90dead 100644 --- a/packages/tests/src/e2e/samples/ProvisionStockUpdate.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionStockUpdate.tests.ts @@ -32,6 +32,6 @@ class StockUpdateTestCase extends CaseFactory { new StockUpdateTestCase( TemplateProjectFolder.StockUpdate, 15772706, - "v-ivanchen@microsoft.com", + "qidon@microsoft.com", ["bot"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTabOutlookAddin.tests.ts b/packages/tests/src/e2e/samples/ProvisionTabOutlookAddin.tests.ts index fc4960a7e3..ed53e7149a 100644 --- a/packages/tests/src/e2e/samples/ProvisionTabOutlookAddin.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTabOutlookAddin.tests.ts @@ -13,5 +13,5 @@ class OutlookTabTestCase extends CaseFactory {} new OutlookTabTestCase( TemplateProjectFolder.OutlookTab, 24132142, - "v-ivanchen@microsoft.com" + "huajiezhang@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTabSSOApimProxy.tests.ts b/packages/tests/src/e2e/samples/ProvisionTabSSOApimProxy.tests.ts index e0fc88f24c..7452cef419 100644 --- a/packages/tests/src/e2e/samples/ProvisionTabSSOApimProxy.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTabSSOApimProxy.tests.ts @@ -13,6 +13,6 @@ class TabSSOApimProxyTestCase extends CaseFactory {} new TabSSOApimProxyTestCase( TemplateProjectFolder.TabSSOApimProxy, 25191528, - "v-ivanchen@microsoft.com", + "bowen.song@microsoft.com", ["tab", "aad"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTeamDashboard.tests.ts b/packages/tests/src/e2e/samples/ProvisionTeamDashboard.tests.ts index 2a987cf1c5..197355acd6 100644 --- a/packages/tests/src/e2e/samples/ProvisionTeamDashboard.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTeamDashboard.tests.ts @@ -13,5 +13,5 @@ class DashboardTestCase extends CaseFactory {} new DashboardTestCase( TemplateProjectFolder.Dashboard, 24132131, - "v-ivanchen@microsoft.com" + "huimiao@microsoft.com" ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTodoListBackend.tests.ts b/packages/tests/src/e2e/samples/ProvisionTodoListBackend.tests.ts index 6bcfcb5a9e..f6dc592aba 100644 --- a/packages/tests/src/e2e/samples/ProvisionTodoListBackend.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTodoListBackend.tests.ts @@ -26,6 +26,6 @@ class TodoListBackendTestCase extends CaseFactory { new TodoListBackendTestCase( TemplateProjectFolder.TodoListBackend, 15277465, - "v-ivanchen@microsoft.com", + "junhan@microsoft.com", ["aad", "tab", "function", "sql"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTodoListM365.tests.ts b/packages/tests/src/e2e/samples/ProvisionTodoListM365.tests.ts index d5c166e262..0f574d5c52 100644 --- a/packages/tests/src/e2e/samples/ProvisionTodoListM365.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTodoListM365.tests.ts @@ -13,6 +13,6 @@ class TodoListM365TestCase extends CaseFactory {} new TodoListM365TestCase( TemplateProjectFolder.TodoListM365, 15277470, - "v-ivanchen@microsoft.com", + "qidon@microsoft.com", ["aad", "tab", "function"] ).test(); diff --git a/packages/tests/src/e2e/samples/ProvisionTodoListSpfx.tests.ts b/packages/tests/src/e2e/samples/ProvisionTodoListSpfx.tests.ts index 6295b7ce02..d7b1d2fec0 100644 --- a/packages/tests/src/e2e/samples/ProvisionTodoListSpfx.tests.ts +++ b/packages/tests/src/e2e/samples/ProvisionTodoListSpfx.tests.ts @@ -21,6 +21,6 @@ class TodoListSpfxTestCase extends CaseFactory { new TodoListSpfxTestCase( TemplateProjectFolder.TodoListSpfx, 15277466, - "v-ivanchen@microsoft.com", + "ning.tang@microsoft.com", ["spfx"] ).test(); diff --git a/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts b/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts index e596f030d3..9cd76bb1b2 100644 --- a/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts +++ b/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts @@ -38,13 +38,15 @@ describe("Office Addin TaskPane Scaffold", function () { { testPlanCaseId: 17132789, author: "huajiezhang@microsoft.com" }, async function () { { - const result = await Executor.createProject( - testFolder, - appName, - Capability.TaskPane, - ProgrammingLanguage.TS - ); - expect(result.success).to.be.true; + //Temporarily comment test cases and refine it after release process is finished + // const result = await Executor.createProject( + // testFolder, + // appName, + // Capability.TaskPane, + // ProgrammingLanguage.TS + // ); + // expect(result.success).to.be.true; + expect(true).to.be.true; } } ); diff --git a/packages/tests/src/scripts/testPlan.ts b/packages/tests/src/scripts/testPlan.ts index 05fb2f4599..4fea64ef86 100644 --- a/packages/tests/src/scripts/testPlan.ts +++ b/packages/tests/src/scripts/testPlan.ts @@ -423,6 +423,10 @@ class ADOTestPlanClient { } } +async function sleep(ms: number) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + /** * subcommand list * @@ -456,9 +460,10 @@ async function main() { } const testPlan = await ADOTestPlanClient.CloneTestPlan(tpn); + // wait for a short time to complete clone + await sleep(30 * 1000); console.log(testPlan.id); - - break; + return testPlan.id; } case "archive": { diff --git a/packages/tests/src/ui-test/cliHelper.ts b/packages/tests/src/ui-test/cliHelper.ts index 52beb81c4f..027b1ac42e 100644 --- a/packages/tests/src/ui-test/cliHelper.ts +++ b/packages/tests/src/ui-test/cliHelper.ts @@ -130,6 +130,69 @@ export class CliHelper { } } + static async provisionProject2( + projectPath: string, + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv + ) { + const result = await execAsyncWithRetry( + `teamsapp provision --env ${env} --interactive false --verbose ${option}`, + { + cwd: projectPath, + env: processEnv ? processEnv : process.env, + timeout: 0, + } + ); + + if (result.stderr) { + console.error( + `[Failed] provision ${projectPath}. Error message: ${result.stderr}` + ); + } else { + console.log(`[Successfully] provision ${projectPath}`); + } + } + + static async showVersion( + projectPath: string, + processEnv?: NodeJS.ProcessEnv + ) { + const result = await execAsyncWithRetry(`teamsapp --version`, { + cwd: projectPath, + env: processEnv ? processEnv : process.env, + timeout: 0, + }); + + console.log(`Cli Version: ${result.stdout}`); + } + + static async deployAll( + projectPath: string, + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv, + retries?: number, + newCommand?: string + ) { + const result = await execAsyncWithRetry( + `teamsapp deploy --env ${env} --interactive false --verbose ${option}`, + { + cwd: projectPath, + env: processEnv ? processEnv : process.env, + timeout: 0, + }, + retries, + newCommand + ); + const message = `deploy all resources for ${projectPath}`; + if (result.stderr) { + console.error(`[Failed] ${message}. Error message: ${result.stderr}`); + } else { + console.log(`[Successfully] ${message}`); + } + } + static async publishProject( projectPath: string, env: "local" | "dev" = "local", diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-reprovision-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-reprovision-win-only.test.ts index 12e9e724e8..a69275cf3b 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-reprovision-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-reprovision-win-only.test.ts @@ -9,9 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Notification, Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - reRunProvision, - runDeploy, + deployProject, + provisionProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -72,18 +71,12 @@ describe("Remote debug Tests", function () { async function () { const driver = VSBrowser.instance.driver; await createNewProject("bot", appName); - await runProvision(appName); - await clearNotifications(); + await provisionProject(appName, projectPath); await cleanUpResourceGroup(appName, "dev"); await createResourceGroup(appName, "dev", "westus"); - await reRunProvision(); - await getNotification( - Notification.ProvisionSucceeded, - Timeout.longTimeWait, - 8, - ["Error", "Failed"] - ); - await runDeploy(Timeout.botDeploy); + // rerun provision + await provisionProject(appName, projectPath, false); + await deployProject(projectPath, Timeout.botDeploy); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-ts-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-ts-win-only.test.ts index daf65217df..da476e966b 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-ts-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-ts-win-only.test.ts @@ -9,8 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -65,8 +65,8 @@ describe("Remote debug Tests", function () { async function () { const driver = VSBrowser.instance.driver; await createNewProject("bot", appName, "TypeScript"); - await runProvision(appName); - await runDeploy(Timeout.botDeploy); + await provisionProject(appName, projectPath); + await deployProject(projectPath, Timeout.botDeploy); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-win-only.test.ts index c94010436f..1bbe4ed5f0 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-bot-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-bot-win-only.test.ts @@ -9,8 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -65,8 +65,8 @@ describe("Remote debug Tests", function () { async function () { const driver = VSBrowser.instance.driver; await createNewProject("bot", appName); - await runProvision(appName); - await runDeploy(Timeout.botDeploy); + await provisionProject(appName, projectPath); + await deployProject(projectPath, Timeout.botDeploy); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-ts-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-ts-win-only.test.ts index a8974bf0b4..56d32db9c6 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-ts-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-ts-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -8,6 +11,8 @@ import { RemoteDebugTestContext, runProvision, runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -63,8 +68,8 @@ describe("Remote debug Tests", function () { //create tab project const driver = VSBrowser.instance.driver; await createNewProject("m365lp", appName, "TypeScript"); - await runProvision(appName); - await runDeploy(); + await provisionProject(appName, projectPath); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-win-only.test.ts index f570694898..6950220052 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-m365lp-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -6,8 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -63,8 +66,8 @@ describe("Remote debug Tests", function () { //create tab project const driver = VSBrowser.instance.driver; await createNewProject("m365lp", appName); - await runProvision(appName); - await runDeploy(); + await provisionProject(appName, projectPath); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-aad-deploy-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-aad-deploy-win-only.test.ts index 6ab3617bbe..abb0c40c29 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-aad-deploy-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-aad-deploy-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -7,13 +10,12 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, getAadObjectId, + provisionProject, } from "./remotedebugContext"; import { execCommandIfExist, createNewProject, - getNotification, runDeployAadAppManifest, } from "../../utils/vscodeOperation"; import { Env } from "../../utils/env"; @@ -67,7 +69,7 @@ describe("Remote debug Tests", function () { //create tab project const driver = VSBrowser.instance.driver; await createNewProject("tab", appName); - await runProvision(appName); + await provisionProject(appName, projectPath); await updateAadTemplate(projectPath, "-updated"); await driver.sleep(Timeout.shortTimeWait); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-ts-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-ts-win-only.test.ts index c53aeec16f..5dfc06acc9 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-ts-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-ts-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -6,9 +9,9 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout, ValidationContent } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, setSkuNameToB1, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -66,8 +69,8 @@ describe("Remote debug Tests", function () { await createNewProject("tabnsso", appName, "TypeScript"); await setSkuNameToB1(projectPath); await driver.sleep(Timeout.shortTimeWait); - await runProvision(appName); - await runDeploy(); + await provisionProject(appName, projectPath); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-win-only.test.ts index b1ad8a4801..b162478e60 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-nosso-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -6,9 +9,9 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout, ValidationContent } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, setSkuNameToB1, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -66,8 +69,8 @@ describe("Remote debug Tests", function () { await createNewProject("tabnsso", appName); await setSkuNameToB1(projectPath); await driver.sleep(Timeout.shortTimeWait); - await runProvision(appName); - await runDeploy(); + await provisionProject(appName, projectPath); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-publish.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-publish.test.ts index 853e935a51..9fa5e9f13a 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-publish.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-publish.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ @@ -6,7 +9,7 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, + provisionProject, runPublish, } from "./remotedebugContext"; import { @@ -62,7 +65,7 @@ describe("Remote debug Tests", function () { //create tab project const driver = VSBrowser.instance.driver; await createNewProject("tab", appName); - await runProvision(appName); + await provisionProject(appName, projectPath); await runPublish(); await runPublish(true); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-regen-appid-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-regen-appid-win-only.test.ts index 91b1af18a3..0770f2d45a 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-regen-appid-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-regen-appid-win-only.test.ts @@ -9,21 +9,16 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout, ValidationContent } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - reRunProvision, - runDeploy, setSkuNameToB1, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, createNewProject, clearNotifications, } from "../../utils/vscodeOperation"; -import { - initPage, - validateBasicTab, - validateTab, -} from "../../utils/playwrightOperation"; +import { initPage, validateBasicTab } from "../../utils/playwrightOperation"; import { Env } from "../../utils/env"; import { it } from "../../utils/it"; import { cleanAppStudio } from "../../utils/cleanHelper"; @@ -76,11 +71,12 @@ describe("Remote debug Tests", function () { await createNewProject("tabnsso", appName); await setSkuNameToB1(projectPath); await driver.sleep(Timeout.shortTimeWait); - await runProvision(appName); + await provisionProject(appName, projectPath); await clearNotifications(); await cleanAppStudio(appName); - await reRunProvision(); - await runDeploy(); + // rerun provision + await provisionProject(appName, projectPath, false); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-reprovision-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-reprovision-win-only.test.ts index 252e519a01..709482d496 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-tab-reprovision-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-tab-reprovision-win-only.test.ts @@ -9,10 +9,9 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout, ValidationContent } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - reRunProvision, - runDeploy, setSkuNameToB1, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -75,12 +74,13 @@ describe("Remote debug Tests", function () { await createNewProject("tabnsso", appName); await setSkuNameToB1(projectPath); await driver.sleep(Timeout.shortTimeWait); - await runProvision(appName); + await provisionProject(appName, projectPath); await clearNotifications(); await cleanUpResourceGroup(appName, "dev"); - await createResourceGroup(appName, "dev"); - await reRunProvision(); - await runDeploy(); + await createResourceGroup(appName, "dev", "westus"); + // rerun provision + await provisionProject(appName, projectPath, false); + await deployProject(projectPath); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-ts-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-ts-win-only.test.ts index b72256b1d4..82e0af6ce7 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-ts-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-ts-win-only.test.ts @@ -9,8 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -74,8 +74,8 @@ describe("Remote debug Tests", function () { const driver = VSBrowser.instance.driver; await createNewProject("workflow", appName, "TypeScript"); validateFileExist(projectPath, "src/index.ts"); - await runProvision(appName); - await runDeploy(Timeout.botDeploy); + await provisionProject(appName, projectPath); + await deployProject(projectPath, Timeout.botDeploy); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-win-only.test.ts b/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-win-only.test.ts index 1d29c23776..54066528fc 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-win-only.test.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebug-workflow-bot-win-only.test.ts @@ -9,8 +9,8 @@ import { VSBrowser } from "vscode-extension-tester"; import { Timeout } from "../../utils/constants"; import { RemoteDebugTestContext, - runProvision, - runDeploy, + provisionProject, + deployProject, } from "./remotedebugContext"; import { execCommandIfExist, @@ -74,8 +74,8 @@ describe("Remote debug Tests", function () { const driver = VSBrowser.instance.driver; await createNewProject("workflow", appName); validateFileExist(projectPath, "src/index.js"); - await runProvision(appName); - await runDeploy(Timeout.botDeploy); + await provisionProject(appName, projectPath); + await deployProject(projectPath, Timeout.botDeploy); const teamsAppId = await remoteDebugTestContext.getTeamsAppId( projectPath ); diff --git a/packages/tests/src/ui-test/remotedebug/remotedebugContext.ts b/packages/tests/src/ui-test/remotedebug/remotedebugContext.ts index 3df95b540f..e88601f329 100644 --- a/packages/tests/src/ui-test/remotedebug/remotedebugContext.ts +++ b/packages/tests/src/ui-test/remotedebug/remotedebugContext.ts @@ -17,6 +17,7 @@ import { cleanAppStudio, cleanUpLocalProject, cleanUpResourceGroup, + createResourceGroup, } from "../../utils/cleanHelper"; import { execCommandIfExist, @@ -26,6 +27,7 @@ import { import { ModalDialog, InputBox, VSBrowser } from "vscode-extension-tester"; import { dotenvUtil } from "../../utils/envUtil"; import { execAsync } from "../../utils/commonUtils"; +import { CliHelper } from "../cliHelper"; export class RemoteDebugTestContext extends TestContext { public testName: string; @@ -181,6 +183,90 @@ export async function inputSqlUserName( await input.confirm(); } +export async function provisionProject( + appName: string, + projectPath = "", + createRg = true, + tool: "ttk" | "cli" = "cli", + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv +) { + if (tool === "cli") { + await runCliProvision( + projectPath, + appName, + createRg, + option, + env, + processEnv + ); + } else { + await runProvision(appName); + } +} + +export async function deployProject( + projectPath: string, + waitTime: number = Timeout.tabDeploy, + tool: "ttk" | "cli" = "cli", + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv, + retries?: number, + newCommand?: string +) { + if (tool === "cli") { + await runCliDeploy( + projectPath, + option, + env, + processEnv, + retries, + newCommand + ); + } else { + await runDeploy(waitTime); + } +} + +export async function runCliProvision( + projectPath: string, + appName: string, + createRg = true, + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv +) { + if (createRg) { + await createResourceGroup(appName, env, "westus"); + } + const resourceGroupName = `${appName}-${env}-rg`; + await CliHelper.showVersion(projectPath, processEnv); + await CliHelper.provisionProject2(projectPath, option, env, { + ...process.env, + AZURE_RESOURCE_GROUP_NAME: resourceGroupName, + }); +} + +export async function runCliDeploy( + projectPath: string, + option = "", + env: "dev" | "local" = "dev", + processEnv?: NodeJS.ProcessEnv, + retries?: number, + newCommand?: string +) { + await CliHelper.deployAll( + projectPath, + option, + env, + processEnv, + retries, + newCommand + ); +} + export async function runProvision( appName: string, envName = "dev", diff --git a/packages/tests/src/ui-test/samples/sample-localdebug-chef-bot.test.ts b/packages/tests/src/ui-test/samples/sample-localdebug-chef-bot.test.ts index 8dbe95750c..1eeaab934c 100644 --- a/packages/tests/src/ui-test/samples/sample-localdebug-chef-bot.test.ts +++ b/packages/tests/src/ui-test/samples/sample-localdebug-chef-bot.test.ts @@ -23,15 +23,11 @@ class ChefBotTestCase extends CaseFactory { sampledebugContext: SampledebugContext, env: "local" | "dev" ): Promise { - const envFile = path.resolve( - sampledebugContext.projectPath, - "env", - `.env.${env}` - ); - let OPENAI_API_KEY = fs.readFileSync(envFile, "utf-8"); - OPENAI_API_KEY += "\nSECRET_OPENAI_API_KEY=yourapikey"; - fs.writeFileSync(envFile, OPENAI_API_KEY); - console.log(`add OPENAI_API_KEY ${OPENAI_API_KEY} to .env.${env} file`); + const envFile = path.resolve(sampledebugContext.projectPath, ".env"); + // create .env file + fs.writeFileSync(envFile, "OPENAI_KEY=yourapikey"); + console.log(`add OPENAI_KEY=yourapikey to .env file`); + await sampledebugContext.prepareDebug("yarn"); } override async onValidate(page: Page): Promise { console.log("Moked api key. Only verify happy path..."); @@ -58,7 +54,7 @@ new ChefBotTestCase( "local", [LocalDebugTaskLabel.StartLocalTunnel, LocalDebugTaskLabel.StartBotApp], { - debug: "cli", - testRootFolder: path.resolve(os.homedir(), "resourse"), // fix yarn error + repoPath: "./resource/js/samples", + testRootFolder: path.resolve(os.homedir(), "resource"), } ).test(); diff --git a/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts b/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts index 122a7495b5..2b46387931 100644 --- a/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts +++ b/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts @@ -31,7 +31,7 @@ class DashboardTestCase extends CaseFactory { teamsAppId, Env.username, Env.password, - undefined, + { dashboardFlag: true }, true, true ); diff --git a/packages/tests/src/ui-test/samples/sample-localdebug-proactive-message.test.ts b/packages/tests/src/ui-test/samples/sample-localdebug-proactive-message.test.ts index 632d5b62b2..2fc111fb78 100644 --- a/packages/tests/src/ui-test/samples/sample-localdebug-proactive-message.test.ts +++ b/packages/tests/src/ui-test/samples/sample-localdebug-proactive-message.test.ts @@ -30,6 +30,6 @@ new ProactiveMessagingTestCase( "local", [LocalDebugTaskLabel.StartLocalTunnel, LocalDebugTaskLabel.StartBot], { - testRootFolder: "./resource/samples", + repoPath: "./resource/samples", } ).test(); diff --git a/packages/tests/src/ui-test/samples/sample-remotedebug-chef-bot.test.ts b/packages/tests/src/ui-test/samples/sample-remotedebug-chef-bot.test.ts index 05799c3b86..cbfcc46d12 100644 --- a/packages/tests/src/ui-test/samples/sample-remotedebug-chef-bot.test.ts +++ b/packages/tests/src/ui-test/samples/sample-remotedebug-chef-bot.test.ts @@ -19,17 +19,21 @@ class ChefBotTestCase extends CaseFactory { sampledebugContext: SampledebugContext, env: "local" | "dev" ): Promise { - const envFile = path.resolve( - sampledebugContext.projectPath, - "env", - `.env.${env}` - ); - let OPENAI_API_KEY = fs.readFileSync(envFile, "utf-8"); - OPENAI_API_KEY += "\nSECRET_OPENAI_API_KEY=yourapikey"; - fs.writeFileSync(envFile, OPENAI_API_KEY); - console.log(`add OPENAI_API_KEY ${OPENAI_API_KEY} to .env.${env} file`); + const envFile = path.resolve(sampledebugContext.projectPath, ".env"); + // create .env file + fs.writeFileSync(envFile, "OPENAI_KEY=yourapikey"); + console.log(`add OPENAI_KEY=yourapikey to .env file`); + await sampledebugContext.prepareDebug("yarn"); } override async onValidate(page: Page): Promise { + console.log("Moked api key. Only verify happy path..."); + return await validateWelcomeAndReplyBot(page, { + hasCommandReplyValidation: true, + botCommand: "helloWorld", + expectedReplyMessage: ValidationContent.AiBotErrorMessage, + }); + } + public override async onCliValidate(page: Page): Promise { console.log("Mocked api key. Only verify happy path..."); return await validateWelcomeAndReplyBot(page, { hasCommandReplyValidation: true, @@ -45,5 +49,8 @@ new ChefBotTestCase( "v-ivanchen@microsoft.com", "dev", [], - { testRootFolder: path.resolve(os.homedir(), "resourse") } // fix yarn error + { + repoPath: "./resource/js/samples", + testRootFolder: path.resolve(os.homedir(), "resource"), + } ).test(); diff --git a/packages/tests/src/ui-test/samples/sample-remotedebug-proactive-message.test.ts b/packages/tests/src/ui-test/samples/sample-remotedebug-proactive-message.test.ts index 706dba7171..f8023c0701 100644 --- a/packages/tests/src/ui-test/samples/sample-remotedebug-proactive-message.test.ts +++ b/packages/tests/src/ui-test/samples/sample-remotedebug-proactive-message.test.ts @@ -41,5 +41,5 @@ new ProactiveMessagingTestCase( "v-ivanchen@microsoft.com", "dev", [], - { testRootFolder: "./resource/samples" } + { repoPath: "./resource/samples" } ).test(); diff --git a/packages/tests/src/ui-test/samples/sampleCaseFactory.ts b/packages/tests/src/ui-test/samples/sampleCaseFactory.ts index 91b3f86ec7..78319f8009 100644 --- a/packages/tests/src/ui-test/samples/sampleCaseFactory.ts +++ b/packages/tests/src/ui-test/samples/sampleCaseFactory.ts @@ -122,6 +122,7 @@ export abstract class CaseFactory { skipDebug?: boolean; debug?: "cli" | "ttk"; botFlag?: boolean; + repoPath?: string; }; public constructor( @@ -142,6 +143,7 @@ export abstract class CaseFactory { skipDebug?: boolean; debug?: "cli" | "ttk"; botFlag?: boolean; + repoPath?: string; } = {} ) { this.sampleName = sampleName; @@ -292,7 +294,8 @@ export abstract class CaseFactory { sampledebugContext = new SampledebugContext( sampleName, sampleProjectMap[sampleName], - options?.testRootFolder ?? "./resource" + options?.testRootFolder ?? "./resource", + options?.repoPath ?? "./resource" ); await sampledebugContext.before(); // use before middleware to process typical sample diff --git a/packages/tests/src/ui-test/samples/sampledebugContext.ts b/packages/tests/src/ui-test/samples/sampledebugContext.ts index 3ac04b7cd9..1176720f18 100644 --- a/packages/tests/src/ui-test/samples/sampledebugContext.ts +++ b/packages/tests/src/ui-test/samples/sampledebugContext.ts @@ -31,6 +31,7 @@ import { cleanUpLocalProject, cleanUpResourceGroup, } from "../../utils/cleanHelper"; +import { Executor } from "../../utils/executor"; export class SampledebugContext extends TestContext { public readonly appName: string; @@ -41,15 +42,18 @@ export class SampledebugContext extends TestContext { public env: "dev" | "local" = "dev"; public originSample: TemplateProjectFolder; public rgName: string; + public readonly repoPath: string; constructor( sampleName: TemplateProject, originSample: TemplateProjectFolder, - testRootFolder = "./resource" + testRootFolder = "./resource", + repoPath = "./resource" ) { super(sampleName); this.sampleName = sampleName; this.originSample = originSample; + this.repoPath = repoPath; if (sampleName.length >= 20) { this.appName = getSampleAppName( sampleName @@ -141,16 +145,12 @@ export class SampledebugContext extends TestContext { public async openResourceFolder(): Promise { console.log("start to open project: ", this.sampleName); - // two repos have different sample path - const oldPath = path.resolve( - this.testRootFolder == "./resource/samples" - ? "./resource/samples" - : "./resource", - this.originSample - ); + const oldPath = path.resolve(this.repoPath, this.originSample); // move old sample to project path await fs.mkdir(this.projectPath); try { + console.log("oldPath: ", oldPath); + console.log("newPath: ", this.projectPath); await fs.copy(oldPath, this.projectPath); await openExistingProject(this.projectPath); console.log( @@ -160,6 +160,7 @@ export class SampledebugContext extends TestContext { this.projectPath ); } catch (error) { + console.log(error); throw new Error(`Failed to open project: ${this.sampleName}`); } } @@ -375,4 +376,43 @@ export class SampledebugContext extends TestContext { console.log('Failed to edit ".env" file.'); } } + + public async prepareDebug(tool: "npm" | "yarn"): Promise { + { + console.log(`executor command: npm install yarn`); + const { stderr, stdout } = await Executor.execute( + `npm install yarn --force`, + this.projectPath + ); + console.log("stdout: ", stdout); + console.log("stderr: ", stderr); + } + { + console.log(`executor command: corepack enable`); + const { stderr, stdout } = await Executor.execute( + `corepack enable`, + this.projectPath + ); + console.log("stdout: ", stdout); + console.log("stderr: ", stderr); + } + { + console.log(`executor command: ${tool} install`); + const { stderr, stdout } = await Executor.execute( + `${tool} install`, + this.projectPath + ); + console.log("stdout: ", stdout); + console.log("stderr: ", stderr); + } + { + console.log(`executor command: ${tool} build`); + const { stderr, stdout } = await Executor.execute( + `${tool} build`, + this.projectPath + ); + console.log("stdout: ", stdout); + console.log("stderr: ", stderr); + } + } } diff --git a/packages/tests/src/ui-test/treeview/treeview-collaboration-win-only.test.ts b/packages/tests/src/ui-test/treeview/treeview-collaboration-win-only.test.ts index 67d7b6dd2d..84a06d64d7 100644 --- a/packages/tests/src/ui-test/treeview/treeview-collaboration-win-only.test.ts +++ b/packages/tests/src/ui-test/treeview/treeview-collaboration-win-only.test.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + /** * @author Helly Zhang */ diff --git a/packages/tests/src/utils/azureCliHelper.ts b/packages/tests/src/utils/azureCliHelper.ts index cb9deab0d4..fe995f2698 100644 --- a/packages/tests/src/utils/azureCliHelper.ts +++ b/packages/tests/src/utils/azureCliHelper.ts @@ -48,8 +48,7 @@ export class AzSqlHelper { public async createTable(sqlServerEndpoint: string) { // login console.log(`Logging in...`); - const { success: loginSuccess } = await AzSqlHelper.login(); - if (!loginSuccess) return; + await AzSqlHelper.login(); // add firewall rule console.log(`Adding firewall rule...`); @@ -68,8 +67,7 @@ export class AzSqlHelper { public async createSql() { // login console.log(`Logging in...`); - const { success: loginSuccess } = await AzSqlHelper.login(); - if (!loginSuccess) return; + await AzSqlHelper.login(); // create resource group console.log("Creating resource group: ", this.resourceGroupName, "..."); @@ -109,12 +107,8 @@ export class AzSqlHelper { } static async login() { - const command = `az login --service-principal -u ${Env["AZURE_CLIENT_ID"]} -p ${Env["AZURE_CLIENT_SECRET"]} -t ${Env["azureTenantId"]}`; - const { success } = await Executor.execute(command, process.cwd()); - if (!success) { - console.error(`Failed to login`); - return { success: false }; - } + const command = `az login -u ${Env["azureAccountName"]} -p '${Env["azureAccountPassword"]}'`; + await Executor.execute(command, process.cwd()); // set subscription const subscription = Env["azureSubscriptionId"]; const setSubscriptionCommand = `az account set --subscription ${subscription}`; @@ -210,8 +204,7 @@ export class AzServiceBusHelper { public async createServiceBus() { // login console.log(`Logging in...`); - const { success: loginSuccess } = await AzServiceBusHelper.login(); - if (!loginSuccess) return; + await AzServiceBusHelper.login(); // create resource group console.log("Creating resource group: ", this.resourceGroupName, "..."); @@ -244,12 +237,9 @@ export class AzServiceBusHelper { } static async login() { - const command = `az login --service-principal -u ${Env["AZURE_CLIENT_ID"]} -p ${Env["AZURE_CLIENT_SECRET"]} -t ${Env["azureTenantId"]}`; - const { success } = await Executor.execute(command, process.cwd()); - if (!success) { - console.error(`Failed to login`); - return { success: false }; - } + const command = `az login -u ${Env["azureAccountName"]} -p '${Env["azureAccountPassword"]}'`; + await Executor.execute(command, process.cwd()); + // set subscription const subscription = Env["azureSubscriptionId"]; const setSubscriptionCommand = `az account set --subscription ${subscription}`; diff --git a/packages/tests/src/utils/commonUtils.ts b/packages/tests/src/utils/commonUtils.ts index 7c0cb58db2..6cef9a8d8d 100644 --- a/packages/tests/src/utils/commonUtils.ts +++ b/packages/tests/src/utils/commonUtils.ts @@ -33,6 +33,7 @@ export async function execAsyncWithRetry( options.cwd ? options.cwd : "", options.env ); + return result; } catch (e: any) { console.log( `Run \`${command}\` failed with error msg: ${JSON.stringify(e)}.` diff --git a/packages/tests/src/utils/constants.ts b/packages/tests/src/utils/constants.ts index f4b91c7d6c..523d89535a 100644 --- a/packages/tests/src/utils/constants.ts +++ b/packages/tests/src/utils/constants.ts @@ -80,7 +80,7 @@ export enum TemplateProjectFolder { OutlookTab = "hello-world-teams-tab-and-outlook-add-in", AssistDashboard = "developer-assist-dashboard", DiceRoller = "live-share-dice-roller", - ChefBot = "teams-chef-bot", + ChefBot = "04.ai.a.teamsChefBot", GraphConnectorBot = "graph-connector-bot", SpfxProductivity = "spfx-productivity-dashboard", RetailDashboard = "react-retail-dashboard", diff --git a/packages/tests/src/utils/executor.ts b/packages/tests/src/utils/executor.ts index d1751df101..350b5393cb 100644 --- a/packages/tests/src/utils/executor.ts +++ b/packages/tests/src/utils/executor.ts @@ -468,6 +468,8 @@ export class Executor { envContent = fs.readFileSync(envFile, "utf-8"); } catch (error) { console.log("read file error", error); + console.log("create .env.local file"); + fs.writeFileSync(envFile, ""); } const domainRegex = /Connect via browser: https:\/\/(\S+)/; const endpointRegex = /Connect via browser: (\S+)/; diff --git a/packages/tests/src/utils/playwrightOperation.ts b/packages/tests/src/utils/playwrightOperation.ts index dfb16bcb16..085c453719 100644 --- a/packages/tests/src/utils/playwrightOperation.ts +++ b/packages/tests/src/utils/playwrightOperation.ts @@ -80,7 +80,7 @@ export const debugInitMap: Record Promise> = { await startDebugging(); }, [TemplateProject.ChefBot]: async () => { - await startDebugging(); + await startDebugging("Debug (Chrome)"); }, [TemplateProject.GraphConnectorBot]: async () => { await startDebugging(); @@ -209,6 +209,11 @@ export async function initPage( popup.waitForNavigation(), ]); await popup.click("input.button[type='submit'][value='Accept']"); + try { + await popup?.close(); + } catch (error) { + console.log("popup is closed"); + } } } else { await addBtn?.click(); @@ -358,6 +363,11 @@ export async function reopenPage( popup.waitForNavigation(), ]); await popup.click("input.button[type='submit'][value='Accept']"); + try { + await popup?.close(); + } catch (error) { + console.log("popup is closed"); + } } } else { await addBtn?.click(); @@ -1010,10 +1020,16 @@ export async function validateReactTab( timeout: Timeout.playwrightConsentPageReload, }) .catch(() => {}); + console.log("click accept button"); await popup.click("input.button[type='submit'][value='Accept']"); + await page.waitForTimeout(Timeout.shortTimeLoading); + } + if (popup && !popup?.isClosed()) { + await popup.close(); + throw "popup not close."; } }); - + await page.waitForTimeout(Timeout.shortTimeLoading); console.log("verify function info"); const backendElement = await frame?.waitForSelector( 'pre:has-text("receivedHTTPRequestBody")' @@ -1075,9 +1091,16 @@ export async function validateReactOutlookTab( timeout: Timeout.playwrightConsentPageReload, }) .catch(() => {}); + console.log("click accept button"); await popup.click("input.button[type='submit'][value='Accept']"); + await page.waitForTimeout(Timeout.shortTimeLoading); + } + if (popup && !popup?.isClosed()) { + await popup.close(); + throw "popup not close."; } }); + await page.waitForTimeout(Timeout.shortTimeLoading); console.log("verify function info"); const backendElement = await frame?.waitForSelector( @@ -2225,7 +2248,6 @@ export async function validateGraphConnector( ); const frame = await frameElementHandle?.contentFrame(); try { - const startBtn = await frame?.waitForSelector('button:has-text("Start")'); await RetryHandler.retry(async () => { console.log("Before popup"); const [popup] = await Promise.all([ @@ -2239,24 +2261,49 @@ export async function validateGraphConnector( .catch(() => popup) ) .catch(() => {}), - startBtn?.click(), + frame?.click('button:has-text("Start")', { + timeout: Timeout.playwrightAddAppButton, + force: true, + noWaitAfter: true, + clickCount: 2, + delay: 10000, + }), ]); console.log("after popup"); if (popup && !popup?.isClosed()) { + await popup.screenshot({ + path: getPlaywrightScreenshotPath("popup_before"), + fullPage: true, + }); await popup .click('button:has-text("Reload")', { timeout: Timeout.playwrightConsentPageReload, }) .catch(() => {}); + console.log("click accept button"); await popup.click("input.button[type='submit'][value='Accept']"); + await page.waitForTimeout(Timeout.shortTimeLoading); + await page.screenshot({ + path: getPlaywrightScreenshotPath("popup_after"), + fullPage: true, + }); + } + if (popup && !popup?.isClosed()) { + await popup.close(); + throw "popup not close."; } - - await frame?.waitForSelector(`div:has-text("${options?.displayName}")`); }); + await page.waitForTimeout(Timeout.shortTimeLoading); + await frame?.waitForSelector(`div:has-text("${options?.displayName}")`); page.waitForTimeout(1000); } catch (e: any) { console.log(`[Command not executed successfully] ${e.message}`); + await page.screenshot({ + path: getPlaywrightScreenshotPath("error"), + fullPage: true, + }); + throw e; } await page.waitForTimeout(Timeout.shortTimeLoading); diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index 515ad9865b..1409829fae 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,9 +1,41 @@ # Changelog +> Note: This changelog only includes the changes for the stable versions of Teams Toolkit. For the changelog of pre-released versions, please refer to the [Teams Toolkit Pre-release Changelog](https://github.com/OfficeDev/TeamsFx/blob/dev/packages/vscode-extension/PRERELEASE.md). + +## 5.6.0 - Mar 12, 2024 + +This minor version update of Teams Toolkit includes new features and bug fixes based on your feedback. The new features include Deploy Tab Apps to Static Web App, Teams Toolkit CLI v3 and many other enhancements. We previously shared these incremental changes in the prerelease version and through a blog post: + +- [Janaury Prerelease](https://devblogs.microsoft.com/microsoft365dev/teams-toolkit-for-visual-studio-code-update-january-2024/): Deploy Tab Apps to Static Web App, Teams Toolkit CLI v3, new Link Unfurling sample app and many other enhancements. + +We've listened to your feedback and included these additional new features, enhancements, and bug fixes to this release. + +New features: + +- **Deploy Tab Apps to Static Web App**: Azure Static Web Apps, an automatic service for building and deploying full-stack web apps to Azure from a code repository, is now the default solution for deploying Tab-based applications in Teams Toolkit. If you prefer the old way using Azure Storage, please refer to this [sample](https://github.com/OfficeDev/TeamsFx-Samples/tree/dev/hello-world-tab-codespaces). +- **Teams Toolkit CLI ([`@microsoft/teamsapp-cli`](https://www.npmjs.com/package/@microsoft/teamsapp-cli)) `v3.0.0`**. Teams Toolkit CLI version 3 is now released in stable version. Major changes include: + ![Teams Toolkit CLI](https://camo.githubusercontent.com/67608a468cbd406d6ff18585c8bc3b34d3d97d0a8ef525bdf516ca23fd5e32dd/68747470733a2f2f616b612e6d732f636c692d6865726f2d696d616765) + - **New Command Signature**: Teams Toolkit CLI now starts with `teamsapp` as the root command signature for more clarity. We recommend changing your scripts to use `teamsapp` as the command prefix. + - **New Command Structure**: Teams Toolkit CLI now has a new command structure that is more intuitive and easier to use. You can find the new command structure in the [Teams Toolkit CLI Command Reference](https://aka.ms/teamsfx-toolkit-cli). + - **New Doctor Command**: `teamsapp doctor` command is a new command that helps diagnose and fix common issues with Teams Toolkit and Teams application development. + +Enhancements: + +- **Format Reddit Link into Adaptive Card Sample**: This sample application demonstrates how to format a Reddit link into an Adaptive Card in Microsoft Teams conversations. + ![Link Unfurling Sample](https://github.com/OfficeDev/TeamsFx/assets/11220663/0d44f8c3-d02e-4912-bfa2-6ed3fdb29c1b) +- **Clean up `.deployment` Folder in between Deployments**: Teams Toolkit now cleans up the `.deployment` folder in the build directory before each deployment, addressing a [known issue](https://github.com/OfficeDev/TeamsFx/issues/10075) and reducing deployment time. +- **Optimized Dev Tunnel Expiration**: Inactive Dev Tunnel instances will now be automatically cleaned up after an hour, mitigating Dev Tunnel instance limitation errors. +- **Log Level Settings**: Added log level settings for controlling the verbosity of Teams Toolkit logs. You can find the settings in the [User and Workspace Settings](https://code.visualstudio.com/docs/getstarted/settings) under the `Teams Toolkit` section. + ![Logs](https://github.com/OfficeDev/TeamsFx/assets/11220663/3a1fc3a0-d69b-446e-8db2-0c756a18f95e) +- **Richer Information in Sample App Details Page**: The Sample app detail page now includes additional details from the project README file, such as the project description, prerequisites, and steps to run the project. +- **Improved Troubleshooting for Multi-tenant Scenario**: Teams Toolkit now provides a [troubleshooting guide](https://aka.ms/teamsfx-multi-tenant) for scenarios where `aadApp/update` action fails with a `HostNameNotOnVerifiedDomain` error in multi-tenant setups. +- **Optimized SPFx Solution Version Handling**: Teams Toolkit now compares the SPFx solution version between global installations and the one used by Teams Toolkit when developers add additional web parts. Developers will be prompted if there's a need to install or upgrade the solution version when differences are detected. + ## 5.4.1 - Feb 07, 2024 + Hotfix version. -We made UI and docs updates to multiple places according to [Latest updates to the Microsoft 365 Developer Program](https://devblogs.microsoft.com/microsoft365dev/stay-ahead-of-the-game-with-the-latest-updates-to-the-microsoft-365-developer-program/). +We have made UI and docs updates to multiple places according to the [latest updates to the Microsoft 365 Developer Program](https://devblogs.microsoft.com/microsoft365dev/stay-ahead-of-the-game-with-the-latest-updates-to-the-microsoft-365-developer-program/). ## 5.4.0 - Dec 18, 2023 @@ -30,7 +62,7 @@ New features: Enhancement: - New samples in the Sample Gallery: - ![new samples](https://github.com/OfficeDev/TeamsFx/assets/113089977/2af41ec4-ee19-4b66-a58a-d2d8bdbbbd60) + ![new samples](https://github.com/OfficeDev/TeamsFx/assets/113089977/2af41ec4-ee19-4b66-a58a-d2d8bdbbbd60) - Large Scale Notification Bot: send individual chat messages to a large number of users in a tenant - Graph Connector Bot: Teams command bot that queries custom data ingested into Microsoft Graph using Graph connector. @@ -44,7 +76,7 @@ Enhancement: ![Provision Region](https://github.com/OfficeDev/TeamsFx/assets/113089977/97867d08-b7af-4eae-b1e7-d0102e1a1361) - Automatic `npm install` for SPFx Tab App ![npm install for SPFx](https://github.com/OfficeDev/TeamsFx/assets/113089977/514d262d-9695-40dc-91aa-5c35044a319d) -- Teams Toolkit CLI Enhancement including: Commands have been reorganized into a hierarchical structure, added a teamsfx list command, improve the help command readability, outputs have been refreshed and log levels have been streamlined for clarity. +- Teams Toolkit CLI Enhancement including: Commands have been reorganized into a hierarchical structure, added a teamsfx list command, improve the help command readability, outputs have been refreshed and log levels have been streamlined for clarity. - Update Teams AI chat bot template to use latest teams-ai library. Bug Fixes: @@ -66,15 +98,15 @@ We've listened to your feedback and included these additional new features, enha New features: - Import an existing SharePoint Framework solution and continue development with Teams Toolkit. - ![SPFx Existing App](https://github.com/OfficeDev/TeamsFx/assets/11220663/3944f5c8-6c8c-4b4d-8df8-dc4f45b5967f) + ![SPFx Existing App](https://github.com/OfficeDev/TeamsFx/assets/11220663/3944f5c8-6c8c-4b4d-8df8-dc4f45b5967f) - A new link unfurling project template to help you get started with displaying rich content from links in Teams messages and Outlook emails. - ![Link Unfurling](https://github.com/OfficeDev/TeamsFx/assets/11220663/6e8b982a-0531-4ec1-8420-f6f17955ff40) + ![Link Unfurling](https://github.com/OfficeDev/TeamsFx/assets/11220663/6e8b982a-0531-4ec1-8420-f6f17955ff40) - A new AI Chat Bot project template to help you get started with building a GPT-like chat bot with AI capabilities using the [Teams AI Library](https://github.com/microsoft/teams-ai). ![AI Bot](https://github.com/OfficeDev/TeamsFx/assets/11220663/86a90d2a-efc3-4d8b-9e8c-5d34a1e8c081) - The Sample Gallery has a new sample, One Productivity Hub using Graph Toolkit with SPFx, that shows you how to build a Tab for viewing your calendar events, to-do tasks, and files using Microsoft Graph Toolkit components and a SharePoint provider. - ![SPFx Sample](https://github.com/OfficeDev/TeamsFx/assets/11220663/084ac508-49ea-4b30-854c-8b4d578ff6ee) + ![SPFx Sample](https://github.com/OfficeDev/TeamsFx/assets/11220663/084ac508-49ea-4b30-854c-8b4d578ff6ee) - Run life-cycle commands like Provision, Deploy, and Publish using new CodeLens hints added in-line to `teamsapp.yml`` when editing the file. - ![Inline Commands](https://github.com/OfficeDev/TeamsFx/assets/11220663/f6897b26-0e3c-441c-b028-32093e8322a7) + ![Inline Commands](https://github.com/OfficeDev/TeamsFx/assets/11220663/f6897b26-0e3c-441c-b028-32093e8322a7) Bug fixes: @@ -111,7 +143,7 @@ New features: - Simplified the Basic Tab project template by removing the dependency on React, single sign-on, and complicated example code. Use this template like an empty starting point for Tab apps. - You can customize which version of Azure Functions Core Tools is used with the `devTool/install` action. If not specified, the default version used is `4.0.4670` and [supports Node.js 18](https://learn.microsoft.com/azure/azure-functions/functions-versions?tabs=v4&pivots=programming-language-typescript#languages). - We've re-categorized the project templates to use familiar terminology that matches the documentation and platform. - ![create-new-app](https://github.com/OfficeDev/TeamsFx/assets/11220663/fe3ac358-775d-4deb-9b1e-a9eb4d932e56) + ![create-new-app](https://github.com/OfficeDev/TeamsFx/assets/11220663/fe3ac358-775d-4deb-9b1e-a9eb4d932e56) Enhancements: diff --git a/packages/vscode-extension/PRERELEASE.md b/packages/vscode-extension/PRERELEASE.md index c280df7d26..a2b152a4a9 100644 --- a/packages/vscode-extension/PRERELEASE.md +++ b/packages/vscode-extension/PRERELEASE.md @@ -2,6 +2,50 @@ ## Changelog +> Note: This changelog only includes the changes for the pre-release versions of Teams Toolkit. For the changelog of stable versions, please refer to the [Teams Toolkit Changelog](https://github.com/OfficeDev/TeamsFx/blob/dev/packages/vscode-extension/CHANGELOG.md). + +### April 01, 2024 + +#### New Features + +- **Word, Excel and PowerPoint Add-ins in Teams Toolkit** +![WXP Add-in](https://github.com/OfficeDev/TeamsFx/assets/11220663/30679a8c-b0b0-4b1c-ad4f-114547a12a6b) +Teams Toolkit now supports Microsoft Word, Excel, or PowerPoint JavaScript add-in development. This support enables developers to quickly get started and build add-ins with high productivity, featuring: + - Code samples for various add-in types such as task pane, content, or ribbon. Developers can customize these samples to create their own add-in projects for Word, Excel and PowerPoints. + - A side pane offering a unified and centralized experience for checking dependencies, running and debugging add-ins, managing lifecycle, leveraging utility, getting help, and providing feedback. + +### March 19, 2024 + +#### New Features + +- **Build Your Own Copilots in Teams with Teams AI Library** +![Custom Copilots](https://github.com/OfficeDev/TeamsFx/assets/11220663/0387a2ce-ec39-4c72-aabc-1ec2b9e85d59) +We have enhanced the user experience for developers to create their custom copilots, an AI-powered intelligent chatbot for Teams, with the following improvements: + - Streamlined UX for scaffolding, including top-level entry points and easy configuration of LLM services and credentials during the scaffolding flow. + - New application templates allowing developers to build an AI Agent from scratch. + - Python language support for building a `Basic AI Chatbot`. + +#### Enhancements + +- Updated the default app icon in the Teams Toolkit-generated app templates and samples with Microsoft 365 and Copilot-themed colors. +- Added `LLM.Description` in the app manifest for bot-based message extensions when used as copilot plugin for better reasoning with LLMs. To utilize this feature, please enable the `Develop Copilot Plugin` feature setting via Visual Studio Code in the [User and Workspace Settings](https://code.visualstudio.com/docs/getstarted/settings) and create a new app via `Create a New App` -> `Message Extension` -> `Custom Search Results` -> `Start with Bot`. +- Improved Azure account authentication with a built-in Microsoft authentication provider in Visual Studio Code. This enhancement increases the reliability of Azure authentication, especially when using a proxy. +- Upgraded `Custom Search Results` (Start with a New API) template to Azure Functions v4, the officially recommended version with better support. See more details for [Azure Functions runtime versions overview](https://learn.microsoft.com/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-javascript). +- Multiple parameters are now supported for API-based message extensions. +- Updated `Teams Chef Bot` sample to [teams-ai repository](https://github.com/microsoft/teams-ai/tree/main/js/samples/04.ai.a.teamsChefBot). + +#### Bug Fixes + +- Fixed an issue where an empty env file path might appear in error messages. [#11024](https://github.com/OfficeDev/TeamsFx/pull/11024) +- Fixed an issue where `arm/deploy.UnhandledError` might appear. [#10911](https://github.com/OfficeDev/TeamsFx/pull/10911) +- Fixed an issue with inconsistent capitalizations in the project creation dialog. [#10792](https://github.com/OfficeDev/TeamsFx/pull/10792) +- Fixed an issue with Teams Toolkit CLI where `Error: TeamsfxCLI.CannotDetectRunCommand` might appear when using the `teamsapp preview` command. [#10808](https://github.com/OfficeDev/TeamsFx/pull/10808) +- Fixed an issue with unclear error messages when sideloading the app using an unsupported file format. [#10799](https://github.com/OfficeDev/TeamsFx/pull/10799) +- Fixed an issue where an unexpected error might occur when executing `teamsapp account login azure`. [#11015](https://github.com/OfficeDev/TeamsFx/pull/11015) +- Fixed broken links in README documentation. [#10836](https://github.com/OfficeDev/TeamsFx/pull/10836), [#10831](https://github.com/OfficeDev/TeamsFx/pull/10831) +- Fixed an issue where featured samples are not shown in the full list. [#10841](https://github.com/OfficeDev/TeamsFx/pull/10841) + + ### January 23, 2024 #### New Features diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 1ee196deab..2e491a2481 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "ms-teams-vscode-extension", "displayName": "Teams Toolkit", "description": "Create, debug, and deploy Teams apps with Teams Toolkit", - "version": "5.4.1", + "version": "5.6.0", "publisher": "TeamsDevApp", "author": "Microsoft Corporation", "private": true, diff --git a/packages/vscode-extension/package.nls.json b/packages/vscode-extension/package.nls.json index 7d2c1ed56e..708717b329 100644 --- a/packages/vscode-extension/package.nls.json +++ b/packages/vscode-extension/package.nls.json @@ -167,7 +167,7 @@ "teamstoolkit.commandsTreeViewProvider.createOfficeAddInDescription": "Create a new add-in project of Word, Excel, or Powerpoint", "teamstoolkit.commandsTreeViewProvider.checkAndInstallDependenciesTitle": "Check and Install Dependencies", "teamstoolkit.commandsTreeViewProvider.checkAndInstallDependenciesDescription": "Check and install dependencies", - "teamstoolkit.commandsTreeViewProvider.officeDevLocalDebugTitle": "Preview Your Add-in (F5)", + "teamstoolkit.commandsTreeViewProvider.officeDevLocalDebugTitle": "Preview Your Office Add-in (F5)", "teamstoolkit.commandsTreeViewProvider.officeDevLocalDebugDescription": "Local debug your Add-in App", "teamstoolkit.commandsTreeViewProvider.validateManifestTitle": "Validate Manifest File", "teamstoolkit.commandsTreeViewProvider.validateManifestDescription": "Validate the manifest file of Office add-ins project", @@ -179,7 +179,7 @@ "teamstoolkit.commandsTreeViewProvider.officeAddIn.getStartedDescription": "Learn more about how to create Office Add-in project", "teamstoolkit.commandsTreeViewProvider.officeAddIn.documentationTitle": "Documentation", "teamstoolkit.commandsTreeViewProvider.officeAddIn.documentationDescription": "The documentation about how to create Office Add-in project", - "teamstoolkit.commandsTreeViewProvider.officeAddIn.stopDebugTitle": "Stop Previewing your Office Add-in", + "teamstoolkit.commandsTreeViewProvider.officeAddIn.stopDebugTitle": "Stop Previewing Your Office Add-in", "teamstoolkit.commandsTreeViewProvider.officeAddIn.stopDebugDescription": "Stop debugging the Office Add-in project", "teamstoolkit.common.readMore": "Read more", "teamstoolkit.common.signin": "Sign in", @@ -221,7 +221,6 @@ "teamstoolkit.handlers.installAdaptiveCardExt": "To preview and debug Adaptive Cards, we recommend to use the \"Adaptive Card Previewer\" extension.", "_teamstoolkit.handlers.installAdaptiveCardExt": "product name, no need to translate 'Adaptive Card Previewer'.", "teamstoolkit.handlers.autoInstallDependency": "Dependency installation in progress...", - "teamstoolkit.officeAddIn.terminal.open": "Run tasks in this terminal for Office add-in", "teamstoolkit.handlers.adaptiveCardExtUsage": "Type \"Adaptive Card: Open Preview\" in command pallete to start previewing current Adaptive Card file.", "teamstoolkit.handlers.invalidProject": "Unable to debug Teams App. This is not a valid Teams project.", "teamstoolkit.handlers.localDebugDescription": "[%s] is successfully created at [local address](%s). Continue to debug your app in Test Tool or Teams.", @@ -442,9 +441,7 @@ "teamstoolkit.officeAddIn.terminal.validateManifest": "Validating manifest...", "teamstoolkit.officeAddIn.terminal.stopDebugging": "Stopping debugging...", "teamstoolkit.officeAddIn.terminal.generateManifestGUID": "Generating manifest GUID...", - "teamstoolkit.officeAddIn.terminal.terminate": "* Ctrl +C to close the terminal.", - "teamstoolkit.officeAddIn.terminal.fail.tips": "couldn't complete!", - "teamstoolkit.officeAddIn.terminal.success.tips": "completed successfully!", + "teamstoolkit.officeAddIn.terminal.terminate": "* Terminal will be reused by tasks, press any key to close it.", "teamstoolkit.officeAddIn.terminal.manifest.notfound":"Manifest xml file not found", "teamstoolkit.officeAddIn.workspace.invalid": "Invalid workspace path" } \ No newline at end of file diff --git a/packages/vscode-extension/src/commonlib/azureLogin.ts b/packages/vscode-extension/src/commonlib/azureLogin.ts index 78a3d213e7..4dcfa82849 100644 --- a/packages/vscode-extension/src/commonlib/azureLogin.ts +++ b/packages/vscode-extension/src/commonlib/azureLogin.ts @@ -30,7 +30,7 @@ import { TelemetryErrorType, } from "../telemetry/extTelemetryEvents"; import { VS_CODE_UI } from "../extension"; -import { AzureScopes } from "@microsoft/teamsfx-core"; +import { AzureScopes, globalStateGet, globalStateUpdate } from "@microsoft/teamsfx-core"; import { getDefaultString, localize } from "../utils/localizeUtils"; import { Microsoft, @@ -38,6 +38,8 @@ import { getSessionFromVSCode, } from "./vscodeAzureSubscriptionProvider"; +const showAzureSignOutHelp = "ShowAzureSignOutHelp"; + export class AzureAccountManager extends login implements AzureAccountProvider { private static instance: AzureAccountManager; private static subscriptionId: string | undefined; @@ -112,9 +114,15 @@ export class AzureAccountManager extends login implements AzureAccountProvider { localize("teamstoolkit.codeFlowLogin.loginTimeoutDescription") ); } - void vscode.window.showInformationMessage( - localize("teamstoolkit.commands.azureAccount.signOutHelp") - ); + if (await globalStateGet(showAzureSignOutHelp, true)) { + const userClicked = await vscode.window.showInformationMessage( + localize("teamstoolkit.commands.azureAccount.signOutHelp"), + "Got it" + ); + if (userClicked === "Got it") { + await globalStateUpdate(showAzureSignOutHelp, false); + } + } } catch (e) { AzureAccountManager.currentStatus = loggedOut; void this.notifyStatus(); diff --git a/packages/vscode-extension/src/controls/sampleGallery/sampleCard.tsx b/packages/vscode-extension/src/controls/sampleGallery/sampleCard.tsx index 8e2f3d3720..ed344ab3dc 100644 --- a/packages/vscode-extension/src/controls/sampleGallery/sampleCard.tsx +++ b/packages/vscode-extension/src/controls/sampleGallery/sampleCard.tsx @@ -16,25 +16,14 @@ export default class SampleCard extends React.Component { - const downloadUrlInfo = sample.downloadUrlInfo; - this.setState({ - imageUrl: `https://media.githubusercontent.com/media/${downloadUrlInfo.owner}/${downloadUrlInfo.repository}/${downloadUrlInfo.ref}/${downloadUrlInfo.dir}/${sample.thumbnailPath}`, - }); - }} - /> - ); + const previewImage = ; const legacySampleImage = (
diff --git a/packages/vscode-extension/src/controls/webviewPanel.ts b/packages/vscode-extension/src/controls/webviewPanel.ts index e3d97eaa5e..16fab4e1ff 100644 --- a/packages/vscode-extension/src/controls/webviewPanel.ts +++ b/packages/vscode-extension/src/controls/webviewPanel.ts @@ -293,9 +293,9 @@ export class WebviewPanel { private replaceRelativeImagePaths(htmlContent: string, sample: SampleConfig) { const urlInfo = sample.downloadUrlInfo; - const imageUrlBase = `https://raw.githubusercontent.com/${urlInfo.owner}/${urlInfo.repository}/${urlInfo.ref}/${urlInfo.dir}`; + const imageUrl = `https://github.com/${urlInfo.owner}/${urlInfo.repository}/blob/${urlInfo.ref}/${urlInfo.dir}/${sample.thumbnailPath}?raw=1`; const imageRegex = /img\s+src="([^"]+)"/gm; - return htmlContent.replace(imageRegex, `img src="${imageUrlBase}/$1"`); + return htmlContent.replace(imageRegex, `img src="${imageUrl}"`); } private getWebpageTitle(panelType: PanelType): string { diff --git a/packages/vscode-extension/src/debug/taskTerminal/officeDevTerminal.ts b/packages/vscode-extension/src/debug/taskTerminal/officeDevTerminal.ts index e7b0a165be..33340dd463 100644 --- a/packages/vscode-extension/src/debug/taskTerminal/officeDevTerminal.ts +++ b/packages/vscode-extension/src/debug/taskTerminal/officeDevTerminal.ts @@ -1,21 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { find } from "lodash"; import * as cp from "child_process"; import * as vscode from "vscode"; import * as globalVariables from "../../globalVariables"; import { FxError, Result, Void, ok } from "@microsoft/teamsfx-api"; // eslint-disable-next-line import/no-cycle import { BaseTaskTerminal, ControlCodes } from "./baseTaskTerminal"; -import { fetchManifestList } from "@microsoft/teamsfx-core"; +import { OfficeManifestType, fetchManifestList } from "@microsoft/teamsfx-core"; import { localize } from "../../utils/localizeUtils"; -export const triggerInstall = "trigger install dependencies"; -export const triggerValidate = "trigger validate"; -export const triggerStopDebug = "trigger stop debug"; -export const triggerGenerateGUID = "generate manifest GUID"; +export enum TriggerCmdType { + triggerInstall = "trigger install dependencies", + triggerValidate = "trigger validate", + triggerStopDebug = "trigger stop debug", + triggerGenerateGUID = "generate manifest GUID", +} + +enum ProcessStatus { + notStarted, + running, + completed, +} export class OfficeDevTerminal extends BaseTaskTerminal { - private static instance: vscode.Terminal | undefined; + private status = ProcessStatus.notStarted; constructor() { super(); @@ -26,65 +35,66 @@ export class OfficeDevTerminal extends BaseTaskTerminal { } async open() { - this.writeEmitter.fire( - `${this.color(localize("teamstoolkit.officeAddIn.terminal.open"), "green")}\r\n` - ); await this.do(); } close(): void { - this.stop() - .catch((error) => { - this.writeEmitter.fire(`${error?.message as string}\r\n`); - }) - .finally(() => { - OfficeDevTerminal.instance?.dispose(); - OfficeDevTerminal.instance = undefined; - }); + this.stop().catch((error) => { + this.writeEmitter.fire(`${error?.message as string}\r\n`); + }); } handleInput(data: string): void { if (data.includes(ControlCodes.CtrlC)) { - this.stop() - .catch((error) => { - this.writeEmitter.fire(`${error?.message as string}\r\n`); - }) - .finally(() => { - OfficeDevTerminal.instance?.dispose(); - OfficeDevTerminal.instance = undefined; - }); - } else if (data.startsWith(triggerInstall)) { - this.writeEmitter.fire( - `\r\n${this.color( - localize("teamstoolkit.officeAddIn.terminal.installDependency"), - "yellow" - )}\r\n` - ); - this.installDependencies(); - } else if (data.startsWith(triggerValidate)) { - this.writeEmitter.fire( - `\r\n${this.color( - localize("teamstoolkit.officeAddIn.terminal.validateManifest"), - "yellow" - )}\r\n` - ); - this.runValidate(); - } else if (data.startsWith(triggerStopDebug)) { - this.writeEmitter.fire( - `\r\n${this.color( - localize("teamstoolkit.officeAddIn.terminal.stopDebugging"), - "yellow" - )}\r\n` - ); - this.stopDebug(); - } else if (data.startsWith(triggerGenerateGUID)) { - this.writeEmitter.fire( - `\r\n${this.color( - localize("teamstoolkit.officeAddIn.terminal.generateManifestGUID"), - "yellow" - )}\r\n` - ); - this.generateManifestGUID(); + this.stop().catch((error) => { + this.writeEmitter.fire(`${error?.message as string}\r\n`); + }); + } else if (data.startsWith(TriggerCmdType.triggerInstall)) { + if (this.status != ProcessStatus.running) { + this.writeEmitter.fire( + `\r\n${this.color( + localize("teamstoolkit.officeAddIn.terminal.installDependency"), + "yellow" + )}\r\n` + ); + this.installDependencies(); + this.status = ProcessStatus.running; + } + } else if (data.startsWith(TriggerCmdType.triggerValidate)) { + if (this.status != ProcessStatus.running) { + this.writeEmitter.fire( + `\r\n${this.color( + localize("teamstoolkit.officeAddIn.terminal.validateManifest"), + "yellow" + )}\r\n` + ); + this.runValidate(); + this.status = ProcessStatus.running; + } + } else if (data.startsWith(TriggerCmdType.triggerStopDebug)) { + if (this.status != ProcessStatus.running) { + this.writeEmitter.fire( + `\r\n${this.color( + localize("teamstoolkit.officeAddIn.terminal.stopDebugging"), + "yellow" + )}\r\n` + ); + this.stopDebug(); + this.status = ProcessStatus.running; + } + } else if (data.startsWith(TriggerCmdType.triggerGenerateGUID)) { + if (this.status != ProcessStatus.running) { + this.writeEmitter.fire( + `\r\n${this.color( + localize("teamstoolkit.officeAddIn.terminal.generateManifestGUID"), + "yellow" + )}\r\n` + ); + this.generateManifestGUID(); + this.status = ProcessStatus.running; + } + } else if (this.status == ProcessStatus.completed) { + this.closeEmitter.fire(0); } } @@ -110,25 +120,9 @@ export class OfficeDevTerminal extends BaseTaskTerminal { this.writeEmitter.fire(line); }); - childProc.on("exit", (code: number) => { - if (code == 0) { - this.writeEmitter.fire( - this.color( - `${cmdStr} ${localize("teamstoolkit.officeAddIn.terminal.success.tips")}`, - "green" - ) + "\r\n" - ); - } else { - this.writeEmitter.fire( - this.color( - `${cmdStr} ${localize("teamstoolkit.officeAddIn.terminal.fail.tips")}`, - "red" - ) + "\r\n" - ); - } - this.writeEmitter.fire( - this.color(localize("teamstoolkit.officeAddIn.terminal.terminate"), "green") + "\r\n" - ); + childProc.on("exit", () => { + this.writeEmitter.fire(localize("teamstoolkit.officeAddIn.terminal.terminate") + "\r\n"); + this.status = ProcessStatus.completed; }); } @@ -161,13 +155,13 @@ export class OfficeDevTerminal extends BaseTaskTerminal { public installDependencies() { const cmd = "npm"; - const args = ["install"]; + const args = ["install", "--color=always"]; this.startChildProcess(cmd, args); } private getManifest(): string | undefined { const workspacePath = globalVariables.workspaceUri?.fsPath; - const manifestList = fetchManifestList(workspacePath); + const manifestList = fetchManifestList(workspacePath, OfficeManifestType.XmlAddIn); if (!manifestList || manifestList.length == 0) { this.writeEmitter.fire( this.color(`${localize("teamstoolkit.officeAddIn.terminal.manifest.notfound")}\r\n`, "red") @@ -191,13 +185,34 @@ export class OfficeDevTerminal extends BaseTaskTerminal { } } - public static getInstance() { - if (!OfficeDevTerminal.instance) { - OfficeDevTerminal.instance = vscode.window.createTerminal({ - name: "OfficeAddInDev task", + public static getTerminalTitle(triggerCmd: TriggerCmdType): string | undefined { + switch (triggerCmd) { + case TriggerCmdType.triggerInstall: + return localize("teamstoolkit.commandsTreeViewProvider.checkAndInstallDependenciesTitle"); + case TriggerCmdType.triggerGenerateGUID: + return localize("teamstoolkit.codeLens.generateManifestGUID"); + case TriggerCmdType.triggerStopDebug: + return localize("teamstoolkit.commandsTreeViewProvider.officeAddIn.stopDebugTitle"); + case TriggerCmdType.triggerValidate: + return localize("teamstoolkit.commandsTreeViewProvider.validateManifestTitle"); + default: + return undefined; + } + } + + public static getInstance(triggerCmd: TriggerCmdType): vscode.Terminal { + let terminal: vscode.Terminal | undefined; + const terminalTitle = OfficeDevTerminal.getTerminalTitle(triggerCmd); + if ( + vscode.window.terminals.length === 0 || + (terminal = find(vscode.window.terminals, (value) => value.name === terminalTitle)) === + undefined + ) { + terminal = vscode.window.createTerminal({ + name: terminalTitle || "officeAddInDev task", pty: new OfficeDevTerminal(), }); } - return OfficeDevTerminal.instance; + return terminal; } } diff --git a/packages/vscode-extension/src/handlers.ts b/packages/vscode-extension/src/handlers.ts index 24ef39ddd5..0c7ecdc64b 100644 --- a/packages/vscode-extension/src/handlers.ts +++ b/packages/vscode-extension/src/handlers.ts @@ -1957,6 +1957,7 @@ export async function cmpAccountsHandler(args: any[]) { quickPick.onDidChangeSelection((selection) => { if (selection[0]) { (selection[0] as VscQuickPickItem).function().catch(console.error); + quickPick.hide(); } }); quickPick.onDidHide(() => quickPick.dispose()); @@ -2265,10 +2266,9 @@ export async function signOutAzure(isFromTreeView: boolean) { : TelemetryTriggerFrom.CommandPalette, [TelemetryProperty.AccountType]: AccountType.Azure, }); - const result = await AzureAccountManager.signout(); - if (result) { - accountTreeViewProviderInstance.azureAccountNode.setSignedOut(); - } + await vscode.window.showInformationMessage( + localize("teamstoolkit.commands.azureAccount.signOutHelp") + ); } export async function signOutM365(isFromTreeView: boolean) { diff --git a/packages/vscode-extension/src/officeDevHandlers.ts b/packages/vscode-extension/src/officeDevHandlers.ts index 1d9d3c367b..3bcfbf4b4c 100644 --- a/packages/vscode-extension/src/officeDevHandlers.ts +++ b/packages/vscode-extension/src/officeDevHandlers.ts @@ -13,13 +13,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { Uri } from "vscode"; import { GlobalKey } from "./constants"; -import { - OfficeDevTerminal, - triggerGenerateGUID, - triggerInstall, - triggerStopDebug, - triggerValidate, -} from "./debug/taskTerminal/officeDevTerminal"; +import { OfficeDevTerminal, TriggerCmdType } from "./debug/taskTerminal/officeDevTerminal"; import { VS_CODE_UI } from "./extension"; import * as globalVariables from "./globalVariables"; import { @@ -29,60 +23,102 @@ import { openSampleReadmeHandler, showLocalDebugMessage, } from "./handlers"; -import { TelemetryTriggerFrom } from "./telemetry/extTelemetryEvents"; -import { isTriggerFromWalkThrough } from "./utils/commonUtils"; +import { TelemetryTriggerFrom, VSCodeWindowChoice } from "./telemetry/extTelemetryEvents"; +import { isTriggerFromWalkThrough, getTriggerFromProperty } from "./utils/commonUtils"; import { localize } from "./utils/localizeUtils"; +import { ExtTelemetry } from "./telemetry/extTelemetry"; +import { TelemetryEvent, TelemetryProperty } from "./telemetry/extTelemetryEvents"; export async function openOfficePartnerCenterHandler( args?: any[] ): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_partner_center", + }); const url = "https://aka.ms/WXPAddinPublish"; return VS_CODE_UI.openUrl(url); } export async function openGetStartedLinkHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_get_started", + }); const url = "https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins"; return VS_CODE_UI.openUrl(url); } export async function openOfficeDevDeployHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_deploy", + }); const url = "https://aka.ms/WXPAddinDeploy"; return VS_CODE_UI.openUrl(url); } export async function publishToAppSourceHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_publish", + }); const url = "https://learn.microsoft.com/partner-center/marketplace/submit-to-appsource-via-partner-center"; return VS_CODE_UI.openUrl(url); } -export async function openDebugLinkHandler(): Promise> { +export async function openDebugLinkHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_debug", + }); return VS_CODE_UI.openUrl( "https://learn.microsoft.com/office/dev/add-ins/testing/debug-add-ins-overview" ); } export async function openDocumentHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_document", + }); return VS_CODE_UI.openUrl("https://learn.microsoft.com/office/dev/add-ins/"); } export async function openDevelopmentLinkHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_development", + }); return VS_CODE_UI.openUrl( "https://learn.microsoft.com/office/dev/add-ins/develop/develop-overview" ); } export async function openLifecycleLinkHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_lifecycle", + }); return VS_CODE_UI.openUrl( "https://learn.microsoft.com/office/dev/add-ins/overview/core-concepts-office-add-ins" ); } export async function openHelpFeedbackLinkHandler(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_feedback", + }); return VS_CODE_UI.openUrl("https://learn.microsoft.com/answers/tags/9/m365"); } export async function openReportIssues(args?: any[]): Promise> { + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.Documentation, { + ...getTriggerFromProperty(args), + [TelemetryProperty.DocumentationName]: "office_report", + }); return VS_CODE_UI.openUrl("https://github.com/OfficeDev/office-js/issues"); } @@ -93,16 +129,24 @@ export async function openScriptLabLink(args?: any[]): Promise> { - const terminal = OfficeDevTerminal.getInstance(); + ExtTelemetry.sendTelemetryEvent( + TelemetryEvent.validateAddInManifest, + getTriggerFromProperty(args) + ); + const terminal = OfficeDevTerminal.getInstance(TriggerCmdType.triggerValidate); terminal.show(); - terminal.sendText(triggerValidate); + terminal.sendText(TriggerCmdType.triggerValidate); return Promise.resolve(ok(null)); } export function installOfficeAddInDependencies(args?: any[]): Promise> { - const terminal = OfficeDevTerminal.getInstance(); + ExtTelemetry.sendTelemetryEvent( + TelemetryEvent.installAddInDependencies, + getTriggerFromProperty(args) + ); + const terminal = OfficeDevTerminal.getInstance(TriggerCmdType.triggerInstall); terminal.show(); - terminal.sendText(triggerInstall); + terminal.sendText(TriggerCmdType.triggerInstall); return Promise.resolve(ok(null)); } @@ -127,16 +171,18 @@ export async function popupOfficeAddInDependenciesMessage() { } export function stopOfficeAddInDebug(args?: any[]): Promise> { - const terminal = OfficeDevTerminal.getInstance(); + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.stopAddInDebug, getTriggerFromProperty(args)); + const terminal = OfficeDevTerminal.getInstance(TriggerCmdType.triggerStopDebug); terminal.show(); - terminal.sendText(triggerStopDebug); + terminal.sendText(TriggerCmdType.triggerStopDebug); return Promise.resolve(ok(null)); } export function generateManifestGUID(args?: any[]): Promise> { - const terminal = OfficeDevTerminal.getInstance(); + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.generateAddInGUID, getTriggerFromProperty(args)); + const terminal = OfficeDevTerminal.getInstance(TriggerCmdType.triggerGenerateGUID); terminal.show(); - terminal.sendText(triggerGenerateGUID); + terminal.sendText(TriggerCmdType.triggerGenerateGUID); return Promise.resolve(ok(null)); } @@ -161,6 +207,9 @@ export async function openOfficeDevFolder( if (warnings?.length) { await globalStateUpdate(GlobalKey.CreateWarnings, JSON.stringify(warnings)); } + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.openNewOfficeAddInProject, { + [TelemetryProperty.VscWindow]: VSCodeWindowChoice.NewWindowByDefault, + }); await vscode.commands.executeCommand("vscode.openFolder", folderPath, true); } diff --git a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts index c29cdc4d16..60af88b16c 100644 --- a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts +++ b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts @@ -256,6 +256,13 @@ export enum TelemetryEvent { ShowScaffoldingWarningSummaryError = "show-scaffolding-warning-summary-error", FindSimilarIssues = "find-similar-issues", + + //Office add-in related + validateAddInManifest = "validate-addin-manifest", + installAddInDependencies = "install-addin-dependencies", + stopAddInDebug = "stop-office-addin-debug", + generateAddInGUID = "generate-addin-guid", + openNewOfficeAddInProject = "open-new-office-addin-project", } export enum TelemetryProperty { diff --git a/packages/vscode-extension/test/extension/extTelemetry.test.ts b/packages/vscode-extension/test/extension/extTelemetry.test.ts index ae7e898593..591ebfd044 100644 --- a/packages/vscode-extension/test/extension/extTelemetry.test.ts +++ b/packages/vscode-extension/test/extension/extTelemetry.test.ts @@ -151,8 +151,8 @@ describe("ExtTelemetry", () => { "settings-version": "1.0.0", "error-type": "user", "error-name": "UserTestError", - "error-message": error.message, - "error-stack": error.stack, + // "error-message": error.message, + // "error-stack": error.stack, "error-code": "test.UserTestError", "error-component": "", "error-method": "", diff --git a/packages/vscode-extension/test/extension/handlers.test.ts b/packages/vscode-extension/test/extension/handlers.test.ts index 721e4c3e28..14ddcb7a5d 100644 --- a/packages/vscode-extension/test/extension/handlers.test.ts +++ b/packages/vscode-extension/test/extension/handlers.test.ts @@ -45,7 +45,7 @@ import commandController from "../../src/commandController"; import { AzureAccountManager } from "../../src/commonlib/azureLogin"; import { signedIn, signedOut } from "../../src/commonlib/common/constant"; import { VsCodeLogProvider } from "../../src/commonlib/log"; -import M365TokenInstance from "../../src/commonlib/m365Login"; +import M365TokenInstance, { M365Login } from "../../src/commonlib/m365Login"; import { DeveloperPortalHomeLink, GlobalKey } from "../../src/constants"; import { PanelType } from "../../src/controls/PanelType"; import { WebviewPanel } from "../../src/controls/webviewPanel"; @@ -1070,12 +1070,14 @@ describe("handlers", () => { it("signOutAzure", async () => { Object.setPrototypeOf(AzureAccountManager, sandbox.stub()); - const signOut = sandbox.stub(AzureAccountManager.getInstance(), "signout"); + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); const sendTelemetryEvent = sandbox.stub(ExtTelemetry, "sendTelemetryEvent"); await handlers.signOutAzure(false); - sandbox.assert.calledOnce(signOut); + sandbox.assert.calledOnce(showMessageStub); }); describe("decryptSecret", function () { @@ -2227,7 +2229,9 @@ describe("handlers", () => { }); it("cmpAccountsHandler", async () => { - const AzureSignOutStub = sandbox.stub(AzureAccountManager.prototype, "signout"); + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); const M365SignOutStub = sandbox.stub(M365TokenInstance, "signout"); sandbox .stub(M365TokenInstance, "getStatus") @@ -2235,9 +2239,13 @@ describe("handlers", () => { sandbox .stub(AzureAccountManager.prototype, "getStatus") .resolves({ status: "SignedIn", accountInfo: { upn: "test.email.com" } }); + let changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any = () => {}; const stubQuickPick = { items: [], - onDidChangeSelection: () => { + onDidChangeSelection: ( + _changeSelectionCallback: (e: readonly vscode.QuickPickItem[]) => any + ) => { + changeSelectionCallback = _changeSelectionCallback; return { dispose: () => {}, }; @@ -2248,19 +2256,23 @@ describe("handlers", () => { }; }, show: () => {}, + hide: () => {}, onDidAccept: () => {}, }; + const hideStub = sandbox.stub(stubQuickPick, "hide"); sandbox.stub(vscode.window, "createQuickPick").returns(stubQuickPick as any); sandbox.stub(extension.VS_CODE_UI, "selectOption").resolves(ok({ result: "unknown" } as any)); await handlers.cmpAccountsHandler([]); + changeSelectionCallback([stubQuickPick.items[1]]); for (const i of stubQuickPick.items) { await (i as any).function(); } - chai.assert.isTrue(AzureSignOutStub.calledOnce); + chai.assert.isTrue(showMessageStub.calledTwice); chai.assert.isTrue(M365SignOutStub.calledOnce); + chai.assert.isTrue(hideStub.calledOnce); }); it("updatePreviewManifest", async () => { @@ -2634,13 +2646,15 @@ describe("autoOpenProjectHandler", () => { }; }); sandbox.stub(vscode.extensions, "getExtension"); - const signoutStub = sandbox.stub(AzureAccountManager.prototype, "signout"); + const showMessageStub = sandbox + .stub(vscode.window, "showInformationMessage") + .resolves(undefined); await handlers.registerAccountMenuCommands({ subscriptions: [], } as unknown as vscode.ExtensionContext); - chai.assert.isTrue(signoutStub.called); + chai.assert.isTrue(showMessageStub.called); }); it("registerAccountMenuCommands() - error", async () => { @@ -2648,15 +2662,13 @@ describe("autoOpenProjectHandler", () => { sandbox .stub(vscode.commands, "registerCommand") .callsFake((command: string, callback: (...args: any[]) => any) => { - callback({ contextValue: "signedinAzure" }).then(() => {}); + callback({ contextValue: "signedinM365" }).then(() => {}); return { dispose: () => {}, }; }); sandbox.stub(vscode.extensions, "getExtension"); - const signoutStub = sandbox - .stub(AzureAccountManager.prototype, "signout") - .throws(new UserCancelError()); + const signoutStub = sandbox.stub(M365Login.prototype, "signout").throws(new UserCancelError()); await handlers.registerAccountMenuCommands({ subscriptions: [], diff --git a/packages/vscode-extension/test/extension/officeDevHandler.test.ts b/packages/vscode-extension/test/extension/officeDevHandler.test.ts index d39054f009..76345bcb90 100644 --- a/packages/vscode-extension/test/extension/officeDevHandler.test.ts +++ b/packages/vscode-extension/test/extension/officeDevHandler.test.ts @@ -6,12 +6,7 @@ import * as mockfs from "mock-fs"; import * as sinon from "sinon"; import * as vscode from "vscode"; import { Terminal } from "vscode"; -import { - OfficeDevTerminal, - triggerGenerateGUID, - triggerInstall, - triggerValidate, -} from "../../src/debug/taskTerminal/officeDevTerminal"; +import { OfficeDevTerminal, TriggerCmdType } from "../../src/debug/taskTerminal/officeDevTerminal"; import * as extension from "../../src/extension"; import * as globalVariables from "../../src/globalVariables"; import * as handlers from "../../src/handlers"; @@ -284,14 +279,14 @@ describe("OfficeDevTerminal", () => { const result = await officeDevHandlers.validateOfficeAddInManifest(); chai.expect(result.isOk()).to.be.true; sinon.assert.calledOnce(showStub); - sinon.assert.calledWith(sendTextStub, triggerValidate); // replace triggerValidate with actual value + sinon.assert.calledWith(sendTextStub, TriggerCmdType.triggerValidate); // replace triggerValidate with actual value }); it("should install Office AddIn Dependencies", async () => { const result = await officeDevHandlers.installOfficeAddInDependencies(); chai.expect(result.isOk()).to.be.true; sinon.assert.calledOnce(showStub); - sinon.assert.calledWith(sendTextStub, triggerInstall); // replace triggerInstall with actual value + sinon.assert.calledWith(sendTextStub, TriggerCmdType.triggerInstall); // replace triggerInstall with actual value }); }); @@ -354,7 +349,7 @@ describe("generateManifestGUID", () => { sinon.assert.calledOnce(getInstanceStub); sinon.assert.calledOnce(showStub); sinon.assert.calledOnce(sendTextStub); - sinon.assert.calledWithExactly(sendTextStub, triggerGenerateGUID); + sinon.assert.calledWithExactly(sendTextStub, TriggerCmdType.triggerGenerateGUID); sinon.restore(); }); }); diff --git a/packages/vscode-ui/package.json b/packages/vscode-ui/package.json index b4a60eaff3..f5737876b2 100644 --- a/packages/vscode-ui/package.json +++ b/packages/vscode-ui/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/vscode-ui", - "version": "1.0.0", + "version": "1.0.1", "main": "build/index.js", "types": "build/index.d.ts", "license": "MIT", diff --git a/templates/common/api-plugin-existing-api/README.md b/templates/common/api-plugin-existing-api/README.md index e69de29bb2..a5d20aa5d8 100644 --- a/templates/common/api-plugin-existing-api/README.md +++ b/templates/common/api-plugin-existing-api/README.md @@ -0,0 +1,42 @@ +# Overview of Custom Search Results app template + +## Build a message extension from OpenAPI description document + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Create Teams app by clicking `Provision` in "Lifecycle" section. +4. Select `Preivew in Copilot (Edge)` or `Preview in Copilot (Chrome)` from the launch configuration dropdown. +5. Open the `Copilot` app and send a prompt to trigger your plugin. + +## What's included in the template + +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response templates for API responses | +| `env` | Environment files | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | + +## Addition information and references + +- [Extend Microsoft Copilot for Microsoft 365](https://aka.ms/teamsfx-copilot-plugin) \ No newline at end of file diff --git a/templates/common/office-xml-addin-common/teamsapp.yml.tpl b/templates/common/office-xml-addin-common/teamsapp.yml.tpl new file mode 100644 index 0000000000..358212d2aa --- /dev/null +++ b/templates/common/office-xml-addin-common/teamsapp.yml.tpl @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.4 \ No newline at end of file diff --git a/templates/constraints/yml/actions/aadAppCreate.mustache b/templates/constraints/yml/actions/aadAppCreate.mustache index 920754734c..2840f3bb99 100644 --- a/templates/constraints/yml/actions/aadAppCreate.mustache +++ b/templates/constraints/yml/actions/aadAppCreate.mustache @@ -8,7 +8,12 @@ # defined here. name: {{appName}} # If the value is false, the action will not generate client secret for you + {{#skipClientSecret}} + generateClientSecret: false + {{/skipClientSecret}} + {{^skipClientSecret}} generateClientSecret: true + {{/skipClientSecret}} # Authenticate users with a Microsoft work or school account in your # organization's Microsoft Entra tenant (for example, single tenant). signInAudience: AzureADMyOrg @@ -16,9 +21,11 @@ # specified environment variable(s). writeToEnvironmentFile: clientId: AAD_APP_CLIENT_ID + {{^skipClientSecret}} # Environment variable that starts with `SECRET_` will be stored to the # .env.{envName}.user environment file clientSecret: SECRET_AAD_APP_CLIENT_SECRET + {{/skipClientSecret}} objectId: AAD_APP_OBJECT_ID tenantId: AAD_APP_TENANT_ID authority: AAD_APP_OAUTH_AUTHORITY diff --git a/templates/constraints/yml/actions/apiKeyUpdate.mustache b/templates/constraints/yml/actions/apiKeyUpdate.mustache new file mode 100644 index 0000000000..5bd878e1d5 --- /dev/null +++ b/templates/constraints/yml/actions/apiKeyUpdate.mustache @@ -0,0 +1,14 @@ + # Update API KEY + - uses: apiKey/update + with: + # Name of the API Key + name: {{ApiSpecAuthName}} + # Teams app ID + appId: ${{TEAMS_APP_ID}} + {{#apiSpecPath}} + # Path to OpenAPI description document + apiSpecPath: {{{apiSpecPath}}} + {{/apiSpecPath}} + {{#registrationId}} + registrationId: {{{registrationId}}} + {{/registrationId}} diff --git a/templates/constraints/yml/actions/botAadAppCreate.mustache b/templates/constraints/yml/actions/botAadAppCreate.mustache index f0ac7d557c..8dbb487177 100644 --- a/templates/constraints/yml/actions/botAadAppCreate.mustache +++ b/templates/constraints/yml/actions/botAadAppCreate.mustache @@ -1,10 +1,14 @@ # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD \ No newline at end of file + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID \ No newline at end of file diff --git a/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..e43cadcb62 --- /dev/null +++ b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,31 @@ +{{#header}} version: 1.1.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} COPILOT {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +{{#fileCreateOrUpdateJsonFile}} +{ + "profileName": "Microsoft Teams (browser):", + "commandLineArgs": "\"host start --port 5130 --pause-on-error\"", + "launchUrl": "\"https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}\"", + "launchSettings": true, + "hotReload": true +} +{{/fileCreateOrUpdateJsonFile}} + diff --git a/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..b5a7eac101 --- /dev/null +++ b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,31 @@ +{{#header}} version: 1.1.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunDotnetCommand}} publish {{/cliRunDotnetCommand}} +{{#azureFunctionsZipDeploy}} + artifactFolder: bin/Release/{{TargetFramework}}/publish, + resourceId: ${{API_FUNCTION_RESOURCE_ID}} +{{/azureFunctionsZipDeploy}} diff --git a/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..60067aa177 --- /dev/null +++ b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,25 @@ +{{#header}} version: 1.0.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} FUNC, FUNC_NAME: repair {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +deploy: +{{#devToolInstall}} func, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} + +{{#cliRunNpmCommand}} install, args: install --no-audit {{/cliRunNpmCommand}} diff --git a/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..f08ea877a9 --- /dev/null +++ b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,37 @@ +{{#header}} version: 1.0.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunNpmCommand}} install, args: install --production {{/cliRunNpmCommand}} + +{{#azureFunctionsZipDeploy}} resourceId: ${{API_FUNCTION_RESOURCE_ID}}, ignoreFile: .funcignore {{/azureFunctionsZipDeploy}} + +# Triggered when 'teamsapp publish' is executed +publish: +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} +{{#teamsAppUpdate}} {{/teamsAppUpdate}} +{{#teamsAppPublishAppPackage}} {{/teamsAppPublishAppPackage}} diff --git a/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache index 5fc2ae4f9b..80d9bff8b4 100644 --- a/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache +++ b/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache @@ -1,11 +1,13 @@ -{{#header}} version: v1.4 {{/header}} +{{#header}} version: v1.5 {{/header}} provision: {{#teamsAppCreate}} {{/teamsAppCreate}} {{#script}} FUNC, FUNC_NAME: repair {{/script}} -{{#apiKeyRegister}}ApiSpecAuthName: x-api-key, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: X_API_KEY_REGISTRATION_ID{{/apiKeyRegister}} +{{#apiKeyRegister}}ApiSpecAuthName: apiKey, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: APIKEY_REGISTRATION_ID{{/apiKeyRegister}} + +{{#apiKeyUpdate}}ApiSpecAuthName: apiKey, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, registrationId: ${{APIKEY_REGISTRATION_ID}}{{/apiKeyUpdate}} {{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} diff --git a/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache index fb74295349..d674c31b24 100644 --- a/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache +++ b/templates/constraints/yml/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache @@ -1,4 +1,4 @@ -{{#header}} version: v1.4 {{/header}} +{{#header}} version: v1.5 {{/header}} environmentFolderPath: ./env @@ -8,7 +8,9 @@ provision: {{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} -{{#apiKeyRegister}}ApiSpecAuthName: x-api-key, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: X_API_KEY_REGISTRATION_ID{{/apiKeyRegister}} +{{#apiKeyRegister}}ApiSpecAuthName: apiKey, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: APIKEY_REGISTRATION_ID{{/apiKeyRegister}} + +{{#apiKeyUpdate}}ApiSpecAuthName: apiKey, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, registrationId: ${{APIKEY_REGISTRATION_ID}}{{/apiKeyUpdate}} {{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} diff --git a/templates/constraints/yml/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl.mustache index 594ee2fc47..b89b00eceb 100644 --- a/templates/constraints/yml/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl.mustache +++ b/templates/constraints/yml/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl.mustache @@ -20,7 +20,7 @@ provision: {{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} deploy: -{{#devToolInstall}} devCert, func, dotnet, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} +{{#devToolInstall}} devCert, func, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} {{#cliRunNpmCommand}} install, args: install --no-audit {{/cliRunNpmCommand}} diff --git a/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..60067aa177 --- /dev/null +++ b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,25 @@ +{{#header}} version: 1.0.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} FUNC, FUNC_NAME: repair {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +deploy: +{{#devToolInstall}} func, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} + +{{#cliRunNpmCommand}} install, args: install --no-audit {{/cliRunNpmCommand}} diff --git a/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..d0b79fb865 --- /dev/null +++ b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,39 @@ +{{#header}} version: 1.0.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunNpmCommand}} install, args: install {{/cliRunNpmCommand}} + +{{#cliRunNpmCommand}} args: run build --if-present, build {{/cliRunNpmCommand}} + +{{#azureFunctionsZipDeploy}} resourceId: ${{API_FUNCTION_RESOURCE_ID}}, ignoreFile: .funcignore {{/azureFunctionsZipDeploy}} + +# Triggered when 'teamsapp publish' is executed +publish: +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} +{{#teamsAppUpdate}} {{/teamsAppUpdate}} +{{#teamsAppPublishAppPackage}} {{/teamsAppPublishAppPackage}} diff --git a/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache index 5fc2ae4f9b..80d9bff8b4 100644 --- a/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache +++ b/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl.mustache @@ -1,11 +1,13 @@ -{{#header}} version: v1.4 {{/header}} +{{#header}} version: v1.5 {{/header}} provision: {{#teamsAppCreate}} {{/teamsAppCreate}} {{#script}} FUNC, FUNC_NAME: repair {{/script}} -{{#apiKeyRegister}}ApiSpecAuthName: x-api-key, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: X_API_KEY_REGISTRATION_ID{{/apiKeyRegister}} +{{#apiKeyRegister}}ApiSpecAuthName: apiKey, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: APIKEY_REGISTRATION_ID{{/apiKeyRegister}} + +{{#apiKeyUpdate}}ApiSpecAuthName: apiKey, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, registrationId: ${{APIKEY_REGISTRATION_ID}}{{/apiKeyUpdate}} {{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} diff --git a/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache index be85ec2b6c..6a553344f6 100644 --- a/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache +++ b/templates/constraints/yml/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl.mustache @@ -1,4 +1,4 @@ -{{#header}} version: v1.4 {{/header}} +{{#header}} version: v1.5 {{/header}} environmentFolderPath: ./env @@ -8,7 +8,9 @@ provision: {{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} -{{#apiKeyRegister}}ApiSpecAuthName: x-api-key, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: X_API_KEY_REGISTRATION_ID{{/apiKeyRegister}} +{{#apiKeyRegister}}ApiSpecAuthName: apiKey, primaryClientSecret: ${{SECRET_API_KEY}}, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, ApiSpecAuthRegistrationIdEnvName: APIKEY_REGISTRATION_ID{{/apiKeyRegister}} + +{{#apiKeyUpdate}}ApiSpecAuthName: apiKey, apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml, registrationId: ${{APIKEY_REGISTRATION_ID}}{{/apiKeyUpdate}} {{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} diff --git a/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 76% rename from templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index e0bb57be79..1c66703830 100644 --- a/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -38,13 +38,21 @@ Before running or debugging your bot, please follow these steps to setup your ow SECRET_OPENAI_ASSISTANT_ID= ``` 2. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -3. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +3. Right-click the `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 4. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 5. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 6. In the launched browser, select the Add button to load the app in Teams 7. In the chat bar, type and send anything to your bot to trigger a response +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + ### Debug bot app in Teams App Test Tool 1. Fill in both OpenAI API Key and the created Assistant ID into `appsettings.TestTool.json` ``` @@ -54,7 +62,9 @@ to install the app to } ``` 2. Select `Teams App Test Tool (browser)` in debug dropdown menu -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 4. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response ## Extend the AI Assistant Bot template with more AI capabilities diff --git a/templates/csharp/ai-assistant-bot/teamsapp.local.yml.tpl b/templates/csharp/ai-assistant-bot/teamsapp.local.yml.tpl index 89e4548b59..88ce51fd1b 100644 --- a/templates/csharp/ai-assistant-bot/teamsapp.local.yml.tpl +++ b/templates/csharp/ai-assistant-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/ai-assistant-bot/teamsapp.yml.tpl b/templates/csharp/ai-assistant-bot/teamsapp.yml.tpl index fcd8a2c873..61ef0acb12 100644 --- a/templates/csharp/ai-assistant-bot/teamsapp.yml.tpl +++ b/templates/csharp/ai-assistant-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 75% rename from templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 542fb1152f..d977d6f7fe 100644 --- a/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -25,13 +25,21 @@ The app template is built using the Teams AI library, which provides the capabil 2. If using Azure OpenAI, update "gpt-35-turbo" in `Program.cs` to your own model deployment name 3. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -4. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +4. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 5. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 6. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 7. In the launched browser, select the Add button to load the app in Teams 8. In the chat bar, type and send anything to your bot to trigger a response +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + ### Debug bot app in Teams App Test Tool 1. Fill in your OpenAI API Key or Azure OpenAI settings in `appsettings.TestTool.json` @@ -50,7 +58,9 @@ to install the app to 2. If using Azure OpenAI, update "gpt-35-turbo" in `Program.cs` to your own model deployment name 3. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response ## Extend the AI Chat Bot template with more AI capabilities diff --git a/templates/csharp/ai-bot/teamsapp.local.yml.tpl b/templates/csharp/ai-bot/teamsapp.local.yml.tpl index 2c44fee972..aae887c91a 100644 --- a/templates/csharp/ai-bot/teamsapp.local.yml.tpl +++ b/templates/csharp/ai-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/ai-bot/teamsapp.yml.tpl b/templates/csharp/ai-bot/teamsapp.yml.tpl index fcd8a2c873..61ef0acb12 100644 --- a/templates/csharp/ai-bot/teamsapp.yml.tpl +++ b/templates/csharp/ai-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/api-message-extension-sso/.gitignore b/templates/csharp/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..a19acf5d9b --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.gitignore @@ -0,0 +1,25 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +local.settings.json +.deployment + +# User-specific files +*.user + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Notification local store +.notification.localstore.json diff --git a/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md similarity index 100% rename from templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl new file mode 100644 index 0000000000..91e258e9b5 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl @@ -0,0 +1,9 @@ +{ + "profiles": { + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl new file mode 100644 index 0000000000..a31df153ea --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl new file mode 100644 index 0000000000..9c141db6c7 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Microsoft Teams (browser) + + \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl new file mode 100644 index 0000000000..dbbf83d021 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl @@ -0,0 +1,22 @@ +[ + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + } +] \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/GettingStarted.md b/templates/csharp/api-message-extension-sso/GettingStarted.md new file mode 100644 index 0000000000..ffe08739a2 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/GettingStarted.md @@ -0,0 +1,26 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. +2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +5. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl b/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl new file mode 100644 index 0000000000..3f80846657 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl @@ -0,0 +1,17 @@ +namespace {{SafeProjectName}}.Models +{ + public class RepairModel + { + public string Id { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public string AssignedTo { get; set; } + + public string Date { get; set; } + + public string Image { get; set; } + } +} diff --git a/templates/csharp/api-message-extension-sso/Program.cs b/templates/csharp/api-message-extension-sso/Program.cs new file mode 100644 index 0000000000..cd97ae1f66 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Program.cs @@ -0,0 +1,7 @@ +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + +host.Run(); \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json b/templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl similarity index 70% rename from templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json rename to templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl index 29ab7d974e..0e93831305 100644 --- a/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json +++ b/templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl @@ -1,5 +1,6 @@ { "profiles": { +{{^isNewProjectTypeEnabled}} "Microsoft Teams (browser)": { "commandName": "Project", "commandLineArgs": "host start --port 5130 --pause-on-error", @@ -23,5 +24,17 @@ // }, // "hotReloadProfile": "aspnetcore" //} +{{/isNewProjectTypeEnabled}} +{{#isNewProjectTypeEnabled}} + "Start Project": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } +{{/isNewProjectTypeEnabled}} } } diff --git a/templates/csharp/api-message-extension-sso/Repair.cs.tpl b/templates/csharp/api-message-extension-sso/Repair.cs.tpl new file mode 100644 index 0000000000..5f3bfca385 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Repair.cs.tpl @@ -0,0 +1,46 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace {{SafeProjectName}} +{ + public class Repair + { + private readonly ILogger _logger; + + public Repair(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("repair")] + public async Task RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req) + { + // Log that the HTTP trigger function received a request. + _logger.LogInformation("C# HTTP trigger function processed a request."); + + // Get the query parameters from the request. + string assignedTo = req.Query["assignedTo"]; + + // Get the repair records. + var repairRecords = RepairData.GetRepairs(); + + // Filter the repair records by the assignedTo query parameter. + var repairs = repairRecords.Where(r => + { + // Split assignedTo into firstName and lastName + var parts = r.AssignedTo.Split(' '); + + // Check if the assignedTo query parameter matches the repair record's assignedTo value, or the repair record's firstName or lastName. + return r.AssignedTo.Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase) || + parts[0].Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase) || + parts[1].Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase); + }); + + // Return filtered repair records, or an empty array if no records were found. + var response = req.CreateResponse(); + await response.WriteAsJsonAsync(new { results = repairs }); + return response; + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/RepairData.cs.tpl b/templates/csharp/api-message-extension-sso/RepairData.cs.tpl new file mode 100644 index 0000000000..f8dda33584 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/RepairData.cs.tpl @@ -0,0 +1,62 @@ +using {{SafeProjectName}}.Models; + +namespace {{SafeProjectName}} +{ + public class RepairData + { + public static List GetRepairs() + { + return new List + { + new() { + Id = "1", + Title = "Oil change", + Description = "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + AssignedTo = "Karin Blair", + Date = "2023-05-23", + Image = "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + new() { + Id = "2", + Title = "Brake repairs", + Description = "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + AssignedTo = "Issac Fielder", + Date = "2023-05-24", + Image = "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + new() { + Id = "3", + Title = "Tire service", + Description = "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + AssignedTo = "Karin Blair", + Date = "2023-05-24", + Image = "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + new() { + Id = "4", + Title = "Battery replacement", + Description = "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + AssignedTo = "Ashley McCarthy", + Date ="2023-05-25", + Image = "https://i.stack.imgur.com/4ftuj.jpg" + }, + new() { + Id = "5", + Title = "Engine tune-up", + Description = "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + AssignedTo = "Karin Blair", + Date = "2023-05-28", + Image = "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + new() { + Id = "6", + Title = "Suspension and steering repairs", + Description = "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + AssignedTo = "Daisy Phillips", + Date = "2023-05-29", + Image = "https://i.stack.imgur.com/4v5OI.jpg" + } + }; + } + } +} diff --git a/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl b/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..7ca98042d0 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Search for repairs + description: Search for repairs info with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: string + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/color.png b/templates/csharp/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000..2d7e85c9e9 Binary files /dev/null and b/templates/csharp/api-message-extension-sso/appPackage/color.png differ diff --git a/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/outline.png b/templates/csharp/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000..245fa194db Binary files /dev/null and b/templates/csharp/api-message-extension-sso/appPackage/outline.png differ diff --git a/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/csharp/api-message-extension-sso/env/.env.dev b/templates/csharp/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..0833c0a922 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/env/.env.dev @@ -0,0 +1,18 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= +API_FUNCTION_RESOURCE_ID= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/env/.env.local b/templates/csharp/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..de0f992dcb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/env/.env.local @@ -0,0 +1,12 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +TEAMSFX_M365_USER_NAME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/host.json b/templates/csharp/api-message-extension-sso/host.json new file mode 100644 index 0000000000..a8dd88f8b6 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/host.json @@ -0,0 +1,8 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "Function": "Information" + } + } +} diff --git a/templates/csharp/api-message-extension-sso/infra/azure.bicep b/templates/csharp/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..4fb7488354 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet-isolated' // Use .NET isolated process + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'SCM_ZIPDEPLOY_DONOT_PRESERVE_FILETIME' + value: '1' // Zipdeploy files will always be updated. Detail: https://aka.ms/teamsfx-zipdeploy-donot-preserve-filetime + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/local.settings.json b/templates/csharp/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..8eea88f48a --- /dev/null +++ b/templates/csharp/api-message-extension-sso/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } +} diff --git a/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..28a4de1669 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set OPENAPI_SERVER_URL for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env OPENAPI_SERVER_URL=https://${{DEV_TUNNEL_URL}}"; + echo "::set-teamsfx-env OPENAPI_SERVER_DOMAIN=${{DEV_TUNNEL_URL}}"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID +{{^isNewProjectTypeEnabled}} + + # Create or update debug profile in lauchsettings file + - uses: file/createOrUpdateJsonFile + with: + target: ./Properties/launchSettings.json + content: + profiles: + Microsoft Teams (browser): + commandName: "Project" + commandLineArgs: "host start --port 5130 --pause-on-error" + dotnetRunMessages: true + launchBrowser: true + launchUrl: "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + environmentVariables: + ASPNETCORE_ENVIRONMENT: "Development" + hotReloadProfile: "aspnetcore" +{{/isNewProjectTypeEnabled}} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl b/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..a0f3c087d7 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,147 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release {{ProjectName}}.csproj +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: bin/Release/{{TargetFramework}}/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl b/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl new file mode 100644 index 0000000000..27f0dceb7f --- /dev/null +++ b/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl @@ -0,0 +1,45 @@ + + + + {{TargetFramework}} + enable + v4 + Exe + {{SafeProjectName}} + + + +{{^isNewProjectTypeEnabled}} + +{{/isNewProjectTypeEnabled}} + + + +{{^isNewProjectTypeEnabled}} + + + + + +{{/isNewProjectTypeEnabled}} + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + + + diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..31714b1b1a --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,29 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. When Teams launches in the browser, you can open the Copilot app and send a prompt to trigger your plugin. +6. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl new file mode 100644 index 0000000000..91e258e9b5 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl @@ -0,0 +1,9 @@ +{ + "profiles": { + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl new file mode 100644 index 0000000000..a31df153ea --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl new file mode 100644 index 0000000000..9c141db6c7 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Microsoft Teams (browser) + + \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl new file mode 100644 index 0000000000..dbbf83d021 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl @@ -0,0 +1,22 @@ +[ + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + } +] \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl b/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl new file mode 100644 index 0000000000..0e93831305 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl @@ -0,0 +1,40 @@ +{ + "profiles": { +{{^isNewProjectTypeEnabled}} + "Microsoft Teams (browser)": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } + //// Uncomment following profile to debug project only (without launching Teams) + //, + //"Start Project (not in Teams)": { + // "commandName": "Project", + // "commandLineArgs": "host start --port 5130 --pause-on-error", + // "dotnetRunMessages": true, + // "applicationUrl": "https://localhost:7130;http://localhost:5130", + // "environmentVariables": { + // "ASPNETCORE_ENVIRONMENT": "Development" + // }, + // "hotReloadProfile": "aspnetcore" + //} +{{/isNewProjectTypeEnabled}} +{{#isNewProjectTypeEnabled}} + "Start Project": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } +{{/isNewProjectTypeEnabled}} + } +} diff --git a/templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json b/templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl similarity index 96% rename from templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json rename to templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl index 7154b72c7c..fb11edba12 100644 --- a/templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json +++ b/templates/csharp/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl @@ -1,6 +1,6 @@ { "schema_version": "v2", - "name_for_human": "Repair Search Plugin", + "name_for_human": "{{appName}}${{APP_NAME_SUFFIX}}", "description_for_human": "Track your repair records", "description_for_model": "Plugin for searching a repair list, you can search by who's assigned to the repair.", "functions": [ diff --git a/templates/csharp/api-plugin-from-scratch/appPackage/manifest.json.tpl b/templates/csharp/api-plugin-from-scratch/appPackage/manifest.json.tpl index 3ed557b7e7..9ebc549abb 100644 --- a/templates/csharp/api-plugin-from-scratch/appPackage/manifest.json.tpl +++ b/templates/csharp/api-plugin-from-scratch/appPackage/manifest.json.tpl @@ -23,7 +23,7 @@ "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." }, "accentColor": "#FFFFFF", - "apiPlugins": [ + "plugins": [ { "pluginFile": "ai-plugin.json" } diff --git a/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 8506c51770..caa266bd19 100644 --- a/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,18 +4,42 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/command-and-response/teamsapp.local.yml.tpl b/templates/csharp/command-and-response/teamsapp.local.yml.tpl index d561c60a53..b9c58793cf 100644 --- a/templates/csharp/command-and-response/teamsapp.local.yml.tpl +++ b/templates/csharp/command-and-response/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/command-and-response/teamsapp.yml.tpl b/templates/csharp/command-and-response/teamsapp.yml.tpl index fcd8a2c873..61ef0acb12 100644 --- a/templates/csharp/command-and-response/teamsapp.yml.tpl +++ b/templates/csharp/command-and-response/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 80% rename from templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 721a4362d9..af4ff4ea9f 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -22,13 +22,15 @@ SECRET_API_KEY= ``` -### Debug app in Teams Web Client +### Start the app in Teams Web Client 1. If you haven't added your own API Key, please follow the above steps to add your own API Key. -2. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. -3. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +2. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png). +3. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. 4. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. 5. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 6. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/Repair.cs.tpl b/templates/csharp/copilot-plugin-from-scratch-api-key/Repair.cs.tpl index b058d6aee2..7d8908c43f 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/Repair.cs.tpl +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/Repair.cs.tpl @@ -62,15 +62,15 @@ namespace {{SafeProjectName}} */ private bool IsApiKeyValid(HttpRequestData req) { - // Try to get the value of the 'x-api-key' header from the request. + // Try to get the value of the 'Authorization' header from the request. // If the header is not present, return false. - if (!req.Headers.TryGetValues("x-api-key", out var apiKeyValue)) + if (!req.Headers.TryGetValues("Authorization", out var authValue)) { return false; } - // Get the first value of the 'x-api-key' header. - var apiKey = apiKeyValue.FirstOrDefault(); + // Get the api key value from the 'Authorization' header. + var apiKey = authValue.FirstOrDefault().Replace("Bearer", "").Trim(); // Get the API key from the configuration. var configApiKey = _configuration["API_KEY"]; diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml b/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml index f6c3fa8ff2..fc3e5f6da5 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml @@ -8,10 +8,9 @@ servers: description: The repair api server components: securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: x-api-key + apiKey: + type: http + scheme: bearer paths: /repair: get: @@ -25,6 +24,8 @@ paths: schema: type: string required: false + security: + - apiKey: [] responses: '200': description: A list of repairs diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl b/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl index 6b7bc23629..2de42871af 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl @@ -50,7 +50,7 @@ "authorization": { "authType": "apiSecretServiceAuth", "apiSecretServiceAuthConfiguration": { - "apiSecretRegistrationId": "${{X_API_KEY_REGISTRATION_ID}}" + "apiSecretRegistrationId": "${{APIKEY_REGISTRATION_ID}}" } } } diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl b/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl index ae65565ec3..2998943de9 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl @@ -44,9 +44,9 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key - clientSecret: ${{SECRET_API_KEY}} + primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID appId: ${{TEAMS_APP_ID}} # Path to OpenAPI description document @@ -54,7 +54,7 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl b/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl index 3c1178e444..291cedf931 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl @@ -46,9 +46,9 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key - clientSecret: ${{SECRET_API_KEY}} + primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID appId: ${{TEAMS_APP_ID}} # Path to OpenAPI description document @@ -56,7 +56,7 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..c31b8e6094 --- /dev/null +++ b/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,28 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 79083a58a6..30d2579c56 100644 --- a/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -3,18 +3,42 @@ ## Quick Start {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send anything to your bot to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/default-bot/teamsapp.local.yml.tpl b/templates/csharp/default-bot/teamsapp.local.yml.tpl index d561c60a53..b9c58793cf 100644 --- a/templates/csharp/default-bot/teamsapp.local.yml.tpl +++ b/templates/csharp/default-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/default-bot/teamsapp.yml.tpl b/templates/csharp/default-bot/teamsapp.yml.tpl index fcd8a2c873..61ef0acb12 100644 --- a/templates/csharp/default-bot/teamsapp.yml.tpl +++ b/templates/csharp/default-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 8c1602728c..0000000000 --- a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,26 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can unfurl links from ".botframework.com" domain. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -Learn more advanced topic like how to customize your link unfurling code in -tutorials at https://aka.ms/teamsfx-extend-link-unfurling - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..acd1d04574 --- /dev/null +++ b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,40 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can unfurl links from ".botframework.com" domain. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +Learn more advanced topic like how to customize your link unfurling code in +tutorials at https://aka.ms/teamsfx-extend-link-unfurling + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/link-unfurling/teamsapp.local.yml.tpl b/templates/csharp/link-unfurling/teamsapp.local.yml.tpl index 84800141fa..f75b5c07bd 100644 --- a/templates/csharp/link-unfurling/teamsapp.local.yml.tpl +++ b/templates/csharp/link-unfurling/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/link-unfurling/teamsapp.yml.tpl b/templates/csharp/link-unfurling/teamsapp.yml.tpl index 01eccbaf78..43e7ea6052 100644 --- a/templates/csharp/link-unfurling/teamsapp.yml.tpl +++ b/templates/csharp/link-unfurling/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 72% rename from templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl index b269e41742..a4c4232e7e 100644 --- a/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -3,10 +3,12 @@ ## Quick Start 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. You can trigger "create card" command from compose message area, the command box, or directly from a message. diff --git a/templates/csharp/message-extension-action/teamsapp.local.yml.tpl b/templates/csharp/message-extension-action/teamsapp.local.yml.tpl index 2c630ecb63..a96ecfdbab 100644 --- a/templates/csharp/message-extension-action/teamsapp.local.yml.tpl +++ b/templates/csharp/message-extension-action/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/message-extension-action/teamsapp.yml.tpl b/templates/csharp/message-extension-action/teamsapp.yml.tpl index 2e80f815ef..42a46cb9c5 100644 --- a/templates/csharp/message-extension-action/teamsapp.yml.tpl +++ b/templates/csharp/message-extension-action/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 11a3c36284..0000000000 --- a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,36 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -> **Prerequisites** -> -> To run the app template in your local dev machine, you will need: -> -> - [Visual Studio 2022](https://aka.ms/vs) 17.8 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs). -> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). -> - Join Microsoft 365 Copilot Plugin development [early access program](https://aka.ms/plugins-dev-waitlist). - -1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. -2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want - to install the app to. -4. To directly trigger the Message Extension in Teams, you can: - 1. In the debug dropdown menu, select `Microsoft Teams (browser)`. - 2. In the launched browser, select the Add button to load the app in Teams. - 3. You can search NuGet package from compose message area, or from the command box. -5. To trigger the Message Extension through Copilot, you can: - 1. In the debug dropdown menu, select `Copilot (browser)`. - 2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. - 3. Open the Copilot app and send a prompt to trigger your plugin. - 4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. - > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. - -## Learn more - -- [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..f3a4034110 --- /dev/null +++ b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,45 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run the app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.8 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). +> - Join Microsoft 365 Copilot Plugin development [early access program](https://aka.ms/plugins-dev-waitlist). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png). +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want + to install the app to. +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams. +6. You can search NuGet package from compose message area, or from the command box. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Copilot +1. Select `Copilot (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-copilot.png) +2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. +3. Open the Copilot app and send a prompt to trigger your plugin. +4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. + > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. + +## Learn more + +- [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-copilot/GettingStarted.md b/templates/csharp/message-extension-copilot/GettingStarted.md index 11a3c36284..998d982ed2 100644 --- a/templates/csharp/message-extension-copilot/GettingStarted.md +++ b/templates/csharp/message-extension-copilot/GettingStarted.md @@ -21,7 +21,7 @@ 5. To trigger the Message Extension through Copilot, you can: 1. In the debug dropdown menu, select `Copilot (browser)`. 2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. - 3. Open the Copilot app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. diff --git a/templates/csharp/message-extension-copilot/teamsapp.local.yml.tpl b/templates/csharp/message-extension-copilot/teamsapp.local.yml.tpl index 208b46d307..90b6f61625 100644 --- a/templates/csharp/message-extension-copilot/teamsapp.local.yml.tpl +++ b/templates/csharp/message-extension-copilot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/message-extension-copilot/teamsapp.yml.tpl b/templates/csharp/message-extension-copilot/teamsapp.yml.tpl index c2ec1181da..e963e4c067 100644 --- a/templates/csharp/message-extension-copilot/teamsapp.yml.tpl +++ b/templates/csharp/message-extension-copilot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index f9bfd88ef1..0000000000 --- a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,23 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can search nuget package from compose message area, or from the command box. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..b93c167e0e --- /dev/null +++ b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can search nuget package from compose message area, or from the command box. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-search/teamsapp.local.yml.tpl b/templates/csharp/message-extension-search/teamsapp.local.yml.tpl index 84800141fa..f75b5c07bd 100644 --- a/templates/csharp/message-extension-search/teamsapp.local.yml.tpl +++ b/templates/csharp/message-extension-search/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/message-extension-search/teamsapp.yml.tpl b/templates/csharp/message-extension-search/teamsapp.yml.tpl index 1635022db6..f2d19807d3 100644 --- a/templates/csharp/message-extension-search/teamsapp.yml.tpl +++ b/templates/csharp/message-extension-search/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index a28c3ef4d1..0000000000 --- a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,23 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can play with this app to create an adaptive card, search for an NuGet package or unfurl links from ".botframework.com" domain. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..3bdb40521c --- /dev/null +++ b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can play with this app to create an adaptive card, search for an NuGet package or unfurl links from ".botframework.com" domain. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension/teamsapp.local.yml.tpl b/templates/csharp/message-extension/teamsapp.local.yml.tpl index 84800141fa..f75b5c07bd 100644 --- a/templates/csharp/message-extension/teamsapp.local.yml.tpl +++ b/templates/csharp/message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/message-extension/teamsapp.yml.tpl b/templates/csharp/message-extension/teamsapp.yml.tpl index e10ff81880..43e7ea6052 100644 --- a/templates/csharp/message-extension/teamsapp.yml.tpl +++ b/templates/csharp/message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: @@ -104,6 +108,7 @@ deploy: {{^PlaceProjectFileInSolutionDir}} workingDirectory: ../{{ProjectName}} {{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} # Deploy your application to Azure App Service using the zip deploy feature. # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. - uses: azureAppService/zipDeploy diff --git a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index f4c4abd07e..0000000000 --- a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,24 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -This sample is configured as interactive server-side rendering. -For more details about Blazor render mode, please refer to [ASP.NET Core Blazor render modes | Microsoft Learn](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes). - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..e6833ef5c5 --- /dev/null +++ b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +This sample is configured as interactive server-side rendering. +For more details about Blazor render mode, please refer to [ASP.NET Core Blazor render modes | Microsoft Learn](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes). + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab-ssr/Components/App.razor.tpl b/templates/csharp/non-sso-tab-ssr/Components/App.razor.tpl index 26b0ffe11f..a8c200dc55 100644 --- a/templates/csharp/non-sso-tab-ssr/Components/App.razor.tpl +++ b/templates/csharp/non-sso-tab-ssr/Components/App.razor.tpl @@ -10,7 +10,7 @@ - + diff --git a/templates/csharp/non-sso-tab-ssr/Components/Pages/Hello.razor b/templates/csharp/non-sso-tab-ssr/Components/Pages/Hello.razor index 182daadf8f..f598ea416e 100644 --- a/templates/csharp/non-sso-tab-ssr/Components/Pages/Hello.razor +++ b/templates/csharp/non-sso-tab-ssr/Components/Pages/Hello.razor @@ -1,5 +1,4 @@ @inject MicrosoftTeams MicrosoftTeams; -@rendermode InteractiveServer @if(isLoading) { diff --git a/templates/csharp/non-sso-tab-ssr/Components/Pages/TabConfig.razor b/templates/csharp/non-sso-tab-ssr/Components/Pages/TabConfig.razor index 4262607b99..7b53353949 100644 --- a/templates/csharp/non-sso-tab-ssr/Components/Pages/TabConfig.razor +++ b/templates/csharp/non-sso-tab-ssr/Components/Pages/TabConfig.razor @@ -18,12 +18,13 @@ { if(firstRender) { + var baseUri = new Uri(NavigationManager.BaseUri); var settings = new TeamsInstanceSettings { SuggestedDisplayName = "My Tab", EntityId = _entityId.ToString(), - ContentUrl = $"{NavigationManager.BaseUri}/tab", - WebsiteUrl = $"{NavigationManager.BaseUri}/tab" + ContentUrl = new Uri(baseUri, "tab").ToString(), + WebsiteUrl = new Uri(baseUri, "tab").ToString(), }; await MicrosoftTeams.InitializeAsync(); diff --git a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 230489b216..0000000000 --- a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,21 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..afd06b3f50 --- /dev/null +++ b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,34 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab/Pages/TabConfig.razor b/templates/csharp/non-sso-tab/Pages/TabConfig.razor index 4262607b99..7b53353949 100644 --- a/templates/csharp/non-sso-tab/Pages/TabConfig.razor +++ b/templates/csharp/non-sso-tab/Pages/TabConfig.razor @@ -18,12 +18,13 @@ { if(firstRender) { + var baseUri = new Uri(NavigationManager.BaseUri); var settings = new TeamsInstanceSettings { SuggestedDisplayName = "My Tab", EntityId = _entityId.ToString(), - ContentUrl = $"{NavigationManager.BaseUri}/tab", - WebsiteUrl = $"{NavigationManager.BaseUri}/tab" + ContentUrl = new Uri(baseUri, "tab").ToString(), + WebsiteUrl = new Uri(baseUri, "tab").ToString(), }; await MicrosoftTeams.InitializeAsync(); diff --git a/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.local.yml.tpl b/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.yml.tpl b/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.yml.tpl index 0c2c40c29c..ece4a3c7ab 100644 --- a/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.yml.tpl +++ b/templates/csharp/notification-http-timer-trigger-isolated/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-timer-trigger/teamsapp.local.yml.tpl b/templates/csharp/notification-http-timer-trigger/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-http-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-http-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-http-timer-trigger/teamsapp.yml.tpl b/templates/csharp/notification-http-timer-trigger/teamsapp.yml.tpl index a176ef85dc..bb97fbc639 100644 --- a/templates/csharp/notification-http-timer-trigger/teamsapp.yml.tpl +++ b/templates/csharp/notification-http-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-trigger-isolated/teamsapp.local.yml.tpl b/templates/csharp/notification-http-trigger-isolated/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-http-trigger-isolated/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-http-trigger-isolated/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-http-trigger-isolated/teamsapp.yml.tpl b/templates/csharp/notification-http-trigger-isolated/teamsapp.yml.tpl index 0c2c40c29c..ece4a3c7ab 100644 --- a/templates/csharp/notification-http-trigger-isolated/teamsapp.yml.tpl +++ b/templates/csharp/notification-http-trigger-isolated/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-trigger/teamsapp.local.yml.tpl b/templates/csharp/notification-http-trigger/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-http-trigger/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-http-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-http-trigger/teamsapp.yml.tpl b/templates/csharp/notification-http-trigger/teamsapp.yml.tpl index a176ef85dc..bb97fbc639 100644 --- a/templates/csharp/notification-http-trigger/teamsapp.yml.tpl +++ b/templates/csharp/notification-http-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-timer-trigger-isolated/teamsapp.local.yml.tpl b/templates/csharp/notification-timer-trigger-isolated/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-timer-trigger-isolated/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-timer-trigger-isolated/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-timer-trigger-isolated/teamsapp.yml.tpl b/templates/csharp/notification-timer-trigger-isolated/teamsapp.yml.tpl index 0c2c40c29c..ece4a3c7ab 100644 --- a/templates/csharp/notification-timer-trigger-isolated/teamsapp.yml.tpl +++ b/templates/csharp/notification-timer-trigger-isolated/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-timer-trigger/teamsapp.local.yml.tpl b/templates/csharp/notification-timer-trigger/teamsapp.local.yml.tpl index a4b717c837..df988f0ccc 100644 --- a/templates/csharp/notification-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-timer-trigger/teamsapp.yml.tpl b/templates/csharp/notification-timer-trigger/teamsapp.yml.tpl index a176ef85dc..bb97fbc639 100644 --- a/templates/csharp/notification-timer-trigger/teamsapp.yml.tpl +++ b/templates/csharp/notification-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl index bd15b6acd1..194c2d7667 100644 --- a/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. Open Windows PowerShell and post a HTTP request to trigger the notification: @@ -12,16 +13,40 @@ {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. Open Windows PowerShell and post a HTTP request to trigger the notification: Invoke-WebRequest -Uri "http://localhost:5130/api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-webapi/teamsapp.local.yml.tpl b/templates/csharp/notification-webapi/teamsapp.local.yml.tpl index e7d8e3536a..016999e165 100644 --- a/templates/csharp/notification-webapi/teamsapp.local.yml.tpl +++ b/templates/csharp/notification-webapi/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/notification-webapi/teamsapp.yml.tpl b/templates/csharp/notification-webapi/teamsapp.yml.tpl index 41e9b06520..3c6f65a1ee 100644 --- a/templates/csharp/notification-webapi/teamsapp.yml.tpl +++ b/templates/csharp/notification-webapi/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 50% rename from templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl index b7146ac967..bf4b3ca6ea 100644 --- a/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -2,12 +2,25 @@ ## Quick Start -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 4. In the launched browser, select the Add button to load the app in Teams +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/sso-tab-ssr/Components/App.razor.tpl b/templates/csharp/sso-tab-ssr/Components/App.razor.tpl index b840d7cabc..738497350f 100644 --- a/templates/csharp/sso-tab-ssr/Components/App.razor.tpl +++ b/templates/csharp/sso-tab-ssr/Components/App.razor.tpl @@ -11,7 +11,7 @@ - + diff --git a/templates/csharp/sso-tab-ssr/Components/Pages/TabConfig.razor b/templates/csharp/sso-tab-ssr/Components/Pages/TabConfig.razor index 4262607b99..7b53353949 100644 --- a/templates/csharp/sso-tab-ssr/Components/Pages/TabConfig.razor +++ b/templates/csharp/sso-tab-ssr/Components/Pages/TabConfig.razor @@ -18,12 +18,13 @@ { if(firstRender) { + var baseUri = new Uri(NavigationManager.BaseUri); var settings = new TeamsInstanceSettings { SuggestedDisplayName = "My Tab", EntityId = _entityId.ToString(), - ContentUrl = $"{NavigationManager.BaseUri}/tab", - WebsiteUrl = $"{NavigationManager.BaseUri}/tab" + ContentUrl = new Uri(baseUri, "tab").ToString(), + WebsiteUrl = new Uri(baseUri, "tab").ToString(), }; await MicrosoftTeams.InitializeAsync(); diff --git a/templates/csharp/sso-tab-ssr/Components/Pages/Welcome.razor b/templates/csharp/sso-tab-ssr/Components/Pages/Welcome.razor index 47eb80b686..cf1a7a41bb 100644 --- a/templates/csharp/sso-tab-ssr/Components/Pages/Welcome.razor +++ b/templates/csharp/sso-tab-ssr/Components/Pages/Welcome.razor @@ -5,7 +5,6 @@ @inject IWebHostEnvironment HostEnvironment @inject IConfiguration Configuration @inject NavigationManager MyNavigationManager -@rendermode InteractiveServer @if (isLoading) { diff --git a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 5d08813450..0000000000 --- a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,24 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -Note: This sample will only provision single tenant Microsoft Entra app. -For multi-tenant support, please refer to https://aka.ms/teamsfx-multi-tenant. - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..c78b7b2a89 --- /dev/null +++ b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +Note: This sample will only provision single tenant Microsoft Entra app. +For multi-tenant support, please refer to https://aka.ms/teamsfx-multi-tenant. + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/sso-tab/Pages/TabConfig.razor b/templates/csharp/sso-tab/Pages/TabConfig.razor index 4262607b99..7b53353949 100644 --- a/templates/csharp/sso-tab/Pages/TabConfig.razor +++ b/templates/csharp/sso-tab/Pages/TabConfig.razor @@ -18,12 +18,13 @@ { if(firstRender) { + var baseUri = new Uri(NavigationManager.BaseUri); var settings = new TeamsInstanceSettings { SuggestedDisplayName = "My Tab", EntityId = _entityId.ToString(), - ContentUrl = $"{NavigationManager.BaseUri}/tab", - WebsiteUrl = $"{NavigationManager.BaseUri}/tab" + ContentUrl = new Uri(baseUri, "tab").ToString(), + WebsiteUrl = new Uri(baseUri, "tab").ToString(), }; await MicrosoftTeams.InitializeAsync(); diff --git a/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl index a9910a0993..f7a8901d3e 100644 --- a/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,18 +4,43 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams app dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams app dependencies 3. If prompted, sign in with an M365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/workflow/teamsapp.local.yml.tpl b/templates/csharp/workflow/teamsapp.local.yml.tpl index d561c60a53..b9c58793cf 100644 --- a/templates/csharp/workflow/teamsapp.local.yml.tpl +++ b/templates/csharp/workflow/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile diff --git a/templates/csharp/workflow/teamsapp.yml.tpl b/templates/csharp/workflow/teamsapp.yml.tpl index fcd8a2c873..61ef0acb12 100644 --- a/templates/csharp/workflow/teamsapp.yml.tpl +++ b/templates/csharp/workflow/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/ai-assistant-bot/src/index.js b/templates/js/ai-assistant-bot/src/index.js index cde05997bd..9c91e984e7 100644 --- a/templates/js/ai-assistant-bot/src/index.js +++ b/templates/js/ai-assistant-bot/src/index.js @@ -33,17 +33,20 @@ const onTurnErrorHandler = async (context, error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity("The bot encountered an error or bug."); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/js/ai-assistant-bot/teamsapp.local.yml.tpl b/templates/js/ai-assistant-bot/teamsapp.local.yml.tpl index 1e877247d9..a5e20f2fb0 100644 --- a/templates/js/ai-assistant-bot/teamsapp.local.yml.tpl +++ b/templates/js/ai-assistant-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/ai-assistant-bot/teamsapp.yml.tpl b/templates/js/ai-assistant-bot/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/ai-assistant-bot/teamsapp.yml.tpl +++ b/templates/js/ai-assistant-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/ai-bot/README.md.tpl b/templates/js/ai-bot/README.md.tpl index cb7a20cae6..460daf9f2c 100644 --- a/templates/js/ai-bot/README.md.tpl +++ b/templates/js/ai-bot/README.md.tpl @@ -114,4 +114,4 @@ You can follow [Get started with Teams AI library](https://learn.microsoft.com/e - [Teams AI library](https://aka.ms/teams-ai-library) - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) -- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/js/ai-bot/src/index.js b/templates/js/ai-bot/src/index.js index cde05997bd..9c91e984e7 100644 --- a/templates/js/ai-bot/src/index.js +++ b/templates/js/ai-bot/src/index.js @@ -33,17 +33,20 @@ const onTurnErrorHandler = async (context, error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity("The bot encountered an error or bug."); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/js/ai-bot/teamsapp.local.yml.tpl b/templates/js/ai-bot/teamsapp.local.yml.tpl index c67b760811..81d724d58c 100644 --- a/templates/js/ai-bot/teamsapp.local.yml.tpl +++ b/templates/js/ai-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/ai-bot/teamsapp.yml.tpl b/templates/js/ai-bot/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/ai-bot/teamsapp.yml.tpl +++ b/templates/js/ai-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/api-message-extension-sso/.funcignore b/templates/js/api-message-extension-sso/.funcignore new file mode 100644 index 0000000000..20a67199c6 --- /dev/null +++ b/templates/js/api-message-extension-sso/.funcignore @@ -0,0 +1,18 @@ +.funcignore +*.js.map +.git* +.localConfigs +.vscode +local.settings.json +test +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.gitignore b/templates/js/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..3cda0399bd --- /dev/null +++ b/templates/js/api-message-extension-sso/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.vscode/extensions.json b/templates/js/api-message-extension-sso/.vscode/extensions.json new file mode 100644 index 0000000000..92a389add7 --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "teamsdevapp.ms-teams-vscode-extension" + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.vscode/launch.json b/templates/js/api-message-extension-sso/.vscode/launch.json new file mode 100644 index 0000000000..9ad7575a0b --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/launch.json @@ -0,0 +1,95 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/js/api-message-extension-sso/.vscode/settings.json b/templates/js/api-message-extension-sso/.vscode/settings.json new file mode 100644 index 0000000000..0ed7b2e738 --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/templates/js/api-message-extension-sso/.vscode/tasks.json b/templates/js/api-message-extension-sso/.vscode/tasks.json new file mode 100644 index 0000000000..f6fc1bebad --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/tasks.json @@ -0,0 +1,116 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + "domain": "OPENAPI_SERVER_DOMAIN" // output tunnel domain as OPENAPI_SERVER_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + } + } + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/README.md b/templates/js/api-message-extension-sso/README.md new file mode 100644 index 0000000000..8f8e78aaa9 --- /dev/null +++ b/templates/js/api-message-extension-sso/README.md @@ -0,0 +1,60 @@ +# Overview of Custom Search Results app template + +## Build a message extension from a new API with Azure Functions + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)` from the launch configuration dropdown. +4. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## What's included in the template + +| Folder | Contents | +| ------------ | ----------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the repair API | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------- | +| `src/functions/repair.js` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/responseTemplates/repair.json` | A generated Adaptive Card that used to render API response. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | + +## How Microsoft Entra works + +![microsoft-entra-flow](https://github.com/OfficeDev/TeamsFx/assets/107838226/846e7a60-8cc1-4d8b-852e-2aec93b61fe9) + +> **Note**: The Azure Active Directory (AAD) flow is only functional in remote environments. It cannot be tested in a local environment due to the lack of authentication support in Azure Function core tools. + +## Addition information and references + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) diff --git a/templates/js/api-message-extension-sso/aad.manifest.json.tpl b/templates/js/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/js/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..b19421902f --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Search for repairs + description: Search for repairs info with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: integer + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/color.png b/templates/js/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000..2d7e85c9e9 Binary files /dev/null and b/templates/js/api-message-extension-sso/appPackage/color.png differ diff --git a/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/outline.png b/templates/js/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000..245fa194db Binary files /dev/null and b/templates/js/api-message-extension-sso/appPackage/outline.png differ diff --git a/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/js/api-message-extension-sso/env/.env.dev b/templates/js/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..b83a22d12f --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.dev @@ -0,0 +1,19 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.dev.user b/templates/js/api-message-extension-sso/env/.env.dev.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.dev.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.local b/templates/js/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..1ff4229ff7 --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.local @@ -0,0 +1,18 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PACKAGE_PATH= +FUNC_ENDPOINT= +API_FUNCTION_ENDPOINT= +TEAMS_APP_TENANT_ID= +TEAMS_APP_UPDATE_TIME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= + +# Generated during deploy, you can also add your own variables. +FUNC_PATH= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.local.user b/templates/js/api-message-extension-sso/env/.env.local.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.local.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/host.json b/templates/js/api-message-extension-sso/host.json new file mode 100644 index 0000000000..9df913614d --- /dev/null +++ b/templates/js/api-message-extension-sso/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/infra/azure.bicep b/templates/js/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..9532cee661 --- /dev/null +++ b/templates/js/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/local.settings.json b/templates/js/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..ee43d724a2 --- /dev/null +++ b/templates/js/api-message-extension-sso/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + } +} diff --git a/templates/js/api-message-extension-sso/package.json.tpl b/templates/js/api-message-extension-sso/package.json.tpl new file mode 100644 index 0000000000..b16d0c06a8 --- /dev/null +++ b/templates/js/api-message-extension-sso/package.json.tpl @@ -0,0 +1,17 @@ +{ + "name": "{{SafeProjectNameLowerCase}}", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --javascript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0" + }, + "main": "src/functions/*.js" +} diff --git a/templates/js/api-message-extension-sso/src/functions/repair.js b/templates/js/api-message-extension-sso/src/functions/repair.js new file mode 100644 index 0000000000..21ad967665 --- /dev/null +++ b/templates/js/api-message-extension-sso/src/functions/repair.js @@ -0,0 +1,51 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for + * complete Azure Functions developer guide. + */ +const { app } = require("@azure/functions"); +/** + * This function handles the HTTP request and returns the repair information. + * + * @param req - The HTTP request. + * @param context - The Azure Functions context object. + * @returns A promise that resolves with the HTTP response containing the repair information. + */ +async function repair(req, context) { + context.log("HTTP trigger function processed a request."); + // Initialize response. + const res = { + status: 200, + jsonBody: { + results: [], + }, + }; + + // Get the assignedTo query parameter. + const assignedTo = req.query.get("assignedTo"); + + // If the assignedTo query parameter is not provided, return all repair records. + if (!assignedTo) { + return res; + } + + // Get the repair records from the data.json file. + const repairRecords = require("../repairsData.json"); + + // Filter the repair records by the assignedTo query parameter. + const repairs = repairRecords.filter((item) => { + const query = assignedTo.trim().toLowerCase(); + const fullName = item.assignedTo.toLowerCase(); + const [firstName, lastName] = fullName.split(" "); + return fullName === query || firstName === query || lastName === query; + }); + + // Return filtered repair records, or an empty array if no records were found. + res.jsonBody.results = repairs ?? []; + return res; +} + +app.http("repair", { + methods: ["GET"], + authLevel: "anonymous", + handler: repair, +}); diff --git a/templates/js/api-message-extension-sso/src/repairsData.json b/templates/js/api-message-extension-sso/src/repairsData.json new file mode 100644 index 0000000000..428ab008a0 --- /dev/null +++ b/templates/js/api-message-extension-sso/src/repairsData.json @@ -0,0 +1,50 @@ +[ + { + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "id": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "id": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + { + "id": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "id": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + { + "id": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..33495c4121 --- /dev/null +++ b/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=repair"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/templates/js/api-message-extension-sso/teamsapp.yml.tpl b/templates/js/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..3cbad35f50 --- /dev/null +++ b/templates/js/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,173 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/js/api-plugin-from-scratch/.vscode/launch.json b/templates/js/api-plugin-from-scratch/.vscode/launch.json index 9ad7575a0b..f5e8f96eb3 100644 --- a/templates/js/api-plugin-from-scratch/.vscode/launch.json +++ b/templates/js/api-plugin-from-scratch/.vscode/launch.json @@ -30,7 +30,7 @@ "internalConsoleOptions": "neverOpen" }, { - "name": "Preview in Teams (Edge)", + "name": "Preview in Copilot (Edge)", "type": "msedge", "request": "launch", "url": "https://teams.microsoft.com?${account-hint}", @@ -41,7 +41,7 @@ "internalConsoleOptions": "neverOpen" }, { - "name": "Preview in Teams (Chrome)", + "name": "Preview in Copilot (Chrome)", "type": "chrome", "request": "launch", "url": "https://teams.microsoft.com?${account-hint}", @@ -66,7 +66,7 @@ ], "compounds": [ { - "name": "Debug in Teams (Edge)", + "name": "Debug in Copilot (Edge)", "configurations": [ "Launch App in Teams (Edge)", "Attach to Backend" @@ -79,7 +79,7 @@ "stopAll": true }, { - "name": "Debug in Teams (Chrome)", + "name": "Debug in Copilot (Chrome)", "configurations": [ "Launch App in Teams (Chrome)", "Attach to Backend" diff --git a/templates/js/api-plugin-from-scratch/README.md b/templates/js/api-plugin-from-scratch/README.md index fe297b3904..6462dbb278 100644 --- a/templates/js/api-plugin-from-scratch/README.md +++ b/templates/js/api-plugin-from-scratch/README.md @@ -1,8 +1,8 @@ -# Overview of API Plugin from New API Template +# Overview of the Copilot Plugin template -## Build an API Plugin from a new API with Azure Functions +## Build a Copilot Plugin from a new API with Azure Functions -This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: +This app template allows Microsoft Copilot for Microsoft 365 to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: - Retrieve real-time information, for example, latest news coverage on a product launch. - Retrieve knowledge-based information, for example, my team’s design files in Figma. @@ -16,6 +16,7 @@ This app template allows Teams to interact directly with third-party data, apps, > - [Node.js](https://nodejs.org/), supported versions: 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli) +> - [Copilot for Microsoft 365 license](https://learn.microsoft.com/microsoft-365-copilot/extensibility/prerequisites#prerequisites) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. @@ -30,16 +31,17 @@ This app template allows Teams to interact directly with third-party data, apps, | `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the repair API | +| `src` | The source code for the repair API | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| -------------------------------------------- | ------------------------------------------------------------------- | -| `src/functions/repair.js` | The main file of a function in Azure Functions. | -| `src/repairsData.json` | The data source for the repair API. | -| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | -| `appPackage/ai-plugin.json` | The manifest file for the API plugin. | +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `src/functions/repair.js` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/manifest.json` | Teams application manifest that defines metadata for your plugin inside Microsoft Teams. | +| `appPackage/ai-plugin.json` | The manifest file for your Copilot Plugin that contains information for your API and used by LLM. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. diff --git a/templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json b/templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl similarity index 96% rename from templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json rename to templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl index 7154b72c7c..fb11edba12 100644 --- a/templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json +++ b/templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl @@ -1,6 +1,6 @@ { "schema_version": "v2", - "name_for_human": "Repair Search Plugin", + "name_for_human": "{{appName}}${{APP_NAME_SUFFIX}}", "description_for_human": "Track your repair records", "description_for_model": "Plugin for searching a repair list, you can search by who's assigned to the repair.", "functions": [ diff --git a/templates/js/api-plugin-from-scratch/appPackage/manifest.json.tpl b/templates/js/api-plugin-from-scratch/appPackage/manifest.json.tpl index 3ed557b7e7..9ebc549abb 100644 --- a/templates/js/api-plugin-from-scratch/appPackage/manifest.json.tpl +++ b/templates/js/api-plugin-from-scratch/appPackage/manifest.json.tpl @@ -23,7 +23,7 @@ "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." }, "accentColor": "#FFFFFF", - "apiPlugins": [ + "plugins": [ { "pluginFile": "ai-plugin.json" } diff --git a/templates/js/command-and-response/package.json.tpl b/templates/js/command-and-response/package.json.tpl index 28615e9fa0..d5d767abf4 100644 --- a/templates/js/command-and-response/package.json.tpl +++ b/templates/js/command-and-response/package.json.tpl @@ -23,7 +23,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, diff --git a/templates/js/command-and-response/teamsapp.local.yml.tpl b/templates/js/command-and-response/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/js/command-and-response/teamsapp.local.yml.tpl +++ b/templates/js/command-and-response/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/command-and-response/teamsapp.yml.tpl b/templates/js/command-and-response/teamsapp.yml.tpl index f79ac7d10d..a675d8dcc4 100644 --- a/templates/js/command-and-response/teamsapp.yml.tpl +++ b/templates/js/command-and-response/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml b/templates/js/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml index 450ea862ae..f2cc3ab0cd 100644 --- a/templates/js/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml +++ b/templates/js/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml @@ -8,10 +8,9 @@ servers: description: The repair api server components: securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: x-api-key + apiKey: + type: http + scheme: bearer paths: /repair: get: @@ -25,8 +24,8 @@ paths: schema: type: string required: false - security: - - ApiKeyAuth: [] + security: + - apiKey: [] responses: '200': description: A list of repairs diff --git a/templates/js/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl b/templates/js/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl index 6b7bc23629..2de42871af 100644 --- a/templates/js/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl +++ b/templates/js/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl @@ -50,7 +50,7 @@ "authorization": { "authType": "apiSecretServiceAuth", "apiSecretServiceAuthConfiguration": { - "apiSecretRegistrationId": "${{X_API_KEY_REGISTRATION_ID}}" + "apiSecretRegistrationId": "${{APIKEY_REGISTRATION_ID}}" } } } diff --git a/templates/js/copilot-plugin-from-scratch-api-key/src/functions/repair.js b/templates/js/copilot-plugin-from-scratch-api-key/src/functions/repair.js index bcb358d72f..eaea22b407 100644 --- a/templates/js/copilot-plugin-from-scratch-api-key/src/functions/repair.js +++ b/templates/js/copilot-plugin-from-scratch-api-key/src/functions/repair.js @@ -62,7 +62,7 @@ async function repair(req, context) { * @param req - The HTTP request. */ function isApiKeyValid(req) { - const apiKey = req.headers.get("x-api-key"); + const apiKey = req.headers.get("Authorization")?.replace("Bearer ", "").trim(); return apiKey === process.env.API_KEY; } diff --git a/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl b/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl index 3ed2e96e6f..92a108e9ee 100644 --- a/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl +++ b/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.4 +version: v1.5 provision: # Creates a Teams app @@ -25,7 +25,7 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID @@ -35,7 +35,18 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID + + # Update API KEY + - uses: apiKey/update + with: + # Name of the API Key + name: apiKey + # Teams app ID + appId: ${{TEAMS_APP_ID}} + # Path to OpenAPI description document + apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml + registrationId: ${{APIKEY_REGISTRATION_ID}} # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl b/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl index f56330ee9d..0955cc52d0 100644 --- a/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl +++ b/templates/js/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.4 +version: v1.5 environmentFolderPath: ./env @@ -46,7 +46,7 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID @@ -56,7 +56,18 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID + + # Update API KEY + - uses: apiKey/update + with: + # Name of the API Key + name: apiKey + # Teams app ID + appId: ${{TEAMS_APP_ID}} + # Path to OpenAPI description document + apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml + registrationId: ${{APIKEY_REGISTRATION_ID}} # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/js/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl b/templates/js/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl index 31f3eccff7..632d4bfd69 100644 --- a/templates/js/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl +++ b/templates/js/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl b/templates/js/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl +++ b/templates/js/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/custom-copilot-assistant-new/README.md.tpl b/templates/js/custom-copilot-assistant-new/README.md.tpl index 4942a18445..7aea8d6015 100644 --- a/templates/js/custom-copilot-assistant-new/README.md.tpl +++ b/templates/js/custom-copilot-assistant-new/README.md.tpl @@ -33,7 +33,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -48,7 +48,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/js/custom-copilot-assistant-new/teamsapp.local.yml.tpl b/templates/js/custom-copilot-assistant-new/teamsapp.local.yml.tpl index 86a5368f1f..1676b1c6f7 100644 --- a/templates/js/custom-copilot-assistant-new/teamsapp.local.yml.tpl +++ b/templates/js/custom-copilot-assistant-new/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/custom-copilot-assistant-new/teamsapp.yml.tpl b/templates/js/custom-copilot-assistant-new/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/custom-copilot-assistant-new/teamsapp.yml.tpl +++ b/templates/js/custom-copilot-assistant-new/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/custom-copilot-basic/README.md.tpl b/templates/js/custom-copilot-basic/README.md.tpl index 247f02d6c4..0cdd9abc9f 100644 --- a/templates/js/custom-copilot-basic/README.md.tpl +++ b/templates/js/custom-copilot-basic/README.md.tpl @@ -16,13 +16,13 @@ The app template is built using the Teams AI library, which provides the capabil > > To run the Basic AI Chatbot template in your local dev machine, you will need: > -> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Node.js](https://nodejs.org/), supported versions: 16, 18. {{^enableTestToolByDefault}} -> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). {{/enableTestToolByDefault}} -> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli). {{#useOpenAI}} -> - An account with [OpenAI](https://platform.openai.com/) +> - An account with [OpenAI](https://platform.openai.com/). {{/useOpenAI}} {{#useAzureOpenAI}} > - Prepare your own [Azure OpenAI](https://aka.ms/oai/access) resource. @@ -34,7 +34,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -49,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/js/custom-copilot-basic/teamsapp.local.yml.tpl b/templates/js/custom-copilot-basic/teamsapp.local.yml.tpl index 86a5368f1f..1676b1c6f7 100644 --- a/templates/js/custom-copilot-basic/teamsapp.local.yml.tpl +++ b/templates/js/custom-copilot-basic/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/custom-copilot-basic/teamsapp.yml.tpl b/templates/js/custom-copilot-basic/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/custom-copilot-basic/teamsapp.yml.tpl +++ b/templates/js/custom-copilot-basic/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/custom-copilot-rag-custom-api/.webappignore b/templates/js/custom-copilot-rag-custom-api/.webappignore index 18a015a2a3..543734d3ac 100644 --- a/templates/js/custom-copilot-rag-custom-api/.webappignore +++ b/templates/js/custom-copilot-rag-custom-api/.webappignore @@ -23,5 +23,7 @@ teamsapp.*.yml /node_modules/.bin /node_modules/ts-node /node_modules/typescript -/appPackage/ +/appPackage/build/ +/appPackage/*.png +/appPackage/manifest.json /infra/ \ No newline at end of file diff --git a/templates/js/custom-copilot-rag-custom-api/README.md.tpl b/templates/js/custom-copilot-rag-custom-api/README.md.tpl index 799675b507..6b41e671f0 100644 --- a/templates/js/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/js/custom-copilot-rag-custom-api/README.md.tpl @@ -1,20 +1,20 @@ -# Overview of the Basic AI Chatbot template +# Overview of the Custom Copilot from Custom API template -This template showcases a bot app that responds to user questions like ChatGPT. This enables your users to talk with the AI bot in Teams. +This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. - -- [Overview of the Basic AI Chatbot template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Basic AI Chatbot template](#get-started-with-the-basic-ai-chatbot-template) + +- [Overview of the Custom Copilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Copilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-basic-ai-chatbot-template-with-more-ai-capabilities) + - [Extend the Custom Copilot from Custom API template with more APIs](#extend-the-custom-copilot-from-custom-api-template-with-more-apis) - [Additional information and references](#additional-information-and-references) -## Get started with the Basic AI Chatbot template +## Get started with the Custom Copilot from Custom API template > **Prerequisites** > -> To run the Basic AI Chatbot template in your local dev machine, you will need: +> To run the Custom Copilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} @@ -34,15 +34,14 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. -1. In `src/app/app.js`, update `azureDefaultDeployment` to your own model deployment name. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT=` and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. **Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: -![ai chat bot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/81f985a1-b81d-4c27-a82a-73a9b65ece1f) {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't yet. @@ -50,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT= and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. @@ -58,7 +57,7 @@ The app template is built using the Teams AI library, which provides the capabil **Congratulations**! You are running an application that can now interact with users in Teams: -![ai chat bot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/19f4c825-c296-4d29-a957-bedb88b6aa5b) {{/enableTestToolByDefault}} ## What's included in the template @@ -67,6 +66,7 @@ The app template is built using the Teams AI library, which provides the capabil | - | - | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | +| `appPackage/apiSpecificationFile` | Generated API spec file | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | | `src` | The source code for the application | @@ -80,7 +80,9 @@ The following files can be customized and demonstrate an example implementation |`src/config.js`| Defines the environment variables.| |`src/prompts/chat/skprompt.txt`| Defines the prompt.| |`src/prompts/chat/config.json`| Configures the prompt.| -|`src/app/app.js`| Handles business logics for the Basic AI Chatbot.| +|`src.primpts/chat/actions.json`| List of available actions.| +|`src/app/app.js`| Handles business logics for the AI bot.| +|`src/app/utility.js`| Utility methods for the AI bot.| The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. @@ -90,9 +92,72 @@ The following are Teams Toolkit specific project files. You can [visit a complet |`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| |`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| -## Extend the Basic AI Chatbot template with more AI capabilities - -You can follow [Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities. +## Extend the Custom Copilot from Custom API template with more APIs + +You can follow the following steps to extend the Custom Copilot from Custom API template with more APIs. + +1. Update `./appPackage/apiSpecificationFile/openapi.*` + + Copy corresponding part of the API you want to add from your spec, and append to `./appPackage/apiSpecificationFile/openapi.*`. + +1. Update `./src/prompts/chat/actions.json` + + Fill necessary info and properties for path, query and/or body for the API in the following object, and add it in the array in `./src/prompts/chat/actions.json`. + ``` + { + "name": "${{YOUR-API-NAME}}", + "description": "${{YOUR-API-DESCRIPTION}}", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "${{YOUR-PROPERTY-NAME}}": { + "type": "${{YOUR-PROPERTY-TYPE}}", + "description": "${{YOUR-PROPERTY-DESCRIPTION}}", + } + // You can add more query properties here + } + }, + "path": { + // Same as query properties + }, + "body": { + // Same as query properties + } + } + } + } + ``` + +1. Update `./src/adaptiveCards` + + Create a new file with name `${{YOUR-API-NAME}}.json`, and fill in the adaptive card for the API response of your API. + +1. Update `./src/app/app.js` + + Add following code before `module.exports = app;`. Remember to replace necessary info. + + ``` + app.ai.action(${{YOUR-API-NAME}}, async (context: TurnContext, state: ApplicationTurnState, parameter: any) => { + const client = await api.getClient(); + + const path = client.paths[${{YOUR-API-PATH}}]; + if (path && path.${{YOUR-API-METHOD}}) { + const result = await path.${{YOUR-API-METHOD}}(parameter.path, parameter.body, { + params: parameter.query, + }); + const card = generateAdaptiveCard("../adaptiveCards/${{YOUR-API-NAME}}.json", result); + await context.sendActivity({ attachments: [card] }); + } else { + await context.sendActivity("no result"); + } + return "result"; + }); + ``` + +1. Run `Local Debug` or `Provision` and `Deploy` to run this app again. ## Additional information and references - [Teams AI library](https://aka.ms/teams-ai-library) diff --git a/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl b/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl index eaeeb233a6..81e8292e6f 100644 --- a/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl +++ b/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl @@ -24,7 +24,7 @@ "value": "${{AZURE_OPENAI_ENDPOINT}}" }, "azureOpenAIDeployment": { - "value": "${{AZURE_OPENAI_DEPLOYMENT}} + "value": "${{AZURE_OPENAI_DEPLOYMENT}}" }, {{/useAzureOpenAI}} "webAppSKU": { diff --git a/templates/js/custom-copilot-rag-custom-api/src/adapter.js b/templates/js/custom-copilot-rag-custom-api/src/adapter.js index c0929d1888..8fa2f6feb7 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/adapter.js +++ b/templates/js/custom-copilot-rag-custom-api/src/adapter.js @@ -39,8 +39,7 @@ const onTurnErrorHandler = async (context, error) => { ); // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + await context.sendActivity(`The bot encountered an error or bug: ${error.message}`); } }; diff --git a/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl b/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl index bbf0806805..40b53a6e8c 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl +++ b/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl @@ -38,7 +38,7 @@ const app = new Application({ }, }); -const generateAdaptiveCard = require("./utility.js"); +const { generateAdaptiveCard, addAuthConfig } = require("./utility.js"); const yaml = require("js-yaml"); const { OpenAPIClientAxios } = require("openapi-client-axios"); const fs = require("fs-extra"); diff --git a/templates/js/custom-copilot-rag-custom-api/src/app/utility.js b/templates/js/custom-copilot-rag-custom-api/src/app/utility.js index 7d5266edf9..74b79ba94d 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/app/utility.js +++ b/templates/js/custom-copilot-rag-custom-api/src/app/utility.js @@ -12,4 +12,29 @@ function generateAdaptiveCard(templatePath, result) { const card = CardFactory.adaptiveCard(cardContent); return card; } -module.exports = generateAdaptiveCard; + +function addAuthConfig(client) { + // This part is sample code for adding authentication to the client. + // Please replace it with your own authentication logic. + // Please refer to https://openapistack.co/docs/openapi-client-axios/intro/ for more info about the client. + /* + client.interceptors.request.use((config) => { + // You can specify different authentication methods for different urls and methods. + if (config.url == "your-url" && config.method == "your-method") { + // You can update the target url + config.url = "your-new-url"; + + // For Basic Authentication + config.headers["Authorization"] = `Basic ${btoa("Your-Username:Your-Password")}`; + + // For Cookie + config.headers["Cookie"] = `Your-Cookie`; + + // For Bearer Token + config.headers["Authorization"] = `Bearer "Your-Token"`; + } + return config; + }); + */ +} +module.exports = { generateAdaptiveCard, addAuthConfig }; diff --git a/templates/js/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl b/templates/js/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl index 4a07c05f81..267e41f9e0 100644 --- a/templates/js/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl +++ b/templates/js/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/custom-copilot-rag-custom-api/teamsapp.yml.tpl b/templates/js/custom-copilot-rag-custom-api/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/js/custom-copilot-rag-custom-api/teamsapp.yml.tpl +++ b/templates/js/custom-copilot-rag-custom-api/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/dashboard-tab/README.md b/templates/js/dashboard-tab/README.md index d93ed0a2f6..3a196dbf23 100644 --- a/templates/js/dashboard-tab/README.md +++ b/templates/js/dashboard-tab/README.md @@ -14,7 +14,7 @@ This template showcases an app that embeds a canvas containing multiple cards th > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -29,7 +29,7 @@ This template showcases an app that embeds a canvas containing multiple cards th ## What's included in the template | Folder | Contents | -| - | - | +| ------------ | --------------------------------------------------- | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | | `env` | Environment files | @@ -38,32 +38,32 @@ This template showcases an app that embeds a canvas containing multiple cards th The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | +| File | Contents | +| ------------------------------------ | --------------------------------------------------- | | `src/services/chartService.js` | A data retrieve implementation for the chart widget | | `src/services/listService.js` | A data retrieve implementation for the list widget | -| `src/dashboards/SampleDashboard.jsx` | A sample dashboard layout implementation | -| `src/styles/ChartWidget.css` | The chart widget style file | -| `src/styles/ListWidget.css` | The list widget style file | -| `src/widgets/ChartWidget.jsx` | A widget implementation that can display a chart | -| `src/widgets/ListWidget.jsx` | A widget implementation that can display a list | -| `src/App.css` | The style of application route | -| `src/App.jsx` | Application route | +| `src/dashboards/SampleDashboard.jsx` | A sample dashboard layout implementation | +| `src/styles/ChartWidget.css` | The chart widget style file | +| `src/styles/ListWidget.css` | The list widget style file | +| `src/widgets/ChartWidget.jsx` | A widget implementation that can display a chart | +| `src/widgets/ListWidget.jsx` | A widget implementation that can display a list | +| `src/App.css` | The style of application route | +| `src/App.jsx` | Application route | The following are project-related files. You generally will not need to customize these files. -| File | Contents | -| - | -| -| `src/index.css` | The style of application entry point | -| `src/index.jsx` | Application entry point | -| `src/internal/context.jsx` | TeamsFx Context | +| File | Contents | +| -------------------------- | ------------------------------------ | +| `src/index.css` | The style of application entry point | +| `src/index.jsx` | Application entry point | +| `src/internal/context.jsx` | TeamsFx Context | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the Dashboard template to add a new widget @@ -92,8 +92,8 @@ export const getSampleData = () => { Create a widget file in the `src/widgets` folder. Inherit the `BaseWidget` class from `@microsoft/teamsfx-react`. The following table lists the methods that you can override to customize your widget. -| Methods | Function | -| - | - | +| Methods | Function | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `getData()` | This method is used to get the data for the widget. You can implement it to get data from the backend service or from the Microsoft Graph API | | `header()` | Customize the content of the widget header | | `body()` | Customize the content of the widget body | diff --git a/templates/js/default-bot-message-extension/README.md b/templates/js/default-bot-message-extension/README.md index 62f3e11181..9e951d63b7 100644 --- a/templates/js/default-bot-message-extension/README.md +++ b/templates/js/default-bot-message-extension/README.md @@ -34,7 +34,8 @@ This is a simple hello world application with both Bot and Message extension cap ## Edit the manifest You can find the Teams app manifest in `./appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -42,8 +43,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEPLOYMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -87,33 +88,29 @@ This template provides some sample functionality: - You can create and send an adaptive card. - ![CreateCard](./images/AdaptiveCard.png) + ![CreateCard](https://github.com/OfficeDev/TeamsFx/assets/86260893/a0a8304b-3074-4eb8-9097-655cdda0b937) - You can share a message in an adaptive card form. - ![ShareMessage](./images/ShareMessage.png) + ![ShareMessage](https://github.com/OfficeDev/TeamsFx/assets/86260893/a7d4dd7b-6466-4e89-8f42-b93629a90bc8) - You can paste a link that "unfurls" (`.botframework.com` is monitored in this template) and a card will be rendered. - ![ComposeArea](./images/LinkUnfurlingImage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/2b155dc8-9c01-4f14-8e2f-d179b81e97c6) To trigger these functions, there are multiple entry points: -- `@mention` Your message extension, from the `search box area`. - - ![AtBotFromSearch](./images/AtBotFromSearch.png) - -- `@mention` your message extension from the `compose message area`. +- Type a `/` in the command box and select your message extension. - ![AtBotFromMessage](./images/AtBotInMessage.png) + ![AtBotFromSearch](https://github.com/OfficeDev/TeamsFx/assets/86260893/d9ee7f72-0248-4a35-ae4d-e09d447614e6) - Click the `...` under compose message area, find your message extension. - ![ComposeArea](./images/ThreeDot.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/f447f015-bb68-4ae2-9e0a-aae69c00c328) - Click the `...` next to any messages you received or sent. - ![ComposeArea](./images/ThreeDotOnMessage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/0237dc5a-8b4d-4f52-a2fb-95ad17264c90) ## Further reading diff --git a/templates/js/default-bot-message-extension/images/AdaptiveCard.png b/templates/js/default-bot-message-extension/images/AdaptiveCard.png deleted file mode 100644 index 98cfad6eef..0000000000 Binary files a/templates/js/default-bot-message-extension/images/AdaptiveCard.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/AtBotFromSearch.png b/templates/js/default-bot-message-extension/images/AtBotFromSearch.png deleted file mode 100644 index 5cf1bf5502..0000000000 Binary files a/templates/js/default-bot-message-extension/images/AtBotFromSearch.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/AtBotInMessage.png b/templates/js/default-bot-message-extension/images/AtBotInMessage.png deleted file mode 100644 index e5f8767e1f..0000000000 Binary files a/templates/js/default-bot-message-extension/images/AtBotInMessage.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/LinkUnfurlingImage.png b/templates/js/default-bot-message-extension/images/LinkUnfurlingImage.png deleted file mode 100644 index f288ff5f70..0000000000 Binary files a/templates/js/default-bot-message-extension/images/LinkUnfurlingImage.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/ShareMessage.png b/templates/js/default-bot-message-extension/images/ShareMessage.png deleted file mode 100644 index 702769abc7..0000000000 Binary files a/templates/js/default-bot-message-extension/images/ShareMessage.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/ThreeDot.png b/templates/js/default-bot-message-extension/images/ThreeDot.png deleted file mode 100644 index bbc1df4ff8..0000000000 Binary files a/templates/js/default-bot-message-extension/images/ThreeDot.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/images/ThreeDotOnMessage.png b/templates/js/default-bot-message-extension/images/ThreeDotOnMessage.png deleted file mode 100644 index f7e8c43f83..0000000000 Binary files a/templates/js/default-bot-message-extension/images/ThreeDotOnMessage.png and /dev/null differ diff --git a/templates/js/default-bot-message-extension/teamsapp.local.yml.tpl b/templates/js/default-bot-message-extension/teamsapp.local.yml.tpl index 46ef202622..c8d05a82a6 100644 --- a/templates/js/default-bot-message-extension/teamsapp.local.yml.tpl +++ b/templates/js/default-bot-message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/default-bot-message-extension/teamsapp.yml.tpl b/templates/js/default-bot-message-extension/teamsapp.yml.tpl index 507468c667..37b3b749cf 100644 --- a/templates/js/default-bot-message-extension/teamsapp.yml.tpl +++ b/templates/js/default-bot-message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/default-bot/README.md.tpl b/templates/js/default-bot/README.md.tpl index cb281837cb..2b9e7119ae 100644 --- a/templates/js/default-bot/README.md.tpl +++ b/templates/js/default-bot/README.md.tpl @@ -38,7 +38,7 @@ A bot interaction can be a quick question and answer, or it can be a complex con **Congratulations**! You are running an application that can now interact with users in Teams: -![basic bot](https://github.com/OfficeDev/TeamsFx/assets/25220706/8f5645ed-1cd9-43fd-9513-b0c9697d7dc0) +![basic bot](https://github.com/OfficeDev/TeamsFx/assets/25220706/170096d2-b353-4d4e-b55a-2c8ae4d97514) {{/enableTestToolByDefault}} ## What's included in the template @@ -79,5 +79,5 @@ Following documentation will help you to extend the Basic Bot template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/js/default-bot/index.js b/templates/js/default-bot/index.js index e390545db1..aab0bf9e4e 100644 --- a/templates/js/default-bot/index.js +++ b/templates/js/default-bot/index.js @@ -35,9 +35,12 @@ adapter.onTurnError = async (context, error) => { // configuration instructions. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a message to the user - await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a message to the user + await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Create the bot that will handle incoming messages. diff --git a/templates/js/default-bot/teamsapp.local.yml.tpl b/templates/js/default-bot/teamsapp.local.yml.tpl index 46ef202622..c8d05a82a6 100644 --- a/templates/js/default-bot/teamsapp.local.yml.tpl +++ b/templates/js/default-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/default-bot/teamsapp.yml.tpl b/templates/js/default-bot/teamsapp.yml.tpl index 507468c667..37b3b749cf 100644 --- a/templates/js/default-bot/teamsapp.yml.tpl +++ b/templates/js/default-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/link-unfurling/.gitignore b/templates/js/link-unfurling/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/js/link-unfurling/.gitignore +++ b/templates/js/link-unfurling/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/js/link-unfurling/.localConfigs.testTool b/templates/js/link-unfurling/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/js/link-unfurling/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/ts/message-extension/.vscode/launch.json b/templates/js/link-unfurling/.vscode/launch.json.tpl similarity index 91% rename from templates/ts/message-extension/.vscode/launch.json rename to templates/js/link-unfurling/.vscode/launch.json.tpl index 5b26f736e0..a729ee63f8 100644 --- a/templates/ts/message-extension/.vscode/launch.json +++ b/templates/js/link-unfurling/.vscode/launch.json.tpl @@ -115,6 +115,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ diff --git a/templates/js/link-unfurling/.vscode/tasks.json b/templates/js/link-unfurling/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/js/link-unfurling/.vscode/tasks.json +++ b/templates/js/link-unfurling/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/js/link-unfurling/.webappignore b/templates/js/link-unfurling/.webappignore index 2ab9014ed9..a6ef2018df 100644 --- a/templates/js/link-unfurling/.webappignore +++ b/templates/js/link-unfurling/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/js/link-unfurling/README.md b/templates/js/link-unfurling/README.md index 616aa1d056..f5c123dec8 100644 --- a/templates/js/link-unfurling/README.md +++ b/templates/js/link-unfurling/README.md @@ -20,22 +20,22 @@ This template showcases an app that unfurls a link into an adaptive card when UR ## What's included in the template -| Folder / File | Contents | -| - | - | -| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | -| `teamsapp.local.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging | -| `.vscode/` | VSCode files for local debug | -| `src/` | The source code for the link unfurling application | -| `appPackage/` | Templates for the Teams application manifest | -| `infra/` | Templates for provisioning Azure resources | +| Folder / File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging | +| `.vscode/` | VSCode files for local debug | +| `src/` | The source code for the link unfurling application | +| `appPackage/` | Templates for the Teams application manifest | +| `infra/` | Templates for provisioning Azure resources | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -| `src/index.js` | Application entry point and `restify` handlers | -| `src/linkUnfurlingApp.js`| The teams activity handler | -| `src/adaptiveCards/helloWorldCard.json` | The adaptive card | +| File | Contents | +| --------------------------------------- | ---------------------------------------------- | +| `src/index.js` | Application entry point and `restify` handlers | +| `src/linkUnfurlingApp.js` | The teams activity handler | +| `src/adaptiveCards/helloWorldCard.json` | The adaptive card | ## Extend this template diff --git a/templates/js/link-unfurling/env/.env.testtool b/templates/js/link-unfurling/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/js/link-unfurling/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/js/link-unfurling/package.json.tpl b/templates/js/link-unfurling/package.json.tpl index 83288adcc7..f71a4912e2 100644 --- a/templates/js/link-unfurling/package.json.tpl +++ b/templates/js/link-unfurling/package.json.tpl @@ -10,6 +10,8 @@ "main": "./src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT ./src/index.js", "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1", diff --git a/templates/js/link-unfurling/teamsapp.local.yml.tpl b/templates/js/link-unfurling/teamsapp.local.yml.tpl index 3cff312998..7698aba333 100644 --- a/templates/js/link-unfurling/teamsapp.local.yml.tpl +++ b/templates/js/link-unfurling/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/link-unfurling/teamsapp.testtool.yml b/templates/js/link-unfurling/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/js/link-unfurling/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/js/link-unfurling/teamsapp.yml.tpl b/templates/js/link-unfurling/teamsapp.yml.tpl index bab0488770..f889c67b70 100644 --- a/templates/js/link-unfurling/teamsapp.yml.tpl +++ b/templates/js/link-unfurling/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/m365-message-extension/.gitignore b/templates/js/m365-message-extension/.gitignore index e567799519..04227ab59b 100644 --- a/templates/js/m365-message-extension/.gitignore +++ b/templates/js/m365-message-extension/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/js/m365-message-extension/.localConfigs.testTool b/templates/js/m365-message-extension/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/js/m365-message-extension/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/js/message-extension/.vscode/launch.json b/templates/js/m365-message-extension/.vscode/launch.json.tpl similarity index 91% rename from templates/js/message-extension/.vscode/launch.json rename to templates/js/m365-message-extension/.vscode/launch.json.tpl index 5b26f736e0..a729ee63f8 100644 --- a/templates/js/message-extension/.vscode/launch.json +++ b/templates/js/m365-message-extension/.vscode/launch.json.tpl @@ -115,6 +115,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ diff --git a/templates/js/m365-message-extension/.vscode/tasks.json b/templates/js/m365-message-extension/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/js/m365-message-extension/.vscode/tasks.json +++ b/templates/js/m365-message-extension/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/js/m365-message-extension/.webappignore b/templates/js/m365-message-extension/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/js/m365-message-extension/.webappignore +++ b/templates/js/m365-message-extension/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/js/m365-message-extension/README.md b/templates/js/m365-message-extension/README.md index d90a2ab2e0..c609776bea 100644 --- a/templates/js/m365-message-extension/README.md +++ b/templates/js/m365-message-extension/README.md @@ -11,7 +11,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -19,36 +19,36 @@ This app template is a search-based [message extension](https://docs.microsoft.c 3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. 5. To trigger the Message Extension, you can: - 1. In Teams: `@mention` Your message extension from the `search box area`, `@mention` your message extension from the `compose message area` or click the `...` under compose message area to find your message extension. + 1. In Teams: Click the `...` under compose message area to find your message extension. 2. In Outlook: click the `More apps` icon under compose email area to find your message extension. **Congratulations**! You are running an application that can now search npm registries in Teams and Outlook. -![Search app demo](https://user-images.githubusercontent.com/11220663/167868361-40ffaaa3-0300-4313-ae22-0f0bab49c329.png) +![Search app demo](https://github.com/OfficeDev/TeamsFx/assets/25220706/27fefae9-c51f-49af-a175-c8c9d5a71af0) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode/` | VSCode files for debugging | -| `appPackage/` | Templates for the Teams application manifest | -| `env/` | Environment files | -| `infra/` | Templates for provisioning Azure resources | -| `src/` | The source code for the search application | +| Folder | Contents | +| ------------- | -------------------------------------------- | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `env/` | Environment files | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the search application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/searchApp.js`| Handles the business logic for this app template to query npm registry and return result list.| -|`src/index.js`| `index.js` is used to setup and configure the Message Extension.| +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------------------- | +| `src/searchApp.js` | Handles the business logic for this app template to query npm registry and return result list. | +| `src/index.js` | `index.js` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -64,5 +64,5 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/js/m365-message-extension/env/.env.testtool b/templates/js/m365-message-extension/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/js/m365-message-extension/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/js/m365-message-extension/package.json.tpl b/templates/js/m365-message-extension/package.json.tpl index 11c9eb593c..0f7221863a 100644 --- a/templates/js/m365-message-extension/package.json.tpl +++ b/templates/js/m365-message-extension/package.json.tpl @@ -13,6 +13,8 @@ "main": "./src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --inspect=9239 --signal SIGINT ./src/index.js", "start": "node ./src/index.js", "watch": "nodemon ./src/index.js" diff --git a/templates/js/m365-message-extension/teamsapp.local.yml.tpl b/templates/js/m365-message-extension/teamsapp.local.yml.tpl index b3d8de2a1e..ede659a783 100644 --- a/templates/js/m365-message-extension/teamsapp.local.yml.tpl +++ b/templates/js/m365-message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/m365-message-extension/teamsapp.testtool.yml b/templates/js/m365-message-extension/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/js/m365-message-extension/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/js/m365-message-extension/teamsapp.yml.tpl b/templates/js/m365-message-extension/teamsapp.yml.tpl index 823ced54a9..b634e1bf99 100644 --- a/templates/js/m365-message-extension/teamsapp.yml.tpl +++ b/templates/js/m365-message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/message-extension-action/.gitignore b/templates/js/message-extension-action/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/js/message-extension-action/.gitignore +++ b/templates/js/message-extension-action/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/js/message-extension-action/.localConfigs.testTool b/templates/js/message-extension-action/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/js/message-extension-action/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/ts/message-extension-action/.vscode/launch.json b/templates/js/message-extension-action/.vscode/launch.json.tpl similarity index 84% rename from templates/ts/message-extension-action/.vscode/launch.json rename to templates/js/message-extension-action/.vscode/launch.json.tpl index 030f8cd628..a0369287c8 100644 --- a/templates/ts/message-extension-action/.vscode/launch.json +++ b/templates/js/message-extension-action/.vscode/launch.json.tpl @@ -65,6 +65,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ diff --git a/templates/js/message-extension-action/.vscode/tasks.json b/templates/js/message-extension-action/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/js/message-extension-action/.vscode/tasks.json +++ b/templates/js/message-extension-action/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/js/message-extension-action/.webappignore b/templates/js/message-extension-action/.webappignore index 2ab9014ed9..a6ef2018df 100644 --- a/templates/js/message-extension-action/.webappignore +++ b/templates/js/message-extension-action/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/js/message-extension-action/README.md b/templates/js/message-extension-action/README.md index 6741d62576..5ab23e8e25 100644 --- a/templates/js/message-extension-action/README.md +++ b/templates/js/message-extension-action/README.md @@ -18,35 +18,35 @@ This app template implements action command that allows you to present your user 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. 3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. -5. To trigger the action command, you can click the `...` under compose message area, click `...`-> `More actions` beside a message, or @ your message extension app from the command box. +5. To trigger the action command, you can click the `...` under compose message area to find your message extension. **Congratulations**! You are running an application that can share information in rich format by creating an Adaptive Card in Teams. -![action-ME](https://github.com/OfficeDev/TeamsFx/assets/11220663/4af867b1-0b4b-4665-ac43-badf56106d84) +![action-ME](https://github.com/OfficeDev/TeamsFx/assets/25220706/378ea4d7-9332-4aec-9f85-59891d086b80) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode/` | VSCode files for debugging | -| `appPackage/` | Templates for the Teams application manifest | -| `env/` | Environment files | -| `infra/` | Templates for provisioning Azure resources | -| `src/` | The source code for the action application | +| Folder | Contents | +| ------------- | -------------------------------------------- | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `env/` | Environment files | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the action application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/actionApp.js`| Handles the business logic for this app template to collect form input and process data.| -|`src/index.js`| `index.js` is used to setup and configure the Message Extension.| +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------------- | +| `src/actionApp.js` | Handles the business logic for this app template to collect form input and process data. | +| `src/index.js` | `index.js` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -62,5 +62,6 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) -- [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) \ No newline at end of file +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) +- [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) + diff --git a/templates/js/message-extension-action/env/.env.testtool b/templates/js/message-extension-action/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/js/message-extension-action/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/js/message-extension-action/package.json.tpl b/templates/js/message-extension-action/package.json.tpl index 8cb916b177..285eaf703b 100644 --- a/templates/js/message-extension-action/package.json.tpl +++ b/templates/js/message-extension-action/package.json.tpl @@ -10,6 +10,8 @@ "main": "./src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT ./src/index.js", "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1", diff --git a/templates/js/message-extension-action/teamsapp.local.yml.tpl b/templates/js/message-extension-action/teamsapp.local.yml.tpl index 68663eafc7..4be79436a7 100644 --- a/templates/js/message-extension-action/teamsapp.local.yml.tpl +++ b/templates/js/message-extension-action/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/message-extension-action/teamsapp.testtool.yml b/templates/js/message-extension-action/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/js/message-extension-action/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/js/message-extension-action/teamsapp.yml.tpl b/templates/js/message-extension-action/teamsapp.yml.tpl index a8d4a94100..3d1aa00ebb 100644 --- a/templates/js/message-extension-action/teamsapp.yml.tpl +++ b/templates/js/message-extension-action/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/message-extension-copilot/README.md b/templates/js/message-extension-copilot/README.md index d9eb78292e..58327ac2c4 100644 --- a/templates/js/message-extension-copilot/README.md +++ b/templates/js/message-extension-copilot/README.md @@ -24,7 +24,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c 4. To trigger the Message Extension through Copilot, you can: 1. Select `Debug in Copilot (Edge)` or `Debug in Copilot (Chrome)` from the launch configuration dropdown. 2. When Teams launches in the browser, click the `Apps` icon from Teams client left rail to open Teams app store and search for `Copilot`. - 3. Open the `Copilot` app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NPM package information. For example: `Find the npm package info on teamsfx-react`. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. @@ -70,6 +70,6 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) - [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) diff --git a/templates/js/message-extension-copilot/teamsapp.local.yml.tpl b/templates/js/message-extension-copilot/teamsapp.local.yml.tpl index b3d8de2a1e..ede659a783 100644 --- a/templates/js/message-extension-copilot/teamsapp.local.yml.tpl +++ b/templates/js/message-extension-copilot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/message-extension-copilot/teamsapp.yml.tpl b/templates/js/message-extension-copilot/teamsapp.yml.tpl index 823ced54a9..b634e1bf99 100644 --- a/templates/js/message-extension-copilot/teamsapp.yml.tpl +++ b/templates/js/message-extension-copilot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/message-extension/.gitignore b/templates/js/message-extension/.gitignore index e567799519..04227ab59b 100644 --- a/templates/js/message-extension/.gitignore +++ b/templates/js/message-extension/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/js/message-extension/.localConfigs.testTool b/templates/js/message-extension/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/js/message-extension/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/js/link-unfurling/.vscode/launch.json b/templates/js/message-extension/.vscode/launch.json.tpl similarity index 90% rename from templates/js/link-unfurling/.vscode/launch.json rename to templates/js/message-extension/.vscode/launch.json.tpl index 6a87b6a80d..a729ee63f8 100644 --- a/templates/js/link-unfurling/.vscode/launch.json +++ b/templates/js/message-extension/.vscode/launch.json.tpl @@ -115,6 +115,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ @@ -168,4 +185,4 @@ "stopAll": true } ] -} +} \ No newline at end of file diff --git a/templates/js/message-extension/.vscode/tasks.json b/templates/js/message-extension/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/js/message-extension/.vscode/tasks.json +++ b/templates/js/message-extension/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/js/message-extension/.webappignore b/templates/js/message-extension/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/js/message-extension/.webappignore +++ b/templates/js/message-extension/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/js/message-extension/README.md b/templates/js/message-extension/README.md index b6c8fcfc54..f801d9eb18 100644 --- a/templates/js/message-extension/README.md +++ b/templates/js/message-extension/README.md @@ -3,6 +3,7 @@ A Message Extension allows users to interact with your web service while composing messages in the Microsoft Teams client. Users can invoke your web service to assist message composition, from the message compose box, or from the search bar. This app template has a search command, an action command and a link unfurling. + 1. The search command allows users to search an external system and share results through the compose message area of the Microsoft Teams client. 2. The action command allows you to present your users with a modal pop-up called a task module in Teams. The task module collects or displays information, processes the interaction, and sends the information back to Teams. 3. With link unfurling, an app can unfurl a link into an adaptive card when URLs with a particular domain are pasted into the compose message area in Microsoft Teams or email body in Outlook. @@ -26,31 +27,30 @@ This app template has a search command, an action command and a link unfurling. 2. In Outlook: click the `More apps` icon under compose email area to find your message extension. Only search command and link unfurling works in Outlook. 6. Paste a link ending with `.botframework.com` into compose message area in Teams or email body in Outlook. You should see an adaptive card unfurled. -![Search app demo](https://user-images.githubusercontent.com/11220663/167868361-40ffaaa3-0300-4313-ae22-0f0bab49c329.png) -![action-ME](https://github.com/OfficeDev/TeamsFx/assets/11220663/4af867b1-0b4b-4665-ac43-badf56106d84) +![action-ME](https://github.com/OfficeDev/TeamsFx/assets/25220706/378ea4d7-9332-4aec-9f85-59891d086b80) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/teamsBot.js`| Handles the business logic for this app template to query npm registry and return result list to Teams.| -|`src/index.js`| `index.js` is used to setup and configure the Message Extension.| +| File | Contents | +| ----------------- | ------------------------------------------------------------------------------------------------------- | +| `src/teamsBot.js` | Handles the business logic for this app template to query npm registry and return result list to Teams. | +| `src/index.js` | `index.js` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -66,5 +66,5 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/js/message-extension/env/.env.testtool b/templates/js/message-extension/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/js/message-extension/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/js/message-extension/package.json.tpl b/templates/js/message-extension/package.json.tpl index 4f65975f6e..bdff1a09c5 100644 --- a/templates/js/message-extension/package.json.tpl +++ b/templates/js/message-extension/package.json.tpl @@ -13,6 +13,8 @@ "main": "./src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --inspect=9239 --signal SIGINT ./src/index.js", "start": "node .src/index.js", "watch": "nodemon ./src/index.js", @@ -30,4 +32,4 @@ "env-cmd": "^10.1.0", "nodemon": "^2.0.7" } -} +} \ No newline at end of file diff --git a/templates/js/message-extension/teamsapp.local.yml.tpl b/templates/js/message-extension/teamsapp.local.yml.tpl index 3cff312998..7698aba333 100644 --- a/templates/js/message-extension/teamsapp.local.yml.tpl +++ b/templates/js/message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/message-extension/teamsapp.testtool.yml b/templates/js/message-extension/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/js/message-extension/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/js/message-extension/teamsapp.yml.tpl b/templates/js/message-extension/teamsapp.yml.tpl index 823ced54a9..b634e1bf99 100644 --- a/templates/js/message-extension/teamsapp.yml.tpl +++ b/templates/js/message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/non-sso-tab-default-bot/bot/README.md b/templates/js/non-sso-tab-default-bot/bot/README.md index 65dc22570a..907729ae43 100644 --- a/templates/js/non-sso-tab-default-bot/bot/README.md +++ b/templates/js/non-sso-tab-default-bot/bot/README.md @@ -34,7 +34,8 @@ This is a simple hello world application with both Bot and Message extension cap ## Edit the manifest You can find the Teams app manifest in `../appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -42,8 +43,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEPLOYMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -87,33 +88,29 @@ This template provides some sample functionality: - You can create and send an adaptive card. - ![CreateCard](./images/AdaptiveCard.png) + ![CreateCard](https://github.com/OfficeDev/TeamsFx/assets/86260893/a0a8304b-3074-4eb8-9097-655cdda0b937) - You can share a message in an adaptive card form. - ![ShareMessage](./images/ShareMessage.png) + ![ShareMessage](https://github.com/OfficeDev/TeamsFx/assets/86260893/a7d4dd7b-6466-4e89-8f42-b93629a90bc8) - You can paste a link that "unfurls" (`.botframework.com` is monitored in this template) and a card will be rendered. - ![ComposeArea](./images/LinkUnfurlingImage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/2b155dc8-9c01-4f14-8e2f-d179b81e97c6) To trigger these functions, there are multiple entry points: -- `@mention` Your message extension, from the `search box area`. - - ![AtBotFromSearch](./images/AtBotFromSearch.png) +- Type a `/` in the command box and select your message extension. -- `@mention` your message extension from the `compose message area`. - - ![AtBotFromMessage](./images/AtBotInMessage.png) + ![AtBotFromSearch](https://github.com/OfficeDev/TeamsFx/assets/86260893/d9ee7f72-0248-4a35-ae4d-e09d447614e6) - Click the `...` under compose message area, find your message extension. - ![ComposeArea](./images/ThreeDot.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/f447f015-bb68-4ae2-9e0a-aae69c00c328) - Click the `...` next to any messages you received or sent. - ![ComposeArea](./images/ThreeDotOnMessage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/0237dc5a-8b4d-4f52-a2fb-95ad17264c90) ## Further reading @@ -127,4 +124,5 @@ To trigger these functions, there are multiple entry points: - [Search Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) - [Action Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) -- [Link Unfurling](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet) \ No newline at end of file +- [Link Unfurling](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet) + diff --git a/templates/js/non-sso-tab-default-bot/bot/images/AdaptiveCard.png b/templates/js/non-sso-tab-default-bot/bot/images/AdaptiveCard.png deleted file mode 100644 index 98cfad6eef..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/AdaptiveCard.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png b/templates/js/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png deleted file mode 100644 index 5cf1bf5502..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/AtBotInMessage.png b/templates/js/non-sso-tab-default-bot/bot/images/AtBotInMessage.png deleted file mode 100644 index e5f8767e1f..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/AtBotInMessage.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png b/templates/js/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png deleted file mode 100644 index f288ff5f70..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/ShareMessage.png b/templates/js/non-sso-tab-default-bot/bot/images/ShareMessage.png deleted file mode 100644 index 702769abc7..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/ShareMessage.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/ThreeDot.png b/templates/js/non-sso-tab-default-bot/bot/images/ThreeDot.png deleted file mode 100644 index bbc1df4ff8..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/ThreeDot.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png b/templates/js/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png deleted file mode 100644 index f7e8c43f83..0000000000 Binary files a/templates/js/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png and /dev/null differ diff --git a/templates/js/non-sso-tab-default-bot/bot/index.js b/templates/js/non-sso-tab-default-bot/bot/index.js index e390545db1..aab0bf9e4e 100644 --- a/templates/js/non-sso-tab-default-bot/bot/index.js +++ b/templates/js/non-sso-tab-default-bot/bot/index.js @@ -35,9 +35,12 @@ adapter.onTurnError = async (context, error) => { // configuration instructions. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a message to the user - await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a message to the user + await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Create the bot that will handle incoming messages. diff --git a/templates/js/non-sso-tab-default-bot/tab/README.md b/templates/js/non-sso-tab-default-bot/tab/README.md index 19ed3778a9..ec72151097 100644 --- a/templates/js/non-sso-tab-default-bot/tab/README.md +++ b/templates/js/non-sso-tab-default-bot/tab/README.md @@ -20,7 +20,8 @@ Microsoft Teams supports the ability to run web-based UI inside "custom tabs" th ## Edit the manifest You can find the Teams app manifest in `../appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -28,8 +29,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEVELOPMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -69,4 +70,5 @@ Once deployed, you may want to distribute your application to your organization' Microsoft Teams provides a mechanism by which an application can obtain the signed-in Teams user token to access Microsoft Graph (and other APIs). Teams Toolkit facilitates this interaction by abstracting some of the Microsoft Entra flows and integrations behind some simple, high-level APIs. This enables you to add single sign-on (SSO) features easily to your Teams application. -Please follow this [document](https://aka.ms/teamsfx-add-sso-new) to add single sign on for your project. \ No newline at end of file +Please follow this [document](https://aka.ms/teamsfx-add-sso-new) to add single sign on for your project. + diff --git a/templates/js/non-sso-tab-default-bot/teamsapp.local.yml.tpl b/templates/js/non-sso-tab-default-bot/teamsapp.local.yml.tpl index 75b43a8807..b755aba0c0 100644 --- a/templates/js/non-sso-tab-default-bot/teamsapp.local.yml.tpl +++ b/templates/js/non-sso-tab-default-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/non-sso-tab-default-bot/teamsapp.yml.tpl b/templates/js/non-sso-tab-default-bot/teamsapp.yml.tpl index f4797c559a..2b6ebf3725 100644 --- a/templates/js/non-sso-tab-default-bot/teamsapp.yml.tpl +++ b/templates/js/non-sso-tab-default-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/non-sso-tab/README.md b/templates/js/non-sso-tab/README.md index b646ea402e..3179d83bb1 100644 --- a/templates/js/non-sso-tab/README.md +++ b/templates/js/non-sso-tab/README.md @@ -11,7 +11,7 @@ This template showcases how Microsoft Teams supports the ability to run web-base > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -25,29 +25,29 @@ This template showcases how Microsoft Teams supports the ability to run web-base ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the Teams application | +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the Teams application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/static/scripts/teamsapp.js`|A script that calls `teamsjs` SDK to get the context of on which Microsoft 365 application your app is running.| -|`src/static/styles/custom.css`|css file for the app.| -|`src/static/views/hello.html`|html file for the app.| -|`src/app.js`|Starting a restify server.| +| File | Contents | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `src/static/scripts/teamsapp.js` | A script that calls `teamsjs` SDK to get the context of on which Microsoft 365 application your app is running. | +| `src/static/styles/custom.css` | css file for the app. | +| `src/static/views/hello.html` | html file for the app. | +| `src/app.js` | Starting a restify server. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the Basic Tab template diff --git a/templates/js/notification-http-timer-trigger/package.json.tpl b/templates/js/notification-http-timer-trigger/package.json.tpl index ad136a8ea1..4accd23a28 100644 --- a/templates/js/notification-http-timer-trigger/package.json.tpl +++ b/templates/js/notification-http-timer-trigger/package.json.tpl @@ -22,11 +22,11 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { "azurite": "^3.16.0", "env-cmd": "^10.1.0" } -} \ No newline at end of file +} diff --git a/templates/js/notification-http-timer-trigger/teamsapp.local.yml.tpl b/templates/js/notification-http-timer-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/js/notification-http-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/js/notification-http-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/notification-http-timer-trigger/teamsapp.yml.tpl b/templates/js/notification-http-timer-trigger/teamsapp.yml.tpl index 3189607aef..fcfcefa389 100644 --- a/templates/js/notification-http-timer-trigger/teamsapp.yml.tpl +++ b/templates/js/notification-http-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/notification-http-trigger/package.json.tpl b/templates/js/notification-http-trigger/package.json.tpl index ad136a8ea1..4accd23a28 100644 --- a/templates/js/notification-http-trigger/package.json.tpl +++ b/templates/js/notification-http-trigger/package.json.tpl @@ -22,11 +22,11 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { "azurite": "^3.16.0", "env-cmd": "^10.1.0" } -} \ No newline at end of file +} diff --git a/templates/js/notification-http-trigger/teamsapp.local.yml.tpl b/templates/js/notification-http-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/js/notification-http-trigger/teamsapp.local.yml.tpl +++ b/templates/js/notification-http-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/notification-http-trigger/teamsapp.yml.tpl b/templates/js/notification-http-trigger/teamsapp.yml.tpl index 3189607aef..fcfcefa389 100644 --- a/templates/js/notification-http-trigger/teamsapp.yml.tpl +++ b/templates/js/notification-http-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/notification-restify/package.json.tpl b/templates/js/notification-restify/package.json.tpl index c1a329f7be..9bfe0158a0 100644 --- a/templates/js/notification-restify/package.json.tpl +++ b/templates/js/notification-restify/package.json.tpl @@ -23,7 +23,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, @@ -31,4 +31,4 @@ "nodemon": "^2.0.7", "env-cmd": "^10.1.0" } -} \ No newline at end of file +} diff --git a/templates/js/notification-restify/teamsapp.local.yml.tpl b/templates/js/notification-restify/teamsapp.local.yml.tpl index eed6751121..9e9efa6acb 100644 --- a/templates/js/notification-restify/teamsapp.local.yml.tpl +++ b/templates/js/notification-restify/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/notification-restify/teamsapp.yml.tpl b/templates/js/notification-restify/teamsapp.yml.tpl index e37a3d1e07..d690369df8 100644 --- a/templates/js/notification-restify/teamsapp.yml.tpl +++ b/templates/js/notification-restify/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/notification-timer-trigger/package.json.tpl b/templates/js/notification-timer-trigger/package.json.tpl index ad136a8ea1..4accd23a28 100644 --- a/templates/js/notification-timer-trigger/package.json.tpl +++ b/templates/js/notification-timer-trigger/package.json.tpl @@ -22,11 +22,11 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { "azurite": "^3.16.0", "env-cmd": "^10.1.0" } -} \ No newline at end of file +} diff --git a/templates/js/notification-timer-trigger/teamsapp.local.yml.tpl b/templates/js/notification-timer-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/js/notification-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/js/notification-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/notification-timer-trigger/teamsapp.yml.tpl b/templates/js/notification-timer-trigger/teamsapp.yml.tpl index 3189607aef..fcfcefa389 100644 --- a/templates/js/notification-timer-trigger/teamsapp.yml.tpl +++ b/templates/js/notification-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/js/office-json-addin/env/.env.dev b/templates/js/office-json-addin/env/.env.dev index 8043fefee4..e25ded0f91 100644 --- a/templates/js/office-json-addin/env/.env.dev +++ b/templates/js/office-json-addin/env/.env.dev @@ -10,6 +10,6 @@ AZURE_RESOURCE_GROUP_NAME= RESOURCE_SUFFIX= # Generated during provision, you can also add your own variables. -AZURE_STATIC_WEB_APPS_RESOURCE_ID= +ADDIN_AZURE_STORAGE_RESOURCE_ID= ADDIN_DOMAIN= ADDIN_ENDPOINT= \ No newline at end of file diff --git a/templates/js/office-json-addin/infra/azure.bicep b/templates/js/office-json-addin/infra/azure.bicep index 72c2af26df..4876fd8c94 100644 --- a/templates/js/office-json-addin/infra/azure.bicep +++ b/templates/js/office-json-addin/infra/azure.bicep @@ -1,25 +1,27 @@ @maxLength(20) @minLength(4) param resourceBaseName string -param staticWebAppSku string +param storageSku string -param staticWebAppName string = resourceBaseName +param storageName string = resourceBaseName +param location string = resourceGroup().location -// Azure Static Web Apps that hosts your static web site -resource swa 'Microsoft.Web/staticSites@2022-09-01' = { - name: staticWebAppName - // SWA do not need location setting - location: 'centralus' +// Azure Storage that hosts your static web site +resource storage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + kind: 'StorageV2' + location: location + name: storageName + properties: { + supportsHttpsTrafficOnly: true + } sku: { - name: staticWebAppSku - tier: staticWebAppSku + name: storageSku } - properties:{} } -var siteDomain = swa.properties.defaultHostname +var siteDomain = replace(replace(storage.properties.primaryEndpoints.web, 'https://', ''), '/', '') // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. -output AZURE_STATIC_WEB_APPS_RESOURCE_ID string = swa.id +output ADDIN_AZURE_STORAGE_RESOURCE_ID string = storage.id // used in deploy stage output ADDIN_DOMAIN string = siteDomain output ADDIN_ENDPOINT string = 'https://${siteDomain}' diff --git a/templates/js/office-json-addin/infra/azure.parameters.json b/templates/js/office-json-addin/infra/azure.parameters.json index 0a6927bc1b..585e718632 100644 --- a/templates/js/office-json-addin/infra/azure.parameters.json +++ b/templates/js/office-json-addin/infra/azure.parameters.json @@ -1,12 +1,12 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "tab${{RESOURCE_SUFFIX}}" - }, - "staticWebAppSku": { - "value": "Free" - } + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "tab${{RESOURCE_SUFFIX}}" + }, + "storageSku": { + "value": "Standard_LRS" } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/templates/js/office-json-addin/teamsapp.yml b/templates/js/office-json-addin/teamsapp.yml index 21ab335502..5e542ad089 100644 --- a/templates/js/office-json-addin/teamsapp.yml +++ b/templates/js/office-json-addin/teamsapp.yml @@ -51,8 +51,14 @@ deploy: name: build app with: args: run build --if-present - # Deploy bits to Azure Static Web Apps - - uses: cli/runNpxCommand - name: deploy to Azure Static Web Apps + # Deploy bits to Azure Storage Static Website + - uses: azureStorage/deploy with: - args: '@azure/static-web-apps-cli deploy dist -d ${{SECRET_TAB_SWA_DEPLOYMENT_TOKEN}} --env production' + workingDirectory: . + # Deploy base folder + artifactFolder: dist + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{ADDIN_AZURE_STORAGE_RESOURCE_ID}} \ No newline at end of file diff --git a/templates/js/office-xml-addin-excel-cf/README.md b/templates/js/office-xml-addin-excel-cf/README.md index e283dc8add..47eaf9fe27 100644 --- a/templates/js/office-xml-addin-excel-cf/README.md +++ b/templates/js/office-xml-addin-excel-cf/README.md @@ -16,10 +16,14 @@ You can use this repository as a sample to base your own custom functions projec ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. ## Debugging custom functions @@ -35,27 +39,18 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` ## Additional resources - [Custom functions overview](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-overview) -- [Custom functions best practices](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-best-practices) - [Custom functions runtime](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-runtime) +- [Custom functions troubleshoot](https://learn.microsoft.com/en-us/office/dev/add-ins/excel/custom-functions-troubleshooting) - [Office Add-ins documentation](https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins) - More Office Add-ins samples at [OfficeDev on Github](https://github.com/officedev) diff --git a/templates/js/office-xml-addin-excel-manifest-only/README.md b/templates/js/office-xml-addin-excel-manifest-only/README.md index 0124f1035a..27698aed88 100644 --- a/templates/js/office-xml-addin-excel-manifest-only/README.md +++ b/templates/js/office-xml-addin-excel-manifest-only/README.md @@ -13,18 +13,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./manifest.xml` file in the root directory of the project defines the settings and capabilities of the add-in. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-excel-manifest-only/manifest.xml b/templates/js/office-xml-addin-excel-manifest-only/manifest.xml index e375c103e5..8b774a70c4 100644 --- a/templates/js/office-xml-addin-excel-manifest-only/manifest.xml +++ b/templates/js/office-xml-addin-excel-manifest-only/manifest.xml @@ -24,7 +24,7 @@ - + diff --git a/templates/js/office-xml-addin-excel-react/README.md b/templates/js/office-xml-addin-excel-react/README.md index f34ea3311e..9627397bc5 100644 --- a/templates/js/office-xml-addin-excel-react/README.md +++ b/templates/js/office-xml-addin-excel-react/README.md @@ -9,10 +9,14 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.jsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-excel-sso/README.md b/templates/js/office-xml-addin-excel-sso/README.md index 5f0e54120b..55cfdddb3c 100644 --- a/templates/js/office-xml-addin-excel-sso/README.md +++ b/templates/js/office-xml-addin-excel-sso/README.md @@ -9,6 +9,10 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-excel-taskpane/README.md b/templates/js/office-xml-addin-excel-taskpane/README.md index 85aeea1256..bb25930969 100644 --- a/templates/js/office-xml-addin-excel-taskpane/README.md +++ b/templates/js/office-xml-addin-excel-taskpane/README.md @@ -9,10 +9,14 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-powerpoint-manifest-only/README.md b/templates/js/office-xml-addin-powerpoint-manifest-only/README.md index fc625d0122..d39b66f7c7 100644 --- a/templates/js/office-xml-addin-powerpoint-manifest-only/README.md +++ b/templates/js/office-xml-addin-powerpoint-manifest-only/README.md @@ -13,18 +13,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./manifest.xml` file in the root directory of the project defines the settings and capabilities of the add-in. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-powerpoint-manifest-only/manifest.xml b/templates/js/office-xml-addin-powerpoint-manifest-only/manifest.xml index a8cab26cb8..c13162db53 100644 --- a/templates/js/office-xml-addin-powerpoint-manifest-only/manifest.xml +++ b/templates/js/office-xml-addin-powerpoint-manifest-only/manifest.xml @@ -24,7 +24,7 @@ - + diff --git a/templates/js/office-xml-addin-powerpoint-react/README.md b/templates/js/office-xml-addin-powerpoint-react/README.md index 197ef19017..49f5de3352 100644 --- a/templates/js/office-xml-addin-powerpoint-react/README.md +++ b/templates/js/office-xml-addin-powerpoint-react/README.md @@ -9,10 +9,14 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Run and Debug PowerPoint Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.jsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-powerpoint-sso/README.md b/templates/js/office-xml-addin-powerpoint-sso/README.md index 7e99fc5981..79054736f9 100644 --- a/templates/js/office-xml-addin-powerpoint-sso/README.md +++ b/templates/js/office-xml-addin-powerpoint-sso/README.md @@ -9,6 +9,10 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-powerpoint-taskpane/README.md b/templates/js/office-xml-addin-powerpoint-taskpane/README.md index 0b07c4c1a5..bcd0fd733c 100644 --- a/templates/js/office-xml-addin-powerpoint-taskpane/README.md +++ b/templates/js/office-xml-addin-powerpoint-taskpane/README.md @@ -9,10 +9,14 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Run and Debug PowerPoint Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-word-manifest-only/README.md b/templates/js/office-xml-addin-word-manifest-only/README.md index 7efd70be1d..ab70f06287 100644 --- a/templates/js/office-xml-addin-word-manifest-only/README.md +++ b/templates/js/office-xml-addin-word-manifest-only/README.md @@ -13,18 +13,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./manifest.xml` file in the root directory of the project defines the settings and capabilities of the add-in. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-word-manifest-only/manifest.xml b/templates/js/office-xml-addin-word-manifest-only/manifest.xml index 0293507849..794faaa810 100644 --- a/templates/js/office-xml-addin-word-manifest-only/manifest.xml +++ b/templates/js/office-xml-addin-word-manifest-only/manifest.xml @@ -24,7 +24,7 @@ - + diff --git a/templates/js/office-xml-addin-word-react/README.md b/templates/js/office-xml-addin-word-react/README.md index d9dc17292e..d133c12884 100644 --- a/templates/js/office-xml-addin-word-react/README.md +++ b/templates/js/office-xml-addin-word-react/README.md @@ -9,10 +9,14 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Run and Debug Word Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.jsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-word-sso/README.md b/templates/js/office-xml-addin-word-sso/README.md index 743afc3980..0430e83f12 100644 --- a/templates/js/office-xml-addin-word-sso/README.md +++ b/templates/js/office-xml-addin-word-sso/README.md @@ -9,6 +9,10 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/office-xml-addin-word-taskpane/README.md b/templates/js/office-xml-addin-word-taskpane/README.md index 373124756a..1cdfd3be56 100644 --- a/templates/js/office-xml-addin-word-taskpane/README.md +++ b/templates/js/office-xml-addin-word-taskpane/README.md @@ -9,10 +9,14 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Run and Debug Word Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.js` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/js/sso-tab-with-obo-flow/README.md b/templates/js/sso-tab-with-obo-flow/README.md index 3bbb8f394c..31dc55a061 100644 --- a/templates/js/sso-tab-with-obo-flow/README.md +++ b/templates/js/sso-tab-with-obo-flow/README.md @@ -2,7 +2,7 @@ This app showcases how to craft a visually appealing web page that can be embedded in Microsoft Teams, Outlook and the Microsoft 365 app with React and Fluent UI. The app also enhances the end-user experiences with built-in single sign-on and data from Microsoft Graph. -This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to implement SSO, and uses Azure Function as middle-tier service, and make authenticated requests to call Graph from Azure Function. +This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to implement SSO, and uses Azure Functions as middle-tier service, and make authenticated requests to call Graph from Azure Functions. ## Get started with the React with Fluent UI template @@ -13,7 +13,7 @@ This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure > - [Node.js](https://nodejs.org/), supported versions: 18, 20 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -27,22 +27,22 @@ This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the frontend of the Tab application. Implemented with Fluent UI Framework. | -| `api` | The source code for the backend of the Tab application. Implemented single-sign-on with OBO flow using Azure Function. | +| Folder | Contents | +| ------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the frontend of the Tab application. Implemented with Fluent UI Framework. | +| `api` | The source code for the backend of the Tab application. Implemented single-sign-on with OBO flow using Azure Functions. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions.| -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| -|`aad.manifest.json`|This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app.| +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | ## Extend the React with Fluent UI template diff --git a/templates/js/sso-tab-with-obo-flow/api/README.md b/templates/js/sso-tab-with-obo-flow/api/README.md index 335747f92e..f779a3d1d9 100644 --- a/templates/js/sso-tab-with-obo-flow/api/README.md +++ b/templates/js/sso-tab-with-obo-flow/api/README.md @@ -10,15 +10,15 @@ Azure Functions are a great way to add server-side behaviors to any Teams applic ## Develop -The Teams Toolkit IDE Extension and TeamsFx CLI provide template code for you to get started with Azure Functions for your Teams application. Microsoft Teams Framework simplifies the task of establishing the user's identity within the Azure Function. +The Teams Toolkit IDE Extension and TeamsFx CLI provide template code for you to get started with Azure Functions for your Teams application. Microsoft Teams Framework simplifies the task of establishing the user's identity within the Azure Functions. The template handles calls from your Teams "custom tab" (client-side of your app), initializes the TeamsFx SDK to access the current user context, and demonstrates how to obtain a pre-authenticated Microsoft Graph Client. Microsoft Graph is the "data plane" of Microsoft 365 - you can use it to access content within Microsoft 365 in your company. With it you can read and write documents, SharePoint collections, Teams channels, and many other entities within Microsoft 365. Read more about [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview). -You can add your logic to the single Azure Function created by this template, as well as add more functions as necessary. See [Azure Functions developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference) for more information. +You can add your logic to the single Azure Functions created by this template, as well as add more functions as necessary. See [Azure Functions developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference) for more information. ### Call the Function -To call your Azure Function, the client sends an HTTP request with an SSO token in the `Authorization` header. Here is an example: +To call your Azure Functions, the client sends an HTTP request with an SSO token in the `Authorization` header. Here is an example: ```js import { TeamsUserCredentialAuthConfig, TeamsUserCredential } from "@microsoft/teamsfx"; @@ -39,19 +39,19 @@ const response = await axios.default.get(endpoint + "/api/" + functionName, { ### Add More Functions -- From Visual Studio Code, open the command palette, select `Teams: Add Resources` and select `Azure Function App`. +- From Visual Studio Code, open the command palette, select `Teams: Add Resources` and select `Azure Functions App`. ## Change Node.js runtime version -By default, Teams Toolkit and TeamsFx CLI will provision an Azure function app with function runtime version 3, and node runtime version 12. You can change the node version through Azure Portal. +By default, Teams Toolkit and TeamsFx CLI will provision an Azure functions app with function runtime version 3, and node runtime version 12. You can change the node version through Azure Portal. - Sign in to [Azure Portal](https://azure.microsoft.com/). -- Find your application's resource group and Azure Function app resource. The resource group name and the Azure function app name are stored in your project configuration file `.fx/env.*.json`. You can find them by searching the key `resourceGroupName` and `functionAppName` in that file. -- After enter the home page of the Azure function app, you can find a navigation item called `Configuration` under `settings` group. +- Find your application's resource group and Azure Functions app resource. The resource group name and the Azure functions app name are stored in your project configuration file `.fx/env.*.json`. You can find them by searching the key `resourceGroupName` and `functionAppName` in that file. +- After enter the home page of the Azure functions app, you can find a navigation item called `Configuration` under `settings` group. - Click `Configuration`, you would see a list of settings. Then click `WEBSITE_NODE_DEFAULT_VERSION` and update the value to `~16` or `~18` according to your requirement. - After Click `OK` button, don't forget to click `Save` button on the top of the page. -Then following requests sent to the Azure function app will be handled by new node runtime version. +Then following requests sent to the Azure functions app will be handled by new node runtime version. ## Debug @@ -70,8 +70,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the command palette and select: `Teams: Provision`.
  • Open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision`.
  • Run command `teamsapp deploy`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. diff --git a/templates/js/sso-tab-with-obo-flow/api/package.json b/templates/js/sso-tab-with-obo-flow/api/package.json index fbd714b972..8d3afc1f33 100644 --- a/templates/js/sso-tab-with-obo-flow/api/package.json +++ b/templates/js/sso-tab-with-obo-flow/api/package.json @@ -2,7 +2,7 @@ "name": "teamsfx-template-api", "version": "1.0.0", "engines": { - "node": "16 || 18" + "node": "18 || 20" }, "main": "src/functions/*.js", "scripts": { diff --git a/templates/js/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.jsx b/templates/js/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.jsx index d532a3d4d9..84bd029488 100644 --- a/templates/js/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.jsx +++ b/templates/js/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.jsx @@ -23,14 +23,14 @@ async function callFunction(teamsUserCredential) { } catch (err) { let funcErrorMsg = ""; if (err?.response?.status === 404) { - funcErrorMsg = `There may be a problem with the deployment of Azure Function App, please deploy Azure Function (Run command palette "Teams: Deploy") first before running this App`; + funcErrorMsg = `There may be a problem with the deployment of Azure Functions App, please deploy Azure Functions (Run command palette "Teams: Deploy") first before running this App`; } else if (err.message === "Network Error") { funcErrorMsg = - "Cannot call Azure Function due to network error, please check your network connection status and "; + "Cannot call Azure Functions due to network error, please check your network connection status and "; if (err.config.url.indexOf("localhost") >= 0) { - funcErrorMsg += `make sure to start Azure Function locally (Run "npm run start" command inside api folder from terminal) first before running this App`; + funcErrorMsg += `make sure to start Azure Functions locally (Run "npm run start" command inside api folder from terminal) first before running this App`; } else { - funcErrorMsg += `make sure to provision and deploy Azure Function (Run command palette "Teams: Provision" and "Teams: Deploy") first before running this App`; + funcErrorMsg += `make sure to provision and deploy Azure Functions (Run command palette "Teams: Provision" and "Teams: Deploy") first before running this App`; } } else { funcErrorMsg = err.message; @@ -45,7 +45,7 @@ async function callFunction(teamsUserCredential) { export function AzureFunctions(props) { const [needConsent, setNeedConsent] = useState(false); const { codePath, docsUrl } = { - codePath: `api/${functionName}/index.js`, + codePath: `api/src/functions/${functionName}.js`, docsUrl: "https://aka.ms/teamsfx-azure-functions", ...props, }; @@ -69,13 +69,13 @@ export function AzureFunctions(props) { }); return (
-

Call your Azure Function

+

Call your Azure Functions

An Azure Functions app is running. Authorize this app and click below to call it for a response:

{loading && (
@@ -85,7 +85,7 @@ export function AzureFunctions(props) {
       {!loading && !!data && !error && 
{JSON.stringify(data, null, 2)}
} {!loading && !data && !error &&
}
       {!loading && !!error && 
{error.toString()}
} -

How to edit the Azure Function

+

How to edit the Azure Functions

See the code in {codePath} to add your business logic.

diff --git a/templates/js/sso-tab-with-obo-flow/src/components/sample/EditCode.jsx b/templates/js/sso-tab-with-obo-flow/src/components/sample/EditCode.jsx index 949ca752bb..861a7b21a4 100644 --- a/templates/js/sso-tab-with-obo-flow/src/components/sample/EditCode.jsx +++ b/templates/js/sso-tab-with-obo-flow/src/components/sample/EditCode.jsx @@ -6,8 +6,8 @@ var functionName = config.apiName || "myFunc"; export function EditCode(props) { const { showFunction, tabCodeEntry, functionCodePath } = { showFunction: true, - tabCodeEntry: "tabs/src/index.jsx", - functionCodePath: `api/${functionName}/index.js`, + tabCodeEntry: "src/index.jsx", + functionCodePath: `api/src/functions/${functionName}.js`, ...props, }; return ( diff --git a/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl b/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl index 7fcf3cfe29..fa5f1014d5 100644 --- a/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl +++ b/templates/js/sso-tab-with-obo-flow/teamsapp.local.yml.tpl @@ -106,14 +106,12 @@ deploy: func: version: ~4.0.5455 symlinkDir: ./devTools/func - dotnet: true # Write the information of installed development tool(s) into environment # file for the specified environment variable(s). writeToEnvironmentFile: sslCertFile: SSL_CRT_FILE sslKeyFile: SSL_KEY_FILE funcPath: FUNC_PATH - dotnetPath: DOTNET_PATH # Run npm command - uses: cli/runNpmCommand diff --git a/templates/js/workflow/package.json.tpl b/templates/js/workflow/package.json.tpl index 6627d5e2ff..784a461916 100644 --- a/templates/js/workflow/package.json.tpl +++ b/templates/js/workflow/package.json.tpl @@ -23,7 +23,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, @@ -31,4 +31,4 @@ "env-cmd": "^10.1.0", "nodemon": "^2.0.7" } -} +} \ No newline at end of file diff --git a/templates/js/workflow/teamsapp.local.yml.tpl b/templates/js/workflow/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/js/workflow/teamsapp.local.yml.tpl +++ b/templates/js/workflow/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/js/workflow/teamsapp.yml.tpl b/templates/js/workflow/teamsapp.yml.tpl index e37a3d1e07..d690369df8 100644 --- a/templates/js/workflow/teamsapp.yml.tpl +++ b/templates/js/workflow/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/package.json b/templates/package.json index a4190c5e6d..95deec28d9 100644 --- a/templates/package.json +++ b/templates/package.json @@ -1,6 +1,6 @@ { "name": "templates", - "version": "4.2.0-alpha", + "version": "4.2.0", "private": "true", "license": "MIT", "scripts": { diff --git a/templates/python/custom-copilot-basic/README.md.tpl b/templates/python/custom-copilot-basic/README.md.tpl index c3e51444fc..3cee69b179 100644 --- a/templates/python/custom-copilot-basic/README.md.tpl +++ b/templates/python/custom-copilot-basic/README.md.tpl @@ -4,10 +4,10 @@ This template showcases a bot app that responds to user questions like an AI ass The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. -- [Overview of the Basic AI Chatbot template](#overview-of-the-ai-chat-bot-template) - - [Get started with the Basic AI Chatbot template](#get-started-with-the-ai-chat-bot-template) +- [Overview of the Basic AI Chatbot template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Basic AI Chatbot template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-ai-chat-bot-template-with-more-ai-capabilities) + - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-basic-ai-chatbot-template-with-more-ai-capabilities) - [Additional information and references](#additional-information-and-references) ## Get started with the Basic AI Chatbot template @@ -16,9 +16,9 @@ The app template is built using the Teams AI library, which provides the capabil > > To run the Basic AI Chatbot template in your local dev machine, you will need: > -> - [Python](https://www.python.org/), version 3.8 or higher -> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher -> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli) +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli). {{#useAzureOpenAI}} > - An account with [Azure OpenAI](https://aka.ms/oai/access). {{/useAzureOpenAI}} @@ -26,14 +26,14 @@ The app template is built using the Teams AI library, which provides the capabil > - An account with [OpenAI](https://platform.openai.com/). {{/useOpenAI}} {{^enableTestToolByDefault}} -> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). {{/enableTestToolByDefault}} {{#enableTestToolByDefault}} > - [Node.js](https://nodejs.org/) (supported versions: 16, 18) for local debug in Test Tool. {{/enableTestToolByDefault}} -1. First, Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. -1. select the Teams Toolkit icon on the left in the VS Code toolbar. +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. {{#enableTestToolByDefault}} {{#useAzureOpenAI}} 1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. @@ -42,15 +42,8 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. 1. In this template, default model name is `gpt-3.5-turbo`. If you want to use a different model from OpenAI, fill in your model name in [src/config.py](./src/config.py). {{/useOpenAI}} -1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. -1. You will receive a welcome message from the bot, or send any message to get a response. - -**Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: - -![ai chat bot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} -1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. {{#useAzureOpenAI}} 1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. {{/useAzureOpenAI}} @@ -58,12 +51,26 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. 1. In this template, default model name is `gpt-3.5-turbo`. If you want to use a different model from OpenAI, fill in your model name in [src/config.py](./src/config.py). {{/useOpenAI}} +{{/enableTestToolByDefault}} + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +{{#enableTestToolByDefault}} +1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +{{/enableTestToolByDefault}} 1. You will receive a welcome message from the bot, or send any message to get a response. **Congratulations**! You are running an application that can now interact with users in Teams: +{{#enableTestToolByDefault}} +![ai chat bot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} ![ai chat bot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) {{/enableTestToolByDefault}} @@ -109,4 +116,7 @@ You can follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic- - [Teams AI library](https://aka.ms/teams-ai-library) - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) -- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use Test Tool to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool log. You can wait for Python launch console ready and then refresh the front end web page. \ No newline at end of file diff --git a/templates/python/custom-copilot-basic/infra/azure.bicep.tpl b/templates/python/custom-copilot-basic/infra/azure.bicep.tpl index 608b54e549..1f22628590 100644 --- a/templates/python/custom-copilot-basic/infra/azure.bicep.tpl +++ b/templates/python/custom-copilot-basic/infra/azure.bicep.tpl @@ -59,6 +59,10 @@ resource webApp 'Microsoft.Web/sites@2021-02-01' = { appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' linuxFxVersion: pythonVersion appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } { name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' value: 'true' diff --git a/templates/python/custom-copilot-basic/src/requirements.txt b/templates/python/custom-copilot-basic/src/requirements.txt index 32dea3ac52..1ba1feadad 100644 --- a/templates/python/custom-copilot-basic/src/requirements.txt +++ b/templates/python/custom-copilot-basic/src/requirements.txt @@ -1,3 +1,3 @@ python-dotenv aiohttp -teams-ai~=1.0.0 \ No newline at end of file +teams-ai~=1.0.1 \ No newline at end of file diff --git a/templates/python/custom-copilot-basic/teamsapp.local.yml.tpl b/templates/python/custom-copilot-basic/teamsapp.local.yml.tpl index fd718b61e3..63e5646c6e 100644 --- a/templates/python/custom-copilot-basic/teamsapp.local.yml.tpl +++ b/templates/python/custom-copilot-basic/teamsapp.local.yml.tpl @@ -14,16 +14,20 @@ provision: writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - # Create or reuse an existing Azure Active Directory application for bot. - - uses: botAadApp/create + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - # The Azure Active Directory application's display name - name: {{appName}}-${{TEAMSFX_ENV}} + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: - # The Azure Active Directory application's client id created for bot. - botId: BOT_ID - # The Azure Active Directory application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/python/custom-copilot-basic/teamsapp.yml.tpl b/templates/python/custom-copilot-basic/teamsapp.yml.tpl index 4dcf12589c..3f2f0de1e4 100644 --- a/templates/python/custom-copilot-basic/teamsapp.yml.tpl +++ b/templates/python/custom-copilot-basic/teamsapp.yml.tpl @@ -17,16 +17,20 @@ provision: writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - # Create or reuse an existing Azure Active Directory application for bot. - - uses: botAadApp/create + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - # The Azure Active Directory application's display name - name: {{appName}}-${{TEAMSFX_ENV}} + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: - # The Azure Active Directory application's client id created for bot. - botId: BOT_ID - # The Azure Active Directory application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: @@ -93,3 +97,40 @@ deploy: # You can replace it with your existing Azure Resource id # or add it to your environment variable file. resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.gitignore b/templates/python/custom-copilot-rag-azure-ai-search/.gitignore new file mode 100644 index 0000000000..ec1f6285c6 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.gitignore @@ -0,0 +1,15 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.vscode/extensions.json b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/extensions.json new file mode 100644 index 0000000000..760a0b1d8f --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.vscode/launch.json.tpl b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/launch.json.tpl new file mode 100644 index 0000000000..f17c73596d --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/launch.json.tpl @@ -0,0 +1,134 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "program": "${workspaceFolder}/src/app.py", + "request": "launch", + "cwd": "${workspaceFolder}/src/", + "console": "integratedTerminal", + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start", + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug (Edge)", + "configurations": [ + "Launch App (Edge)", + "Start Python" + ], + "cascadeTerminateToConfigurations": [ + "Start Python" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { +{{#enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Start Python" + ], + "cascadeTerminateToConfigurations": [ + "Start Python" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { +{{#enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Start Python", + "Start Test Tool", + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { +{{#enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} + "order": 1 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.vscode/settings.json b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/settings.json new file mode 100644 index 0000000000..0d3ba10b02 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.vscode/tasks.json b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/tasks.json new file mode 100644 index 0000000000..cd77312c80 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.vscode/tasks.json @@ -0,0 +1,108 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/.webappignore b/templates/python/custom-copilot-rag-azure-ai-search/.webappignore new file mode 100644 index 0000000000..fc2332c6bc --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/.webappignore @@ -0,0 +1,15 @@ +.venv/ +.vscode/ +appPackage/ +devTools/ +infra/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +.gitignore + +indexers/ \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/README.md.tpl b/templates/python/custom-copilot-rag-azure-ai-search/README.md.tpl new file mode 100644 index 0000000000..c5280bbabc --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/README.md.tpl @@ -0,0 +1,148 @@ +# Overview of the AI Search Bot template + +This template showcases a bot app that responds to user questions like an AI assistant according to data from Azure Search. This enables your users to talk with the AI assistant in Teams to find information. + +The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. + +- [Overview of the AI Search Bot template](#overview-of-the-ai-search-bot-template) + - [Get started with the AI Search Bot template](#get-started-with-the-ai-search-bot-template) + - [What's included in the template](#whats-included-in-the-template) + - [Extend the AI Search Bot template with more AI capabilities](#extend-the-ai-search-bot-template-with-more-ai-capabilities) + - [Additional information and references](#additional-information-and-references) + +## Get started with the AI Search Bot template + +> **Prerequisites** +> +> To run the AI Search Bot template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli). +{{#useAzureOpenAI}} +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +{{/useAzureOpenAI}} +{{#useOpenAI}} +> - An account with [OpenAI](https://platform.openai.com/). +{{/useOpenAI}} +> - An [Azure Search service](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search). +{{^enableTestToolByDefault}} +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +> - [Node.js](https://nodejs.org/) (supported versions: 16, 18) for local debug in Test Tool. +{{/enableTestToolByDefault}} + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +{{#enableTestToolByDefault}} +{{#useAzureOpenAI}} +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME`, endpoint `AZURE_OPENAI_ENDPOINT` and embedding deployment name `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`. +{{/useAzureOpenAI}} +{{#useOpenAI}} +1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. +1. In this template, default model name is `gpt-3.5-turbo` and default embedding model name is `text-embedding-ada-002`. If you want to use different models from OpenAI, fill in your model names in [src/config.py](./src/config.py). +{{/useOpenAI}} +1. In file *env/.env.local.user*, fill in your Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT`. +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +{{#useAzureOpenAI}} +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME`, endpoint `AZURE_OPENAI_ENDPOINT` and embedding deployment name `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`. +{{/useAzureOpenAI}} +{{#useOpenAI}} +1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. +1. In this template, default model name is `gpt-3.5-turbo` and default embedding model name is `text-embedding-ada-002`. If you want to use different models from OpenAI, fill in your model names in [src/config.py](./src/config.py). +{{/useOpenAI}} +1. In file *env/.env.local.user*, fill in your Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT`. +{{/enableTestToolByDefault}} + +### Setting up index and documents +{{^enableTestToolByDefault}} +1. Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT` are loaded from *env/.env.local.user*. Please make sure you have already configured them. +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +1. Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT` are loaded from *env/.env.testtool.user*. Please make sure you have already configured them. +{{/enableTestToolByDefault}} +1. Use command `python src/indexers/setup.py` to create index and upload documents in `src/indexers/data`. +1. You will see the following information indicated the success of setup: + ``` + Create index succeeded. If it does not exist, wait for 5 seconds... + Upload new documents succeeded. If they do not exist, wait for several seconds... + setup finished + ``` +1. Once you're done using the sample it's good practice to delete the index. You can do so with the command `python src/indexers/delete.py`. + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +{{^enableTestToolByDefault}} +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. +{{/enableTestToolByDefault}} +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +{{#enableTestToolByDefault}} +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/3e0de761-b4c8-4ae2-9ede-8e9922e54765) +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/2c17e3e8-09c1-42b6-b47a-ac4234343883) +{{/enableTestToolByDefault}} + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/bot.py`| Handles business logics for the AI Search Bot.| +|`src/config.py`| Defines the environment variables.| +|`src/app.py`| Main module of the AI Search Bot, hosts a aiohttp api server for the app.| +|`src/azure_ai_search_data_source.py.py`| Handles data search logics.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| + +The following files are scripts and raw texts that help you to prepare or clean data source in Azure Search. + +| File | Contents | +| - | - | +|`src/indexers/get_data.py`| Fetches data and creates embedding vectors.| +|`src/indexers/data/*.md`| Raw text data source.| +|`src/indexers/setup.py`| A script to create index and upload documents.| +|`src/indexers/delete.py`| A script to delete index and documents.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the Basic AI Chatbot template with more AI capabilities + +You can follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities, like: +- [Customize prompt](https://aka.ms/teamsfx-basic-ai-chatbot#customize-prompt) +- [Customize user input](https://aka.ms/teamsfx-basic-ai-chatbot#customize-user-input) +- [Customize conversation history](https://aka.ms/teamsfx-basic-ai-chatbot#customize-conversation-history) +- [Customize model type](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-type) +- [Customize model parameters](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-parameters) +- [Handle messages with image](https://aka.ms/teamsfx-basic-ai-chatbot#handle-messages-with-image) + +## Additional information and references +- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/appPackage/color.png b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/color.png new file mode 100644 index 0000000000..2d7e85c9e9 Binary files /dev/null and b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/color.png differ diff --git a/templates/python/custom-copilot-rag-azure-ai-search/appPackage/manifest.json.tpl b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..1309b98c88 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/manifest.json.tpl @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", + "manifestVersion": "1.16", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "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": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "full name for {{appName}}" + }, + "description": { + "short": "short description for {{appName}}", + "full": "full description for {{appName}}" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/python/custom-copilot-rag-azure-ai-search/appPackage/outline.png b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/outline.png new file mode 100644 index 0000000000..e8cb4b6ba4 Binary files /dev/null and b/templates/python/custom-copilot-rag-azure-ai-search/appPackage/outline.png differ diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev new file mode 100644 index 0000000000..8172044cec --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev @@ -0,0 +1,17 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev.user.tpl b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev.user.tpl new file mode 100644 index 0000000000..947e5a84e4 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.dev.user.tpl @@ -0,0 +1,31 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +AZURE_OPENAI_EMBEDDING_DEPLOYMENT= +{{/useAzureOpenAI}} + +SECRET_AZURE_SEARCH_KEY= +AZURE_SEARCH_ENDPOINT= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local new file mode 100644 index 0000000000..589a4dea65 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local @@ -0,0 +1,12 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +BOT_DOMAIN= +BOT_ENDPOINT= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local.user.tpl b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local.user.tpl new file mode 100644 index 0000000000..0d32eb7c2c --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.local.user.tpl @@ -0,0 +1,32 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# If you're adding a secret value, add SECRET_ prefix to the name so Teams Toolkit can handle them properly +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +AZURE_OPENAI_EMBEDDING_DEPLOYMENT= +{{/useAzureOpenAI}} + +SECRET_AZURE_SEARCH_KEY= +AZURE_SEARCH_ENDPOINT= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool new file mode 100644 index 0000000000..53abad07db --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool.user.tpl b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool.user.tpl new file mode 100644 index 0000000000..0d32eb7c2c --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/env/.env.testtool.user.tpl @@ -0,0 +1,32 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# If you're adding a secret value, add SECRET_ prefix to the name so Teams Toolkit can handle them properly +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +AZURE_OPENAI_EMBEDDING_DEPLOYMENT= +{{/useAzureOpenAI}} + +SECRET_AZURE_SEARCH_KEY= +AZURE_SEARCH_ENDPOINT= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.bicep.tpl b/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.bicep.tpl new file mode 100644 index 0000000000..611c5eb17d --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.bicep.tpl @@ -0,0 +1,135 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +{{#useAzureOpenAI}} +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string +param azureOpenaiEmbeddingDeployment string +{{/useAzureOpenAI}} +{{#useOpenAI}} +@secure() +@description('Required in your bot project to access OpenAI service. You can get it from OpenAI > API > API Key') +param openaiKey string +{{/useOpenAI}} + +@secure() +@description('Required in your bot project to access Azure Search service. You can get it from Azure Portal > Azure Search > Keys > Admin Key') +param azureSearchKey string +param azureSearchEndpoint string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + {{#useAzureOpenAI}} + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + { + name: 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT' + value: azureOpenaiEmbeddingDeployment + } + {{/useAzureOpenAI}} + {{#useOpenAI}} + { + name: 'OPENAI_API_KEY' + value: openaiKey + } + {{/useOpenAI}} + { + name: 'AZURE_SEARCH_KEY' + value: azureSearchKey + } + { + name: 'AZURE_SEARCH_ENDPOINT' + value: azureSearchEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName diff --git a/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.parameters.json.tpl b/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..9e608ebeca --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/infra/azure.parameters.json.tpl @@ -0,0 +1,49 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "botAadAppClientId": { + "value": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + {{#useAzureOpenAI}} + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "azureOpenaiEmbeddingDeployment" : { + "value": "${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}}" + }, + {{/useAzureOpenAI}} + {{#useOpenAI}} + "openaiKey": { + "value": "${{SECRET_OPENAI_API_KEY}}" + }, + {{/useOpenAI}} + "azureSearchKey": { + "value": "${{SECRET_AZURE_SEARCH_KEY}}" + }, + "azureSearchEndpoint": { + "value": "${{AZURE_SEARCH_ENDPOINT}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "AISearch-py" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/azurebot.bicep b/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..ab67c7a56b --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param botAadAppClientId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: botAadAppClientId + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/readme.md b/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/app.py b/templates/python/custom-copilot-rag-azure-ai-search/src/app.py new file mode 100644 index 0000000000..910476d014 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio +from http import HTTPStatus +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/azure_ai_search_data_source.py.tpl b/templates/python/custom-copilot-rag-azure-ai-search/src/azure_ai_search_data_source.py.tpl new file mode 100644 index 0000000000..c23e4c42c3 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/azure_ai_search_data_source.py.tpl @@ -0,0 +1,111 @@ +from dataclasses import dataclass +from typing import Optional, List +from azure.search.documents.indexes.models import _edm as EDM +from azure.search.documents.models import VectorQuery, VectorizedQuery +{{#useAzureOpenAI}} +from teams.ai.embeddings import AzureOpenAIEmbeddings, AzureOpenAIEmbeddingsOptions +{{/useAzureOpenAI}} +{{#useOpenAI}} +from teams.ai.embeddings import OpenAIEmbeddings, OpenAIEmbeddingsOptions +{{/useOpenAI}} +from teams.state.memory import Memory +from teams.state.state import TurnContext +from teams.ai.tokenizers import Tokenizer +from teams.ai.data_sources import DataSource + +from config import Config + +async def get_embedding_vector(text: str): + {{#useAzureOpenAI}} + embeddings = AzureOpenAIEmbeddings(AzureOpenAIEmbeddingsOptions( + azure_api_key=Config.AZURE_OPENAI_API_KEY, + azure_endpoint=Config.AZURE_OPENAI_ENDPOINT, + azure_deployment=Config.AZURE_OPENAI_EMBEDDING_DEPLOYMENT + )) + {{/useAzureOpenAI}} + {{#useOpenAI}} + embedding=OpenAIEmbeddings(OpenAIEmbeddingsOptions( + api_key=Config.OPENAI_API_KEY, + model=Config.OPENAI_EMBEDDING_DEPLOYMENT, + )) + {{/useOpenAI}} + + result = await embeddings.create_embeddings(text) + if (result.status != 'success' or not result.output): + raise Exception(f"Failed to generate embeddings for description: {text}") + + return result.output[0] + +@dataclass +class Doc: + docId: Optional[str] = None + docTitle: Optional[str] = None + description: Optional[str] = None + descriptionVector: Optional[List[float]] = None + +@dataclass +class AzureAISearchDataSourceOptions: + name: str + indexName: str + azureAISearchApiKey: str + azureAISearchEndpoint: str + +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient +import json + +@dataclass +class Result: + def __init__(self, output, length, too_long): + self.output = output + self.length = length + self.too_long = too_long + +class AzureAISearchDataSource(DataSource): + def __init__(self, options: AzureAISearchDataSourceOptions): + self.name = options.name + self.options = options + self.searchClient = SearchClient( + options.azureAISearchEndpoint, + options.indexName, + AzureKeyCredential(options.azureAISearchApiKey) + ) + + def name(self): + return self.name + + async def render_data(self, _context: TurnContext, memory: Memory, tokenizer: Tokenizer, maxTokens: int): + query = memory.get('temp.input') + embedding = await get_embedding_vector(query) + vector_query = VectorizedQuery(vector=embedding, k_nearest_neighbors=2, fields="descriptionVector") + + if not query: + return Result('', 0, False) + + selectedFields = [ + 'docTitle', + 'description', + 'descriptionVector', + ] + + searchResults = self.searchClient.search( + search_text=query, + select=selectedFields, + vector_queries=[vector_query], + ) + + if not searchResults: + return Result('', 0, False) + + usedTokens = 0 + doc = '' + for result in searchResults: + tokens = len(tokenizer.encode(json.dumps(result["description"]))) + + if usedTokens + tokens > maxTokens: + break + + doc += json.dumps(result["description"]) + usedTokens += tokens + + return Result(doc, usedTokens, usedTokens > maxTokens) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/bot.py.tpl b/templates/python/custom-copilot-rag-azure-ai-search/src/bot.py.tpl new file mode 100644 index 0000000000..c4be18578a --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/bot.py.tpl @@ -0,0 +1,85 @@ +import asyncio +from dataclasses import dataclass +import json +import os +import sys +import traceback +from typing import Generic, TypeVar + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.ai.actions import ActionTypes +from teams.state import TurnState + +from azure_ai_search_data_source import AzureAISearchDataSource, AzureAISearchDataSourceOptions +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +{{#useAzureOpenAI}} +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) +{{/useAzureOpenAI}} +{{#useOpenAI}} +model = OpenAIModel( + OpenAIModelOptions( + api_key=config.OPENAI_API_KEY, + default_model=config.OPENAI_MODEL_NAME, + ) +) +{{/useOpenAI}} + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +prompts.add_data_source( + AzureAISearchDataSource( + AzureAISearchDataSourceOptions( + name='azure-ai-search', + indexName='contoso-electronics', + azureAISearchApiKey=config.AZURE_SEARCH_KEY, + azureAISearchEndpoint=config.AZURE_SEARCH_ENDPOINT, + ) + ) +) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="chat") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/config.py.tpl b/templates/python/custom-copilot-rag-azure-ai-search/src/config.py.tpl new file mode 100644 index 0000000000..0b61ca9113 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/config.py.tpl @@ -0,0 +1,31 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint + AZURE_OPENAI_EMBEDDING_DEPLOYMENT = os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT"] # Azure OpenAI embedding deployment + {{/useAzureOpenAI}} + {{#useOpenAI}} + OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] # OpenAI API key + OPENAI_MODEL_NAME='gpt-3.5-turbo' # OpenAI model name. You can use any other model name from OpenAI. + OPENAI_EMBEDDING_DEPLOYMENT='text-embedding-ada-002' # OpenAI embedding model. You can use any other embedding model from OpenAI. + {{/useOpenAI}} + AZURE_SEARCH_KEY = os.environ["AZURE_SEARCH_KEY"] # Azure Search key + AZURE_SEARCH_ENDPOINT = os.environ["AZURE_SEARCH_ENDPOINT"] # Azure Search endpoint + diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md new file mode 100644 index 0000000000..6878a8e204 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md @@ -0,0 +1,48 @@ +# Contoso Electronics Company Overview + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## History + +Contoso Electronics, a pioneering force in the tech industry, was founded in 1985 by visionary entrepreneurs with a passion for innovation. Over the years, the company has played a pivotal role in shaping the landscape of consumer electronics. + +| Year | Milestone | +|------|-----------| +| 1985 | Company founded with a focus on cutting-edge technology | +| 1990 | Launched the first-ever handheld personal computer | +| 2000 | Introduced groundbreaking advancements in AI and robotics | +| 2015 | Expansion into sustainable and eco-friendly product lines | + +## Company Overview + +At Contoso Electronics, we take pride in fostering a dynamic and inclusive workplace. Our dedicated team of experts collaborates to create innovative solutions that empower and connect people globally. + +### Core Values + +- **Innovation:** Constantly pushing the boundaries of technology. +- **Diversity:** Embracing different perspectives for creative excellence. +- **Sustainability:** Committed to eco-friendly practices in our products. + +## Vacation Perks + +We believe in work-life balance and understand the importance of well-deserved breaks. Our vacation perks are designed to help our employees recharge and return with renewed enthusiasm. + +| Vacation Tier | Duration | Additional Benefits | +|---------------|----------|---------------------| +| Standard | 2 weeks | Health and wellness stipend | +| Senior | 4 weeks | Travel vouchers for a dream destination | +| Executive | 6 weeks | Luxury resort getaway with family | + +## Employee Recognition + +Recognizing the hard work and dedication of our employees is at the core of our culture. Here are some ways we celebrate achievements: + +- Monthly "Innovator of the Month" awards +- Annual gala with awards for outstanding contributions +- Team-building retreats for high-performing departments + +## Join Us! + +Contoso Electronics is always on the lookout for talented individuals who share our passion for innovation. If you're ready to be part of a dynamic team shaping the future of technology, check out our [careers page](http://www.contoso.com) for exciting opportunities. + +[Learn more about Contoso Electronics!](http://www.contoso.com) diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md new file mode 100644 index 0000000000..1d97d5117e --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md @@ -0,0 +1,36 @@ +# Contoso Electronics PerksPlus Program + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Overview +Introducing PerksPlus - the ultimate benefits program designed to support the health and wellness of employees. With PerksPlus, employees have the opportunity to expense up to $1000 for fitness-related programs, making it easier and more affordable to maintain a healthy lifestyle. PerksPlus is not only designed to support employees' physical health, but also their mental health. Regular exercise has been shown to reduce stress, improve mood, and enhance overall well-being. With PerksPlus, employees can invest in their health and wellness, while enjoying the peace of mind that comes with knowing they are getting the support they need to lead a healthy life. +What is Covered? + +PerksPlus covers a wide range of fitness activities, including but not limited to: +* Gym memberships +* Personal training sessions +* Yoga and Pilates classes +* Fitness equipment purchases +* Sports team fees +* Health retreats and spas +* Outdoor adventure activities (such as rock climbing, hiking, and kayaking) +* Group fitness classes (such as dance, martial arts, and cycling) +* Virtual fitness programs (such as online yoga and workout classes) + +In addition to the wide range of fitness activities covered by PerksPlus, the program also covers a variety of lessons and experiences that promote health and wellness. Some of the lessons covered under PerksPlus include: +* Skiing and snowboarding lessons +* Scuba diving lessons +* Surfing lessons +* Horseback riding lessons + +These lessons provide employees with the opportunity to try new things, challenge themselves, and improve their physical skills. They are also a great way to relieve stress and have fun while staying active. + +With PerksPlus, employees can choose from a variety of fitness programs to suit their individual needs and preferences. Whether you're looking to improve your physical fitness, reduce stress, or just have some fun, PerksPlus has you covered. + +## What is Not Covered? +In addition to the wide range of activities covered by PerksPlus, there is also a list of things that are not +covered under the program. These include but are not limited to: +* Non-fitness related expenses +* Medical treatments and procedures +* Travel expenses (unless related to a fitness program) +* Food and supplements \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md new file mode 100644 index 0000000000..9da5c6429d --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md @@ -0,0 +1,37 @@ +# Contoso Electronics Plan and Benefit Packages + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Northwind Health Plus + +Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. + +This plan also offers coverage for emergency services, both in-network and out-of-network. + +## Northwind Standard + +Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +## Comparison of Plans + +Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings. + +Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand-name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs. + +Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses. + +Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests. + +Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services. + +## Cost Comparison + +Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics + +| | Northwind Standard | NorthWind Health Plus | +|---------------|----------|---------------------| +| Employee Only | $45.00 | $55.00 | +| Employee +1 | $65.00 | $71.00 | +| Employee +2 or more | $78.00 | $89.00 | \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/delete.py.tpl b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/delete.py.tpl new file mode 100644 index 0000000000..50d1e92bfd --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/delete.py.tpl @@ -0,0 +1,24 @@ +import os +from azure.core.credentials import AzureKeyCredential +from azure.search.documents.indexes import SearchIndexClient + +from dotenv import load_dotenv + +{{#enableTestToolByDefault}} +load_dotenv(f'{os.getcwd()}/env/.env.testtool.user') +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +load_dotenv(f'{os.getcwd()}/env/.env.local.user') +{{/enableTestToolByDefault}} + +def delete_index(client: SearchIndexClient, name: str): + client.delete_index(name) + print(f"Index {name} deleted") + +index = 'contoso-electronics' +search_api_key = os.getenv('SECRET_AZURE_SEARCH_KEY') +search_api_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') +credentials = AzureKeyCredential(search_api_key) + +search_index_client = SearchIndexClient(search_api_endpoint, credentials) +delete_index(search_index_client, index) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/get_data.py b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/get_data.py new file mode 100644 index 0000000000..719f7a5bfa --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/get_data.py @@ -0,0 +1,39 @@ +import os + +async def get_doc_data(embeddings): + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md', 'r') as file: + raw_description1 = file.read() + doc1 = { + "docId": "1", + "docTitle": "Contoso_Electronics_PerkPlus_Program", + "description": raw_description1, + "descriptionVector": await get_embedding_vector(raw_description1, embeddings=embeddings), + } + + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_Company_Overview.md', 'r') as file: + raw_description2 = file.read() + doc2 = { + "docId": "2", + "docTitle": "Contoso_Electronics_Company_Overview", + "description": raw_description2, + "descriptionVector": await get_embedding_vector(raw_description2, embeddings=embeddings), + } + + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_Plan_Benefits.md', 'r') as file: + raw_description3 = file.read() + doc3 = { + "docId": "3", + "docTitle": "Contoso_Electronics_Plan_Benefits", + "description": raw_description3, + "descriptionVector": await get_embedding_vector(raw_description3, embeddings=embeddings), + } + + return [doc1, doc2, doc3] + + +async def get_embedding_vector(text: str, embeddings): + result = await embeddings.create_embeddings(text) + if (result.status != 'success' or not result.output): + raise Exception(f"Failed to generate embeddings for description: {text}") + + return result.output[0] \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/setup.py.tpl b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/setup.py.tpl new file mode 100644 index 0000000000..719c69920d --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/indexers/setup.py.tpl @@ -0,0 +1,103 @@ +import asyncio +import os +from dataclasses import dataclass +from typing import List, Optional + +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, + SimpleField, + SearchableField, + SearchField, + SearchFieldDataType, + ComplexField, + CorsOptions, + VectorSearch, + VectorSearchProfile, + HnswAlgorithmConfiguration +) +{{#useAzureOpenAI}} +from teams.ai.embeddings import AzureOpenAIEmbeddings, AzureOpenAIEmbeddingsOptions +{{/useAzureOpenAI}} +{{#useOpenAI}} +from teams.ai.embeddings import OpenAIEmbeddings, OpenAIEmbeddingsOptions +{{/useOpenAI}} + +from get_data import get_doc_data + +from dotenv import load_dotenv + +{{#enableTestToolByDefault}} +load_dotenv(f'{os.getcwd()}/env/.env.testtool.user') +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +load_dotenv(f'{os.getcwd()}/env/.env.local.user') +{{/enableTestToolByDefault}} + +@dataclass +class Doc: + docId: Optional[str] = None + docTitle: Optional[str] = None + description: Optional[str] = None + descriptionVector: Optional[List[float]] = None + +async def upsert_documents(client: SearchClient, documents: list[Doc]): + return client.merge_or_upload_documents(documents) + +async def create_index_if_not_exists(client: SearchIndexClient, name: str): + doc_index = SearchIndex( + name=name, + fields = [ + SimpleField(name="docId", type=SearchFieldDataType.String, key=True), + SimpleField(name="docTitle", type=SearchFieldDataType.String), + SearchableField(name="description", type=SearchFieldDataType.String, searchable=True), + SearchField(name="descriptionVector", type=SearchFieldDataType.Collection(SearchFieldDataType.Single), searchable=True, vector_search_dimensions=1536, vector_search_profile_name='my-vector-config'), + ], + scoring_profiles=[], + cors_options=CorsOptions(allowed_origins=["*"]), + vector_search = VectorSearch( + profiles=[VectorSearchProfile(name="my-vector-config", algorithm_configuration_name="my-algorithms-config")], + algorithms=[HnswAlgorithmConfiguration(name="my-algorithms-config")], + ) + ) + + client.create_or_update_index(doc_index) + +async def setup(search_api_key, search_api_endpoint): + index = 'contoso-electronics' + + credentials = AzureKeyCredential(search_api_key) + + search_index_client = SearchIndexClient(search_api_endpoint, credentials) + await create_index_if_not_exists(search_index_client, index) + + print("Create index succeeded. If it does not exist, wait for 5 seconds...") + await asyncio.sleep(5) + + search_client = SearchClient(search_api_endpoint, index, credentials) + + {{#useAzureOpenAI}} + embeddings = AzureOpenAIEmbeddings(AzureOpenAIEmbeddingsOptions( + azure_api_key=os.getenv('SECRET_AZURE_OPENAI_API_KEY'), + azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT'), + azure_deployment=os.getenv('AZURE_OPENAI_EMBEDDING_DEPLOYMENT') + )) + {{/useAzureOpenAI}} + {{#useOpenAI}} + embedding=OpenAIEmbeddings(OpenAIEmbeddingsOptions( + api_key=os.getenv('SECRET_OPENAI_API_KEY'), + model=os.getenv('OPENAI_EMBEDDING_DEPLOYMENT') + )) + {{/useOpenAI}} + data = await get_doc_data(embeddings=embeddings) + await upsert_documents(search_client, data) + + print("Upload new documents succeeded. If they do not exist, wait for several seconds...") + +search_api_key = os.getenv('SECRET_AZURE_SEARCH_KEY') +search_api_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') +asyncio.run(setup(search_api_key, search_api_endpoint)) +print("setup finished") + diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/config.json b/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/config.json new file mode 100644 index 0000000000..109caad3b4 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/config.json @@ -0,0 +1,24 @@ +{ + "schema": 1.1, + "description": "Chat with Teams RAG", + "type": "completion", + "completion": { + "model": "gpt-35-turbo", + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 4096, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "none", + "data_sources": { + "azure-ai-search": 2500 + } + } +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/skprompt.txt b/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/skprompt.txt new file mode 100644 index 0000000000..789155ad1b --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +The following is a conversation with an AI assistant, who is an expert on answering questions over the given context. +Responses should be in a short journalistic style with no more than 80 words. +Use the context provided in the `` tags as the source for your answers. \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/src/requirements.txt b/templates/python/custom-copilot-rag-azure-ai-search/src/requirements.txt new file mode 100644 index 0000000000..db8f94a1a6 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/src/requirements.txt @@ -0,0 +1,5 @@ +python-dotenv +aiohttp +azure-search +azure-search-documents +teams-ai~=1.0.1 \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.local.yml.tpl b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..e2a150030e --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.local.yml.tpl @@ -0,0 +1,87 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}-${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: {{appName}} + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}} + {{/useAzureOpenAI}} + {{#useOpenAI}} + OPENAI_API_KEY: ${{SECRET_OPENAI_API_KEY}} + {{/useOpenAI}} + AZURE_SEARCH_KEY: ${{SECRET_AZURE_SEARCH_KEY}} + AZURE_SEARCH_ENDPOINT: ${{AZURE_SEARCH_ENDPOINT}} diff --git a/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.testtool.yml.tpl b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.testtool.yml.tpl new file mode 100644 index 0000000000..bfc9c65191 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.testtool.yml.tpl @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.1.0-beta + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}} + {{/useAzureOpenAI}} + {{#useOpenAI}} + OPENAI_API_KEY: ${{SECRET_OPENAI_API_KEY}} + {{/useOpenAI}} + AZURE_SEARCH_KEY: ${{SECRET_AZURE_SEARCH_KEY}} + AZURE_SEARCH_ENDPOINT: ${{AZURE_SEARCH_ENDPOINT}} diff --git a/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.yml.tpl b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.yml.tpl new file mode 100644 index 0000000000..f755147ce7 --- /dev/null +++ b/templates/python/custom-copilot-rag-azure-ai-search/teamsapp.yml.tpl @@ -0,0 +1,136 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}-${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-tab + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsfx deploy' is executed +deploy: + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: ./src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/python/custom-copilot-rag-customize/.gitignore b/templates/python/custom-copilot-rag-customize/.gitignore new file mode 100644 index 0000000000..58fd2596d0 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.gitignore @@ -0,0 +1,15 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log diff --git a/templates/python/custom-copilot-rag-customize/.vscode/extensions.json b/templates/python/custom-copilot-rag-customize/.vscode/extensions.json new file mode 100644 index 0000000000..760a0b1d8f --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/.vscode/launch.json.tpl b/templates/python/custom-copilot-rag-customize/.vscode/launch.json.tpl new file mode 100644 index 0000000000..f17c73596d --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.vscode/launch.json.tpl @@ -0,0 +1,134 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "program": "${workspaceFolder}/src/app.py", + "request": "launch", + "cwd": "${workspaceFolder}/src/", + "console": "integratedTerminal", + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start", + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug (Edge)", + "configurations": [ + "Launch App (Edge)", + "Start Python" + ], + "cascadeTerminateToConfigurations": [ + "Start Python" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { +{{#enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Start Python" + ], + "cascadeTerminateToConfigurations": [ + "Start Python" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { +{{#enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Start Python", + "Start Test Tool", + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { +{{#enableTestToolByDefault}} + "group": "1-local", +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + "group": "2-local", +{{/enableTestToolByDefault}} + "order": 1 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/.vscode/settings.json b/templates/python/custom-copilot-rag-customize/.vscode/settings.json new file mode 100644 index 0000000000..0d3ba10b02 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/.vscode/tasks.json b/templates/python/custom-copilot-rag-customize/.vscode/tasks.json new file mode 100644 index 0000000000..cd77312c80 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.vscode/tasks.json @@ -0,0 +1,108 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + } + ] +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/.webappignore b/templates/python/custom-copilot-rag-customize/.webappignore new file mode 100644 index 0000000000..0a7d5f2857 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/.webappignore @@ -0,0 +1,13 @@ +.venv/ +.vscode/ +appPackage/ +devTools/ +infra/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +.gitignore diff --git a/templates/python/custom-copilot-rag-customize/README.md.tpl b/templates/python/custom-copilot-rag-customize/README.md.tpl new file mode 100644 index 0000000000..e9c528ae8a --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/README.md.tpl @@ -0,0 +1,121 @@ +# Overview of the Basic RAG Bot template + +It showcases how to build an basic RAG bot in Teams capable of chatting with users but with context provided by customize data source. + +The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. + +- [Overview of the Basic RAG Bot template](#overview-of-the-basic-rag-bot-template) + - [Get started with the Basic RAG Bot template](#get-started-with-the-basic-rag-bot-template) + - [What's included in the template](#whats-included-in-the-template) + - [Extend the Basic RAG Bot template with more AI capabilities](#extend-the-basic-rag-bot-template-with-more-ai-capabilities) + - [Additional information and references](#additional-information-and-references) + +## Get started with the Basic RAG Bot template + +> **Prerequisites** +> +> To run the Basic RAG Bot template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli). +{{#useAzureOpenAI}} +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +{{/useAzureOpenAI}} +{{#useOpenAI}} +> - An account with [OpenAI](https://platform.openai.com/). +{{/useOpenAI}} +{{^enableTestToolByDefault}} +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +> - [Node.js](https://nodejs.org/) (supported versions: 16, 18) for local debug in Test Tool. +{{/enableTestToolByDefault}} + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +{{#enableTestToolByDefault}} +{{#useAzureOpenAI}} +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. +{{/useAzureOpenAI}} +{{#useOpenAI}} +1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. +1. In this template, default model name is `gpt-3.5-turbo`. If you want to use different models from OpenAI, fill in your model names in [src/config.py](./src/config.py). +{{/useOpenAI}} +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +{{#useAzureOpenAI}} +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. +{{/useAzureOpenAI}} +{{#useOpenAI}} +1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY`. +1. In this template, default model name is `gpt-3.5-turbo`. If you want to use different models from OpenAI, fill in your model names in [src/config.py](./src/config.py). +{{/useOpenAI}} +{{/enableTestToolByDefault}} + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +{{^enableTestToolByDefault}} +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. +{{/enableTestToolByDefault}} +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +{{#enableTestToolByDefault}} +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/6658f342-6c27-447a-b791-2f2c400d48f9) +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/d4f9b455-dbb0-4e14-8557-59f9be5c1200) +{{/enableTestToolByDefault}} + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/bot.py`| Handles business logics for the Basic RAG Bot.| +|`src/config.py`| Defines the environment variables.| +|`src/app.py`| Main module of the Basic RAG Bot, hosts a aiohttp api server for the app.| +|`src/my_data_source.py`| Handles local customized text data search logics.| +|`src/data/*.md`| Raw text data source.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the Basic RAG Bot template with more AI capabilities + +You can follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities, like: +- [Customize prompt](https://aka.ms/teamsfx-basic-ai-chatbot#customize-prompt) +- [Customize user input](https://aka.ms/teamsfx-basic-ai-chatbot#customize-user-input) +- [Customize conversation history](https://aka.ms/teamsfx-basic-ai-chatbot#customize-conversation-history) +- [Customize model type](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-type) +- [Customize model parameters](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-parameters) +- [Handle messages with image](https://aka.ms/teamsfx-basic-ai-chatbot#handle-messages-with-image) + +## Additional information and references +- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/appPackage/color.png b/templates/python/custom-copilot-rag-customize/appPackage/color.png new file mode 100644 index 0000000000..2d7e85c9e9 Binary files /dev/null and b/templates/python/custom-copilot-rag-customize/appPackage/color.png differ diff --git a/templates/python/custom-copilot-rag-customize/appPackage/manifest.json.tpl b/templates/python/custom-copilot-rag-customize/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..1309b98c88 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/appPackage/manifest.json.tpl @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", + "manifestVersion": "1.16", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "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": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "full name for {{appName}}" + }, + "description": { + "short": "short description for {{appName}}", + "full": "full description for {{appName}}" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupchat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/python/custom-copilot-rag-customize/appPackage/outline.png b/templates/python/custom-copilot-rag-customize/appPackage/outline.png new file mode 100644 index 0000000000..e8cb4b6ba4 Binary files /dev/null and b/templates/python/custom-copilot-rag-customize/appPackage/outline.png differ diff --git a/templates/python/custom-copilot-rag-customize/env/.env.dev b/templates/python/custom-copilot-rag-customize/env/.env.dev new file mode 100644 index 0000000000..8172044cec --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.dev @@ -0,0 +1,17 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/env/.env.dev.user.tpl b/templates/python/custom-copilot-rag-customize/env/.env.dev.user.tpl new file mode 100644 index 0000000000..a13ba9fe10 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.dev.user.tpl @@ -0,0 +1,27 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +{{/useAzureOpenAI}} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/env/.env.local b/templates/python/custom-copilot-rag-customize/env/.env.local new file mode 100644 index 0000000000..589a4dea65 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.local @@ -0,0 +1,12 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +BOT_DOMAIN= +BOT_ENDPOINT= \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/env/.env.local.user.tpl b/templates/python/custom-copilot-rag-customize/env/.env.local.user.tpl new file mode 100644 index 0000000000..87043cc118 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.local.user.tpl @@ -0,0 +1,28 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# If you're adding a secret value, add SECRET_ prefix to the name so Teams Toolkit can handle them properly +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +{{/useAzureOpenAI}} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/env/.env.testtool b/templates/python/custom-copilot-rag-customize/env/.env.testtool new file mode 100644 index 0000000000..53abad07db --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/env/.env.testtool.user.tpl b/templates/python/custom-copilot-rag-customize/env/.env.testtool.user.tpl new file mode 100644 index 0000000000..23f2a3b952 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/env/.env.testtool.user.tpl @@ -0,0 +1,27 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# If you're adding a secret value, add SECRET_ prefix to the name so Teams Toolkit can handle them properly +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +{{#useOpenAI}} +{{#openAIKey}} +SECRET_OPENAI_API_KEY='{{{openAIKey}}}' +{{/openAIKey}} +{{^openAIKey}} +SECRET_OPENAI_API_KEY= +{{/openAIKey}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +{{#azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY='{{{azureOpenAIKey}}}' +{{/azureOpenAIKey}} +{{^azureOpenAIKey}} +SECRET_AZURE_OPENAI_API_KEY= +{{/azureOpenAIKey}} +AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= +{{#azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT='{{{azureOpenAIEndpoint}}}' +{{/azureOpenAIEndpoint}} +{{^azureOpenAIEndpoint}} +AZURE_OPENAI_ENDPOINT= +{{/azureOpenAIEndpoint}} +{{/useAzureOpenAI}} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/infra/azure.bicep.tpl b/templates/python/custom-copilot-rag-customize/infra/azure.bicep.tpl new file mode 100644 index 0000000000..1f22628590 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/infra/azure.bicep.tpl @@ -0,0 +1,117 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +{{#useAzureOpenAI}} +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string +{{/useAzureOpenAI}} +{{#useOpenAI}} +@secure() +@description('Required in your bot project to access OpenAI service. You can get it from OpenAI > API > API Key') +param openaiKey string +{{/useOpenAI}} + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + {{#useAzureOpenAI}} + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + {{/useAzureOpenAI}} + {{#useOpenAI}} + { + name: 'OPENAI_API_KEY' + value: openaiKey + } + {{/useOpenAI}} + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName diff --git a/templates/python/custom-copilot-rag-customize/infra/azure.parameters.json.tpl b/templates/python/custom-copilot-rag-customize/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..bde7e5185f --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/infra/azure.parameters.json.tpl @@ -0,0 +1,40 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "botAadAppClientId": { + "value": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + {{#useAzureOpenAI}} + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + {{/useAzureOpenAI}} + {{#useOpenAI}} + "openaiKey": { + "value": "${{SECRET_OPENAI_API_KEY}}" + }, + {{/useOpenAI}} + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "{{appName}}" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/infra/botRegistration/azurebot.bicep b/templates/python/custom-copilot-rag-customize/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..ab67c7a56b --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param botAadAppClientId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: botAadAppClientId + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/templates/python/custom-copilot-rag-customize/infra/botRegistration/readme.md b/templates/python/custom-copilot-rag-customize/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/app.py b/templates/python/custom-copilot-rag-customize/src/app.py new file mode 100644 index 0000000000..910476d014 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio +from http import HTTPStatus +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/bot.py.tpl b/templates/python/custom-copilot-rag-customize/src/bot.py.tpl new file mode 100644 index 0000000000..4a8d18ead3 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/bot.py.tpl @@ -0,0 +1,73 @@ +import os +import sys +import traceback + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.state import TurnState + +from my_data_source import MyDataSource + +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +{{#useAzureOpenAI}} +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) +{{/useAzureOpenAI}} +{{#useOpenAI}} +model = OpenAIModel( + OpenAIModelOptions( + api_key=config.OPENAI_API_KEY, + default_model=config.OPENAI_MODEL_NAME, + ) +) +{{/useOpenAI}} + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +my_data_source = MyDataSource('local-search') +prompts.add_data_source(my_data_source) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="chat") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/config.py.tpl b/templates/python/custom-copilot-rag-customize/src/config.py.tpl new file mode 100644 index 0000000000..6d21cec31f --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/config.py.tpl @@ -0,0 +1,26 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint + {{/useAzureOpenAI}} + {{#useOpenAI}} + OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] # OpenAI API key + OPENAI_MODEL_NAME='gpt-3.5-turbo' # OpenAI model name. You can use any other model name from OpenAI. + {{/useOpenAI}} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md new file mode 100644 index 0000000000..6878a8e204 --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md @@ -0,0 +1,48 @@ +# Contoso Electronics Company Overview + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## History + +Contoso Electronics, a pioneering force in the tech industry, was founded in 1985 by visionary entrepreneurs with a passion for innovation. Over the years, the company has played a pivotal role in shaping the landscape of consumer electronics. + +| Year | Milestone | +|------|-----------| +| 1985 | Company founded with a focus on cutting-edge technology | +| 1990 | Launched the first-ever handheld personal computer | +| 2000 | Introduced groundbreaking advancements in AI and robotics | +| 2015 | Expansion into sustainable and eco-friendly product lines | + +## Company Overview + +At Contoso Electronics, we take pride in fostering a dynamic and inclusive workplace. Our dedicated team of experts collaborates to create innovative solutions that empower and connect people globally. + +### Core Values + +- **Innovation:** Constantly pushing the boundaries of technology. +- **Diversity:** Embracing different perspectives for creative excellence. +- **Sustainability:** Committed to eco-friendly practices in our products. + +## Vacation Perks + +We believe in work-life balance and understand the importance of well-deserved breaks. Our vacation perks are designed to help our employees recharge and return with renewed enthusiasm. + +| Vacation Tier | Duration | Additional Benefits | +|---------------|----------|---------------------| +| Standard | 2 weeks | Health and wellness stipend | +| Senior | 4 weeks | Travel vouchers for a dream destination | +| Executive | 6 weeks | Luxury resort getaway with family | + +## Employee Recognition + +Recognizing the hard work and dedication of our employees is at the core of our culture. Here are some ways we celebrate achievements: + +- Monthly "Innovator of the Month" awards +- Annual gala with awards for outstanding contributions +- Team-building retreats for high-performing departments + +## Join Us! + +Contoso Electronics is always on the lookout for talented individuals who share our passion for innovation. If you're ready to be part of a dynamic team shaping the future of technology, check out our [careers page](http://www.contoso.com) for exciting opportunities. + +[Learn more about Contoso Electronics!](http://www.contoso.com) diff --git a/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md new file mode 100644 index 0000000000..1d97d5117e --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md @@ -0,0 +1,36 @@ +# Contoso Electronics PerksPlus Program + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Overview +Introducing PerksPlus - the ultimate benefits program designed to support the health and wellness of employees. With PerksPlus, employees have the opportunity to expense up to $1000 for fitness-related programs, making it easier and more affordable to maintain a healthy lifestyle. PerksPlus is not only designed to support employees' physical health, but also their mental health. Regular exercise has been shown to reduce stress, improve mood, and enhance overall well-being. With PerksPlus, employees can invest in their health and wellness, while enjoying the peace of mind that comes with knowing they are getting the support they need to lead a healthy life. +What is Covered? + +PerksPlus covers a wide range of fitness activities, including but not limited to: +* Gym memberships +* Personal training sessions +* Yoga and Pilates classes +* Fitness equipment purchases +* Sports team fees +* Health retreats and spas +* Outdoor adventure activities (such as rock climbing, hiking, and kayaking) +* Group fitness classes (such as dance, martial arts, and cycling) +* Virtual fitness programs (such as online yoga and workout classes) + +In addition to the wide range of fitness activities covered by PerksPlus, the program also covers a variety of lessons and experiences that promote health and wellness. Some of the lessons covered under PerksPlus include: +* Skiing and snowboarding lessons +* Scuba diving lessons +* Surfing lessons +* Horseback riding lessons + +These lessons provide employees with the opportunity to try new things, challenge themselves, and improve their physical skills. They are also a great way to relieve stress and have fun while staying active. + +With PerksPlus, employees can choose from a variety of fitness programs to suit their individual needs and preferences. Whether you're looking to improve your physical fitness, reduce stress, or just have some fun, PerksPlus has you covered. + +## What is Not Covered? +In addition to the wide range of activities covered by PerksPlus, there is also a list of things that are not +covered under the program. These include but are not limited to: +* Non-fitness related expenses +* Medical treatments and procedures +* Travel expenses (unless related to a fitness program) +* Food and supplements \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md new file mode 100644 index 0000000000..9da5c6429d --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md @@ -0,0 +1,37 @@ +# Contoso Electronics Plan and Benefit Packages + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Northwind Health Plus + +Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. + +This plan also offers coverage for emergency services, both in-network and out-of-network. + +## Northwind Standard + +Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +## Comparison of Plans + +Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings. + +Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand-name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs. + +Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses. + +Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests. + +Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services. + +## Cost Comparison + +Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics + +| | Northwind Standard | NorthWind Health Plus | +|---------------|----------|---------------------| +| Employee Only | $45.00 | $55.00 | +| Employee +1 | $65.00 | $71.00 | +| Employee +2 or more | $78.00 | $89.00 | \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/my_data_source.py b/templates/python/custom-copilot-rag-customize/src/my_data_source.py new file mode 100644 index 0000000000..30ce76871c --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/my_data_source.py @@ -0,0 +1,62 @@ +import os +from dataclasses import dataclass + +from teams.ai.tokenizers import Tokenizer +from teams.ai.data_sources import DataSource +from teams.state.state import TurnContext +from teams.state.memory import Memory + +@dataclass +class Result: + output: str + length: int + too_long: bool + +class MyDataSource(DataSource): + """ + A data source that searches through a local directory of files for a given query. + """ + + def __init__(self, name): + """ + Creates a new instance of the LocalDataSource instance. + Initializes the data source. + """ + self.name = name + + filePath = os.path.join(os.path.dirname(__file__), 'data') + files = os.listdir(filePath) + self._data = [open(os.path.join(filePath, file), 'r').read() for file in files] + + def name(self): + return self.name + + async def render_data(self, context: TurnContext, memory: Memory, tokenizer: Tokenizer, maxTokens: int): + """ + Renders the data source as a string of text. + The returned output should be a string of text that will be injected into the prompt at render time. + """ + query = memory.get('temp.input') + if not query: + return Result('', 0, False) + + result='' + # Text search + for data in self._data: + if query in data: + result += data + # Key word search + if 'history' in query.lower() or 'company' in query.lower(): + result += self._data[0] + if 'perksplus' in query.lower() or 'program' in query.lower(): + result += self._data[1] + if 'northwind' in query.lower() or 'health' in query.lower(): + result += self._data[2] + + return Result(self.formatDocument(result), len(result), False) if result!='' else Result('', 0, False) + + def formatDocument(self, result): + """ + Formats the result string + """ + return f"{result}" \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/prompts/chat/config.json b/templates/python/custom-copilot-rag-customize/src/prompts/chat/config.json new file mode 100644 index 0000000000..8166d24f3d --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/prompts/chat/config.json @@ -0,0 +1,23 @@ +{ + "schema": 1.1, + "description": "Chat with Teams RAG", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 4096, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "none", + "data_sources": { + "local-search": 1200 + } + } +} \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/prompts/chat/skprompt.txt b/templates/python/custom-copilot-rag-customize/src/prompts/chat/skprompt.txt new file mode 100644 index 0000000000..789155ad1b --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +The following is a conversation with an AI assistant, who is an expert on answering questions over the given context. +Responses should be in a short journalistic style with no more than 80 words. +Use the context provided in the `` tags as the source for your answers. \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/src/requirements.txt b/templates/python/custom-copilot-rag-customize/src/requirements.txt new file mode 100644 index 0000000000..1ba1feadad --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/src/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv +aiohttp +teams-ai~=1.0.1 \ No newline at end of file diff --git a/templates/python/custom-copilot-rag-customize/teamsapp.local.yml.tpl b/templates/python/custom-copilot-rag-customize/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..b53a0ae45d --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/teamsapp.local.yml.tpl @@ -0,0 +1,84 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: basicSearch + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + {{#useOpenAI}} + OPENAI_API_KEY: ${{SECRET_OPENAI_API_KEY}} + {{/useOpenAI}} + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + {{/useAzureOpenAI}} diff --git a/templates/python/custom-copilot-rag-customize/teamsapp.testtool.yml.tpl b/templates/python/custom-copilot-rag-customize/teamsapp.testtool.yml.tpl new file mode 100644 index 0000000000..fc4f31a8bd --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/teamsapp.testtool.yml.tpl @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.1.0-beta + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + {{#useOpenAI}} + OPENAI_API_KEY: ${{SECRET_OPENAI_API_KEY}} + {{/useOpenAI}} + {{#useAzureOpenAI}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + {{/useAzureOpenAI}} diff --git a/templates/python/custom-copilot-rag-customize/teamsapp.yml.tpl b/templates/python/custom-copilot-rag-customize/teamsapp.yml.tpl new file mode 100644 index 0000000000..fc2ad0967d --- /dev/null +++ b/templates/python/custom-copilot-rag-customize/teamsapp.yml.tpl @@ -0,0 +1,136 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{TEAMSFX_ENV}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-tab + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsfx deploy' is executed +deploy: + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: ./src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/ts/ai-assistant-bot/README.md.tpl b/templates/ts/ai-assistant-bot/README.md.tpl index 4c8e8a6be9..5f144f58be 100644 --- a/templates/ts/ai-assistant-bot/README.md.tpl +++ b/templates/ts/ai-assistant-bot/README.md.tpl @@ -118,4 +118,4 @@ You can follow [Get started with Teams AI library](https://learn.microsoft.com/e - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) -- [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) \ No newline at end of file +- [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) diff --git a/templates/ts/ai-assistant-bot/src/index.ts b/templates/ts/ai-assistant-bot/src/index.ts index 0d2f084c8e..f3672b16d0 100644 --- a/templates/ts/ai-assistant-bot/src/index.ts +++ b/templates/ts/ai-assistant-bot/src/index.ts @@ -33,17 +33,20 @@ const onTurnErrorHandler = async (context, error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity("The bot encountered an error or bug."); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/ts/ai-assistant-bot/teamsapp.local.yml.tpl b/templates/ts/ai-assistant-bot/teamsapp.local.yml.tpl index 1e877247d9..a5e20f2fb0 100644 --- a/templates/ts/ai-assistant-bot/teamsapp.local.yml.tpl +++ b/templates/ts/ai-assistant-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/ai-assistant-bot/teamsapp.yml.tpl b/templates/ts/ai-assistant-bot/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/ai-assistant-bot/teamsapp.yml.tpl +++ b/templates/ts/ai-assistant-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/ai-bot/README.md.tpl b/templates/ts/ai-bot/README.md.tpl index 3767ab0d3c..9517f8abf2 100644 --- a/templates/ts/ai-bot/README.md.tpl +++ b/templates/ts/ai-bot/README.md.tpl @@ -114,4 +114,4 @@ You can follow [Get started with Teams AI library](https://learn.microsoft.com/e - [Teams AI library](https://aka.ms/teams-ai-library) - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) -- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/ts/ai-bot/src/index.ts b/templates/ts/ai-bot/src/index.ts index 0d2f084c8e..f3672b16d0 100644 --- a/templates/ts/ai-bot/src/index.ts +++ b/templates/ts/ai-bot/src/index.ts @@ -33,17 +33,20 @@ const onTurnErrorHandler = async (context, error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity("The bot encountered an error or bug."); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/ts/ai-bot/teamsapp.local.yml.tpl b/templates/ts/ai-bot/teamsapp.local.yml.tpl index c67b760811..81d724d58c 100644 --- a/templates/ts/ai-bot/teamsapp.local.yml.tpl +++ b/templates/ts/ai-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/ai-bot/teamsapp.yml.tpl b/templates/ts/ai-bot/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/ai-bot/teamsapp.yml.tpl +++ b/templates/ts/ai-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/api-message-extension-sso/.funcignore b/templates/ts/api-message-extension-sso/.funcignore new file mode 100644 index 0000000000..8af9cc6227 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.funcignore @@ -0,0 +1,21 @@ +.funcignore +*.js.map +*.ts +.git* +.localConfigs +.vscode +local.settings.json +test +tsconfig.json +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +tsconfig.json +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/.gitignore b/templates/ts/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..0be3b0521b --- /dev/null +++ b/templates/ts/api-message-extension-sso/.gitignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/.vscode/extensions.json b/templates/ts/api-message-extension-sso/.vscode/extensions.json new file mode 100644 index 0000000000..aac0a6e347 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/ts/api-message-extension-sso/.vscode/launch.json b/templates/ts/api-message-extension-sso/.vscode/launch.json new file mode 100644 index 0000000000..9ad7575a0b --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/launch.json @@ -0,0 +1,95 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/ts/api-message-extension-sso/.vscode/settings.json b/templates/ts/api-message-extension-sso/.vscode/settings.json new file mode 100644 index 0000000000..0ed7b2e738 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/templates/ts/api-message-extension-sso/.vscode/tasks.json b/templates/ts/api-message-extension-sso/.vscode/tasks.json new file mode 100644 index 0000000000..a8b6b007d4 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/tasks.json @@ -0,0 +1,130 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + "domain": "OPENAPI_SERVER_DOMAIN" // output tunnel domain as OPENAPI_SERVER_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + }, + "dependsOn": "Watch backend" + }, + { + "label": "Watch backend", + "type": "shell", + "command": "npm run watch:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$tsc-watch", + "presentation": { + "reveal": "silent" + } + } + ] +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/README.md b/templates/ts/api-message-extension-sso/README.md new file mode 100644 index 0000000000..dbaa25d815 --- /dev/null +++ b/templates/ts/api-message-extension-sso/README.md @@ -0,0 +1,60 @@ +# Overview of Custom Search Results app template + +## Build a message extension from a new API with Azure Functions + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)` from the launch configuration dropdown. +4. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## What's included in the template + +| Folder | Contents | +| ------------ | ----------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the repair API | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------- | +| `src/functions/repair.ts` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/responseTemplates/repair.json` | A generated Adaptive Card that used to render API response. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | + +## How Microsoft Entra works + +![microsoft-entra-flow](https://github.com/OfficeDev/TeamsFx/assets/107838226/846e7a60-8cc1-4d8b-852e-2aec93b61fe9) + +> **Note**: The Azure Active Directory (AAD) flow is only functional in remote environments. It cannot be tested in a local environment due to the lack of authentication support in Azure Function core tools. + +## Addition information and references + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) diff --git a/templates/ts/api-message-extension-sso/aad.manifest.json.tpl b/templates/ts/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/ts/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..f4d0ab88ca --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Returns a repair + description: Returns a repair with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: string + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/color.png b/templates/ts/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000..2d7e85c9e9 Binary files /dev/null and b/templates/ts/api-message-extension-sso/appPackage/color.png differ diff --git a/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/outline.png b/templates/ts/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000..245fa194db Binary files /dev/null and b/templates/ts/api-message-extension-sso/appPackage/outline.png differ diff --git a/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/ts/api-message-extension-sso/env/.env.dev b/templates/ts/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..b83a22d12f --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.dev @@ -0,0 +1,19 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.dev.user b/templates/ts/api-message-extension-sso/env/.env.dev.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.dev.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.local b/templates/ts/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..1ff4229ff7 --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.local @@ -0,0 +1,18 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PACKAGE_PATH= +FUNC_ENDPOINT= +API_FUNCTION_ENDPOINT= +TEAMS_APP_TENANT_ID= +TEAMS_APP_UPDATE_TIME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= + +# Generated during deploy, you can also add your own variables. +FUNC_PATH= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.local.user b/templates/ts/api-message-extension-sso/env/.env.local.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.local.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/host.json b/templates/ts/api-message-extension-sso/host.json new file mode 100644 index 0000000000..9df913614d --- /dev/null +++ b/templates/ts/api-message-extension-sso/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/infra/azure.bicep b/templates/ts/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..9532cee661 --- /dev/null +++ b/templates/ts/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/local.settings.json b/templates/ts/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..7e3601ca41 --- /dev/null +++ b/templates/ts/api-message-extension-sso/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/package.json.tpl b/templates/ts/api-message-extension-sso/package.json.tpl new file mode 100644 index 0000000000..db882e6b86 --- /dev/null +++ b/templates/ts/api-message-extension-sso/package.json.tpl @@ -0,0 +1,23 @@ +{ + "name": "{{SafeProjectNameLowerCase}}", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "build": "tsc", + "watch:teamsfx": "tsc --watch", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "@types/node": "^20.11.26", + "typescript": "^5.4.2" + }, + "main": "dist/src/functions/*.js" +} diff --git a/templates/ts/api-message-extension-sso/src/functions/repair.ts b/templates/ts/api-message-extension-sso/src/functions/repair.ts new file mode 100644 index 0000000000..27fbecc0f9 --- /dev/null +++ b/templates/ts/api-message-extension-sso/src/functions/repair.ts @@ -0,0 +1,56 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions + * developer guide. + */ + +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; + +import repairRecords from "../repairsData.json"; + +/** + * This function handles the HTTP request and returns the repair information. + * + * @param {HttpRequest} req - The HTTP request. + * @param {InvocationContext} context - The Azure Functions context object. + * @returns {Promise} - A promise that resolves with the HTTP response containing the repair information. + */ +export async function repair( + req: HttpRequest, + context: InvocationContext +): Promise { + context.log("HTTP trigger function processed a request."); + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + jsonBody: { + results: [], + }, + }; + + // Get the assignedTo query parameter. + const assignedTo = req.query.get("assignedTo"); + + // If the assignedTo query parameter is not provided, return the response. + if (!assignedTo) { + return res; + } + + // Filter the repair information by the assignedTo query parameter. + const repairs = repairRecords.filter((item) => { + const fullName = item.assignedTo.toLowerCase(); + const query = assignedTo.trim().toLowerCase(); + const [firstName, lastName] = fullName.split(" "); + return fullName === query || firstName === query || lastName === query; + }); + + // Return filtered repair records, or an empty array if no records were found. + res.jsonBody.results = repairs ?? []; + return res; +} + +app.http("repair", { + methods: ["GET"], + authLevel: "anonymous", + handler: repair, +}); diff --git a/templates/ts/api-message-extension-sso/src/repairsData.json b/templates/ts/api-message-extension-sso/src/repairsData.json new file mode 100644 index 0000000000..fd4227e475 --- /dev/null +++ b/templates/ts/api-message-extension-sso/src/repairsData.json @@ -0,0 +1,50 @@ +[ + { + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "id": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "id": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + { + "id": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "id": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + { + "id": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] diff --git a/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..33495c4121 --- /dev/null +++ b/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=repair"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/templates/ts/api-message-extension-sso/teamsapp.yml.tpl b/templates/ts/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..2548124de8 --- /dev/null +++ b/templates/ts/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,178 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/ts/api-message-extension-sso/tsconfig.json b/templates/ts/api-message-extension-sso/tsconfig.json new file mode 100644 index 0000000000..a8d695680c --- /dev/null +++ b/templates/ts/api-message-extension-sso/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "typeRoots": ["./node_modules/@types"] + } +} \ No newline at end of file diff --git a/templates/ts/api-plugin-from-scratch/.vscode/launch.json b/templates/ts/api-plugin-from-scratch/.vscode/launch.json index 9ad7575a0b..f5e8f96eb3 100644 --- a/templates/ts/api-plugin-from-scratch/.vscode/launch.json +++ b/templates/ts/api-plugin-from-scratch/.vscode/launch.json @@ -30,7 +30,7 @@ "internalConsoleOptions": "neverOpen" }, { - "name": "Preview in Teams (Edge)", + "name": "Preview in Copilot (Edge)", "type": "msedge", "request": "launch", "url": "https://teams.microsoft.com?${account-hint}", @@ -41,7 +41,7 @@ "internalConsoleOptions": "neverOpen" }, { - "name": "Preview in Teams (Chrome)", + "name": "Preview in Copilot (Chrome)", "type": "chrome", "request": "launch", "url": "https://teams.microsoft.com?${account-hint}", @@ -66,7 +66,7 @@ ], "compounds": [ { - "name": "Debug in Teams (Edge)", + "name": "Debug in Copilot (Edge)", "configurations": [ "Launch App in Teams (Edge)", "Attach to Backend" @@ -79,7 +79,7 @@ "stopAll": true }, { - "name": "Debug in Teams (Chrome)", + "name": "Debug in Copilot (Chrome)", "configurations": [ "Launch App in Teams (Chrome)", "Attach to Backend" diff --git a/templates/ts/api-plugin-from-scratch/README.md b/templates/ts/api-plugin-from-scratch/README.md index ca5478f8d0..6910063aad 100644 --- a/templates/ts/api-plugin-from-scratch/README.md +++ b/templates/ts/api-plugin-from-scratch/README.md @@ -1,8 +1,8 @@ -# Overview of API Plugin from New API Template +# Overview of the Copilot Plugin template -## Build an API Plugin from a new API with Azure Functions +## Build a Copilot Plugin from a new API with Azure Functions -This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: +This app template allows Microsoft Copilot for Microsoft 365 to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: - Retrieve real-time information, for example, latest news coverage on a product launch. - Retrieve knowledge-based information, for example, my team’s design files in Figma. @@ -16,6 +16,7 @@ This app template allows Teams to interact directly with third-party data, apps, > - [Node.js](https://nodejs.org/), supported versions: 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli) +> - [Copilot for Microsoft 365 license](https://learn.microsoft.com/microsoft-365-copilot/extensibility/prerequisites#prerequisites) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. @@ -30,16 +31,17 @@ This app template allows Teams to interact directly with third-party data, apps, | `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the repair API | +| `src` | The source code for the repair API | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| -------------------------------------------- | ------------------------------------------------------------------- | -| `src/functions/repair.ts` | The main file of a function in Azure Functions. | -| `src/repairsData.json` | The data source for the repair API. | -| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | -| `appPackage/ai-plugin.json` | The manifest file for the API plugin. | +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `src/functions/repair.ts` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/manifest.json` | Teams application manifest that defines metadata for your plugin inside Microsoft Teams. | +| `appPackage/ai-plugin.json` | The manifest file for your Copilot Plugin that contains information for your API and used by LLM. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. diff --git a/templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json b/templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl similarity index 96% rename from templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json rename to templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl index 7154b72c7c..fb11edba12 100644 --- a/templates/js/api-plugin-from-scratch/appPackage/ai-plugin.json +++ b/templates/ts/api-plugin-from-scratch/appPackage/ai-plugin.json.tpl @@ -1,6 +1,6 @@ { "schema_version": "v2", - "name_for_human": "Repair Search Plugin", + "name_for_human": "{{appName}}${{APP_NAME_SUFFIX}}", "description_for_human": "Track your repair records", "description_for_model": "Plugin for searching a repair list, you can search by who's assigned to the repair.", "functions": [ diff --git a/templates/ts/api-plugin-from-scratch/appPackage/manifest.json.tpl b/templates/ts/api-plugin-from-scratch/appPackage/manifest.json.tpl index 3ed557b7e7..9ebc549abb 100644 --- a/templates/ts/api-plugin-from-scratch/appPackage/manifest.json.tpl +++ b/templates/ts/api-plugin-from-scratch/appPackage/manifest.json.tpl @@ -23,7 +23,7 @@ "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." }, "accentColor": "#FFFFFF", - "apiPlugins": [ + "plugins": [ { "pluginFile": "ai-plugin.json" } diff --git a/templates/ts/command-and-response/package.json.tpl b/templates/ts/command-and-response/package.json.tpl index 71eec10a05..d688d97f2b 100644 --- a/templates/ts/command-and-response/package.json.tpl +++ b/templates/ts/command-and-response/package.json.tpl @@ -23,7 +23,7 @@ "url": "https://github.com" }, "dependencies": { - "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, diff --git a/templates/ts/command-and-response/teamsapp.local.yml.tpl b/templates/ts/command-and-response/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/ts/command-and-response/teamsapp.local.yml.tpl +++ b/templates/ts/command-and-response/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/command-and-response/teamsapp.yml.tpl b/templates/ts/command-and-response/teamsapp.yml.tpl index e30197f678..bc4c8c97be 100644 --- a/templates/ts/command-and-response/teamsapp.yml.tpl +++ b/templates/ts/command-and-response/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml b/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml index 28b61078db..32206ea0c3 100644 --- a/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml +++ b/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/apiSpecificationFile/repair.yml @@ -8,10 +8,9 @@ servers: description: The repair api server components: securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: x-api-key + apiKey: + type: http + scheme: bearer paths: /repair: get: @@ -26,7 +25,7 @@ paths: type: string required: false security: - - ApiKeyAuth: [] + - apiKey: [] responses: '200': description: A list of repairs diff --git a/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl b/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl index 6b7bc23629..2de42871af 100644 --- a/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl +++ b/templates/ts/copilot-plugin-from-scratch-api-key/appPackage/manifest.json.tpl @@ -50,7 +50,7 @@ "authorization": { "authType": "apiSecretServiceAuth", "apiSecretServiceAuthConfiguration": { - "apiSecretRegistrationId": "${{X_API_KEY_REGISTRATION_ID}}" + "apiSecretRegistrationId": "${{APIKEY_REGISTRATION_ID}}" } } } diff --git a/templates/ts/copilot-plugin-from-scratch-api-key/src/functions/repair.ts b/templates/ts/copilot-plugin-from-scratch-api-key/src/functions/repair.ts index 36b1317c8f..a1ccdebdbe 100644 --- a/templates/ts/copilot-plugin-from-scratch-api-key/src/functions/repair.ts +++ b/templates/ts/copilot-plugin-from-scratch-api-key/src/functions/repair.ts @@ -66,7 +66,7 @@ export async function repair( * @returns {boolean} - True if the request is authorized, false otherwise. */ function isApiKeyValid(req: HttpRequest): boolean { - const apiKey = req.headers.get("x-api-key"); + const apiKey = req.headers.get("Authorization")?.replace("Bearer ", "").trim(); return apiKey === process.env.API_KEY; } diff --git a/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl b/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl index 3ed2e96e6f..92a108e9ee 100644 --- a/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl +++ b/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.local.yml.tpl @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.4 +version: v1.5 provision: # Creates a Teams app @@ -25,7 +25,7 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID @@ -35,7 +35,18 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID + + # Update API KEY + - uses: apiKey/update + with: + # Name of the API Key + name: apiKey + # Teams app ID + appId: ${{TEAMS_APP_ID}} + # Path to OpenAPI description document + apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml + registrationId: ${{APIKEY_REGISTRATION_ID}} # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl b/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl index 05995a77bf..dbda5dd31e 100644 --- a/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl +++ b/templates/ts/copilot-plugin-from-scratch-api-key/teamsapp.yml.tpl @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.4 +version: v1.5 environmentFolderPath: ./env @@ -46,7 +46,7 @@ provision: - uses: apiKey/register with: # Name of the API Key - name: x-api-key + name: apiKey # Value of the API Key primaryClientSecret: ${{SECRET_API_KEY}} # Teams app ID @@ -56,7 +56,18 @@ provision: # Write the registration information of API Key into environment file for # the specified environment variable(s). writeToEnvironmentFile: - registrationId: X_API_KEY_REGISTRATION_ID + registrationId: APIKEY_REGISTRATION_ID + + # Update API KEY + - uses: apiKey/update + with: + # Name of the API Key + name: apiKey + # Teams app ID + appId: ${{TEAMS_APP_ID}} + # Path to OpenAPI description document + apiSpecPath: ./appPackage/apiSpecificationFile/repair.yml + registrationId: ${{APIKEY_REGISTRATION_ID}} # Validate using manifest schema - uses: teamsApp/validateManifest diff --git a/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl b/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl index 4da8a2bfde..febe966f0b 100644 --- a/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl +++ b/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl @@ -29,6 +29,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { diff --git a/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl b/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl index 31f3eccff7..632d4bfd69 100644 --- a/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl +++ b/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl b/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl +++ b/templates/ts/custom-copilot-assistant-assistants-api/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/custom-copilot-assistant-new/README.md.tpl b/templates/ts/custom-copilot-assistant-new/README.md.tpl index 008a25563b..fc2db97f92 100644 --- a/templates/ts/custom-copilot-assistant-new/README.md.tpl +++ b/templates/ts/custom-copilot-assistant-new/README.md.tpl @@ -33,7 +33,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -48,7 +48,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/ts/custom-copilot-assistant-new/package.json.tpl b/templates/ts/custom-copilot-assistant-new/package.json.tpl index 5639b594a6..20608e7df8 100644 --- a/templates/ts/custom-copilot-assistant-new/package.json.tpl +++ b/templates/ts/custom-copilot-assistant-new/package.json.tpl @@ -28,6 +28,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { diff --git a/templates/ts/custom-copilot-assistant-new/teamsapp.local.yml.tpl b/templates/ts/custom-copilot-assistant-new/teamsapp.local.yml.tpl index 86a5368f1f..1676b1c6f7 100644 --- a/templates/ts/custom-copilot-assistant-new/teamsapp.local.yml.tpl +++ b/templates/ts/custom-copilot-assistant-new/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/custom-copilot-assistant-new/teamsapp.yml.tpl b/templates/ts/custom-copilot-assistant-new/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/custom-copilot-assistant-new/teamsapp.yml.tpl +++ b/templates/ts/custom-copilot-assistant-new/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/custom-copilot-basic/README.md.tpl b/templates/ts/custom-copilot-basic/README.md.tpl index 914de5bba6..c933104000 100644 --- a/templates/ts/custom-copilot-basic/README.md.tpl +++ b/templates/ts/custom-copilot-basic/README.md.tpl @@ -16,13 +16,13 @@ The app template is built using the Teams AI library, which provides the capabil > > To run the Basic AI Chatbot template in your local dev machine, you will need: > -> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Node.js](https://nodejs.org/), supported versions: 16, 18. {{^enableTestToolByDefault}} -> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). {{/enableTestToolByDefault}} -> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli). {{#useOpenAI}} -> - An account with [OpenAI](https://platform.openai.com/) +> - An account with [OpenAI](https://platform.openai.com/). {{/useOpenAI}} {{#useAzureOpenAI}} > - Prepare your own [Azure OpenAI](https://aka.ms/oai/access) resource. @@ -34,7 +34,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -49,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/ts/custom-copilot-basic/package.json.tpl b/templates/ts/custom-copilot-basic/package.json.tpl index 5639b594a6..20608e7df8 100644 --- a/templates/ts/custom-copilot-basic/package.json.tpl +++ b/templates/ts/custom-copilot-basic/package.json.tpl @@ -28,6 +28,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { diff --git a/templates/ts/custom-copilot-basic/teamsapp.local.yml.tpl b/templates/ts/custom-copilot-basic/teamsapp.local.yml.tpl index 86a5368f1f..1676b1c6f7 100644 --- a/templates/ts/custom-copilot-basic/teamsapp.local.yml.tpl +++ b/templates/ts/custom-copilot-basic/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/custom-copilot-basic/teamsapp.yml.tpl b/templates/ts/custom-copilot-basic/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/custom-copilot-basic/teamsapp.yml.tpl +++ b/templates/ts/custom-copilot-basic/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl index a158111b63..b0c9a92d85 100644 --- a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl @@ -1,20 +1,20 @@ -# Overview of the Basic AI Chatbot template +# Overview of the Custom Copilot from Custom API template -This template showcases a bot app that responds to user questions like ChatGPT. This enables your users to talk with the AI bot in Teams. +This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. - -- [Overview of the Basic AI Chatbot template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Basic AI Chatbot template](#get-started-with-the-basic-ai-chatbot-template) + +- [Overview of the Custom Copilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Copilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-basic-ai-chatbot-template-with-more-ai-capabilities) + - [Extend the Custom Copilot from Custom API template with more APIs](#extend-the-custom-copilot-from-custom-api-template-with-more-apis) - [Additional information and references](#additional-information-and-references) -## Get started with the Basic AI Chatbot template +## Get started with the Custom Copilot from Custom API template > **Prerequisites** > -> To run the Basic AI Chatbot template in your local dev machine, you will need: +> To run the Custom Copilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} @@ -34,15 +34,14 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. -1. In `src/app/app.ts`, update `azureDefaultDeployment` to your own model deployment name. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT=` and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. **Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: -![Basic AI Chatbot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/81f985a1-b81d-4c27-a82a-73a9b65ece1f) {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't yet. @@ -50,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT= and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. @@ -58,7 +57,7 @@ The app template is built using the Teams AI library, which provides the capabil **Congratulations**! You are running an application that can now interact with users in Teams: -![Basic AI Chatbot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/19f4c825-c296-4d29-a957-bedb88b6aa5b) {{/enableTestToolByDefault}} ## What's included in the template @@ -67,6 +66,7 @@ The app template is built using the Teams AI library, which provides the capabil | - | - | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | +| `appPackage/apiSpecificationFile` | Generated API spec file | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | | `src` | The source code for the application | @@ -75,12 +75,14 @@ The following files can be customized and demonstrate an example implementation | File | Contents | | - | - | -|`src/index.ts`| Sets up the bot app server.| -|`src/adapter.ts`| Sets up the bot adapter.| -|`src/config.ts`| Defines the environment variables.| +|`src/index.js`| Sets up the bot app server.| +|`src/adapter.js`| Sets up the bot adapter.| +|`src/config.js`| Defines the environment variables.| |`src/prompts/chat/skprompt.txt`| Defines the prompt.| |`src/prompts/chat/config.json`| Configures the prompt.| -|`src/app/app.ts`| Handles business logics for the Basic AI Chatbot.| +|`src.primpts/chat/actions.json`| List of available actions.| +|`src/app/app.js`| Handles business logics for the AI bot.| +|`src/app/utility.js`| Utility methods for the AI bot.| The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. @@ -90,9 +92,72 @@ The following are Teams Toolkit specific project files. You can [visit a complet |`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| |`teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| -## Extend the Basic AI Chatbot template with more AI capabilities - -You can follow [Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities. +## Extend the Custom Copilot from Custom API template with more APIs + +You can follow the following steps to extend the Custom Copilot from Custom API template with more APIs. + +1. Update `./appPackage/apiSpecificationFile/openapi.*` + + Copy corresponding part of the API you want to add from your spec, and append to `./appPackage/apiSpecificationFile/openapi.*`. + +1. Update `./src/prompts/chat/actions.json` + + Fill necessary info and properties for path, query and/or body for the API in the following object, and add it in the array in `./src/prompts/chat/actions.json`. + ``` + { + "name": "${{YOUR-API-NAME}}", + "description": "${{YOUR-API-DESCRIPTION}}", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "${{YOUR-PROPERTY-NAME}}": { + "type": "${{YOUR-PROPERTY-TYPE}}", + "description": "${{YOUR-PROPERTY-DESCRIPTION}}", + } + // You can add more query properties here + } + }, + "path": { + // Same as query properties + }, + "body": { + // Same as query properties + } + } + } + } + ``` + +1. Update `./src/adaptiveCards` + + Create a new file with name `${{YOUR-API-NAME}}.json`, and fill in the adaptive card for the API response of your API. + +1. Update `./src/app/app.ts` + + Add following code before `export default app;`. Remember to replace necessary info. + + ``` + app.ai.action(${{YOUR-API-NAME}}, async (context: TurnContext, state: ApplicationTurnState, parameter: any) => { + const client = await api.getClient(); + + const path = client.paths[${{YOUR-API-PATH}}]; + if (path && path.${{YOUR-API-METHOD}}) { + const result = await path.${{YOUR-API-METHOD}}(parameter.path, parameter.body, { + params: parameter.query, + }); + const card = generateAdaptiveCard("../adaptiveCards/${{YOUR-API-NAME}}.json", result); + await context.sendActivity({ attachments: [card] }); + } else { + await context.sendActivity("no result"); + } + return "result"; + }); + ``` + +1. Run `Local Debug` or `Provision` and `Deploy` to run this app again. ## Additional information and references - [Teams AI library](https://aka.ms/teams-ai-library) diff --git a/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl b/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl index eaeeb233a6..81e8292e6f 100644 --- a/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl @@ -24,7 +24,7 @@ "value": "${{AZURE_OPENAI_ENDPOINT}}" }, "azureOpenAIDeployment": { - "value": "${{AZURE_OPENAI_DEPLOYMENT}} + "value": "${{AZURE_OPENAI_DEPLOYMENT}}" }, {{/useAzureOpenAI}} "webAppSKU": { diff --git a/templates/ts/custom-copilot-rag-custom-api/package.json.tpl b/templates/ts/custom-copilot-rag-custom-api/package.json.tpl index 5b2048de69..3dc5806536 100644 --- a/templates/ts/custom-copilot-rag-custom-api/package.json.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/package.json.tpl @@ -16,7 +16,7 @@ "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", - "build": "tsc --build && shx cp -r ./src/prompts ./lib/src", + "build": "tsc --build && shx cp -r ./src/prompts ./lib/src && shx cp -r ./appPackage ./lib/appPackage && shx cp -r src/adaptiveCards ./lib/src", "start": "node ./lib/src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "watch": "nodemon --exec \"npm run start\"" diff --git a/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts b/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts index 1cf10f4bb8..a0d306983b 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts +++ b/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts @@ -40,8 +40,7 @@ const onTurnErrorHandler = async (context, error) => { ); // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + await context.sendActivity(`The bot encountered an error or bug: ${error.message}`); } }; diff --git a/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl b/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl index efb64db0ec..4893014ba0 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl @@ -37,7 +37,7 @@ const app = new Application({ }, }); -import { generateAdaptiveCard } from "./utility"; +import { generateAdaptiveCard, addAuthConfig } from "./utility"; import { TurnContext, ConversationState } from "botbuilder"; import { TurnState, Memory } from "@microsoft/teams-ai"; import yaml from "js-yaml"; diff --git a/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts b/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts index f14c93e5d5..00ae6c1c61 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts +++ b/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts @@ -1,5 +1,6 @@ import { CardFactory } from "botbuilder"; const ACData = require("adaptivecards-templating"); +import { OpenAPIClient } from "openapi-client-axios"; export function generateAdaptiveCard(templatePath: string, result: any) { if (!result || !result.data) { throw new Error("Get empty result from api call."); @@ -12,3 +13,28 @@ export function generateAdaptiveCard(templatePath: string, result: any) { const card = CardFactory.adaptiveCard(cardContent); return card; } + +export function addAuthConfig(client: OpenAPIClient) { + // This part is sample code for adding authentication to the client. + // Please replace it with your own authentication logic. + // Please refer to https://openapistack.co/docs/openapi-client-axios/intro/ for more info about the client. + /* + client.interceptors.request.use((config) => { + // You can specify different authentication methods for different urls and methods. + if (config.url == "your-url" && config.method == "your-method") { + // You can update the target url + config.url = "your-new-url"; + + // For Basic Authentication + config.headers["Authorization"] = `Basic ${btoa("Your-Username:Your-Password")}`; + + // For Cookie + config.headers["Cookie"] = `Your-Cookie`; + + // For Bearer Token + config.headers["Authorization"] = `Bearer "Your-Token"`; + } + return config; + }); + */ +} diff --git a/templates/ts/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl b/templates/ts/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl index 4a07c05f81..267e41f9e0 100644 --- a/templates/ts/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/custom-copilot-rag-custom-api/teamsapp.yml.tpl b/templates/ts/custom-copilot-rag-custom-api/teamsapp.yml.tpl index ad88b620cc..b253e3db1b 100644 --- a/templates/ts/custom-copilot-rag-custom-api/teamsapp.yml.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/dashboard-tab/README.md b/templates/ts/dashboard-tab/README.md index 3a0cf4794a..65b5b2c411 100644 --- a/templates/ts/dashboard-tab/README.md +++ b/templates/ts/dashboard-tab/README.md @@ -14,7 +14,7 @@ This template showcases an app that embeds a canvas containing multiple cards th > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -29,7 +29,7 @@ This template showcases an app that embeds a canvas containing multiple cards th ## What's included in the template | Folder | Contents | -| - | -| +| ------------ | --------------------------------------------------- | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | | `env` | Environment files | @@ -39,7 +39,7 @@ This template showcases an app that embeds a canvas containing multiple cards th The following files can be customized and demonstrate an example implementation to get you started. | File | Contents | -| - | -| +| ------------------------------------ | -------------------------------------------------- | | `src/models/chartModel.ts` | Data model for the chart widget | | `src/models/listModel.ts` | Data model for the list widget | | `src/services/chartService.ts` | A data retrive implementation for the chart widget | @@ -54,18 +54,18 @@ The following files can be customized and demonstrate an example implementation The following are project-related files. You generally will not need to customize these files. -| File | Contents | -| - | - | -| `src/index.css` | The style of application entry point | -| `src/index.tsx` | Application entry point | -| `src/internal/context.ts` | TeamsFx Context | +| File | Contents | +| ------------------------- | ------------------------------------ | +| `src/index.css` | The style of application entry point | +| `src/index.tsx` | Application entry point | +| `src/internal/context.ts` | TeamsFx Context | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions.| -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the Dashboard template to add a new widget @@ -108,8 +108,8 @@ export const getSampleData = (): SampleModel => { Create a widget file in the `src/widgets` folder. Inherit the `BaseWidget` class from `@microsoft/teamsfx-react`. The following table lists the methods that you can override to customize your widget. -| Methods | Function | -| - | -| +| Methods | Function | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `getData()` | This method is used to get the data for the widget. You can implement it to get data from the backend service or from the Microsoft Graph API | | `header()` | Customize the content of the widget header | | `body()` | Customize the content of the widget body | @@ -189,6 +189,7 @@ override layout(): JSX.Element | undefined { ); } ``` + Congratulations, you've just added your own widget! To learn more about the dashboard template, [visit the documentation](https://aka.ms/teamsfx-dashboard-new). You can find more scenarios like: - [Customize the widget](https://aka.ms/teamsfx-dashboard-new#customize-the-widget) diff --git a/templates/ts/default-bot-message-extension/README.md b/templates/ts/default-bot-message-extension/README.md index 2f0621af79..3fe4da7e04 100644 --- a/templates/ts/default-bot-message-extension/README.md +++ b/templates/ts/default-bot-message-extension/README.md @@ -34,7 +34,8 @@ This is a simple hello world application with both Bot and Message extension cap ## Edit the manifest You can find the Teams app manifest in `./appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -42,8 +43,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEPLOYMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -87,33 +88,29 @@ This template provides some sample functionality: - You can create and send an adaptive card. - ![CreateCard](./images/AdaptiveCard.png) + ![CreateCard](https://github.com/OfficeDev/TeamsFx/assets/86260893/a0a8304b-3074-4eb8-9097-655cdda0b937) - You can share a message in an adaptive card form. - ![ShareMessage](./images/ShareMessage.png) + ![ShareMessage](https://github.com/OfficeDev/TeamsFx/assets/86260893/a7d4dd7b-6466-4e89-8f42-b93629a90bc8) - You can paste a link that "unfurls" (`.botframework.com` is monitored in this template) and a card will be rendered. - ![ComposeArea](./images/LinkUnfurlingImage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/2b155dc8-9c01-4f14-8e2f-d179b81e97c6) To trigger these functions, there are multiple entry points: -- `@mention` Your message extension, from the `search box area`. - - ![AtBotFromSearch](./images/AtBotFromSearch.png) - -- `@mention` your message extension from the `compose message area`. +- Type a `/` in the command box and select your message extension. - ![AtBotFromMessage](./images/AtBotInMessage.png) + ![AtBotFromSearch](https://github.com/OfficeDev/TeamsFx/assets/86260893/d9ee7f72-0248-4a35-ae4d-e09d447614e6) - Click the `...` under compose message area, find your message extension. - ![ComposeArea](./images/ThreeDot.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/f447f015-bb68-4ae2-9e0a-aae69c00c328) - Click the `...` next to any messages you received or sent. - ![ComposeArea](./images/ThreeDotOnMessage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/0237dc5a-8b4d-4f52-a2fb-95ad17264c90) ## Further reading diff --git a/templates/ts/default-bot-message-extension/images/AdaptiveCard.png b/templates/ts/default-bot-message-extension/images/AdaptiveCard.png deleted file mode 100644 index 98cfad6eef..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/AdaptiveCard.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/AtBotFromSearch.png b/templates/ts/default-bot-message-extension/images/AtBotFromSearch.png deleted file mode 100644 index 5cf1bf5502..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/AtBotFromSearch.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/AtBotInMessage.png b/templates/ts/default-bot-message-extension/images/AtBotInMessage.png deleted file mode 100644 index e5f8767e1f..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/AtBotInMessage.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/LinkUnfurlingImage.png b/templates/ts/default-bot-message-extension/images/LinkUnfurlingImage.png deleted file mode 100644 index f288ff5f70..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/LinkUnfurlingImage.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/ShareMessage.png b/templates/ts/default-bot-message-extension/images/ShareMessage.png deleted file mode 100644 index 702769abc7..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/ShareMessage.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/ThreeDot.png b/templates/ts/default-bot-message-extension/images/ThreeDot.png deleted file mode 100644 index bbc1df4ff8..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/ThreeDot.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/images/ThreeDotOnMessage.png b/templates/ts/default-bot-message-extension/images/ThreeDotOnMessage.png deleted file mode 100644 index f7e8c43f83..0000000000 Binary files a/templates/ts/default-bot-message-extension/images/ThreeDotOnMessage.png and /dev/null differ diff --git a/templates/ts/default-bot-message-extension/teamsapp.local.yml.tpl b/templates/ts/default-bot-message-extension/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/ts/default-bot-message-extension/teamsapp.local.yml.tpl +++ b/templates/ts/default-bot-message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/default-bot-message-extension/teamsapp.yml.tpl b/templates/ts/default-bot-message-extension/teamsapp.yml.tpl index 79baa383e4..dab8a15ec4 100644 --- a/templates/ts/default-bot-message-extension/teamsapp.yml.tpl +++ b/templates/ts/default-bot-message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/default-bot/README.md.tpl b/templates/ts/default-bot/README.md.tpl index f85231f5a3..2b7b52ab7e 100644 --- a/templates/ts/default-bot/README.md.tpl +++ b/templates/ts/default-bot/README.md.tpl @@ -38,7 +38,7 @@ A bot interaction can be a quick question and answer, or it can be a complex con **Congratulations**! You are running an application that can now interact with users in Teams: -![basic bot](https://github.com/OfficeDev/TeamsFx/assets/25220706/8f5645ed-1cd9-43fd-9513-b0c9697d7dc0) +![basic bot](https://github.com/OfficeDev/TeamsFx/assets/25220706/170096d2-b353-4d4e-b55a-2c8ae4d97514) {{/enableTestToolByDefault}} ## What's included in the template @@ -79,5 +79,5 @@ Following documentation will help you to extend the Basic Bot template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/ts/default-bot/index.ts b/templates/ts/default-bot/index.ts index 74af87c3d3..555a979b25 100644 --- a/templates/ts/default-bot/index.ts +++ b/templates/ts/default-bot/index.ts @@ -36,17 +36,20 @@ const onTurnErrorHandler = async (context: TurnContext, error: Error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity(`The bot encountered unhandled error:\n ${error.message}`); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity(`The bot encountered unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/ts/default-bot/teamsapp.local.yml.tpl b/templates/ts/default-bot/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/ts/default-bot/teamsapp.local.yml.tpl +++ b/templates/ts/default-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/default-bot/teamsapp.yml.tpl b/templates/ts/default-bot/teamsapp.yml.tpl index 79baa383e4..dab8a15ec4 100644 --- a/templates/ts/default-bot/teamsapp.yml.tpl +++ b/templates/ts/default-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/link-unfurling/.gitignore b/templates/ts/link-unfurling/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/ts/link-unfurling/.gitignore +++ b/templates/ts/link-unfurling/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/ts/link-unfurling/.localConfigs.testTool b/templates/ts/link-unfurling/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/ts/link-unfurling/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/ts/link-unfurling/.vscode/launch.json b/templates/ts/link-unfurling/.vscode/launch.json deleted file mode 100644 index 6a87b6a80d..0000000000 --- a/templates/ts/link-unfurling/.vscode/launch.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Remote in Teams (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "presentation": { - "group": "group 1: Teams", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Teams (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "presentation": { - "group": "group 1: Teams", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Outlook (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "presentation": { - "group": "group 2: Outlook", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Outlook (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "presentation": { - "group": "group 2: Outlook", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Teams (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Teams (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Outlook (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Outlook (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Attach to Local Service", - "type": "node", - "request": "attach", - "port": 9239, - "restart": true, - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - } - ], - "compounds": [ - { - "name": "Debug in Teams (Edge)", - "configurations": [ - "Launch App in Teams (Edge)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 1: Teams", - "order": 1 - }, - "stopAll": true - }, - { - "name": "Debug in Teams (Chrome)", - "configurations": [ - "Launch App in Teams (Chrome)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 1: Teams", - "order": 2 - }, - "stopAll": true - }, - { - "name": "Debug in Outlook (Edge)", - "configurations": [ - "Launch App in Outlook (Edge)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 2: Outlook", - "order": 1 - }, - "stopAll": true - }, - { - "name": "Debug in Outlook (Chrome)", - "configurations": [ - "Launch App in Outlook (Chrome)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 2: Outlook", - "order": 2 - }, - "stopAll": true - } - ] -} diff --git a/templates/js/m365-message-extension/.vscode/launch.json b/templates/ts/link-unfurling/.vscode/launch.json.tpl similarity index 90% rename from templates/js/m365-message-extension/.vscode/launch.json rename to templates/ts/link-unfurling/.vscode/launch.json.tpl index 6a87b6a80d..a729ee63f8 100644 --- a/templates/js/m365-message-extension/.vscode/launch.json +++ b/templates/ts/link-unfurling/.vscode/launch.json.tpl @@ -115,6 +115,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ @@ -168,4 +185,4 @@ "stopAll": true } ] -} +} \ No newline at end of file diff --git a/templates/ts/link-unfurling/.vscode/tasks.json b/templates/ts/link-unfurling/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/ts/link-unfurling/.vscode/tasks.json +++ b/templates/ts/link-unfurling/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/ts/link-unfurling/.webappignore b/templates/ts/link-unfurling/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/ts/link-unfurling/.webappignore +++ b/templates/ts/link-unfurling/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/ts/link-unfurling/README.md b/templates/ts/link-unfurling/README.md index 23b6e01a05..4efd38d1d5 100644 --- a/templates/ts/link-unfurling/README.md +++ b/templates/ts/link-unfurling/README.md @@ -20,22 +20,22 @@ This template showcases an app that unfurls a link into an adaptive card when UR ## What's included in the template -| Folder / File | Contents | -| - | - | -| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | -| `teamsapp.local.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging | -| `.vscode/` | VSCode files for local debug | -| `src/` | The source code for the link unfurling application | -| `appPackage/` | Templates for the Teams application manifest | -| `infra/` | Templates for provisioning Azure resources | +| Folder / File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging | +| `.vscode/` | VSCode files for local debug | +| `src/` | The source code for the link unfurling application | +| `appPackage/` | Templates for the Teams application manifest | +| `infra/` | Templates for provisioning Azure resources | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -| `src/index.ts` | Application entry point and `restify` handlers | -| `src/linkUnfurlingApp.ts`| The teams activity handler | -| `src/adaptiveCards/helloWorldCard.json` | The adaptive card | +| File | Contents | +| --------------------------------------- | ---------------------------------------------- | +| `src/index.ts` | Application entry point and `restify` handlers | +| `src/linkUnfurlingApp.ts` | The teams activity handler | +| `src/adaptiveCards/helloWorldCard.json` | The adaptive card | ## Extend this template diff --git a/templates/ts/link-unfurling/env/.env.testtool b/templates/ts/link-unfurling/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/ts/link-unfurling/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/ts/link-unfurling/package.json.tpl b/templates/ts/link-unfurling/package.json.tpl index f5911a6fd6..afa9f5d970 100644 --- a/templates/ts/link-unfurling/package.json.tpl +++ b/templates/ts/link-unfurling/package.json.tpl @@ -10,6 +10,8 @@ "main": "./lib/src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", "build": "tsc --build", "start": "node ./lib/src/index.js", diff --git a/templates/ts/link-unfurling/teamsapp.local.yml.tpl b/templates/ts/link-unfurling/teamsapp.local.yml.tpl index 5c7f136898..ac5b78b935 100644 --- a/templates/ts/link-unfurling/teamsapp.local.yml.tpl +++ b/templates/ts/link-unfurling/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/link-unfurling/teamsapp.testtool.yml b/templates/ts/link-unfurling/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/ts/link-unfurling/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/ts/link-unfurling/teamsapp.yml.tpl b/templates/ts/link-unfurling/teamsapp.yml.tpl index 42655c2cc1..f4f04c1ca2 100644 --- a/templates/ts/link-unfurling/teamsapp.yml.tpl +++ b/templates/ts/link-unfurling/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/m365-message-extension/.gitignore b/templates/ts/m365-message-extension/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/ts/m365-message-extension/.gitignore +++ b/templates/ts/m365-message-extension/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/ts/m365-message-extension/.localConfigs.testTool b/templates/ts/m365-message-extension/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/ts/m365-message-extension/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/ts/m365-message-extension/.vscode/launch.json b/templates/ts/m365-message-extension/.vscode/launch.json deleted file mode 100644 index 6a87b6a80d..0000000000 --- a/templates/ts/m365-message-extension/.vscode/launch.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Remote in Teams (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "presentation": { - "group": "group 1: Teams", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Teams (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "presentation": { - "group": "group 1: Teams", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Outlook (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "presentation": { - "group": "group 2: Outlook", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch Remote in Outlook (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "presentation": { - "group": "group 2: Outlook", - "order": 3 - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Teams (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Teams (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Outlook (Edge)", - "type": "msedge", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Launch App in Outlook (Chrome)", - "type": "chrome", - "request": "launch", - "url": "https://outlook.office.com/mail?${account-hint}", - "cascadeTerminateToConfigurations": [ - "Attach to Local Service" - ], - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - }, - { - "name": "Attach to Local Service", - "type": "node", - "request": "attach", - "port": 9239, - "restart": true, - "presentation": { - "group": "all", - "hidden": true - }, - "internalConsoleOptions": "neverOpen" - } - ], - "compounds": [ - { - "name": "Debug in Teams (Edge)", - "configurations": [ - "Launch App in Teams (Edge)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 1: Teams", - "order": 1 - }, - "stopAll": true - }, - { - "name": "Debug in Teams (Chrome)", - "configurations": [ - "Launch App in Teams (Chrome)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 1: Teams", - "order": 2 - }, - "stopAll": true - }, - { - "name": "Debug in Outlook (Edge)", - "configurations": [ - "Launch App in Outlook (Edge)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 2: Outlook", - "order": 1 - }, - "stopAll": true - }, - { - "name": "Debug in Outlook (Chrome)", - "configurations": [ - "Launch App in Outlook (Chrome)", - "Attach to Local Service" - ], - "preLaunchTask": "Start Teams App Locally", - "presentation": { - "group": "group 2: Outlook", - "order": 2 - }, - "stopAll": true - } - ] -} diff --git a/templates/ts/m365-message-extension/.vscode/launch.json.tpl b/templates/ts/m365-message-extension/.vscode/launch.json.tpl new file mode 100644 index 0000000000..a729ee63f8 --- /dev/null +++ b/templates/ts/m365-message-extension/.vscode/launch.json.tpl @@ -0,0 +1,188 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Edge)", + "configurations": [ + "Launch App in Outlook (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Chrome)", + "configurations": [ + "Launch App in Outlook (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/ts/m365-message-extension/.vscode/tasks.json b/templates/ts/m365-message-extension/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/ts/m365-message-extension/.vscode/tasks.json +++ b/templates/ts/m365-message-extension/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/ts/m365-message-extension/.webappignore b/templates/ts/m365-message-extension/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/ts/m365-message-extension/.webappignore +++ b/templates/ts/m365-message-extension/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/ts/m365-message-extension/README.md b/templates/ts/m365-message-extension/README.md index 511da284aa..210b71f8ea 100644 --- a/templates/ts/m365-message-extension/README.md +++ b/templates/ts/m365-message-extension/README.md @@ -11,7 +11,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -19,36 +19,36 @@ This app template is a search-based [message extension](https://docs.microsoft.c 3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. 5. To trigger the Message Extension, you can: - 1. In Teams: `@mention` Your message extension from the `search box area`, `@mention` your message extension from the `compose message area` or click the `...` under compose message area to find your message extension. + 1. In Teams: Click the `...` under compose message area to find your message extension. 2. In Outlook: click the `More apps` icon under compose email area to find your message extension. **Congratulations**! You are running an application that can now search npm registries in Teams and Outlook. -![Search app demo](https://user-images.githubusercontent.com/11220663/167868361-40ffaaa3-0300-4313-ae22-0f0bab49c329.png) +![Search app demo](https://github.com/OfficeDev/TeamsFx/assets/25220706/27fefae9-c51f-49af-a175-c8c9d5a71af0) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode/` | VSCode files for debugging | -| `appPackage/` | Templates for the Teams application manifest | -| `env/` | Environment files | -| `infra/` | Templates for provisioning Azure resources | -| `src/` | The source code for the search application | +| Folder | Contents | +| ------------- | -------------------------------------------- | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `env/` | Environment files | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the search application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/searchApp.ts`| Handles the business logic for this app template to query npm registry and return result list.| -|`src/index.ts`| `index.ts` is used to setup and configure the Message Extension.| +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------------------- | +| `src/searchApp.ts` | Handles the business logic for this app template to query npm registry and return result list. | +| `src/index.ts` | `index.ts` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -64,5 +64,5 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/ts/m365-message-extension/env/.env.testtool b/templates/ts/m365-message-extension/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/ts/m365-message-extension/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/ts/m365-message-extension/package.json.tpl b/templates/ts/m365-message-extension/package.json.tpl index 3116264b93..e027fedac0 100644 --- a/templates/ts/m365-message-extension/package.json.tpl +++ b/templates/ts/m365-message-extension/package.json.tpl @@ -10,6 +10,8 @@ "main": "./lib/src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", "build": "tsc --build", "start": "node ./lib/src/index.js", diff --git a/templates/ts/m365-message-extension/teamsapp.local.yml.tpl b/templates/ts/m365-message-extension/teamsapp.local.yml.tpl index 5c7f136898..ac5b78b935 100644 --- a/templates/ts/m365-message-extension/teamsapp.local.yml.tpl +++ b/templates/ts/m365-message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/m365-message-extension/teamsapp.testtool.yml b/templates/ts/m365-message-extension/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/ts/m365-message-extension/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/ts/m365-message-extension/teamsapp.yml.tpl b/templates/ts/m365-message-extension/teamsapp.yml.tpl index 42655c2cc1..f4f04c1ca2 100644 --- a/templates/ts/m365-message-extension/teamsapp.yml.tpl +++ b/templates/ts/m365-message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/message-extension-action/.gitignore b/templates/ts/message-extension-action/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/ts/message-extension-action/.gitignore +++ b/templates/ts/message-extension-action/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/ts/message-extension-action/.localConfigs.testTool b/templates/ts/message-extension-action/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/ts/message-extension-action/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/js/message-extension-action/.vscode/launch.json b/templates/ts/message-extension-action/.vscode/launch.json.tpl similarity index 84% rename from templates/js/message-extension-action/.vscode/launch.json rename to templates/ts/message-extension-action/.vscode/launch.json.tpl index 030f8cd628..a0369287c8 100644 --- a/templates/js/message-extension-action/.vscode/launch.json +++ b/templates/ts/message-extension-action/.vscode/launch.json.tpl @@ -65,6 +65,23 @@ } ], "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, { "name": "Debug in Teams (Edge)", "configurations": [ diff --git a/templates/ts/message-extension-action/.vscode/tasks.json b/templates/ts/message-extension-action/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/ts/message-extension-action/.vscode/tasks.json +++ b/templates/ts/message-extension-action/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/ts/message-extension-action/.webappignore b/templates/ts/message-extension-action/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/ts/message-extension-action/.webappignore +++ b/templates/ts/message-extension-action/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/ts/message-extension-action/README.md b/templates/ts/message-extension-action/README.md index 3d3c84f596..6c882335a6 100644 --- a/templates/ts/message-extension-action/README.md +++ b/templates/ts/message-extension-action/README.md @@ -18,35 +18,35 @@ This app template implements action command that allows you to present your user 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. 3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. -5. To trigger the action command, you can click the `...` under compose message area, click `...`-> `More actions` beside a message, or @ your message extension app from the command box. +5. To trigger the action command, you can click the `...` under compose message area to find your message extension. **Congratulations**! You are running an application that can share information in rich format by creating an Adaptive Card in Teams. -![action-ME](https://github.com/OfficeDev/TeamsFx/assets/11220663/4af867b1-0b4b-4665-ac43-badf56106d84) +![action-ME](https://github.com/OfficeDev/TeamsFx/assets/25220706/378ea4d7-9332-4aec-9f85-59891d086b80) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode/` | VSCode files for debugging | -| `appPackage/` | Templates for the Teams application manifest | -| `env/` | Environment files | -| `infra/` | Templates for provisioning Azure resources | -| `src/` | The source code for the action application | +| Folder | Contents | +| ------------- | -------------------------------------------- | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `env/` | Environment files | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the action application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/actionApp.ts`| Handles the business logic for this app template to collect form input and process data.| -|`src/index.ts`| `index.ts` is used to setup and configure the Message Extension.| +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------------- | +| `src/actionApp.ts` | Handles the business logic for this app template to collect form input and process data. | +| `src/index.ts` | `index.ts` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -62,5 +62,6 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) -- [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) \ No newline at end of file +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) +- [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) + diff --git a/templates/ts/message-extension-action/env/.env.testtool b/templates/ts/message-extension-action/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/ts/message-extension-action/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/ts/message-extension-action/package.json.tpl b/templates/ts/message-extension-action/package.json.tpl index 23f05939c2..5e670b7827 100644 --- a/templates/ts/message-extension-action/package.json.tpl +++ b/templates/ts/message-extension-action/package.json.tpl @@ -10,6 +10,8 @@ "main": "./lib/src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", "build": "tsc --build", "start": "node ./lib/src/index.js", diff --git a/templates/ts/message-extension-action/teamsapp.local.yml.tpl b/templates/ts/message-extension-action/teamsapp.local.yml.tpl index 68663eafc7..4be79436a7 100644 --- a/templates/ts/message-extension-action/teamsapp.local.yml.tpl +++ b/templates/ts/message-extension-action/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/message-extension-action/teamsapp.testtool.yml b/templates/ts/message-extension-action/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/ts/message-extension-action/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/ts/message-extension-action/teamsapp.yml.tpl b/templates/ts/message-extension-action/teamsapp.yml.tpl index a8d4a94100..3d1aa00ebb 100644 --- a/templates/ts/message-extension-action/teamsapp.yml.tpl +++ b/templates/ts/message-extension-action/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/message-extension-copilot/README.md b/templates/ts/message-extension-copilot/README.md index c004acea06..35de56dd36 100644 --- a/templates/ts/message-extension-copilot/README.md +++ b/templates/ts/message-extension-copilot/README.md @@ -24,7 +24,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c 4. To trigger the Message Extension through Copilot, you can: 1. Select `Debug in Copilot (Edge)` or `Debug in Copilot (Chrome)` from the launch configuration dropdown. 2. When Teams launches in the browser, click the `Apps` icon from Teams client left rail to open Teams app store and search for `Copilot`. - 3. Open the `Copilot` app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NPM package information. For example: `Find the npm package info on teamsfx-react`. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. @@ -70,6 +70,6 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) - [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) diff --git a/templates/ts/message-extension-copilot/teamsapp.local.yml.tpl b/templates/ts/message-extension-copilot/teamsapp.local.yml.tpl index 5c7f136898..ac5b78b935 100644 --- a/templates/ts/message-extension-copilot/teamsapp.local.yml.tpl +++ b/templates/ts/message-extension-copilot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/message-extension-copilot/teamsapp.yml.tpl b/templates/ts/message-extension-copilot/teamsapp.yml.tpl index 42655c2cc1..f4f04c1ca2 100644 --- a/templates/ts/message-extension-copilot/teamsapp.yml.tpl +++ b/templates/ts/message-extension-copilot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/message-extension/.gitignore b/templates/ts/message-extension/.gitignore index f998e96df8..b891a68cb1 100644 --- a/templates/ts/message-extension/.gitignore +++ b/templates/ts/message-extension/.gitignore @@ -2,6 +2,10 @@ env/.env.*.user env/.env.local .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools appPackage/build # dependencies diff --git a/templates/ts/message-extension/.localConfigs.testTool b/templates/ts/message-extension/.localConfigs.testTool new file mode 100644 index 0000000000..4a3e2fafad --- /dev/null +++ b/templates/ts/message-extension/.localConfigs.testTool @@ -0,0 +1,3 @@ +# A gitignored place holder file for local runtime configurations when debug in test tool +BOT_ID= +BOT_PASSWORD= \ No newline at end of file diff --git a/templates/ts/message-extension/.vscode/launch.json.tpl b/templates/ts/message-extension/.vscode/launch.json.tpl new file mode 100644 index 0000000000..a729ee63f8 --- /dev/null +++ b/templates/ts/message-extension/.vscode/launch.json.tpl @@ -0,0 +1,188 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/mail?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Test Tool (Preview)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { +{{#enableMETestToolByDefault}} + "group": "group 0: Teams App Test Tool", +{{/enableMETestToolByDefault}} +{{^enableMETestToolByDefault}} + "group": "group 3: Teams App Test Tool", +{{/enableMETestToolByDefault}} + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Edge)", + "configurations": [ + "Launch App in Outlook (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Chrome)", + "configurations": [ + "Launch App in Outlook (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/ts/message-extension/.vscode/tasks.json b/templates/ts/message-extension/.vscode/tasks.json index 585f86ae9a..53c41778d7 100644 --- a/templates/ts/message-extension/.vscode/tasks.json +++ b/templates/ts/message-extension/.vscode/tasks.json @@ -4,6 +4,105 @@ { "version": "2.0.0", "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, { "label": "Start Teams App Locally", "dependsOn": [ diff --git a/templates/ts/message-extension/.webappignore b/templates/ts/message-extension/.webappignore index 598c568c34..50d2cf4484 100644 --- a/templates/ts/message-extension/.webappignore +++ b/templates/ts/message-extension/.webappignore @@ -2,6 +2,10 @@ .fx .deployment .localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools .vscode *.js.map *.ts.map diff --git a/templates/ts/message-extension/README.md b/templates/ts/message-extension/README.md index 35b7106f62..cbba36e407 100644 --- a/templates/ts/message-extension/README.md +++ b/templates/ts/message-extension/README.md @@ -3,6 +3,7 @@ A Message Extension allows users to interact with your web service while composing messages in the Microsoft Teams client. Users can invoke your web service to assist message composition, from the message compose box, or from the search bar. This app template has a search command, an action command and a link unfurling. + 1. The search command allows users to search an external system and share results through the compose message area of the Microsoft Teams client. 2. The action command allows you to present your users with a modal pop-up called a task module in Teams. The task module collects or displays information, processes the interaction, and sends the information back to Teams. 3. With link unfurling, an app can unfurl a link into an adaptive card when URLs with a particular domain are pasted into the compose message area in Microsoft Teams or email body in Outlook. @@ -28,30 +29,30 @@ This app template has a search command, an action command and a link unfurling. ![Search app demo](https://user-images.githubusercontent.com/11220663/167868361-40ffaaa3-0300-4313-ae22-0f0bab49c329.png) -![action-ME](https://github.com/OfficeDev/TeamsFx/assets/11220663/4af867b1-0b4b-4665-ac43-badf56106d84) +![action-ME](https://github.com/OfficeDev/TeamsFx/assets/25220706/378ea4d7-9332-4aec-9f85-59891d086b80) ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`./src/teamsBot.ts`| Handles the business logic for this app template to query npm registry and return result list to Teams.| -|`./src/index.ts`| `index.ts` is used to setup and configure the Message Extension.| +| File | Contents | +| ------------------- | ------------------------------------------------------------------------------------------------------- | +| `./src/teamsBot.ts` | Handles the business logic for this app template to query npm registry and return result list to Teams. | +| `./src/index.ts` | `index.ts` is used to setup and configure the Message Extension. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the template @@ -67,5 +68,5 @@ Following documentation will help you to extend the template. - [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) - [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) - [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) -- [Develop with Teams Toolkit CLI](https://aka.ms/teamsfx-cli/debug) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) - [Preview the app on mobile clients](https://github.com/OfficeDev/TeamsFx/wiki/Run-and-debug-your-Teams-application-on-iOS-or-Android-client) diff --git a/templates/ts/message-extension/env/.env.testtool b/templates/ts/message-extension/env/.env.testtool new file mode 100644 index 0000000000..43ce12aad3 --- /dev/null +++ b/templates/ts/message-extension/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/ts/message-extension/package.json.tpl b/templates/ts/message-extension/package.json.tpl index 5f9755b377..4d9040d1e3 100644 --- a/templates/ts/message-extension/package.json.tpl +++ b/templates/ts/message-extension/package.json.tpl @@ -10,6 +10,8 @@ "main": "./lib/src/index.js", "scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", "build": "tsc --build", "start": "node ./lib/src/index.js", diff --git a/templates/ts/message-extension/teamsapp.local.yml.tpl b/templates/ts/message-extension/teamsapp.local.yml.tpl index ffde5a4f0b..3759e2bee3 100644 --- a/templates/ts/message-extension/teamsapp.local.yml.tpl +++ b/templates/ts/message-extension/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/message-extension/teamsapp.testtool.yml b/templates/ts/message-extension/teamsapp.testtool.yml new file mode 100644 index 0000000000..eaf11c0c74 --- /dev/null +++ b/templates/ts/message-extension/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.3/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.3 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.0-alpha + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/ts/message-extension/teamsapp.yml.tpl b/templates/ts/message-extension/teamsapp.yml.tpl index 42655c2cc1..f4f04c1ca2 100644 --- a/templates/ts/message-extension/teamsapp.yml.tpl +++ b/templates/ts/message-extension/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/non-sso-tab-default-bot/bot/README.md b/templates/ts/non-sso-tab-default-bot/bot/README.md index be8ee3422e..4b0cf775ed 100644 --- a/templates/ts/non-sso-tab-default-bot/bot/README.md +++ b/templates/ts/non-sso-tab-default-bot/bot/README.md @@ -34,7 +34,8 @@ This is a simple hello world application with both Bot and Message extension cap ## Edit the manifest You can find the Teams app manifest in `../appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -42,8 +43,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEPLOYMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -87,33 +88,29 @@ This template provides some sample functionality: - You can create and send an adaptive card. - ![CreateCard](./images/AdaptiveCard.png) + ![CreateCard](https://github.com/OfficeDev/TeamsFx/assets/86260893/a0a8304b-3074-4eb8-9097-655cdda0b937) - You can share a message in an adaptive card form. - ![ShareMessage](./images/ShareMessage.png) + ![ShareMessage](https://github.com/OfficeDev/TeamsFx/assets/86260893/a7d4dd7b-6466-4e89-8f42-b93629a90bc8) - You can paste a link that "unfurls" (`.botframework.com` is monitored in this template) and a card will be rendered. - ![ComposeArea](./images/LinkUnfurlingImage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/2b155dc8-9c01-4f14-8e2f-d179b81e97c6) To trigger these functions, there are multiple entry points: -- `@mention` Your message extension, from the `search box area`. - - ![AtBotFromSearch](./images/AtBotFromSearch.png) +- Type a `/` in the command box and select your message extension. -- `@mention` your message extension from the `compose message area`. - - ![AtBotFromMessage](./images/AtBotInMessage.png) + ![AtBotFromSearch](https://github.com/OfficeDev/TeamsFx/assets/86260893/d9ee7f72-0248-4a35-ae4d-e09d447614e6) - Click the `...` under compose message area, find your message extension. - ![ComposeArea](./images/ThreeDot.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/f447f015-bb68-4ae2-9e0a-aae69c00c328) - Click the `...` next to any messages you received or sent. - ![ComposeArea](./images/ThreeDotOnMessage.png) + ![ComposeArea](https://github.com/OfficeDev/TeamsFx/assets/86260893/0237dc5a-8b4d-4f52-a2fb-95ad17264c90) ## Further reading @@ -127,4 +124,5 @@ To trigger these functions, there are multiple entry points: - [Search Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command) - [Action Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command) -- [Link Unfurling](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet) \ No newline at end of file +- [Link Unfurling](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet) + diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/AdaptiveCard.png b/templates/ts/non-sso-tab-default-bot/bot/images/AdaptiveCard.png deleted file mode 100644 index 98cfad6eef..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/AdaptiveCard.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png b/templates/ts/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png deleted file mode 100644 index 5cf1bf5502..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/AtBotFromSearch.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/AtBotInMessage.png b/templates/ts/non-sso-tab-default-bot/bot/images/AtBotInMessage.png deleted file mode 100644 index e5f8767e1f..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/AtBotInMessage.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png b/templates/ts/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png deleted file mode 100644 index f288ff5f70..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/LinkUnfurlingImage.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/ShareMessage.png b/templates/ts/non-sso-tab-default-bot/bot/images/ShareMessage.png deleted file mode 100644 index 702769abc7..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/ShareMessage.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDot.png b/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDot.png deleted file mode 100644 index bbc1df4ff8..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDot.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png b/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png deleted file mode 100644 index f7e8c43f83..0000000000 Binary files a/templates/ts/non-sso-tab-default-bot/bot/images/ThreeDotOnMessage.png and /dev/null differ diff --git a/templates/ts/non-sso-tab-default-bot/bot/index.ts b/templates/ts/non-sso-tab-default-bot/bot/index.ts index 74af87c3d3..555a979b25 100644 --- a/templates/ts/non-sso-tab-default-bot/bot/index.ts +++ b/templates/ts/non-sso-tab-default-bot/bot/index.ts @@ -36,17 +36,20 @@ const onTurnErrorHandler = async (context: TurnContext, error: Error) => { // application insights. console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a trace activity, which will be displayed in Bot Framework Emulator - await context.sendTraceActivity( - "OnTurnError Trace", - `${error}`, - "https://www.botframework.com/schemas/error", - "TurnError" - ); + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); - // Send a message to the user - await context.sendActivity(`The bot encountered unhandled error:\n ${error.message}`); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + // Send a message to the user + await context.sendActivity(`The bot encountered unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } }; // Set the onTurnError for the singleton CloudAdapter. diff --git a/templates/ts/non-sso-tab-default-bot/tab/README.md b/templates/ts/non-sso-tab-default-bot/tab/README.md index 19ed3778a9..ec72151097 100644 --- a/templates/ts/non-sso-tab-default-bot/tab/README.md +++ b/templates/ts/non-sso-tab-default-bot/tab/README.md @@ -20,7 +20,8 @@ Microsoft Teams supports the ability to run web-based UI inside "custom tabs" th ## Edit the manifest You can find the Teams app manifest in `../appPackage` folder. The folder contains one manifest file: -* `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +- `manifest.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. @@ -28,8 +29,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the Teams Toolkit and click `Provision` from DEVELOPMENT section or open the command palette and select: `Teams: Provision`.
  • Open the Teams Toolkit and click `Deploy` or open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision --env dev`.
  • Run command: `teamsapp deploy --env dev`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. @@ -69,4 +70,5 @@ Once deployed, you may want to distribute your application to your organization' Microsoft Teams provides a mechanism by which an application can obtain the signed-in Teams user token to access Microsoft Graph (and other APIs). Teams Toolkit facilitates this interaction by abstracting some of the Microsoft Entra flows and integrations behind some simple, high-level APIs. This enables you to add single sign-on (SSO) features easily to your Teams application. -Please follow this [document](https://aka.ms/teamsfx-add-sso-new) to add single sign on for your project. \ No newline at end of file +Please follow this [document](https://aka.ms/teamsfx-add-sso-new) to add single sign on for your project. + diff --git a/templates/ts/non-sso-tab-default-bot/teamsapp.local.yml.tpl b/templates/ts/non-sso-tab-default-bot/teamsapp.local.yml.tpl index 4fea50c6e9..b17ee6f8f7 100644 --- a/templates/ts/non-sso-tab-default-bot/teamsapp.local.yml.tpl +++ b/templates/ts/non-sso-tab-default-bot/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/non-sso-tab-default-bot/teamsapp.yml.tpl b/templates/ts/non-sso-tab-default-bot/teamsapp.yml.tpl index d6cb43d60b..2f5da42b40 100644 --- a/templates/ts/non-sso-tab-default-bot/teamsapp.yml.tpl +++ b/templates/ts/non-sso-tab-default-bot/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/non-sso-tab/README.md b/templates/ts/non-sso-tab/README.md index 88a0ab03ef..c13fd9667f 100644 --- a/templates/ts/non-sso-tab/README.md +++ b/templates/ts/non-sso-tab/README.md @@ -11,7 +11,7 @@ This template showcases how Microsoft Teams supports the ability to run web-base > - [Node.js](https://nodejs.org/), supported versions: 16, 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -25,29 +25,29 @@ This template showcases how Microsoft Teams supports the ability to run web-base ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the Teams application | +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the Teams application | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| - | - | -|`src/static/scripts/teamsapp.js`|A script that calls `teamsjs` SDK to get the context of on which Microsoft 365 application your app is running.| -|`src/static/styles/custom.css`|css file for the app.| -|`src/static/views/hello.html`|html file for the app.| -|`src/app.ts`|Starting a restify server.| +| File | Contents | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `src/static/scripts/teamsapp.js` | A script that calls `teamsjs` SDK to get the context of on which Microsoft 365 application your app is running. | +| `src/static/styles/custom.css` | css file for the app. | +| `src/static/views/hello.html` | html file for the app. | +| `src/app.ts` | Starting a restify server. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions.| -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | ## Extend the Basic Tab template diff --git a/templates/ts/notification-http-timer-trigger/package.json.tpl b/templates/ts/notification-http-timer-trigger/package.json.tpl index 3c22097361..b086ab26d4 100644 --- a/templates/ts/notification-http-timer-trigger/package.json.tpl +++ b/templates/ts/notification-http-timer-trigger/package.json.tpl @@ -26,7 +26,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { @@ -37,4 +37,4 @@ "typescript": "^4.4.4", "shx": "^0.3.4" } -} \ No newline at end of file +} diff --git a/templates/ts/notification-http-timer-trigger/teamsapp.local.yml.tpl b/templates/ts/notification-http-timer-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/ts/notification-http-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/ts/notification-http-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/notification-http-timer-trigger/teamsapp.yml.tpl b/templates/ts/notification-http-timer-trigger/teamsapp.yml.tpl index 043ef7a0f1..b6ef810ca8 100644 --- a/templates/ts/notification-http-timer-trigger/teamsapp.yml.tpl +++ b/templates/ts/notification-http-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/notification-http-trigger/package.json.tpl b/templates/ts/notification-http-trigger/package.json.tpl index 3c22097361..b086ab26d4 100644 --- a/templates/ts/notification-http-trigger/package.json.tpl +++ b/templates/ts/notification-http-trigger/package.json.tpl @@ -26,7 +26,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { @@ -37,4 +37,4 @@ "typescript": "^4.4.4", "shx": "^0.3.4" } -} \ No newline at end of file +} diff --git a/templates/ts/notification-http-trigger/teamsapp.local.yml.tpl b/templates/ts/notification-http-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/ts/notification-http-trigger/teamsapp.local.yml.tpl +++ b/templates/ts/notification-http-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/notification-http-trigger/teamsapp.yml.tpl b/templates/ts/notification-http-trigger/teamsapp.yml.tpl index 043ef7a0f1..b6ef810ca8 100644 --- a/templates/ts/notification-http-trigger/teamsapp.yml.tpl +++ b/templates/ts/notification-http-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/notification-restify/package.json.tpl b/templates/ts/notification-restify/package.json.tpl index 3676f1bf2a..7bd4fda773 100644 --- a/templates/ts/notification-restify/package.json.tpl +++ b/templates/ts/notification-restify/package.json.tpl @@ -24,7 +24,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, @@ -37,4 +37,4 @@ "typescript": "^4.4.4", "shx": "^0.3.4" } -} \ No newline at end of file +} diff --git a/templates/ts/notification-restify/teamsapp.local.yml.tpl b/templates/ts/notification-restify/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/ts/notification-restify/teamsapp.local.yml.tpl +++ b/templates/ts/notification-restify/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/notification-restify/teamsapp.yml.tpl b/templates/ts/notification-restify/teamsapp.yml.tpl index e30197f678..bc4c8c97be 100644 --- a/templates/ts/notification-restify/teamsapp.yml.tpl +++ b/templates/ts/notification-restify/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/notification-timer-trigger/package.json.tpl b/templates/ts/notification-timer-trigger/package.json.tpl index 3c22097361..b086ab26d4 100644 --- a/templates/ts/notification-timer-trigger/package.json.tpl +++ b/templates/ts/notification-timer-trigger/package.json.tpl @@ -26,7 +26,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.3.1-alpha", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0" }, "devDependencies": { @@ -37,4 +37,4 @@ "typescript": "^4.4.4", "shx": "^0.3.4" } -} \ No newline at end of file +} diff --git a/templates/ts/notification-timer-trigger/teamsapp.local.yml.tpl b/templates/ts/notification-timer-trigger/teamsapp.local.yml.tpl index 59b585fbef..52de5734ec 100644 --- a/templates/ts/notification-timer-trigger/teamsapp.local.yml.tpl +++ b/templates/ts/notification-timer-trigger/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/notification-timer-trigger/teamsapp.yml.tpl b/templates/ts/notification-timer-trigger/teamsapp.yml.tpl index 043ef7a0f1..b6ef810ca8 100644 --- a/templates/ts/notification-timer-trigger/teamsapp.yml.tpl +++ b/templates/ts/notification-timer-trigger/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: diff --git a/templates/ts/office-addin/env/.env.dev b/templates/ts/office-addin/env/.env.dev index 8043fefee4..e25ded0f91 100644 --- a/templates/ts/office-addin/env/.env.dev +++ b/templates/ts/office-addin/env/.env.dev @@ -10,6 +10,6 @@ AZURE_RESOURCE_GROUP_NAME= RESOURCE_SUFFIX= # Generated during provision, you can also add your own variables. -AZURE_STATIC_WEB_APPS_RESOURCE_ID= +ADDIN_AZURE_STORAGE_RESOURCE_ID= ADDIN_DOMAIN= ADDIN_ENDPOINT= \ No newline at end of file diff --git a/templates/ts/office-addin/infra/azure.bicep b/templates/ts/office-addin/infra/azure.bicep index 72c2af26df..4876fd8c94 100644 --- a/templates/ts/office-addin/infra/azure.bicep +++ b/templates/ts/office-addin/infra/azure.bicep @@ -1,25 +1,27 @@ @maxLength(20) @minLength(4) param resourceBaseName string -param staticWebAppSku string +param storageSku string -param staticWebAppName string = resourceBaseName +param storageName string = resourceBaseName +param location string = resourceGroup().location -// Azure Static Web Apps that hosts your static web site -resource swa 'Microsoft.Web/staticSites@2022-09-01' = { - name: staticWebAppName - // SWA do not need location setting - location: 'centralus' +// Azure Storage that hosts your static web site +resource storage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + kind: 'StorageV2' + location: location + name: storageName + properties: { + supportsHttpsTrafficOnly: true + } sku: { - name: staticWebAppSku - tier: staticWebAppSku + name: storageSku } - properties:{} } -var siteDomain = swa.properties.defaultHostname +var siteDomain = replace(replace(storage.properties.primaryEndpoints.web, 'https://', ''), '/', '') // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. -output AZURE_STATIC_WEB_APPS_RESOURCE_ID string = swa.id +output ADDIN_AZURE_STORAGE_RESOURCE_ID string = storage.id // used in deploy stage output ADDIN_DOMAIN string = siteDomain output ADDIN_ENDPOINT string = 'https://${siteDomain}' diff --git a/templates/ts/office-addin/infra/azure.parameters.json b/templates/ts/office-addin/infra/azure.parameters.json index 0a6927bc1b..d815d71861 100644 --- a/templates/ts/office-addin/infra/azure.parameters.json +++ b/templates/ts/office-addin/infra/azure.parameters.json @@ -5,8 +5,8 @@ "resourceBaseName": { "value": "tab${{RESOURCE_SUFFIX}}" }, - "staticWebAppSku": { - "value": "Free" + "storageSku": { + "value": "Standard_LRS" } } } \ No newline at end of file diff --git a/templates/ts/office-addin/teamsapp.yml b/templates/ts/office-addin/teamsapp.yml index 21ab335502..9ffc3d066f 100644 --- a/templates/ts/office-addin/teamsapp.yml +++ b/templates/ts/office-addin/teamsapp.yml @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.4/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.4 +version: 1.0.0 environmentFolderPath: ./env @@ -32,13 +32,11 @@ provision: # will use bicep CLI in PATH if you remove this config. bicepCliVersion: v0.9.1 - # Get the deployment token from Azure Static Web Apps - - uses: azureStaticWebApps/getDeploymentToken + - uses: azureStorage/enableStaticWebsite with: - resourceId: ${{AZURE_STATIC_WEB_APPS_RESOURCE_ID}} - # Save deployment token to the environment file for the deployment action - writeToEnvironmentFile: - deploymentToken: SECRET_TAB_SWA_DEPLOYMENT_TOKEN + storageResourceId: ${{ADDIN_AZURE_STORAGE_RESOURCE_ID}} + indexPage: index.html + errorPage: error.html # Triggered when 'teamsapp deploy' is executed deploy: @@ -51,8 +49,14 @@ deploy: name: build app with: args: run build --if-present - # Deploy bits to Azure Static Web Apps - - uses: cli/runNpxCommand - name: deploy to Azure Static Web Apps + # Deploy bits to Azure Storage Static Website + - uses: azureStorage/deploy with: - args: '@azure/static-web-apps-cli deploy dist -d ${{SECRET_TAB_SWA_DEPLOYMENT_TOKEN}} --env production' + workingDirectory: . + # Deploy base folder + artifactFolder: dist + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{ADDIN_AZURE_STORAGE_RESOURCE_ID}} \ No newline at end of file diff --git a/templates/ts/office-json-addin/env/.env.dev b/templates/ts/office-json-addin/env/.env.dev index 8043fefee4..e25ded0f91 100644 --- a/templates/ts/office-json-addin/env/.env.dev +++ b/templates/ts/office-json-addin/env/.env.dev @@ -10,6 +10,6 @@ AZURE_RESOURCE_GROUP_NAME= RESOURCE_SUFFIX= # Generated during provision, you can also add your own variables. -AZURE_STATIC_WEB_APPS_RESOURCE_ID= +ADDIN_AZURE_STORAGE_RESOURCE_ID= ADDIN_DOMAIN= ADDIN_ENDPOINT= \ No newline at end of file diff --git a/templates/ts/office-json-addin/infra/azure.bicep b/templates/ts/office-json-addin/infra/azure.bicep index 72c2af26df..4876fd8c94 100644 --- a/templates/ts/office-json-addin/infra/azure.bicep +++ b/templates/ts/office-json-addin/infra/azure.bicep @@ -1,25 +1,27 @@ @maxLength(20) @minLength(4) param resourceBaseName string -param staticWebAppSku string +param storageSku string -param staticWebAppName string = resourceBaseName +param storageName string = resourceBaseName +param location string = resourceGroup().location -// Azure Static Web Apps that hosts your static web site -resource swa 'Microsoft.Web/staticSites@2022-09-01' = { - name: staticWebAppName - // SWA do not need location setting - location: 'centralus' +// Azure Storage that hosts your static web site +resource storage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + kind: 'StorageV2' + location: location + name: storageName + properties: { + supportsHttpsTrafficOnly: true + } sku: { - name: staticWebAppSku - tier: staticWebAppSku + name: storageSku } - properties:{} } -var siteDomain = swa.properties.defaultHostname +var siteDomain = replace(replace(storage.properties.primaryEndpoints.web, 'https://', ''), '/', '') // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. -output AZURE_STATIC_WEB_APPS_RESOURCE_ID string = swa.id +output ADDIN_AZURE_STORAGE_RESOURCE_ID string = storage.id // used in deploy stage output ADDIN_DOMAIN string = siteDomain output ADDIN_ENDPOINT string = 'https://${siteDomain}' diff --git a/templates/ts/office-json-addin/infra/azure.parameters.json b/templates/ts/office-json-addin/infra/azure.parameters.json index 0a6927bc1b..585e718632 100644 --- a/templates/ts/office-json-addin/infra/azure.parameters.json +++ b/templates/ts/office-json-addin/infra/azure.parameters.json @@ -1,12 +1,12 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "tab${{RESOURCE_SUFFIX}}" - }, - "staticWebAppSku": { - "value": "Free" - } + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "tab${{RESOURCE_SUFFIX}}" + }, + "storageSku": { + "value": "Standard_LRS" } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/templates/ts/office-json-addin/teamsapp.yml b/templates/ts/office-json-addin/teamsapp.yml index 21ab335502..24e9d883c0 100644 --- a/templates/ts/office-json-addin/teamsapp.yml +++ b/templates/ts/office-json-addin/teamsapp.yml @@ -51,8 +51,14 @@ deploy: name: build app with: args: run build --if-present - # Deploy bits to Azure Static Web Apps - - uses: cli/runNpxCommand - name: deploy to Azure Static Web Apps + # Deploy bits to Azure Storage Static Website + - uses: azureStorage/deploy with: - args: '@azure/static-web-apps-cli deploy dist -d ${{SECRET_TAB_SWA_DEPLOYMENT_TOKEN}} --env production' + workingDirectory: . + # Deploy base folder + artifactFolder: dist + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{ADDIN_AZURE_STORAGE_RESOURCE_ID}} diff --git a/templates/ts/office-xml-addin-excel-cf/README.md b/templates/ts/office-xml-addin-excel-cf/README.md index 73df7b256e..826492cb36 100644 --- a/templates/ts/office-xml-addin-excel-cf/README.md +++ b/templates/ts/office-xml-addin-excel-cf/README.md @@ -16,10 +16,14 @@ You can use this repository as a sample to base your own custom functions projec ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. ## Debugging custom functions @@ -35,27 +39,18 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` ## Additional resources - [Custom functions overview](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-overview) -- [Custom functions best practices](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-best-practices) - [Custom functions runtime](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-runtime) +- [Custom functions troubleshoot](https://learn.microsoft.com/en-us/office/dev/add-ins/excel/custom-functions-troubleshooting) - [Office Add-ins documentation](https://learn.microsoft.com/office/dev/add-ins/overview/office-add-ins) - More Office Add-ins samples at [OfficeDev on Github](https://github.com/officedev) diff --git a/templates/ts/office-xml-addin-excel-react/README.md b/templates/ts/office-xml-addin-excel-react/README.md index 44c7c3a31b..6234cdcfe6 100644 --- a/templates/ts/office-xml-addin-excel-react/README.md +++ b/templates/ts/office-xml-addin-excel-react/README.md @@ -9,10 +9,14 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.tsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-excel-sso/README.md b/templates/ts/office-xml-addin-excel-sso/README.md index f2d674b68c..ff224e6e54 100644 --- a/templates/ts/office-xml-addin-excel-sso/README.md +++ b/templates/ts/office-xml-addin-excel-sso/README.md @@ -9,6 +9,10 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-excel-taskpane/README.md b/templates/ts/office-xml-addin-excel-taskpane/README.md index a1ab4eba71..3591bc94af 100644 --- a/templates/ts/office-xml-addin-excel-taskpane/README.md +++ b/templates/ts/office-xml-addin-excel-taskpane/README.md @@ -9,10 +9,14 @@ Excel add-ins are integrations built by third parties into Excel by using [Excel ## Run and Debug Excel Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Excel application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-powerpoint-react/README.md b/templates/ts/office-xml-addin-powerpoint-react/README.md index 1e9dcbca7c..242775d081 100644 --- a/templates/ts/office-xml-addin-powerpoint-react/README.md +++ b/templates/ts/office-xml-addin-powerpoint-react/README.md @@ -9,10 +9,14 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Run and Debug PowerPoint Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.tsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-powerpoint-sso/README.md b/templates/ts/office-xml-addin-powerpoint-sso/README.md index 77a347f2aa..4f352cb769 100644 --- a/templates/ts/office-xml-addin-powerpoint-sso/README.md +++ b/templates/ts/office-xml-addin-powerpoint-sso/README.md @@ -9,6 +9,10 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-powerpoint-taskpane/README.md b/templates/ts/office-xml-addin-powerpoint-taskpane/README.md index b37a8d8ee7..92966a7600 100644 --- a/templates/ts/office-xml-addin-powerpoint-taskpane/README.md +++ b/templates/ts/office-xml-addin-powerpoint-taskpane/README.md @@ -9,10 +9,14 @@ PowerPoint add-ins are integrations built by third parties into PowerPoint by us ## Run and Debug PowerPoint Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the PowerPoint application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-word-react/README.md b/templates/ts/office-xml-addin-word-react/README.md index 6c49574584..ec226688c4 100644 --- a/templates/ts/office-xml-addin-word-react/README.md +++ b/templates/ts/office-xml-addin-word-react/README.md @@ -9,10 +9,14 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Run and Debug Word Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -24,18 +28,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.html` file contains the HTML markup for the task pane. - The `./src/taskpane/**/*.tsx` file contains the react code and Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-word-sso/README.md b/templates/ts/office-xml-addin-word-sso/README.md index 0cbdca9de6..f5f3593f78 100644 --- a/templates/ts/office-xml-addin-word-sso/README.md +++ b/templates/ts/office-xml-addin-word-sso/README.md @@ -9,6 +9,10 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Instructions +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + - Run the following command to configure single-sign on for your add-in project. ```shell @@ -19,7 +23,7 @@ npm run configure-sso - Build the project, start the local web server, and side-load your add-in in the previously selected Office client application by either of the following ways: - By hitting the `F5` key in Visual Studio Code. - - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. + - By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. > [!NOTE] @@ -44,18 +48,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/office-xml-addin-word-taskpane/README.md b/templates/ts/office-xml-addin-word-taskpane/README.md index 6a10c4a52b..2a3978330d 100644 --- a/templates/ts/office-xml-addin-word-taskpane/README.md +++ b/templates/ts/office-xml-addin-word-taskpane/README.md @@ -9,10 +9,14 @@ Word add-ins are integrations built by third parties into Word by using [Word Ja ## Run and Debug Word Add-in +Before run and start the debug, make sure that: +1. Close all opened Office Application windows. +2. Click the *`Check and Install Dependencies`* in Teams Toolkit extension sidebar. + You can run and debug this project by either of the following ways: - By hitting the `F5` key in Visual Studio Code. -- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension side bar. +- By clicking the *`Preview Your Add-in`* in Teams Toolkit extension sidebar. - By running with command `npm run start` in the terminal. @@ -25,18 +29,9 @@ The add-in project that you've created contains sample code for a basic task pan - The `./src/taskpane/taskpane.css` file contains the CSS that's applied to content in the task pane. - The `./src/taskpane/taskpane.ts` file contains the Office JavaScript API code that facilitates interaction between the task pane and the Word application. - -## Edit the manifest - -You can edit the manifest file by either of the following ways: - -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Edit Manifest`*. -- Directly edit and modify the content in `./manifest.xml`. - - ## Validate manifest You can check whether your manifest file is valid by either of the following ways: -- From Visual Studio Code: open Teams Toolkit extension side bar and click *`Validate Manifest`*. +- From Visual Studio Code: open Teams Toolkit extension sidebar and click *`Validate Manifest`*. - From Terminal: run the command `npx --yes office-addin-manifest validate manifest.xml` \ No newline at end of file diff --git a/templates/ts/spfx-tab/src/README.md b/templates/ts/spfx-tab/src/README.md index c8fb2fa809..d743ce8b98 100644 --- a/templates/ts/spfx-tab/src/README.md +++ b/templates/ts/spfx-tab/src/README.md @@ -15,23 +15,22 @@ The SharePoint Framework (SPFx) is a page and web part model that provides full > - An Microsoft 365 account. Get your own free Microsoft 365 tenant from [Microsoft 365 developer program](https://developer.microsoft.com/en-us/microsoft-365/dev-program) > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [TeamsFx CLI](https://aka.ms/teamsfx-toolkit-cli) - ## Solution -Solution|Author(s) ---------|--------- -folder name | Author details (name, company, twitter alias with link) +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | ## Version history -Version|Date|Comments --------|----|-------- -1.1|March 10, 2021|Update comment -1.0|January 29, 2021|Initial release +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | ## Disclaimer -**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** --- @@ -39,24 +38,26 @@ Version|Date|Comments 1. Open the project with VSCode, click `Provision` in LIFECYCLE panel of Teams Toolkit extension. - Or you can use TeamsFx CLI with running this cmd under your project path: - `teamsapp provision` + Or you can use TeamsFx CLI with running this cmd under your project path: + `teamsapp provision` - It will provision an app in Teams App Studio. You may need to login with your Microsoft 365 tenant admin account. + It will provision an app in Teams App Studio. You may need to login with your Microsoft 365 tenant admin account. 2. Build and Deploy your SharePoint Package. - - Click `Deploy` in LIFECYCLE panel of Teams Toolkit extension, or run `Teams: Deploy` from command palette. This will generate a SharePoint package (*.sppkg) under sharepoint/solution folder. - - Or you can use TeamsFx CLI with running this cmd under your project path: - `teamsapp deploy` - - After building the *.sppkg, the Teams Toolkit extension will upload and deploy it to your tenant App Catalog. Only tenant App Catalog site admin has permission to do it. You can create your test tenant following [Setup your Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant). + - Click `Deploy` in LIFECYCLE panel of Teams Toolkit extension, or run `Teams: Deploy` from command palette. This will generate a SharePoint package (\*.sppkg) under sharepoint/solution folder. + + Or you can use TeamsFx CLI with running this cmd under your project path: + `teamsapp deploy` + + - After building the \*.sppkg, the Teams Toolkit extension will upload and deploy it to your tenant App Catalog. Only tenant App Catalog site admin has permission to do it. You can create your test tenant following [Setup your Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant). + 3. Go back to Teams Toolkit extension, click `Teams: Publish` in LIFECYCLE panel. - Or you can use TeamsFx CLI with running this cmd under your project path: - `teamsapp publish` + Or you can use TeamsFx CLI with running this cmd under your project path: + `teamsapp publish` - You will find your app in [Microsoft Teams admin center](https://admin.teams.microsoft.com/policies/manage-apps). Enter your app name in the search box. Click the item and select `Publish` in the Publishing status. + You will find your app in [Microsoft Teams admin center](https://admin.teams.microsoft.com/policies/manage-apps). Enter your app name in the search box. Click the item and select `Publish` in the Publishing status. 4. You may need to wait for a few minutes after publishing your teams app. And then login to Teams, and you will find your app in the `Apps - Built for {your-tenant-name}` category. diff --git a/templates/ts/sso-tab-with-obo-flow/README.md b/templates/ts/sso-tab-with-obo-flow/README.md index ced5ce1f5d..31dc55a061 100644 --- a/templates/ts/sso-tab-with-obo-flow/README.md +++ b/templates/ts/sso-tab-with-obo-flow/README.md @@ -2,7 +2,7 @@ This app showcases how to craft a visually appealing web page that can be embedded in Microsoft Teams, Outlook and the Microsoft 365 app with React and Fluent UI. The app also enhances the end-user experiences with built-in single sign-on and data from Microsoft Graph. -This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to implement SSO, and uses Azure Function as middle-tier service, and make authenticated requests to call Graph from Azure Function. +This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to implement SSO, and uses Azure Functions as middle-tier service, and make authenticated requests to call Graph from Azure Functions. ## Get started with the React with Fluent UI template @@ -10,10 +10,10 @@ This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure > > To run the command bot template in your local dev machine, you will need: > -> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Node.js](https://nodejs.org/), supported versions: 18, 20 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) -> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. @@ -27,22 +27,22 @@ This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure ## What's included in the template -| Folder | Contents | -| - | - | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the Teams application manifest | -| `env` | Environment files | -| `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the frontend of the Tab application. Implemented with Fluent UI Framework. | -| `api` | The source code for the backend of the Tab application. Implemented single-sign-on with OBO flow using Azure Function. | +| Folder | Contents | +| ------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the frontend of the Tab application. Implemented with Fluent UI Framework. | +| `api` | The source code for the backend of the Tab application. Implemented single-sign-on with OBO flow using Azure Functions. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. -| File | Contents | -| - | - | -|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions.| -|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| -|`aad.manifest.json`|This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app.| +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | ## Extend the React with Fluent UI template diff --git a/templates/ts/sso-tab-with-obo-flow/api/README.md b/templates/ts/sso-tab-with-obo-flow/api/README.md index c6c7d8608a..66efa67234 100644 --- a/templates/ts/sso-tab-with-obo-flow/api/README.md +++ b/templates/ts/sso-tab-with-obo-flow/api/README.md @@ -10,15 +10,15 @@ Azure Functions are a great way to add server-side behaviors to any Teams applic ## Develop -The Teams Toolkit IDE Extension and TeamsFx CLI provide template code for you to get started with Azure Functions for your Teams application. Microsoft Teams Framework simplifies the task of establishing the user's identity within the Azure Function. +The Teams Toolkit IDE Extension and TeamsFx CLI provide template code for you to get started with Azure Functions for your Teams application. Microsoft Teams Framework simplifies the task of establishing the user's identity within the Azure Functions. The template handles calls from your Teams "custom tab" (client-side of your app), initializes the TeamsFx SDK to access the current user context, and demonstrates how to obtain a pre-authenticated Microsoft Graph Client. Microsoft Graph is the "data plane" of Microsoft 365 - you can use it to access content within Microsoft 365 in your company. With it you can read and write documents, SharePoint collections, Teams channels, and many other entities within Microsoft 365. Read more about [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview). -You can add your logic to the single Azure Function created by this template, as well as add more functions as necessary. See [Azure Functions developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference) for more information. +You can add your logic to the single Azure Functions created by this template, as well as add more functions as necessary. See [Azure Functions developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference) for more information. ### Call the Function -To call your Azure Function, the client sends an HTTP request with an SSO token in the `Authorization` header. Here is an example: +To call your Azure Functions, the client sends an HTTP request with an SSO token in the `Authorization` header. Here is an example: ```ts import { TeamsUserCredentialAuthConfig, TeamsUserCredential } from "@microsoft/teamsfx"; @@ -39,19 +39,19 @@ const response = await axios.default.get(endpoint + "/api/" + functionName, { ### Add More Functions -- From Visual Studio Code, open the command palette, select `Teams: Add Resources` and select `Azure Function App`. +- From Visual Studio Code, open the command palette, select `Teams: Add Resources` and select `Azure Functions App`. ## Change Node.js runtime version -By default, Teams Toolkit and TeamsFx CLI will provision an Azure function app with function runtime version 3, and node runtime version 12. You can change the node version through Azure Portal. +By default, Teams Toolkit and TeamsFx CLI will provision an Azure functions app with function runtime version 3, and node runtime version 12. You can change the node version through Azure Portal. - Sign in to [Azure Portal](https://azure.microsoft.com/). -- Find your application's resource group and Azure Function app resource. The resource group name and the Azure function app name are stored in your project configuration file `.fx/env.*.json`. You can find them by searching the key `resourceGroupName` and `functionAppName` in that file. -- After enter the home page of the Azure function app, you can find a navigation item called `Configuration` under `settings` group. +- Find your application's resource group and Azure Functions app resource. The resource group name and the Azure functions app name are stored in your project configuration file `.fx/env.*.json`. You can find them by searching the key `resourceGroupName` and `functionAppName` in that file. +- After enter the home page of the Azure Functions app, you can find a navigation item called `Configuration` under `settings` group. - Click `Configuration`, you would see a list of settings. Then click `WEBSITE_NODE_DEFAULT_VERSION` and update the value to `~16` or `~18` according to your requirement. - After Click `OK` button, don't forget to click `Save` button on the top of the page. -Then following requests sent to the Azure function app will be handled by new node runtime version. +Then following requests sent to the Azure Functions app will be handled by new node runtime version. ## Debug @@ -70,8 +70,8 @@ This file contains template arguments with `${{...}}` statements which will be r Deploy your project to Azure by following these steps: -| From Visual Studio Code | From TeamsFx CLI | -| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| From Visual Studio Code | From TeamsFx CLI | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | |
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the command palette and select: `Teams: Provision`.
  • Open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision`.
  • Run command `teamsapp deploy`.
| > Note: Provisioning and deployment may incur charges to your Azure Subscription. diff --git a/templates/ts/sso-tab-with-obo-flow/package.json.tpl b/templates/ts/sso-tab-with-obo-flow/package.json.tpl index 57238550f2..9f23cf8eb1 100644 --- a/templates/ts/sso-tab-with-obo-flow/package.json.tpl +++ b/templates/ts/sso-tab-with-obo-flow/package.json.tpl @@ -2,7 +2,7 @@ "name": "{{SafeProjectNameLowerCase}}", "version": "0.1.0", "engines": { - "node": "16 || 18" + "node": "18 || 20" }, "private": true, "dependencies": { diff --git a/templates/ts/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.tsx b/templates/ts/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.tsx index 7e41a65a38..94e6b39510 100644 --- a/templates/ts/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.tsx +++ b/templates/ts/sso-tab-with-obo-flow/src/components/sample/AzureFunctions.tsx @@ -23,14 +23,14 @@ async function callFunction(teamsUserCredential: TeamsUserCredential) { let funcErrorMsg = ""; if (err?.response?.status === 404) { - funcErrorMsg = `There may be a problem with the deployment of Azure Function App, please deploy Azure Function (Run command palette "Teams: Deploy") first before running this App`; + funcErrorMsg = `There may be a problem with the deployment of Azure Functions App, please deploy Azure Functions (Run command palette "Teams: Deploy") first before running this App`; } else if (err.message === "Network Error") { funcErrorMsg = - "Cannot call Azure Function due to network error, please check your network connection status and "; + "Cannot call Azure Functions due to network error, please check your network connection status and "; if (err.config?.url && err.config.url.indexOf("localhost") >= 0) { - funcErrorMsg += `make sure to start Azure Function locally (Run "npm run start" command inside api folder from terminal) first before running this App`; + funcErrorMsg += `make sure to start Azure Functions locally (Run "npm run start" command inside api folder from terminal) first before running this App`; } else { - funcErrorMsg += `make sure to provision and deploy Azure Function (Run command palette "Teams: Provision" and "Teams: Deploy") first before running this App`; + funcErrorMsg += `make sure to provision and deploy Azure Functions (Run command palette "Teams: Provision" and "Teams: Deploy") first before running this App`; } } else { funcErrorMsg = err.message; @@ -48,7 +48,7 @@ async function callFunction(teamsUserCredential: TeamsUserCredential) { export function AzureFunctions(props: { codePath?: string; docsUrl?: string }) { const [needConsent, setNeedConsent] = useState(false); const { codePath, docsUrl } = { - codePath: `api/${functionName}/index.ts`, + codePath: `api/src/functions/${functionName}.ts`, docsUrl: "https://aka.ms/teamsfx-azure-functions", ...props, }; @@ -72,14 +72,14 @@ export function AzureFunctions(props: { codePath?: string; docsUrl?: string }) { }); return (
-

Call your Azure Function

+

Call your Azure Functions

An Azure Functions app is running. Authorize this app and click below to call it for a response:

{!loading && ( )} {loading && ( @@ -90,7 +90,7 @@ export function AzureFunctions(props: { codePath?: string; docsUrl?: string }) { {!loading && !!data && !error &&
{JSON.stringify(data, null, 2)}
} {!loading && !data && !error &&
}
       {!loading && !!error && 
{(error as any).toString()}
} -

How to edit the Azure Function

+

How to edit the Azure Functions

See the code in {codePath} to add your business logic.

diff --git a/templates/ts/workflow/package.json.tpl b/templates/ts/workflow/package.json.tpl index 843e5c03a0..296e876422 100644 --- a/templates/ts/workflow/package.json.tpl +++ b/templates/ts/workflow/package.json.tpl @@ -24,7 +24,7 @@ }, "dependencies": { "@microsoft/adaptivecards-tools": "^1.0.0", - "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx": "^2.3.1", "botbuilder": "^4.20.0", "restify": "^10.0.0" }, diff --git a/templates/ts/workflow/teamsapp.local.yml.tpl b/templates/ts/workflow/teamsapp.local.yml.tpl index a886dfe614..5c51cea38d 100644 --- a/templates/ts/workflow/teamsapp.local.yml.tpl +++ b/templates/ts/workflow/teamsapp.local.yml.tpl @@ -15,15 +15,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Create or update the bot registration on dev.botframework.com - uses: botFramework/create diff --git a/templates/ts/workflow/teamsapp.yml.tpl b/templates/ts/workflow/teamsapp.yml.tpl index e30197f678..bc4c8c97be 100644 --- a/templates/ts/workflow/teamsapp.yml.tpl +++ b/templates/ts/workflow/teamsapp.yml.tpl @@ -18,15 +18,19 @@ provision: teamsAppId: TEAMS_APP_ID # Create or reuse an existing Microsoft Entra application for bot. - - uses: botAadApp/create + - uses: aadApp/create with: # The Microsoft Entra application's display name name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. - botId: BOT_ID + clientId: BOT_ID # The Microsoft Entra application's client secret created for bot. - botPassword: SECRET_BOT_PASSWORD + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID - uses: arm/deploy # Deploy given ARM templates parallelly. with: