Skip to content

Commit 9b8c7a1

Browse files
committed
feat: list available ".env" variable names for non-hosted apps
1 parent fb0cce1 commit 9b8c7a1

File tree

2 files changed

+126
-99
lines changed

2 files changed

+126
-99
lines changed

cmd/env/list.go

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strings"
2323

2424
"github.com/slackapi/slack-cli/internal/cmdutil"
25+
"github.com/slackapi/slack-cli/internal/hooks"
2526
"github.com/slackapi/slack-cli/internal/prompts"
2627
"github.com/slackapi/slack-cli/internal/shared"
2728
"github.com/slackapi/slack-cli/internal/slacktrace"
@@ -34,11 +35,13 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
3435
Use: "list [flags]",
3536
Short: "List all environment variables for the app",
3637
Long: strings.Join([]string{
37-
"List all of the environment variables of an app deployed to Slack managed",
38-
"infrastructure.",
38+
"List environment variables available to the app at runtime.",
3939
"",
40-
"This command is supported for apps deployed to Slack managed infrastructure but",
41-
"other apps can attempt to run the command with the --force flag.",
40+
"Commands that run in the context of a project source environment variables from",
41+
"the \".env\" file. This includes the \"run\" command.",
42+
"",
43+
"The \"deploy\" command gathers environment variables from the \".env\" file as well",
44+
"unless the app is using ROSI features.",
4245
}, "\n"),
4346
Example: style.ExampleCommandsf([]style.ExampleCommand{
4447
{
@@ -58,17 +61,9 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
5861
return cmd
5962
}
6063

61-
// preRunEnvListCommandFunc determines if the command is supported for a project
62-
// and configures flags
63-
func preRunEnvListCommandFunc(ctx context.Context, clients *shared.ClientFactory) error {
64-
err := cmdutil.IsValidProjectDirectory(clients)
65-
if err != nil {
66-
return err
67-
}
68-
if clients.Config.ForceFlag {
69-
return nil
70-
}
71-
return cmdutil.IsSlackHostedProject(ctx, clients)
64+
// preRunEnvListCommandFunc determines if the command is run in a valid project
65+
func preRunEnvListCommandFunc(_ context.Context, clients *shared.ClientFactory) error {
66+
return cmdutil.IsValidProjectDirectory(clients)
7267
}
7368

7469
// runEnvListCommandFunc outputs environment variables for a selected app
@@ -81,20 +76,34 @@ func runEnvListCommandFunc(
8176
selection, err := appSelectPromptFunc(
8277
ctx,
8378
clients,
84-
prompts.ShowHostedOnly,
79+
prompts.ShowAllEnvironments,
8580
prompts.ShowInstalledAppsOnly,
8681
)
8782
if err != nil {
8883
return err
8984
}
9085

91-
variableNames, err := clients.API().ListVariables(
92-
ctx,
93-
selection.Auth.Token,
94-
selection.App.AppID,
95-
)
96-
if err != nil {
97-
return err
86+
// Gather environment variables for either a ROSI app from the Slack API method
87+
// or read from project files.
88+
var variableNames []string
89+
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
90+
variableNames, err = clients.API().ListVariables(
91+
ctx,
92+
selection.Auth.Token,
93+
selection.App.AppID,
94+
)
95+
if err != nil {
96+
return err
97+
}
98+
} else {
99+
dotEnv, err := hooks.LoadDotEnv(clients.Fs)
100+
if err != nil {
101+
return err
102+
}
103+
variableNames = make([]string, 0, len(dotEnv))
104+
for k := range dotEnv {
105+
variableNames = append(variableNames, k)
106+
}
98107
}
99108

100109
count := len(variableNames)
@@ -112,22 +121,23 @@ func runEnvListCommandFunc(
112121
},
113122
}))
114123

115-
if len(variableNames) <= 0 {
124+
if count <= 0 {
116125
return nil
117126
}
127+
118128
sort.Strings(variableNames)
119-
variableLabel := []string{}
129+
variableLabels := make([]string, 0, count)
120130
for _, v := range variableNames {
121-
variableLabel = append(
122-
variableLabel,
131+
variableLabels = append(
132+
variableLabels,
123133
fmt.Sprintf("%s: %s", v, style.Secondary("***")),
124134
)
125135
}
126136
clients.IO.PrintTrace(ctx, slacktrace.EnvListVariables, variableNames...)
127137
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
128138
Emoji: "evergreen_tree",
129139
Text: "App Environment",
130-
Secondary: variableLabel,
140+
Secondary: variableLabels,
131141
}))
132142

133143
return nil

cmd/env/list_test.go

Lines changed: 88 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,95 +26,30 @@ import (
2626
"github.com/slackapi/slack-cli/internal/slackerror"
2727
"github.com/slackapi/slack-cli/internal/slacktrace"
2828
"github.com/slackapi/slack-cli/test/testutil"
29+
"github.com/spf13/afero"
2930
"github.com/spf13/cobra"
3031
"github.com/stretchr/testify/assert"
3132
"github.com/stretchr/testify/mock"
3233
)
3334

3435
func Test_Env_ListCommandPreRun(t *testing.T) {
3536
tests := map[string]struct {
36-
mockFlagForce bool
37-
mockManifestResponse types.SlackYaml
38-
mockManifestError error
39-
mockManifestSource config.ManifestSource
4037
mockWorkingDirectory string
4138
expectedError error
4239
}{
43-
"continues if the application is hosted on slack": {
44-
mockManifestResponse: types.SlackYaml{
45-
AppManifest: types.AppManifest{
46-
Settings: &types.AppSettings{
47-
FunctionRuntime: types.SlackHosted,
48-
},
49-
},
50-
},
51-
mockManifestError: nil,
52-
mockManifestSource: config.ManifestSourceLocal,
53-
mockWorkingDirectory: "/slack/path/to/project",
54-
expectedError: nil,
55-
},
56-
"errors if the application is not hosted on slack": {
57-
mockManifestResponse: types.SlackYaml{
58-
AppManifest: types.AppManifest{
59-
Settings: &types.AppSettings{
60-
FunctionRuntime: types.Remote,
61-
},
62-
},
63-
},
64-
mockManifestError: nil,
65-
mockManifestSource: config.ManifestSourceLocal,
66-
mockWorkingDirectory: "/slack/path/to/project",
67-
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
68-
},
69-
"continues if the force flag is used in a project": {
70-
mockFlagForce: true,
40+
"continues if the command is run in a project": {
7141
mockWorkingDirectory: "/slack/path/to/project",
7242
expectedError: nil,
7343
},
74-
"errors if the project manifest cannot be retrieved": {
75-
mockManifestResponse: types.SlackYaml{},
76-
mockManifestError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
77-
mockManifestSource: config.ManifestSourceLocal,
78-
mockWorkingDirectory: "/slack/path/to/project",
79-
expectedError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
80-
},
8144
"errors if the command is not run in a project": {
82-
mockManifestResponse: types.SlackYaml{},
83-
mockManifestError: slackerror.New(slackerror.ErrSDKHookNotFound),
8445
mockWorkingDirectory: "",
8546
expectedError: slackerror.New(slackerror.ErrInvalidAppDirectory),
8647
},
87-
"errors if the manifest source is set to remote": {
88-
mockManifestSource: config.ManifestSourceRemote,
89-
mockWorkingDirectory: "/slack/path/to/project",
90-
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
91-
},
9248
}
9349
for name, tc := range tests {
9450
t.Run(name, func(t *testing.T) {
9551
clientsMock := shared.NewClientsMock()
96-
manifestMock := &app.ManifestMockObject{}
97-
manifestMock.On(
98-
"GetManifestLocal",
99-
mock.Anything,
100-
mock.Anything,
101-
mock.Anything,
102-
).Return(
103-
tc.mockManifestResponse,
104-
tc.mockManifestError,
105-
)
106-
clientsMock.AppClient.Manifest = manifestMock
107-
projectConfigMock := config.NewProjectConfigMock()
108-
projectConfigMock.On(
109-
"GetManifestSource",
110-
mock.Anything,
111-
).Return(
112-
tc.mockManifestSource,
113-
nil,
114-
)
115-
clientsMock.Config.ProjectConfig = projectConfigMock
11652
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(cf *shared.ClientFactory) {
117-
cf.Config.ForceFlag = tc.mockFlagForce
11853
cf.SDKConfig.WorkingDirectory = tc.mockWorkingDirectory
11954
})
12055
cmd := NewEnvListCommand(clients)
@@ -129,9 +64,78 @@ func Test_Env_ListCommandPreRun(t *testing.T) {
12964
}
13065

13166
func Test_Env_ListCommand(t *testing.T) {
67+
mockAppSelect := func() {
68+
appSelectMock := prompts.NewAppSelectMock()
69+
appSelectPromptFunc = appSelectMock.AppSelectPrompt
70+
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
71+
}
72+
13273
testutil.TableTestCommand(t, testutil.CommandTests{
133-
"list variables using arguments": {
74+
"lists variables from the .env file": {
13475
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
76+
mockAppSelect()
77+
err := afero.WriteFile(cf.Fs, ".env", []byte("SECRET_KEY=abc123\nAPI_TOKEN=xyz789\n"), 0644)
78+
assert.NoError(t, err)
79+
},
80+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
81+
cm.IO.AssertCalled(
82+
t,
83+
"PrintTrace",
84+
mock.Anything,
85+
slacktrace.EnvListCount,
86+
[]string{
87+
"2",
88+
},
89+
)
90+
cm.IO.AssertCalled(
91+
t,
92+
"PrintTrace",
93+
mock.Anything,
94+
slacktrace.EnvListVariables,
95+
[]string{
96+
"API_TOKEN",
97+
"SECRET_KEY",
98+
},
99+
)
100+
},
101+
},
102+
"lists no variables when the .env file does not exist": {
103+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
104+
mockAppSelect()
105+
},
106+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
107+
cm.IO.AssertCalled(
108+
t,
109+
"PrintTrace",
110+
mock.Anything,
111+
slacktrace.EnvListCount,
112+
[]string{
113+
"0",
114+
},
115+
)
116+
},
117+
},
118+
"lists no variables when the .env file is empty": {
119+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
120+
mockAppSelect()
121+
err := afero.WriteFile(cf.Fs, ".env", []byte(""), 0644)
122+
assert.NoError(t, err)
123+
},
124+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
125+
cm.IO.AssertCalled(
126+
t,
127+
"PrintTrace",
128+
mock.Anything,
129+
slacktrace.EnvListCount,
130+
[]string{
131+
"0",
132+
},
133+
)
134+
},
135+
},
136+
"lists hosted variables using the API": {
137+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
138+
mockAppSelect()
135139
cm.API.On(
136140
"ListVariables",
137141
mock.Anything,
@@ -145,9 +149,22 @@ func Test_Env_ListCommand(t *testing.T) {
145149
},
146150
nil,
147151
)
148-
appSelectMock := prompts.NewAppSelectMock()
149-
appSelectPromptFunc = appSelectMock.AppSelectPrompt
150-
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowHostedOnly, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
152+
manifestMock := &app.ManifestMockObject{}
153+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
154+
types.SlackYaml{
155+
AppManifest: types.AppManifest{
156+
Settings: &types.AppSettings{
157+
FunctionRuntime: types.SlackHosted,
158+
},
159+
},
160+
},
161+
nil,
162+
)
163+
cm.AppClient.Manifest = manifestMock
164+
projectConfigMock := config.NewProjectConfigMock()
165+
projectConfigMock.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
166+
cm.Config.ProjectConfig = projectConfigMock
167+
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
151168
},
152169
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
153170
cm.API.AssertCalled(

0 commit comments

Comments
 (0)