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
27 changes: 16 additions & 11 deletions cmd/env/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,44 +85,49 @@ func preRunEnvAddCommandFunc(ctx context.Context, clients *shared.ClientFactory,
func runEnvAddCommandFunc(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

// Get the workspace from the flag or prompt
selection, err := appSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly)
if err != nil {
return err
// Hosted apps require selecting an app before gathering variable inputs.
hosted := isHostedRuntime(ctx, clients)
var selection prompts.SelectedApp
if hosted {
s, err := appSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly)
if err != nil {
return err
}
selection = s
}

// Get the variable name from the args or prompt
var variableName string
variableName := ""
if len(args) < 1 {
variableName, err = clients.IO.InputPrompt(ctx, "Variable name", iostreams.InputPromptConfig{
name, err := clients.IO.InputPrompt(ctx, "Variable name", iostreams.InputPromptConfig{
Required: false,
})
if err != nil {
return err
}
variableName = name
} else {
variableName = args[0]
}

// Get the variable value from the args or prompt
var variableValue string
variableValue := ""
if len(args) < 2 {
response, err := clients.IO.PasswordPrompt(ctx, "Variable value", iostreams.PasswordPromptConfig{
Flag: clients.Config.Flags.Lookup("value"),
})
if err != nil {
return err
} else {
variableValue = response.Value
}
variableValue = response.Value
} else {
variableValue = args[1]
}

// Add the environment variable using either the Slack API method or the
// project ".env" file depending on the app hosting.
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
err = clients.API().AddVariable(
if hosted && !selection.App.IsDev {
err := clients.API().AddVariable(
ctx,
selection.Auth.Token,
selection.App.AppID,
Expand Down
67 changes: 31 additions & 36 deletions cmd/env/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,33 +204,16 @@ func Test_Env_AddCommand(t *testing.T) {
)
},
},
"add a numeric variable using prompts to the .env file": {
CmdArgs: []string{},
"add a numeric variable to the .env file for remote runtime": {
CmdArgs: []string{"PORT", "3000"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
setupEnvAddDotenvMocks(ctx, cm, cf)
cm.IO.On(
"InputPrompt",
mock.Anything,
"Variable name",
mock.Anything,
).Return(
"PORT",
nil,
)
cm.IO.On(
"PasswordPrompt",
mock.Anything,
"Variable value",
iostreams.MatchPromptConfig(iostreams.PasswordPromptConfig{
Flag: cm.Config.Flags.Lookup("value"),
}),
).Return(
iostreams.PasswordPromptResponse{
Prompt: true,
Value: "3000",
},
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{AppManifest: types.AppManifest{Settings: &types.AppSettings{FunctionRuntime: types.Remote}}},
nil,
)
cm.AppClient.Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.API.AssertNotCalled(t, "AddVariable")
Expand All @@ -239,18 +222,39 @@ func Test_Env_AddCommand(t *testing.T) {
assert.Equal(t, "PORT=3000\n", string(content))
},
},
"add a variable to the .env file for non-hosted app": {
"add a variable to the .env file when no runtime is set": {
CmdArgs: []string{"NEW_VAR", "new_value"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
setupEnvAddDotenvMocks(ctx, cm, cf)
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, nil)
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env", []byte("# Config\nEXISTING=value\n"), 0600)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.API.AssertNotCalled(t, "AddVariable")
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, "# Config\nEXISTING=value\nNEW_VAR=\"new_value\"\n", string(content))
assert.Equal(t, "# Config\nEXISTING=value\n"+`NEW_VAR="new_value"`+"\n", string(content))
},
},
"add a variable to the .env file when manifest fetch errors": {
CmdArgs: []string{"API_KEY", "sk-1234"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
setupEnvAddDotenvMocks(ctx, cm, cf)
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{},
slackerror.New(slackerror.ErrSDKHookNotFound),
)
cm.AppClient.Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.API.AssertNotCalled(t, "AddVariable")
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, `API_KEY="sk-1234"`+"\n", string(content))
},
},
}, func(cf *shared.ClientFactory) *cobra.Command {
Expand Down Expand Up @@ -292,20 +296,11 @@ func setupEnvAddHostedMocks(ctx context.Context, cm *shared.ClientsMock, cf *sha
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
}

// setupEnvAddDotenvMocks prepares common mocks for non-hosted (dotenv) app tests
// setupEnvAddDotenvMocks prepares common mocks for non-hosted (dotenv) app tests.
// Callers must set their own manifest mock on cm.AppClient.Manifest.
func setupEnvAddDotenvMocks(_ context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cf.SDKConfig = hooks.NewSDKConfigMock()
cm.AddDefaultMocks()

mockDevApp := types.App{
TeamID: "T1",
TeamDomain: "team1",
AppID: "A0123456789",
IsDev: true,
}
appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{Auth: mockAuth, App: mockDevApp}, nil)

cm.Config.Flags.String("value", "", "mock value flag")
}
14 changes: 14 additions & 0 deletions cmd/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package env

import (
"context"
"strings"

"github.com/slackapi/slack-cli/internal/prompts"
Expand Down Expand Up @@ -72,3 +73,16 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {

return cmd
}

// isHostedRuntime returns true if the local manifest is for an app that uses
// the Deno Slack SDK function runtime.
//
// It defaults to false when the manifest cannot be fetched, which directs the
// command to use the project ".env" file. Otherwise the API is used.
func isHostedRuntime(ctx context.Context, clients *shared.ClientFactory) bool {
manifest, err := clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
if err != nil {
return false
}
return manifest.IsFunctionRuntimeSlackHosted() || manifest.IsFunctionRuntimeLocal()
}
Comment on lines +77 to +88
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔭 note: I'm not thrilled with the placement of this logic but we might want to keep it scoped to the env commands at this time?

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.

I'm surprised we don't have a helper function already, but I agree let's just scope it to env for now.

68 changes: 68 additions & 0 deletions cmd/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ package env
import (
"testing"

"github.com/slackapi/slack-cli/internal/app"
"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/internal/slackerror"
"github.com/slackapi/slack-cli/test/testutil"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func Test_Env_Command(t *testing.T) {
Expand All @@ -36,3 +42,65 @@ func Test_Env_Command(t *testing.T) {
return cmd
})
}

func Test_isHostedRuntime(t *testing.T) {
tests := map[string]struct {
mockManifest types.SlackYaml
mockError error
expected bool
}{
"returns true for slack hosted runtime": {
mockManifest: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.SlackHosted,
},
},
},
expected: true,
},
"returns true for local runtime": {
mockManifest: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.LocallyRun,
},
},
},
expected: true,
},
"returns false for remote runtime": {
mockManifest: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.Remote,
},
},
},
expected: false,
},
"returns false for empty runtime": {
mockManifest: types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{},
},
},
expected: false,
},
"returns false when manifest fetch fails": {
mockError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
expected: false,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifest, tc.mockError)
clientsMock.AppClient.Manifest = manifestMock
clients := shared.NewClientFactory(clientsMock.MockClientFactory())
assert.Equal(t, tc.expected, isHostedRuntime(ctx, clients))
})
}
}
27 changes: 17 additions & 10 deletions cmd/env/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,27 @@ func runEnvListCommandFunc(
) error {
ctx := cmd.Context()

selection, err := appSelectPromptFunc(
ctx,
clients,
prompts.ShowAllEnvironments,
prompts.ShowInstalledAppsOnly,
)
if err != nil {
return err
// Hosted apps require selecting an app before gathering variables.
hosted := isHostedRuntime(ctx, clients)
var selection prompts.SelectedApp
if hosted {
var err error
selection, err = appSelectPromptFunc(
ctx,
clients,
prompts.ShowAllEnvironments,
prompts.ShowInstalledAppsOnly,
)
if err != nil {
return err
}
}

// Gather environment variables for either a ROSI app from the Slack API method
// Gather environment variables from the Slack API for deployed hosted apps
// or read from project files.
var variableNames []string
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
if hosted && !selection.App.IsDev {
var err error
variableNames, err = clients.API().ListVariables(
ctx,
selection.Auth.Token,
Expand Down
24 changes: 16 additions & 8 deletions cmd/env/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"testing"

"github.com/slackapi/slack-cli/internal/app"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
Expand Down Expand Up @@ -71,9 +70,14 @@ func Test_Env_ListCommand(t *testing.T) {
}

testutil.TableTestCommand(t, testutil.CommandTests{
"lists variables from the .env file": {
"lists variables from the .env file for remote runtime": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{AppManifest: types.AppManifest{Settings: &types.AppSettings{FunctionRuntime: types.Remote}}},
nil,
)
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env", []byte("SECRET_KEY=abc123\nAPI_TOKEN=xyz789\n"), 0644)
assert.NoError(t, err)
},
Expand Down Expand Up @@ -101,7 +105,9 @@ func Test_Env_ListCommand(t *testing.T) {
},
"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()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, nil)
cm.AppClient.Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(
Expand All @@ -117,7 +123,12 @@ func Test_Env_ListCommand(t *testing.T) {
},
"lists no variables when the .env file is empty": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
mockAppSelect()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{},
slackerror.New(slackerror.ErrSDKHookNotFound),
)
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env", []byte(""), 0644)
assert.NoError(t, err)
},
Expand Down Expand Up @@ -161,9 +172,6 @@ func Test_Env_ListCommand(t *testing.T) {
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) {
Expand Down
6 changes: 6 additions & 0 deletions internal/shared/types/app_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ func (manifest *AppManifest) FunctionRuntime() FunctionRuntime {
return manifest.Settings.FunctionRuntime
}

// IsFunctionRuntimeLocal returns true when the function runtime setting
// is local
func (manifest *AppManifest) IsFunctionRuntimeLocal() bool {
return manifest.Settings != nil && manifest.Settings.FunctionRuntime == LocallyRun
}

// IsFunctionRuntimeSlackHosted returns true when the function runtime setting
// is slack hosted
func (manifest *AppManifest) IsFunctionRuntimeSlackHosted() bool {
Expand Down
Loading
Loading