Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cmd/app/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,39 @@ func TestAppAddCommand(t *testing.T) {
cm.API.AssertCalled(t, "DeveloperAppInstall", mock.Anything, mock.Anything, mockOrgAuth.Token, mock.Anything, mock.Anything, mock.Anything, "T123", mock.Anything)
},
},
"errors before creating a new app on an org when the manifest is not org ready": {
CmdArgs: []string{"--" + cmdutil.OrgGrantWorkspaceFlag, "T123"},
ExpectedErrorStrings: []string{slackerror.ErrAppNotOrgReady},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
prepareAddMocks(t, cf, cm, "deployed")
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{
AppManifest: types.AppManifest{
DisplayInformation: types.DisplayInformation{
Name: team1TeamDomain,
},
},
}, nil)
cf.AppClient().Manifest = manifestMock
// Select workspace
appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowHostedOnly, prompts.ShowAllApps).Return(prompts.SelectedApp{App: types.NewApp(), Auth: mockOrgAuth}, nil)
// Mock calls
cm.API.On("ValidateSession", mock.Anything, mock.Anything).Return(api.AuthSession{
UserID: &mockOrgAuth.UserID,
TeamID: &mockOrgAuth.TeamID,
TeamName: &mockOrgAuth.TeamDomain,
}, nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
cm.Config.ProjectConfig = mockProjectConfig
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.API.AssertNotCalled(t, "CreateApp", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
cm.API.AssertNotCalled(t, "DeveloperAppInstall", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
},
},
"adds a new local app when --environment local": {
CmdArgs: []string{"--team", "T123", "--environment", "local"},
ExpectedOutputs: []string{"Creating app manifest", "Installing"},
Expand Down Expand Up @@ -904,6 +937,9 @@ func prepareAddMocks(t *testing.T, clients *shared.ClientFactory, clientsMock *s
Name: team1TeamDomain,
},
Workflows: map[string]types.Workflow{"test_workflow": {Title: "test workflow", InputParameters: types.ToRawJSON(`{}`)}},
Settings: &types.AppSettings{
OrgDeployEnabled: new(true),
},
},
}, nil)
clients.AppClient().Manifest = manifestMock
Expand Down
25 changes: 25 additions & 0 deletions internal/pkg/apps/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac
configureHostedManifest(ctx, clients, &manifest)
}

err = validateManifestOrgReady(app, auth, manifest)
if err != nil {
return app, "", err
}

err = validateManifestForInstall(ctx, clients, token, app, manifest)
if err != nil {
return app, "", err
Expand Down Expand Up @@ -271,6 +276,21 @@ func printNonSuccessInstallState(ctx context.Context, clients *shared.ClientFact
clients.IO.PrintInfo(ctx, false, "%s", status)
}

// validateManifestOrgReady errors when an app would be created or reinstalled on
// an organization without "org_deploy_enabled" set to true in the manifest. Such
// an app installs on the organization but never receives events.
func validateManifestOrgReady(app types.App, auth types.SlackAuth, appManifest types.AppManifest) error {
newOrgApp := app.AppID == "" && auth.IsEnterpriseInstall
uninstalledOrgApp := app.IsUninstalled() && types.IsEnterpriseTeamID(app.TeamID)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔁 question: Should this also cover reinstalling an org app that's still installed? If org_deploy_enabled is removed from the manifest later, slack install or slack run on an installed org app skips this check, since it's neither new nor uninstalled. I'm not sure whether that app keeps receiving events or goes quiet like the new case, so a follow-up might not be needed!

if !newOrgApp && !uninstalledOrgApp {
return nil
}
if appManifest.Settings != nil && appManifest.Settings.OrgDeployEnabled != nil && *appManifest.Settings.OrgDeployEnabled {
return nil
}
return slackerror.New(slackerror.ErrAppNotOrgReady)
}

func validateManifestForInstall(ctx context.Context, clients *shared.ClientFactory, token string, app types.App, appManifest types.AppManifest) error {
validationResult, err := clients.API().ValidateAppManifest(ctx, token, appManifest, app.AppID)

Expand Down Expand Up @@ -402,6 +422,11 @@ func InstallLocalApp(ctx context.Context, clients *shared.ClientFactory, orgGran
configureLocalManifest(ctx, clients, &manifest)
}

err = validateManifestOrgReady(app, auth, manifest)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}

err = validateManifestForInstall(ctx, clients, token, app, manifest)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
Expand Down
62 changes: 62 additions & 0 deletions internal/pkg/apps/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,68 @@ func TestInstallLocalApp(t *testing.T) {
}
}

func TestValidateManifestOrgReady(t *testing.T) {
orgReady := types.AppManifest{Settings: &types.AppSettings{OrgDeployEnabled: new(true)}}
notOrgReady := types.AppManifest{Settings: &types.AppSettings{OrgDeployEnabled: new(false)}}
unset := types.AppManifest{}
orgAuth := types.SlackAuth{TeamID: "E123", IsEnterpriseInstall: true}
workspaceAuth := types.SlackAuth{TeamID: "T123"}
uninstalledOrgApp := types.App{AppID: "A123", TeamID: "E123", InstallStatus: types.AppStatusUninstalled}
installedOrgApp := types.App{AppID: "A123", TeamID: "E123", InstallStatus: types.AppStatusInstalled}

tests := map[string]struct {
app types.App
auth types.SlackAuth
manifest types.AppManifest
err bool
}{
"new org app that is org ready": {
app: types.NewApp(),
auth: orgAuth,
manifest: orgReady,
},
"new org app with org deploy disabled": {
app: types.NewApp(),
auth: orgAuth,
manifest: notOrgReady,
err: true,
},
"new org app with org deploy unset": {
app: types.NewApp(),
auth: orgAuth,
manifest: unset,
err: true,
},
"uninstalled org app with org deploy disabled": {
app: uninstalledOrgApp,
auth: orgAuth,
manifest: notOrgReady,
err: true,
},
"installed org app with org deploy disabled": {
app: installedOrgApp,
auth: orgAuth,
manifest: notOrgReady,
},
"new workspace app with org deploy unset": {
app: types.NewApp(),
auth: workspaceAuth,
manifest: unset,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
err := validateManifestOrgReady(tc.app, tc.auth, tc.manifest)
if tc.err {
require.Error(t, err)
assert.Equal(t, slackerror.ErrAppNotOrgReady, slackerror.ToSlackError(err).Code)
} else {
assert.NoError(t, err)
}
})
}
}

func TestValidateManifestForInstall(t *testing.T) {
tests := map[string]struct {
app types.App
Expand Down
11 changes: 11 additions & 0 deletions internal/slackerror/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ErrAppNotFound = "app_not_found"
ErrAppNotHosted = "app_not_hosted"
ErrAppNotInstalled = "app_not_installed"
ErrAppNotOrgReady = "app_not_org_ready"
ErrAppRemove = "app_remove_error"
ErrAppRenameApp = "app_rename_app"
ErrAppsList = "apps_list_error"
Expand Down Expand Up @@ -475,6 +476,16 @@ Otherwise start your app for local development with: %s`,
Message: "The provided app must be installed on this team",
},

ErrAppNotOrgReady: {
Code: ErrAppNotOrgReady,
Message: "Apps installed on an organization must set \"org_deploy_enabled\" to true in the app manifest",
Remediation: fmt.Sprintf(`Add "org_deploy_enabled": true to the "settings" of the app manifest and try again.

Otherwise create the app on a workspace from app settings, then add it to this project with: %s`,
style.Commandf("app link", false),
Comment on lines +482 to +485

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️‍🗨️ praise: Both paths forward in the remediation is so helpful! The slack app link fallback is a nice option for anyone who can't change the manifest.

),
},

ErrAppRemove: {
Code: ErrAppRemove,
Message: "Couldn't remove your app",
Expand Down
Loading