Skip to content
Open
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
66 changes: 38 additions & 28 deletions cmd/env/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"

"github.com/slackapi/slack-cli/internal/cmdutil"
"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/slacktrace"
Expand All @@ -34,11 +35,13 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
Use: "list [flags]",
Short: "List all environment variables for the app",
Long: strings.Join([]string{
"List all of the environment variables of an app deployed to Slack managed",
"infrastructure.",
"List environment variables available to the app at runtime.",
"",
"This command is supported for apps deployed to Slack managed infrastructure but",
"other apps can attempt to run the command with the --force flag.",
"Commands that run in the context of a project source environment variables from",
"the \".env\" file. This includes the \"run\" command.",
"",
"The \"deploy\" command gathers environment variables from the \".env\" file as well",
"unless the app is using ROSI features.",
Comment on lines +38 to +44
Copy link
Member Author

Choose a reason for hiding this comment

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

📸 Preview

Image

}, "\n"),
Example: style.ExampleCommandsf([]style.ExampleCommand{
{
Expand All @@ -58,17 +61,9 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
return cmd
}

// preRunEnvListCommandFunc determines if the command is supported for a project
// and configures flags
func preRunEnvListCommandFunc(ctx context.Context, clients *shared.ClientFactory) error {
err := cmdutil.IsValidProjectDirectory(clients)
if err != nil {
return err
}
if clients.Config.ForceFlag {
return nil
}
return cmdutil.IsSlackHostedProject(ctx, clients)
// preRunEnvListCommandFunc determines if the command is run in a valid project
func preRunEnvListCommandFunc(_ context.Context, clients *shared.ClientFactory) error {
return cmdutil.IsValidProjectDirectory(clients)
}

// runEnvListCommandFunc outputs environment variables for a selected app
Expand All @@ -81,20 +76,34 @@ func runEnvListCommandFunc(
selection, err := appSelectPromptFunc(
ctx,
clients,
prompts.ShowHostedOnly,
prompts.ShowAllEnvironments,
prompts.ShowInstalledAppsOnly,
)
if err != nil {
return err
}

variableNames, err := clients.API().ListVariables(
ctx,
selection.Auth.Token,
selection.App.AppID,
)
if err != nil {
return err
// Gather environment variables for either a ROSI app from the Slack API method
// or read from project files.
var variableNames []string
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
variableNames, err = clients.API().ListVariables(
ctx,
selection.Auth.Token,
selection.App.AppID,
)
if err != nil {
return err
}
} else {
dotEnv, err := hooks.LoadDotEnv(clients.Fs)
Copy link
Member Author

Choose a reason for hiding this comment

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

🪝 note: We reuse the logic that hooks has for gathering environment variables for confidence in matching!

🐟 ramble: We don't require app selection before this at this point, but we might consider keeping it to avoid breaking change if it becomes required later. FWIW it's required for hosted apps so this is familiar experience I think!

if err != nil {
return err
}
variableNames = make([]string, 0, len(dotEnv))
for k := range dotEnv {
variableNames = append(variableNames, k)
}
}

count := len(variableNames)
Expand All @@ -112,22 +121,23 @@ func runEnvListCommandFunc(
},
}))

if len(variableNames) <= 0 {
if count <= 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice catch i wonder why count was not used here before 🤔

return nil
}

sort.Strings(variableNames)
variableLabel := []string{}
variableLabels := make([]string, 0, count)
for _, v := range variableNames {
variableLabel = append(
variableLabel,
variableLabels = append(
variableLabels,
fmt.Sprintf("%s: %s", v, style.Secondary("***")),
)
}
clients.IO.PrintTrace(ctx, slacktrace.EnvListVariables, variableNames...)
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "App Environment",
Secondary: variableLabel,
Secondary: variableLabels,
}))

return nil
Expand Down
159 changes: 88 additions & 71 deletions cmd/env/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,95 +26,30 @@ import (
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
"github.com/slackapi/slack-cli/test/testutil"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func Test_Env_ListCommandPreRun(t *testing.T) {
tests := map[string]struct {
mockFlagForce bool
mockManifestResponse types.SlackYaml
mockManifestError error
mockManifestSource config.ManifestSource
mockWorkingDirectory string
expectedError error
}{
"continues if the application is hosted on slack": {
mockManifestResponse: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.SlackHosted,
},
},
},
mockManifestError: nil,
mockManifestSource: config.ManifestSourceLocal,
mockWorkingDirectory: "/slack/path/to/project",
expectedError: nil,
},
"errors if the application is not hosted on slack": {
mockManifestResponse: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.Remote,
},
},
},
mockManifestError: nil,
mockManifestSource: config.ManifestSourceLocal,
mockWorkingDirectory: "/slack/path/to/project",
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
},
"continues if the force flag is used in a project": {
mockFlagForce: true,
"continues if the command is run in a project": {
mockWorkingDirectory: "/slack/path/to/project",
expectedError: nil,
},
"errors if the project manifest cannot be retrieved": {
mockManifestResponse: types.SlackYaml{},
mockManifestError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
mockManifestSource: config.ManifestSourceLocal,
mockWorkingDirectory: "/slack/path/to/project",
expectedError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
},
"errors if the command is not run in a project": {
mockManifestResponse: types.SlackYaml{},
mockManifestError: slackerror.New(slackerror.ErrSDKHookNotFound),
mockWorkingDirectory: "",
expectedError: slackerror.New(slackerror.ErrInvalidAppDirectory),
},
"errors if the manifest source is set to remote": {
mockManifestSource: config.ManifestSourceRemote,
mockWorkingDirectory: "/slack/path/to/project",
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
clientsMock := shared.NewClientsMock()
manifestMock := &app.ManifestMockObject{}
manifestMock.On(
"GetManifestLocal",
mock.Anything,
mock.Anything,
mock.Anything,
).Return(
tc.mockManifestResponse,
tc.mockManifestError,
)
clientsMock.AppClient.Manifest = manifestMock
projectConfigMock := config.NewProjectConfigMock()
projectConfigMock.On(
"GetManifestSource",
mock.Anything,
).Return(
tc.mockManifestSource,
nil,
)
clientsMock.Config.ProjectConfig = projectConfigMock
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(cf *shared.ClientFactory) {
cf.Config.ForceFlag = tc.mockFlagForce
cf.SDKConfig.WorkingDirectory = tc.mockWorkingDirectory
})
cmd := NewEnvListCommand(clients)
Expand All @@ -129,9 +64,78 @@ func Test_Env_ListCommandPreRun(t *testing.T) {
}

func Test_Env_ListCommand(t *testing.T) {
mockAppSelect := func() {
appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
}

testutil.TableTestCommand(t, testutil.CommandTests{
"list variables using arguments": {
"lists variables from the .env file": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
err := afero.WriteFile(cf.Fs, ".env", []byte("SECRET_KEY=abc123\nAPI_TOKEN=xyz789\n"), 0644)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(
t,
"PrintTrace",
mock.Anything,
slacktrace.EnvListCount,
[]string{
"2",
},
)
cm.IO.AssertCalled(
t,
"PrintTrace",
mock.Anything,
slacktrace.EnvListVariables,
[]string{
"API_TOKEN",
"SECRET_KEY",
},
)
},
},
"lists no variables when the .env file does not exist": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(
t,
"PrintTrace",
mock.Anything,
slacktrace.EnvListCount,
[]string{
"0",
},
)
},
},
"lists no variables when the .env file is empty": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
err := afero.WriteFile(cf.Fs, ".env", []byte(""), 0644)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(
t,
"PrintTrace",
mock.Anything,
slacktrace.EnvListCount,
[]string{
"0",
},
)
},
},
"lists hosted variables using the API": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
cm.API.On(
"ListVariables",
mock.Anything,
Expand All @@ -145,9 +149,22 @@ func Test_Env_ListCommand(t *testing.T) {
},
nil,
)
appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowHostedOnly, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.SlackHosted,
},
},
},
nil,
)
cm.AppClient.Manifest = manifestMock
projectConfigMock := config.NewProjectConfigMock()
projectConfigMock.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
cm.Config.ProjectConfig = projectConfigMock
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.API.AssertCalled(
Expand Down
Loading