diff --git a/cmd/app/add.go b/cmd/app/add.go index c049ee68..39eb8207 100644 --- a/cmd/app/add.go +++ b/cmd/app/add.go @@ -31,8 +31,12 @@ import ( // Handle to client's function used for testing var runAddCommandFunc = RunAddCommand + +// TODO: this points at apps.Add, which creates the app remotely, installs it, +// and adds it to the project's apps file, so the "install" name is a little +// misleading, but it's tolerable for now. var appInstallProdAppFunc = apps.Add -var appInstallDevAppFunc = apps.InstallLocalApp +var appInstallFunc = apps.Install var appSelectPromptFunc = prompts.AppSelectPrompt // Flags @@ -201,7 +205,7 @@ func printAddSuccess(clients *shared.ClientFactory, cmd *cobra.Command, appInsta func appInstall(ctx context.Context, clients *shared.ClientFactory, selection *prompts.SelectedApp, orgGrantWorkspaceID string) (types.App, types.InstallState, error) { if selection != nil && selection.App.IsDev { // Install local dev app to a team - installedApp, _, installState, err := appInstallDevAppFunc(ctx, clients, "", selection.Auth, selection.App) + installedApp, _, installState, err := appInstallFunc(ctx, clients, selection.Auth, selection.App, apps.InstallOptions{Dev: true}) return installedApp, installState, err } else { installState, installedApp, err := appInstallProdAppFunc(ctx, clients, selection.Auth, selection.App, orgGrantWorkspaceID) diff --git a/internal/pkg/apps/add.go b/internal/pkg/apps/add.go index 81e2462d..2ac891e1 100644 --- a/internal/pkg/apps/add.go +++ b/internal/pkg/apps/add.go @@ -70,7 +70,7 @@ func addAppRemotely(ctx context.Context, clients *shared.ClientFactory, auth typ app.TeamID = auth.TeamID } - app, installState, err := Install(ctx, clients, auth, CreateAppManifestAndInstall, app, orgGrantWorkspaceID) + app, _, installState, err := Install(ctx, clients, auth, app, InstallOptions{OrgGrantWorkspaceID: orgGrantWorkspaceID}) if err != nil { return installState, types.App{}, slackerror.Wrap(err, slackerror.ErrAppAdd) } diff --git a/internal/pkg/apps/install.go b/internal/pkg/apps/install.go index 88f7237d..c382601d 100644 --- a/internal/pkg/apps/install.go +++ b/internal/pkg/apps/install.go @@ -32,26 +32,30 @@ import ( "github.com/slackapi/slack-cli/internal/style" ) -// Constants for onlyCreateUpdateAppManifest parameter -const ( - CreateAppManifestOnly = true - CreateAppManifestAndInstall = false -) - const additionalManifestInfoNotice = "App manifest contains some components that may require additional information" -// Install installs the app to a team -func Install(ctx context.Context, clients *shared.ClientFactory, auth types.SlackAuth, onlyCreateUpdateAppManifest bool, app types.App, orgGrantWorkspaceID string) (types.App, types.InstallState, error) { +// InstallOptions configures how an app is installed to a workspace. +type InstallOptions struct { + // OrgGrantWorkspaceID is the workspace to grant org-wide app access, if any. + OrgGrantWorkspaceID string + // Dev installs a local (slack run) app when true, otherwise a deployed + // (slack deploy) app. It controls the display name, hosted manifest + // defaults, persistence location, and environment token handling. + Dev bool +} + +// Install installs an app to a workspace. +func Install(ctx context.Context, clients *shared.ClientFactory, auth types.SlackAuth, app types.App, opts InstallOptions) (types.App, api.DeveloperAppInstallResult, types.InstallState, error) { span, ctx := opentracing.StartSpanFromContext(ctx, "pkg.apps.install") defer span.Finish() manifestUpdates, err := shouldUpdateManifest(ctx, clients, app, auth) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } manifestCreates, err := shouldCreateManifest(ctx, clients, app) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } // Get the token for the authenticated workspace @@ -59,7 +63,7 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac token := auth.Token authSession, err := apiInterface.ValidateSession(ctx, token) if err != nil { - return types.App{}, "", slackerror.Wrap(err, slackerror.ErrInvalidAuth) + return types.App{}, api.DeveloperAppInstallResult{}, "", slackerror.Wrap(err, slackerror.ErrInvalidAuth) } // Set the user_id, team id, team_domain of team that app belongs to on context @@ -72,7 +76,7 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac clients.EventTracker.SetAuthUserID(*authSession.UserID) } if authSession.EnterpriseID != nil { - config.SetContextEnterpriseID(ctx, *authSession.EnterpriseID) + ctx = config.SetContextEnterpriseID(ctx, *authSession.EnterpriseID) clients.EventTracker.SetAuthEnterpriseID(*authSession.EnterpriseID) app.EnterpriseID = *authSession.EnterpriseID } @@ -83,28 +87,33 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac var slackManifest types.SlackYaml manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) if err != nil { - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates { slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor) if err != nil { - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } } else { slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID) if err != nil { - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } } manifest := slackManifest.AppManifest - if slackManifest.IsFunctionRuntimeSlackHosted() { + if opts.Dev { + appendLocalToDisplayName(&manifest) + if manifest.IsFunctionRuntimeSlackHosted() { + configureLocalManifest(ctx, clients, &manifest) + } + } else if slackManifest.IsFunctionRuntimeSlackHosted() { configureHostedManifest(ctx, clients, &manifest) } err = validateManifestForInstall(ctx, clients, token, app, manifest) if err != nil { - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } start := time.Now() @@ -120,7 +129,7 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac clients.IO.PrintDebug(ctx, "updating app %s", app.AppID) _, err := apiInterface.UpdateApp(ctx, token, app.AppID, manifest, clients.Config.ForceFlag, true) if err != nil { - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } case manifestCreates: _, _ = clients.IO.WriteOut().Write([]byte(style.Sectionf(style.TextSection{ @@ -134,7 +143,7 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac result, err := apiInterface.CreateApp(ctx, token, manifest, false) if err != nil { err = slackerror.Wrap(err, slackerror.ErrAppInstall) - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } clients.IO.PrintDebug(ctx, "created new app ID %s", result.AppID) @@ -142,45 +151,56 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac app.AppID = result.AppID app.TeamID = *authSession.TeamID app.TeamDomain = auth.TeamDomain - // TODO: add enterprise ID and user ID to app? See InstallLocalApp. - // app.EnterpriseID = config.GetContextEnterpriseID(ctx) + if authSession.EnterpriseID != nil { + app.EnterpriseID = *authSession.EnterpriseID + } + if authSession.UserID != nil { + app.UserID = *authSession.UserID + } } + if opts.Dev { + // specifically set app.IsDev to be true for dev installation + app.IsDev = true + } + + // save the updated or created app to the project's apps file if !clients.Config.SkipLocalFs() { - if err := clients.AppClient().SaveDeployed(ctx, app); err != nil { - return types.App{}, "", err + var err error + if opts.Dev { + err = clients.AppClient().SaveLocal(ctx, app) + } else { + err = clients.AppClient().SaveDeployed(ctx, app) + } + if err != nil { + return types.App{}, api.DeveloperAppInstallResult{}, "", err } } caches, err := shouldCacheManifest(ctx, clients, app) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } if caches { saved, err := clients.Config.ProjectConfig.Cache().GetManifestHash(ctx, app.AppID) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } upstream, err := clients.API().ExportAppManifest(ctx, auth.Token, app.AppID) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } hash, err := clients.Config.ProjectConfig.Cache().NewManifestHash(ctx, upstream.Manifest.AppManifest) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } if !hash.Equals(saved) { err := clients.Config.ProjectConfig.Cache().SetManifestHash(ctx, app.AppID, hash) if err != nil { - return types.App{}, "", err + return types.App{}, api.DeveloperAppInstallResult{}, "", err } } } - // Install the app to a workspace - if onlyCreateUpdateAppManifest { - return app, "", nil - } - botScopes := []string{} if manifest.OAuthConfig != nil { botScopes = manifest.OAuthConfig.Scopes.Bot @@ -201,20 +221,20 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac // Note - we use DeveloperAppInstall endpoint for both local (dev) runs // and hosted installs https://github.com/slackapi/slack-cli/pull/456#discussion_r830272175 - result, installState, err := apiInterface.DeveloperAppInstall(ctx, clients.IO, token, app, botScopes, outgoingDomains, orgGrantWorkspaceID, clients.Config.AutoRequestAAAFlag) + result, installState, err := apiInterface.DeveloperAppInstall(ctx, clients.IO, token, app, botScopes, outgoingDomains, opts.OrgGrantWorkspaceID, clients.Config.AutoRequestAAAFlag) if err != nil { err = slackerror.Wrap(err, slackerror.ErrAppInstall) - return app, "", err + return app, api.DeveloperAppInstallResult{}, "", err } if installState != types.InstallSuccess { printNonSuccessInstallState(ctx, clients, installState) - return app, installState, nil + return app, api.DeveloperAppInstallResult{}, installState, nil } if manifest.FunctionRuntime() != types.SlackHosted { if err := setAppEnvironmentTokens(ctx, clients, result); err != nil { - return app, installState, err + return app, result, installState, err } } @@ -241,7 +261,7 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac _, _ = clients.IO.WriteOut().Write([]byte(style.SectionSecondaryf("Finished in %.1fs", time.Since(start).Seconds()))) - return app, types.InstallSuccess, nil + return app, result, types.InstallSuccess, nil } func printNonSuccessInstallState(ctx context.Context, clients *shared.ClientFactory, installState types.InstallState) { @@ -338,203 +358,6 @@ func validateManifestForInstall(ctx context.Context, clients *shared.ClientFacto return nil } -// InstallLocalApp installs a non-hosted local app to a workspace. -func InstallLocalApp(ctx context.Context, clients *shared.ClientFactory, orgGrantWorkspaceID string, auth types.SlackAuth, app types.App) (types.App, api.DeveloperAppInstallResult, types.InstallState, error) { - span, ctx := opentracing.StartSpanFromContext(ctx, "installLocalApp") - defer span.Finish() - - manifestUpdates, err := shouldUpdateManifest(ctx, clients, app, auth) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - manifestCreates, err := shouldCreateManifest(ctx, clients, app) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - - apiInterface := clients.API() - token := auth.Token - authSession, err := apiInterface.ValidateSession(ctx, token) - if err != nil { - return app, api.DeveloperAppInstallResult{}, "", slackerror.Wrap(err, slackerror.ErrInvalidAuth) - } - - // Set the user_id, team id, team_domain of team that app belongs to on context - // TODO: we should probably pick one place to store team/user/enterprise ID - ctx = config.SetContextTeamID(ctx, *authSession.TeamID) - clients.EventTracker.SetAuthTeamID(*authSession.TeamID) - ctx = config.SetContextTeamDomain(ctx, auth.TeamDomain) - if authSession.UserID != nil { - ctx = config.SetContextUserID(ctx, *authSession.UserID) - clients.EventTracker.SetAuthUserID(*authSession.UserID) - } - if authSession.EnterpriseID != nil { - ctx = config.SetContextEnterpriseID(ctx, *authSession.EnterpriseID) - clients.EventTracker.SetAuthEnterpriseID(*authSession.EnterpriseID) - // TODO: add enterprise ID to app? See Install. - // app.EnterpriseID = *authSession.EnterpriseID - } - - // Get the manifest from the local file if the manifest source is local or if we are creating - // a new app. After an app is created, app settings becomes the source of truth for remote - // manifests, so updates and installs always get the latest manifest from app settings. - var slackManifest types.SlackYaml - manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx) - if err != nil { - return app, api.DeveloperAppInstallResult{}, "", err - } - if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates { - slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor) - if err != nil { - return app, api.DeveloperAppInstallResult{}, "", err - } - } else { - slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID) - if err != nil { - return app, api.DeveloperAppInstallResult{}, "", err - } - } - - manifest := slackManifest.AppManifest - appendLocalToDisplayName(&manifest) - if manifest.IsFunctionRuntimeSlackHosted() { - configureLocalManifest(ctx, clients, &manifest) - } - - err = validateManifestForInstall(ctx, clients, token, app, manifest) - if err != nil { - return app, api.DeveloperAppInstallResult{}, "", err - } - - start := time.Now() - switch { - case manifestUpdates: - _, _ = clients.IO.WriteOut().Write([]byte("\n" + style.Sectionf(style.TextSection{ - Emoji: "books", - Text: "App Manifest", - Secondary: []string{ - fmt.Sprintf(`Updated app manifest for "%s" in "%s"`, slackManifest.DisplayInformation.Name, *authSession.TeamName), - }, - }))) - clients.IO.PrintDebug(ctx, "updating app %s", app.AppID) - _, err := apiInterface.UpdateApp(ctx, token, app.AppID, manifest, clients.Config.ForceFlag, true) - if err != nil { - clients.IO.PrintDebug(ctx, "failed updating app %s: %s", app.AppID, err) - return app, api.DeveloperAppInstallResult{}, "", err - } - case manifestCreates: - _, _ = clients.IO.WriteOut().Write([]byte(style.Sectionf(style.TextSection{ - Emoji: "books", - Text: "App Manifest", - Secondary: []string{ - fmt.Sprintf(`Creating app manifest for "%s" in "%s"`, slackManifest.DisplayInformation.Name, *authSession.TeamName), - }, - }))) - clients.IO.PrintDebug(ctx, "app not found so creating a new app") - result, err := apiInterface.CreateApp(ctx, token, manifest, false) - if err != nil { - err = slackerror.Wrap(err, slackerror.ErrAppInstall) - return app, api.DeveloperAppInstallResult{}, "", err - } - clients.IO.PrintDebug(ctx, "created new app ID %s", result.AppID) - - // Set properties on app - app.AppID = result.AppID - // TODO: should we be using context to store this information? seems risky - app.TeamID = config.GetContextTeamID(ctx) - app.TeamDomain = config.GetContextTeamDomain(ctx) - app.EnterpriseID = config.GetContextEnterpriseID(ctx) - app.UserID = *authSession.UserID - } - - // specifically set app.IsDev to be true for dev installation - app.IsDev = true - - // save updated or created app to apps.dev.json - if !clients.Config.SkipLocalFs() { - if err := clients.AppClient().SaveLocal(ctx, app); err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - } - caches, err := shouldCacheManifest(ctx, clients, app) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - if caches { - saved, err := clients.Config.ProjectConfig.Cache().GetManifestHash(ctx, app.AppID) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - upstream, err := clients.API().ExportAppManifest(ctx, auth.Token, app.AppID) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - hash, err := clients.Config.ProjectConfig.Cache().NewManifestHash(ctx, upstream.Manifest.AppManifest) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - if !hash.Equals(saved) { - err := clients.Config.ProjectConfig.Cache().SetManifestHash(ctx, app.AppID, hash) - if err != nil { - return types.App{}, api.DeveloperAppInstallResult{}, "", err - } - } - } - - // install the app - var botScopes []string - if manifest.OAuthConfig != nil { - botScopes = manifest.OAuthConfig.Scopes.Bot - } - - outgoingDomains := []string{} - if manifest.OutgoingDomains != nil { - outgoingDomains = *manifest.OutgoingDomains - } - - _, _ = clients.IO.WriteOut().Write([]byte("\n" + style.Sectionf(style.TextSection{ - Emoji: "house", - Text: "App Install", - Secondary: []string{ - fmt.Sprintf(`Installing "%s" app to "%s"`, manifest.DisplayInformation.Name, *authSession.TeamName), - }, - }))) - var installState types.InstallState - result, installState, err := apiInterface.DeveloperAppInstall(ctx, clients.IO, token, app, botScopes, outgoingDomains, orgGrantWorkspaceID, clients.Config.AutoRequestAAAFlag) - - if err != nil { - err = slackerror.Wrap(err, slackerror.ErrAppInstall) - return app, api.DeveloperAppInstallResult{}, "", err - } - - if installState != types.InstallSuccess { - printNonSuccessInstallState(ctx, clients, installState) - return app, api.DeveloperAppInstallResult{}, installState, nil - } - - if err := setAppEnvironmentTokens(ctx, clients, result); err != nil { - return app, result, installState, err - } - - iconPath := resolveIconPath(ctx, clients, slackManifest.Icon) - if iconPath != "" { - _, iconErr := clients.API().IconSet(ctx, clients.Fs, token, app.AppID, iconPath) - if iconErr != nil { - clients.IO.PrintDebug(ctx, "icon error: %s", iconErr) - _, _ = clients.IO.WriteOut().Write([]byte(style.SectionSecondaryf("Error updating app icon: %s", iconErr))) - } else { - _, _ = clients.IO.WriteOut().Write([]byte(style.SectionSecondaryf("Updated app icon: %s", iconPath))) - } - } - - // update config with latest yaml hash - // env.Hash = slackYaml.Hash - - _, _ = clients.IO.WriteOut().Write([]byte(style.SectionSecondaryf("Finished in %.1fs", time.Since(start).Seconds()))) - - return app, result, types.InstallSuccess, nil -} - // getIconHash returns the MD5 hash of the icon file // func getIconHash(iconPath string) (string, error) { // if iconPath == "" { diff --git a/internal/pkg/apps/install_test.go b/internal/pkg/apps/install_test.go index a1c44222..a1db5d24 100644 --- a/internal/pkg/apps/install_test.go +++ b/internal/pkg/apps/install_test.go @@ -41,6 +41,7 @@ func TestInstall(t *testing.T) { mockUserID := "U001" tests := map[string]struct { + dev bool mockApp types.App mockAPICreate api.CreateAppResult mockAPICreateError error @@ -66,7 +67,7 @@ func TestInstall(t *testing.T) { expectedManifest types.AppManifest expectedUpdate bool }{ - "create a hosted app manifest with expected rosi values": { + "deploy: create a hosted app manifest with expected rosi values": { mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -101,6 +102,7 @@ func TestInstall(t *testing.T) { EnterpriseID: mockEnterpriseID, TeamID: mockTeamID, TeamDomain: mockTeamDomain, + UserID: mockUserID, }, expectedManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ @@ -120,7 +122,7 @@ func TestInstall(t *testing.T) { }, expectedCreate: true, }, - "updates a hosted app manifest with expected rosi values": { + "deploy: updates a hosted app manifest with expected rosi values": { mockApp: types.App{ AppID: "A001", TeamID: mockTeamID, @@ -178,7 +180,7 @@ func TestInstall(t *testing.T) { }, expectedUpdate: true, }, - "avoid changing the manifest if a remote function runtime is specified": { + "deploy: avoid changing the manifest if a remote function runtime is specified": { mockApp: types.App{ AppID: "A002", TeamID: mockTeamID, @@ -244,7 +246,7 @@ func TestInstall(t *testing.T) { }, expectedUpdate: true, }, - "avoid changing the manifest if no function runtime is specified": { + "deploy: avoid changing the manifest if no function runtime is specified": { mockApp: types.App{ AppID: "A003", TeamID: mockTeamID, @@ -297,7 +299,7 @@ func TestInstall(t *testing.T) { }, expectedUpdate: true, }, - "create and install an app with a remote manifest": { + "deploy: create and install an app with a remote manifest": { mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -335,6 +337,7 @@ func TestInstall(t *testing.T) { EnterpriseID: mockEnterpriseID, TeamID: mockTeamID, TeamDomain: mockTeamDomain, + UserID: mockUserID, }, expectedManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ @@ -350,7 +353,7 @@ func TestInstall(t *testing.T) { expectedCreate: true, expectedUpdate: false, }, - "avoids updating an app with a remote manifest": { + "deploy: avoids updating an app with a remote manifest": { mockApp: types.App{ AppID: "A004", TeamID: mockTeamID, @@ -377,7 +380,7 @@ func TestInstall(t *testing.T) { expectedInstallState: "", expectedUpdate: false, }, - "errors if the remote manifest has an unexpected cache": { + "deploy: errors if the remote manifest has an unexpected cache": { mockApp: types.App{ AppID: "A005", TeamID: mockTeamID, @@ -404,7 +407,7 @@ func TestInstall(t *testing.T) { expectedInstallState: "", expectedUpdate: false, }, - "errors if the manifest cache is unset without confirmation": { + "deploy: errors if the manifest cache is unset without confirmation": { mockApp: types.App{ AppID: "A005", TeamID: mockTeamID, @@ -448,7 +451,7 @@ func TestInstall(t *testing.T) { expectedError: slackerror.New(slackerror.ErrAppManifestUpdate), expectedUpdate: false, }, - "continues if the remote manifest cache matches the saved": { + "deploy: continues if the remote manifest cache matches the saved": { mockApp: types.App{ AppID: "A006", TeamID: mockTeamID, @@ -499,214 +502,8 @@ func TestInstall(t *testing.T) { }, expectedUpdate: true, }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - ctx := slackcontext.MockContext(t.Context()) - clientsMock := shared.NewClientsMock() - clientsMock.IO.On("IsTTY").Return(tc.mockIsTTY) - clientsMock.AddDefaultMocks() - clientsMock.API.On( - "CreateApp", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Return( - tc.mockAPICreate, - tc.mockAPICreateError, - ) - clientsMock.API.On( - "DeveloperAppInstall", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Return( - tc.mockAPIInstall, - tc.mockAPIInstallState, - tc.mockAPIInstallError, - ) - clientsMock.API.On( - "ExportAppManifest", - mock.Anything, - mock.Anything, - mock.Anything, - ).Return( - api.ExportAppResult{}, - nil, - ) - clientsMock.API.On( - "ValidateAppManifest", - mock.Anything, - mock.Anything, - mock.Anything, - tc.mockApp.AppID, - ).Return( - api.ValidateAppManifestResult{}, - nil, - ) - clientsMock.API.On( - "UpdateApp", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ).Return( - tc.mockAPIUpdate, - tc.mockAPIUpdateError, - ) - clientsMock.API.On( - "ValidateSession", - mock.Anything, - mock.Anything, - ).Return( - tc.mockAuthSession, - nil, - ) - if tc.mockIsTTY { - clientsMock.IO.On( - "ConfirmPrompt", - mock.Anything, - "Overwrite manifest on app settings with the project's manifest file?", - false, - ).Return( - tc.mockConfirmPrompt, - nil, - ) - } - manifestMock := &app.ManifestMockObject{} - manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifestAppLocal, nil) - manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifestAppRemote, nil) - clientsMock.AppClient.Manifest = manifestMock - mockProjectConfig := config.NewProjectConfigMock() - mockProjectConfig.On("GetManifestSource", mock.Anything).Return(tc.mockManifestSource, nil) - mockProjectCache := cache.NewCacheMock() - mockProjectCache.On( - "GetManifestHash", - mock.Anything, - mock.Anything, - ).Return( - tc.mockManifestHashInitial, - nil, - ) - mockProjectCache.On( - "NewManifestHash", - mock.Anything, - mock.Anything, - ).Return( - tc.mockManifestHashUpdated, - nil, - ) - mockProjectCache.On( - "SetManifestHash", - mock.Anything, - mock.Anything, - mock.Anything, - ).Return(nil) - mockProjectConfig.On("Cache").Return(mockProjectCache) - clientsMock.Config.ProjectConfig = mockProjectConfig - - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - app, state, err := Install( - ctx, - clients, - tc.mockAuth, - false, - tc.mockApp, - tc.mockOrgGrantWorkspaceID, - ) - - if tc.expectedError != nil { - assert.Equal( - t, - slackerror.ToSlackError(tc.expectedError).Code, - slackerror.ToSlackError(err).Code, - ) - } else { - require.NoError(t, err) - } - assert.Equal(t, tc.expectedInstallState, state) - assert.Equal(t, tc.expectedApp, app) - if tc.expectedUpdate { - clientsMock.API.AssertCalled( - t, - "UpdateApp", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ) - clientsMock.API.AssertNotCalled(t, "CreateApp") - } else if tc.expectedCreate { - clientsMock.API.AssertCalled( - t, - "CreateApp", - mock.Anything, - mock.Anything, - mock.Anything, - mock.Anything, - ) - clientsMock.API.AssertNotCalled(t, "UpdateApp") - } - for _, call := range clientsMock.API.Calls { - args := call.Arguments - switch call.Method { - case "CreateApp": - assert.Equal(t, tc.mockAuth.Token, args.Get(1)) - assert.Equal(t, tc.expectedManifest, args.Get(2)) - case "UpdateApp": - assert.Equal(t, tc.mockAuth.Token, args.Get(1)) - assert.Equal(t, tc.mockApp.AppID, args.Get(2)) - assert.Equal(t, tc.expectedManifest, args.Get(3)) - } - } - }) - } -} - -func TestInstallLocalApp(t *testing.T) { - mockEnterpriseID := "E001" - mockTeamID := "T001" - mockTeamDomain := "sandbox" - mockToken := "xoxe.xoxp-example" - mockTrue := true - mockUserID := "U001" - - tests := map[string]struct { - mockApp types.App - mockAPICreate api.CreateAppResult - mockAPICreateError error - mockAPIInstall api.DeveloperAppInstallResult - mockAPIInstallState types.InstallState - mockAPIInstallError error - mockAPIUpdate api.UpdateAppResult - mockAPIUpdateError error - mockAuth types.SlackAuth - mockAuthSession api.AuthSession - mockConfirmPrompt bool - mockIsTTY bool - mockManifest types.SlackYaml - mockManifestHashInitial cache.Hash - mockManifestHashUpdated cache.Hash - mockManifestSource config.ManifestSource - mockOrgGrantWorkspaceID string - expectedApp types.App - expectedCreate bool - expectedInstallState types.InstallState - expectedManifest types.AppManifest - expectedUpdate bool - }{ - "create and install a new ROSI app with a local function runtime using expected rosi defaults": { + "dev: create and install a new ROSI app with a local function runtime using expected rosi defaults": { + dev: true, mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -726,7 +523,20 @@ func TestInstallLocalApp(t *testing.T) { UserID: &mockUserID, }, mockManifestSource: config.ManifestSourceLocal, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + Metadata: &types.ManifestMetadata{ + MajorVersion: 2, + }, + DisplayInformation: types.DisplayInformation{ + Name: "example-1", + }, + Settings: &types.AppSettings{ + FunctionRuntime: types.SlackHosted, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ MajorVersion: 2, @@ -768,7 +578,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: false, }, - "update and install an existing local bolt app with a remote function runtime without manifest changes": { + "dev: update and install an existing local bolt app with a remote function runtime without manifest changes": { + dev: true, mockApp: types.App{ AppID: "A002", TeamID: mockTeamID, @@ -793,7 +604,28 @@ func TestInstallLocalApp(t *testing.T) { mockConfirmPrompt: true, mockIsTTY: true, mockManifestSource: config.ManifestSourceLocal, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + Metadata: &types.ManifestMetadata{ + MajorVersion: 1, + }, + DisplayInformation: types.DisplayInformation{ + Name: "example-2", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-2", + }, + }, + Settings: &types.AppSettings{ + FunctionRuntime: types.Remote, + EventSubscriptions: &types.ManifestEventSubscriptions{ + RequestURL: "https://example.com", + }, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ MajorVersion: 1, @@ -846,7 +678,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: true, }, - "update and install an existing local bolt app without a function runtime without manifest changes": { + "dev: update and install an existing local bolt app without a function runtime without manifest changes": { + dev: true, mockApp: types.App{ AppID: "A003", TeamID: mockTeamID, @@ -872,7 +705,22 @@ func TestInstallLocalApp(t *testing.T) { mockAPIInstallState: types.InstallSuccess, mockConfirmPrompt: true, mockIsTTY: true, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -912,7 +760,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: true, }, - "skip updating and allow installing an existing bolt app with a remote manifest": { + "dev: skip updating and allow installing an existing bolt app with a remote manifest": { + dev: true, mockApp: types.App{ AppID: "A004", IsDev: true, @@ -930,7 +779,22 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -959,7 +823,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: false, }, - "create and install a new ROSI app when manifest is local": { + "dev: create and install a new ROSI app when manifest is local": { + dev: true, mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -978,7 +843,20 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + Metadata: &types.ManifestMetadata{ + MajorVersion: 2, + }, + DisplayInformation: types.DisplayInformation{ + Name: "example-1", + }, + Settings: &types.AppSettings{ + FunctionRuntime: types.SlackHosted, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ MajorVersion: 2, @@ -1021,7 +899,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: false, }, - "update and install an existing ROSI app when manifest is local": { + "dev: update and install an existing ROSI app when manifest is local": { + dev: true, mockApp: types.App{ AppID: "A002", TeamID: mockTeamID, @@ -1043,7 +922,20 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + Metadata: &types.ManifestMetadata{ + MajorVersion: 2, + }, + DisplayInformation: types.DisplayInformation{ + Name: "example-2", + }, + Settings: &types.AppSettings{ + FunctionRuntime: types.SlackHosted, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ Metadata: &types.ManifestMetadata{ MajorVersion: 2, @@ -1086,7 +978,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: true, }, - "create and install a new bolt app when manifest is local": { + "dev: create and install a new bolt app when manifest is local": { + dev: true, mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -1105,7 +998,22 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -1147,7 +1055,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: false, }, - "update and install an existing bolt app with a local manifest": { + "dev: update and install an existing bolt app with a local manifest": { + dev: true, mockApp: types.App{ AppID: "A004", IsDev: true, @@ -1165,7 +1074,22 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -1213,7 +1137,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: true, }, - "create and install a new bolt app when manifest is remote": { + "dev: create and install a new bolt app when manifest is remote": { + dev: true, mockApp: types.App{}, mockAPICreate: api.CreateAppResult{ AppID: "A001", @@ -1232,7 +1157,22 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -1274,7 +1214,8 @@ func TestInstallLocalApp(t *testing.T) { expectedInstallState: types.InstallSuccess, expectedUpdate: false, }, - "skip updating and allow installing an existing bolt app when manifest is remote": { + "dev: skip updating and allow installing an existing bolt app when manifest is remote": { + dev: true, mockApp: types.App{ AppID: "A004", IsDev: true, @@ -1292,7 +1233,22 @@ func TestInstallLocalApp(t *testing.T) { TeamName: &mockTeamDomain, UserID: &mockUserID, }, - mockManifest: types.SlackYaml{ + mockManifestAppLocal: types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: "example-3", + }, + Features: &types.AppFeatures{ + BotUser: types.BotUser{ + DisplayName: "example-3", + }, + }, + Settings: &types.AppSettings{ + SocketModeEnabled: &mockTrue, + }, + }, + }, + mockManifestAppRemote: types.SlackYaml{ AppManifest: types.AppManifest{ DisplayInformation: types.DisplayInformation{ Name: "example-3", @@ -1360,7 +1316,7 @@ func TestInstallLocalApp(t *testing.T) { mock.Anything, mock.Anything, ).Return( - api.ExportAppResult{Manifest: tc.mockManifest}, + api.ExportAppResult{}, nil, ) clientsMock.API.On( @@ -1405,8 +1361,8 @@ func TestInstallLocalApp(t *testing.T) { ) } manifestMock := &app.ManifestMockObject{} - manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifest, nil) - manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifest, nil) + manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifestAppLocal, nil) + manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifestAppRemote, nil) clientsMock.AppClient.Manifest = manifestMock mockProjectConfig := config.NewProjectConfigMock() mockProjectConfig.On("GetManifestSource", mock.Anything).Return(tc.mockManifestSource, nil) @@ -1437,15 +1393,26 @@ func TestInstallLocalApp(t *testing.T) { clientsMock.Config.ProjectConfig = mockProjectConfig clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - app, _, state, err := InstallLocalApp( + app, _, state, err := Install( ctx, clients, - tc.mockOrgGrantWorkspaceID, tc.mockAuth, tc.mockApp, + InstallOptions{ + OrgGrantWorkspaceID: tc.mockOrgGrantWorkspaceID, + Dev: tc.dev, + }, ) - require.NoError(t, err) + if tc.expectedError != nil { + assert.Equal( + t, + slackerror.ToSlackError(tc.expectedError).Code, + slackerror.ToSlackError(err).Code, + ) + } else { + require.NoError(t, err) + } assert.Equal(t, tc.expectedInstallState, state) assert.Equal(t, tc.expectedApp, app) if tc.expectedUpdate { diff --git a/internal/pkg/platform/localserver.go b/internal/pkg/platform/localserver.go index 44cb0939..efc62ba8 100644 --- a/internal/pkg/platform/localserver.go +++ b/internal/pkg/platform/localserver.go @@ -410,7 +410,7 @@ func (r *LocalServer) WatchManifest(ctx context.Context, auth types.SlackAuth, a r.clients.IO.PrintInfo(ctx, false, "%s", style.Secondary(fmt.Sprintf("Manifest change detected: %s, reinstalling app...", event.Path))) // Reinstall the app when manifest changes - if _, _, _, err := apps.InstallLocalApp(ctx, r.clients, "", auth, app); err != nil { + if _, _, _, err := apps.Install(ctx, r.clients, auth, app, apps.InstallOptions{Dev: true}); err != nil { r.clients.IO.PrintError(ctx, "Error: %s", err) } else { r.clients.IO.PrintInfo(ctx, false, "%s", style.Secondary("App successfully reinstalled")) diff --git a/internal/pkg/platform/run.go b/internal/pkg/platform/run.go index cda03117..e9b00684 100644 --- a/internal/pkg/platform/run.go +++ b/internal/pkg/platform/run.go @@ -79,7 +79,10 @@ func Run(ctx context.Context, clients *shared.ClientFactory, runArgs RunArgs) (t } // Update local install - installedApp, localInstallResult, installState, err := apps.InstallLocalApp(ctx, clients, runArgs.OrgGrantWorkspaceID, runArgs.Auth, runArgs.App) + installedApp, localInstallResult, installState, err := apps.Install(ctx, clients, runArgs.Auth, runArgs.App, apps.InstallOptions{ + OrgGrantWorkspaceID: runArgs.OrgGrantWorkspaceID, + Dev: true, + }) if err != nil { return "", slackerror.Wrap(err, slackerror.ErrLocalAppRun) }