diff --git a/cmd/env/list.go b/cmd/env/list.go index f61dd736..a234ad32 100644 --- a/cmd/env/list.go +++ b/cmd/env/list.go @@ -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" @@ -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.", }, "\n"), Example: style.ExampleCommandsf([]style.ExampleCommand{ { @@ -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 @@ -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) + if err != nil { + return err + } + variableNames = make([]string, 0, len(dotEnv)) + for k := range dotEnv { + variableNames = append(variableNames, k) + } } count := len(variableNames) @@ -112,14 +121,15 @@ func runEnvListCommandFunc( }, })) - if len(variableNames) <= 0 { + if count <= 0 { 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("***")), ) } @@ -127,7 +137,7 @@ func runEnvListCommandFunc( clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ Emoji: "evergreen_tree", Text: "App Environment", - Secondary: variableLabel, + Secondary: variableLabels, })) return nil diff --git a/cmd/env/list_test.go b/cmd/env/list_test.go index ba5375e3..5e2b054d 100644 --- a/cmd/env/list_test.go +++ b/cmd/env/list_test.go @@ -26,6 +26,7 @@ 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" @@ -33,88 +34,22 @@ import ( 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) @@ -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, @@ -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(