Skip to content
Draft
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
1 change: 1 addition & 0 deletions cmd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
cmd.AddCommand(NewLinkCommand(clients))
cmd.AddCommand(NewListCommand(clients))
cmd.AddCommand(NewSettingsCommand(clients))
cmd.AddCommand(NewStatusCommand(clients))
cmd.AddCommand(NewUninstallCommand(clients))
cmd.AddCommand(NewUnlinkCommand(clients))

Expand Down
140 changes: 140 additions & 0 deletions cmd/app/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package app

import (
"context"
"fmt"

"github.com/slackapi/slack-cli/internal/cmdutil"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/cobra"
)

// statusAppSelectPromptFunc is a handle to the app select prompt used for testing
var statusAppSelectPromptFunc = prompts.AppSelectPrompt

// NewStatusCommand returns a new Cobra command for app status
func NewStatusCommand(clients *shared.ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Check the install request status of an app",
Long: "Check the install request status of an app on a workspace where direct installation is not permitted",
Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "app status", Meaning: "Check install request status for an app in the current project"},
{Command: "app status --app A0123456789", Meaning: "Check install request status for a specific app"},
}),
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
return cmdutil.IsValidProjectDirectory(clients)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runStatusCommand(cmd, clients)
},
}

return cmd
}

// runStatusCommand executes the app status command
func runStatusCommand(cmd *cobra.Command, clients *shared.ClientFactory) error {
ctx := cmd.Context()

selection, err := statusAppSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAndUninstalledApps)
if err != nil {
return err
}

if selection.App.AppID == "" {
return slackerror.New(slackerror.ErrAppNotFound)
}

appStatus, err := fetchAppInstallRequestStatus(ctx, clients, selection)
if err != nil {
return err
}

clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "clipboard",
Text: "App Install Request Status",
Secondary: formatStatusOutput(selection, appStatus),
}))

return nil
}

// AppInstallRequestStatus represents the approval status of an install request
type AppInstallRequestStatus string

const (
InstallRequestStatusInstalled AppInstallRequestStatus = "Installed"
InstallRequestStatusApproved AppInstallRequestStatus = "Approved"
InstallRequestStatusPending AppInstallRequestStatus = "Pending"
InstallRequestStatusDenied AppInstallRequestStatus = "Denied"
InstallRequestStatusUnknown AppInstallRequestStatus = "Unknown"
)

// fetchAppInstallRequestStatus fetches the install request status for an app
// TODO: Update to use the new API endpoint when available
func fetchAppInstallRequestStatus(ctx context.Context, clients *shared.ClientFactory, selection prompts.SelectedApp) (AppInstallRequestStatus, error) {
result, err := clients.API().GetAppStatus(ctx, selection.Auth.Token, []string{selection.App.AppID}, selection.App.TeamID)
if err != nil {
return InstallRequestStatusUnknown, slackerror.Wrap(err, slackerror.ErrAppNotFound)
}

for _, app := range result.Apps {
if app.AppID == selection.App.AppID {
if app.Installed {
return InstallRequestStatusInstalled, nil
}
// TODO: Map approval_status field from API response when available
// For now, return Unknown for uninstalled apps
return InstallRequestStatusUnknown, nil
}
}

return InstallRequestStatusUnknown, nil
}

// formatStatusOutput formats the status command output
func formatStatusOutput(selection prompts.SelectedApp, status AppInstallRequestStatus) []string {
var output []string

output = append(output, fmt.Sprintf(style.Bold("%s:"), selection.Auth.TeamDomain))
output = append(output, fmt.Sprintf(style.Indent(style.Secondary("App ID: %s")), selection.App.AppID))
output = append(output, fmt.Sprintf(style.Indent(style.Secondary("Team ID: %s")), selection.App.TeamID))
output = append(output, fmt.Sprintf(style.Indent(style.Secondary("Status: %s")), formatStatusLabel(status)))

return output
}

// formatStatusLabel returns a styled label for the given status
func formatStatusLabel(status AppInstallRequestStatus) string {
switch status {
case InstallRequestStatusInstalled:
return style.Green(string(status))
case InstallRequestStatusApproved:
return style.Green(string(status))
case InstallRequestStatusPending:
return style.Warning(string(status))
case InstallRequestStatusDenied:
return style.Red(string(status))
default:
return string(InstallRequestStatusUnknown)
}
}
138 changes: 138 additions & 0 deletions cmd/app/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package app

import (
"context"
"testing"

"github.com/slackapi/slack-cli/internal/api"
"github.com/slackapi/slack-cli/internal/hooks"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/test/testutil"
"github.com/stretchr/testify/assert"
)

func Test_NewStatusCommand(t *testing.T) {
clientsMock := shared.NewClientsMock()
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(clients *shared.ClientFactory) {
clients.SDKConfig = hooks.NewSDKConfigMock()
})

cmd := NewStatusCommand(clients)
assert.Equal(t, "status", cmd.Use)
assert.NotEmpty(t, cmd.Short)
assert.NotEmpty(t, cmd.Long)
}

func Test_runStatusCommand(t *testing.T) {
tests := map[string]struct {
appID string
teamID string
teamDomain string
installed bool
expectedStatus AppInstallRequestStatus
}{
"installed app shows installed status": {
appID: "A0123456789",
teamID: "T0001",
teamDomain: "test-team",
installed: true,
expectedStatus: InstallRequestStatusInstalled,
},
"uninstalled app shows unknown status": {
appID: "A0123456789",
teamID: "T0001",
teamDomain: "test-team",
installed: false,
expectedStatus: InstallRequestStatusUnknown,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(clients *shared.ClientFactory) {
clients.SDKConfig = hooks.NewSDKConfigMock()
})

cmd := NewStatusCommand(clients)
testutil.MockCmdIO(clients.IO, cmd)

statusAppSelectPromptFunc = func(ctx context.Context, clients *shared.ClientFactory, environment prompts.AppEnvironmentType, status prompts.AppInstallStatus, opts ...prompts.AppSelectOption) (prompts.SelectedApp, error) {
return prompts.SelectedApp{
Auth: types.SlackAuth{
Token: "xoxp-test-token",
TeamID: tc.teamID,
TeamDomain: tc.teamDomain,
},
App: types.App{
AppID: tc.appID,
TeamID: tc.teamID,
},
}, nil
}

clientsMock.API.On("GetAppStatus", ctx, "xoxp-test-token", []string{tc.appID}, tc.teamID).Return(api.GetAppStatusResult{
Apps: []api.AppStatusResultAppInfo{
{
AppID: tc.appID,
Installed: tc.installed,
},
},
}, nil)

err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
})
}
}

func Test_formatStatusLabel(t *testing.T) {
tests := map[string]struct {
status AppInstallRequestStatus
contains string
}{
"installed contains status text": {
status: InstallRequestStatusInstalled,
contains: "Installed",
},
"approved contains status text": {
status: InstallRequestStatusApproved,
contains: "Approved",
},
"pending contains status text": {
status: InstallRequestStatusPending,
contains: "Pending",
},
"denied contains status text": {
status: InstallRequestStatusDenied,
contains: "Denied",
},
"unknown contains status text": {
status: InstallRequestStatusUnknown,
contains: "Unknown",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := formatStatusLabel(tc.status)
assert.Contains(t, result, tc.contains)
})
}
}
Loading