diff --git a/cmd/cmdtest.go b/cmd/cmdtest.go index 9ac56658..4000e2d4 100644 --- a/cmd/cmdtest.go +++ b/cmd/cmdtest.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "io" + "os" "testing" "github.com/stretchr/testify/require" @@ -47,3 +48,14 @@ func CallCmd( return out, nil } + +// SetupTestEnvVars sets up and tears down tests for checking that environment variables are set. +func SetupTestEnvVars(_ *testing.T) func(t *testing.T) { + os.Setenv("LD_ACCESS_TOKEN", "testAccessToken") + os.Setenv("LD_BASE_URI", "http://test.com") + + return func(t *testing.T) { + os.Unsetenv("LD_ACCESS_TOKEN") + os.Unsetenv("LD_BASE_URI") + } +} diff --git a/cmd/environments/get_test.go b/cmd/environments/get_test.go index 173fb58d..2bd2c461 100644 --- a/cmd/environments/get_test.go +++ b/cmd/environments/get_test.go @@ -15,11 +15,12 @@ func TestGet(t *testing.T) { errorHelp := ". See `ldcli environments get --help` for supported flags and usage." mockArgs := []interface{}{ "testAccessToken", - "https://app.launchdarkly.com", + "http://test.com", "test-env", "test-proj", } - t.Run("with valid environments calls projects API", func(t *testing.T) { + + t.Run("with valid environments calls API", func(t *testing.T) { client := environments.MockClient{} client. On("Get", mockArgs...). @@ -27,6 +28,26 @@ func TestGet(t *testing.T) { args := []string{ "environments", "get", "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "--environment", "test-env", + "--project", "test-proj", + } + + output, err := cmd.CallCmd(t, &client, nil, nil, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := environments.MockClient{} + client. + On("Get", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "environments", "get", "--environment", "test-env", "--project", "test-proj", } @@ -45,6 +66,7 @@ func TestGet(t *testing.T) { args := []string{ "environments", "get", "--access-token", "testAccessToken", + "--base-uri", "http://test.com", "--environment", "test-env", "--project", "test-proj", } diff --git a/cmd/flags/create_test.go b/cmd/flags/create_test.go index 3f242472..80e798b0 100644 --- a/cmd/flags/create_test.go +++ b/cmd/flags/create_test.go @@ -1,12 +1,12 @@ package flags_test import ( - "ldcli/cmd" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "ldcli/cmd" "ldcli/internal/errors" "ldcli/internal/flags" ) @@ -20,7 +20,8 @@ func TestCreate(t *testing.T) { "test-key", "test-proj-key", } - t.Run("with valid flags calls projects API", func(t *testing.T) { + + t.Run("with valid flags calls API", func(t *testing.T) { client := flags.MockClient{} client. On("Create", mockArgs...). @@ -39,6 +40,25 @@ func TestCreate(t *testing.T) { assert.JSONEq(t, `{"valid": true}`, string(output)) }) + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := flags.MockClient{} + client. + On("Create", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "flags", "create", + "-d", `{"key": "test-key", "name": "test-name"}`, + "--project", "test-proj-key", + } + + output, err := cmd.CallCmd(t, nil, &client, nil, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + t.Run("with an error response is an error", func(t *testing.T) { client := flags.MockClient{} client. diff --git a/cmd/flags/update_test.go b/cmd/flags/update_test.go index 1c30e29f..8c2c1072 100644 --- a/cmd/flags/update_test.go +++ b/cmd/flags/update_test.go @@ -26,7 +26,7 @@ func TestUpdate(t *testing.T) { }, }, } - t.Run("with valid flags calls projects API", func(t *testing.T) { + t.Run("with valid flags calls API", func(t *testing.T) { client := flags.MockClient{} client. On("Update", mockArgs...). @@ -46,6 +46,26 @@ func TestUpdate(t *testing.T) { assert.JSONEq(t, `{"valid": true}`, string(output)) }) + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := flags.MockClient{} + client. + On("Update", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "flags", "update", + "-d", `[{"op": "replace", "path": "/name", "value": "new-name"}]`, + "--flag", "test-key", + "--project", "test-proj-key", + } + + output, err := cmd.CallCmd(t, nil, &client, nil, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + t.Run("with an error response is an error", func(t *testing.T) { client := flags.MockClient{} client. @@ -105,7 +125,7 @@ func TestToggle(t *testing.T) { }, }, } - t.Run("with valid flags calls projects API", func(t *testing.T) { + t.Run("with valid flags calls API", func(t *testing.T) { client := flags.MockClient{} client. On("Update", mockArgs...). diff --git a/cmd/members/create_test.go b/cmd/members/create_test.go index 3777aa16..f48ed688 100644 --- a/cmd/members/create_test.go +++ b/cmd/members/create_test.go @@ -19,6 +19,7 @@ func TestCreate(t *testing.T) { "http://test.com", []members.MemberInput{{Email: "testemail@test.com", Role: role}}, } + t.Run("with valid flags calls members API", func(t *testing.T) { client := members.MockClient{} client. @@ -27,10 +28,31 @@ func TestCreate(t *testing.T) { args := []string{ "members", "create", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "-d", + `[{"email": "testemail@test.com", "role": "writer"}]`, + } + + output, err := cmd.CallCmd(t, nil, nil, &client, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := members.MockClient{} + client. + On("Update", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + client. + On("Create", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "members", + "create", "-d", `[{"email": "testemail@test.com", "role": "writer"}]`, } @@ -49,10 +71,8 @@ func TestCreate(t *testing.T) { args := []string{ "members", "create", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", "-d", `[{"email": "testemail@test.com", "role": "writer"}]`, } diff --git a/cmd/members/invite_test.go b/cmd/members/invite_test.go index dca67ebc..becd88c5 100644 --- a/cmd/members/invite_test.go +++ b/cmd/members/invite_test.go @@ -22,6 +22,7 @@ func TestInvite(t *testing.T) { {Email: "testemail2@test.com", Role: readerRole}, }, } + t.Run("with valid flags calls members API", func(t *testing.T) { client := members.MockClient{} client. @@ -30,10 +31,31 @@ func TestInvite(t *testing.T) { args := []string{ "members", "invite", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "-e", + `testemail1@test.com,testemail2@test.com`, + } + + output, err := cmd.CallCmd(t, nil, nil, &client, nil, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := members.MockClient{} + client. + On("Update", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + client. + On("Create", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "members", + "invite", "-e", `testemail1@test.com,testemail2@test.com`, } @@ -52,10 +74,8 @@ func TestInvite(t *testing.T) { args := []string{ "members", "invite", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", "-e", `testemail1@test.com,testemail2@test.com`, } @@ -99,6 +119,7 @@ func TestInviteWithOptionalRole(t *testing.T) { {Email: "testemail2@test.com", Role: writerRole}, }, } + t.Run("with valid optional long form flag calls members API", func(t *testing.T) { client := members.MockClient{} client. diff --git a/cmd/projects/create_test.go b/cmd/projects/create_test.go index 9082f476..c74b1c39 100644 --- a/cmd/projects/create_test.go +++ b/cmd/projects/create_test.go @@ -19,7 +19,30 @@ func TestCreate(t *testing.T) { "test-name", "test-key", } - t.Run("with valid flags calls projects API", func(t *testing.T) { + + t.Run("with valid flags calls API", func(t *testing.T) { + client := projects.MockClient{} + client. + On("Create", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "projects", + "create", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "-d", + `{"key": "test-key", "name": "test-name"}`, + } + + output, err := cmd.CallCmd(t, nil, nil, nil, &client, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) client := projects.MockClient{} client. On("Create", mockArgs...). @@ -27,10 +50,6 @@ func TestCreate(t *testing.T) { args := []string{ "projects", "create", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", "-d", `{"key": "test-key", "name": "test-name"}`, } diff --git a/cmd/projects/list_test.go b/cmd/projects/list_test.go index 718b08c3..ca3c4728 100644 --- a/cmd/projects/list_test.go +++ b/cmd/projects/list_test.go @@ -17,7 +17,7 @@ func TestList(t *testing.T) { "testAccessToken", "http://test.com", } - t.Run("with valid flags calls projects API", func(t *testing.T) { + t.Run("with valid flags calls API", func(t *testing.T) { client := projects.MockClient{} client. On("List", mockArgs...). @@ -34,6 +34,24 @@ func TestList(t *testing.T) { assert.JSONEq(t, `{"valid": true}`, string(output)) }) + t.Run("with valid flags from environment variables calls API", func(t *testing.T) { + teardownTest := cmd.SetupTestEnvVars(t) + defer teardownTest(t) + client := projects.MockClient{} + client. + On("List", mockArgs...). + Return([]byte(cmd.ValidResponse), nil) + args := []string{ + "projects", + "list", + } + + output, err := cmd.CallCmd(t, nil, nil, nil, &client, args) + + require.NoError(t, err) + assert.JSONEq(t, `{"valid": true}`, string(output)) + }) + t.Run("with an error response is an error", func(t *testing.T) { client := projects.MockClient{} client. diff --git a/cmd/quickstart.go b/cmd/quickstart.go index 23b27eba..51a38f67 100644 --- a/cmd/quickstart.go +++ b/cmd/quickstart.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/viper" "ldcli/cmd/cliflags" + "ldcli/cmd/validators" "ldcli/internal/environments" "ldcli/internal/flags" "ldcli/internal/quickstart" @@ -20,6 +21,7 @@ func NewQuickStartCmd( flagsClient flags.Client, ) *cobra.Command { return &cobra.Command{ + Args: validators.Validate(), Long: "", RunE: runQuickStart(environmentsClient, flagsClient), Short: "Setup guide to create your first feature flag", diff --git a/cmd/root.go b/cmd/root.go index 6da950f7..82089c3d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -39,7 +40,6 @@ func NewRootCommand( cmd.DisableFlagParsing = true } }, - // Handle errors differently based on type. // We don't want to show the usage if the user has the right structure but invalid data such as // the wrong key. @@ -47,6 +47,11 @@ func NewRootCommand( SilenceUsage: true, } + viper.SetEnvPrefix("LD") + replacer := strings.NewReplacer("-", "_") + viper.SetEnvKeyReplacer(replacer) + viper.AutomaticEnv() + cmd.PersistentFlags().String( cliflags.AccessTokenFlag, "", diff --git a/cmd/validators/validators.go b/cmd/validators/validators.go index f527cc53..7e96c79a 100644 --- a/cmd/validators/validators.go +++ b/cmd/validators/validators.go @@ -6,6 +6,7 @@ import ( "net/url" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "ldcli/cmd/cliflags" @@ -15,6 +16,7 @@ import ( // Validate is a validator for commands to print an error when the user input is invalid. func Validate() cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { + rebindFlags(cmd, cmd.ValidArgs) // rebind flags before validating them below commandPath := getCommandPath(cmd) _, err := url.ParseRequestURI(viper.GetString(cliflags.BaseURIFlag)) @@ -52,3 +54,14 @@ func getCommandPath(cmd *cobra.Command) string { return commandPath } + +// rebindFlags sets the command's flags based on the values stored in viper because they may not +// be set yet when they (the flags) are set from environment variables or a configuration file. +func rebindFlags(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlags(cmd.Flags()) + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if viper.IsSet(f.Name) && viper.GetString(f.Name) != "" { + _ = cmd.Flags().Set(f.Name, viper.GetString(f.Name)) + } + }) +}