From 1e12d8c439852e77692d9f1b28522a131fa5492b Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Thu, 4 Sep 2025 08:58:47 -0600 Subject: [PATCH 01/14] wip --- cmd/config/set_test.go | 9 +- cmd/config/unset_test.go | 15 +- cmd/root.go | 16 +- cmd/root_test.go | 15 +- internal/autocompletion/config_args.go | 14 +- internal/autocompletion/root_flags.go | 10 +- .../commands/config/add_profile_internal.go | 100 ++++++--- .../config/add_profile_internal_test.go | 177 ++++++---------- .../config/delete_profile_internal.go | 61 +++++- .../config/delete_profile_internal_test.go | 101 ++++++---- internal/commands/config/get_internal.go | 33 ++- internal/commands/config/get_internal_test.go | 94 +++++---- .../commands/config/list_keys_internal.go | 48 ++++- .../config/list_keys_internal_test.go | 34 +++- .../commands/config/list_profiles_internal.go | 34 +++- .../config/list_profiles_internal_test.go | 34 +++- .../config/set_active_profile_internal.go | 40 +++- .../set_active_profile_internal_test.go | 70 +++++-- internal/commands/config/set_internal.go | 151 +++++++++----- internal/commands/config/set_internal_test.go | 188 ++++++++--------- internal/commands/config/unset_internal.go | 50 ++++- .../commands/config/unset_internal_test.go | 97 +++++---- .../commands/config/view_profile_internal.go | 38 +++- .../config/view_profile_internal_test.go | 70 ++++--- internal/commands/license/license_internal.go | 57 ++++-- .../commands/license/license_internal_test.go | 189 +++++++----------- internal/commands/plugin/add_internal.go | 9 +- internal/commands/plugin/remove_internal.go | 9 +- internal/commands/request/request_internal.go | 9 +- internal/configuration/configuration.go | 32 ++- internal/input/input.go | 27 ++- internal/profiles/koanf.go | 112 +++++++---- internal/profiles/validate.go | 20 +- 33 files changed, 1247 insertions(+), 716 deletions(-) diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index e8a6cb94..b5518200 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -62,10 +62,15 @@ func TestConfigSetCmd_CheckKoanfConfig(t *testing.T) { err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=%s", koanfKey, koanfNewUUID)) testutils.CheckExpectedError(t, err, nil) - koanf := profiles.GetKoanfConfig().KoanfInstance() + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + t.Errorf("Error getting koanf configuration: %v", err) + } + + koanfInstance := koanfConfig.KoanfInstance() profileKoanfKey := "default." + koanfKey - koanfNewValue, ok := koanf.Get(profileKoanfKey).(*customtypes.UUID) + koanfNewValue, ok := koanfInstance.Get(profileKoanfKey).(*customtypes.UUID) if ok && koanfNewValue.String() != koanfNewUUID { t.Errorf("Expected koanf configuration value to be updated") } diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index 430676e6..85748d05 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -43,18 +43,23 @@ func TestConfigUnsetCmd_InvalidKey(t *testing.T) { func TestConfigUnsetCmd_CheckKoanfConfig(t *testing.T) { testutils_koanf.InitKoanfs(t) - koanfConfig := profiles.GetKoanfConfig().KoanfInstance() + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + t.Errorf("Error getting koanf configuration: %v", err) + } + + koanfInstance := koanfConfig.KoanfInstance() koanfKey := options.PingOneAuthenticationWorkerClientIDOption.KoanfKey profileKoanfKey := "default." + koanfKey - koanfOldValue := koanfConfig.String(profileKoanfKey) + koanfOldValue := koanfInstance.String(profileKoanfKey) - err := testutils_cobra.ExecutePingcli(t, "config", "unset", koanfKey) + err = testutils_cobra.ExecutePingcli(t, "config", "unset", koanfKey) testutils.CheckExpectedError(t, err, nil) - koanfConfig = profiles.GetKoanfConfig().KoanfInstance() + koanfInstance = koanfConfig.KoanfInstance() - koanfNewValue := koanfConfig.String(profileKoanfKey) + koanfNewValue := koanfInstance.String(profileKoanfKey) if koanfOldValue == koanfNewValue { t.Errorf("Expected koanf configuration value to be updated. Old: %s, New: %s", koanfOldValue, koanfNewValue) } diff --git a/cmd/root.go b/cmd/root.go index fa7502ef..5351563b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -127,8 +127,13 @@ func initKoanfProfile() { l.Debug().Msgf("Using configuration profile: %s", configFileActiveProfile) } + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + output.SystemError(fmt.Sprintf("Failed to get koanf config: %v", err), nil) + } + // Configure the profile koanf instance - if err := profiles.GetKoanfConfig().ChangeActiveProfile(configFileActiveProfile); err != nil { + if err := koanfConfig.ChangeActiveProfile(configFileActiveProfile); err != nil { output.UserFatal(fmt.Sprintf("Failed to set active profile: %v", err), nil) } @@ -186,14 +191,19 @@ func initKoanf(cfgFile string) { loadKoanfConfig(cfgFile) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + output.SystemError(fmt.Sprintf("Failed to get koanf config: %v", err), nil) + } + // If there are no profiles in the configuration file, seed the default profile - if len(profiles.GetKoanfConfig().ProfileNames()) == 0 { + if len(koanfConfig.ProfileNames()) == 0 { l.Debug().Msgf("No profiles found in configuration file. Creating default profile in configuration file '%s'", cfgFile) createConfigFile(cfgFile) loadKoanfConfig(cfgFile) } - err := profiles.GetKoanfConfig().DefaultMissingKoanfKeys() + err = koanfConfig.DefaultMissingKoanfKeys() if err != nil { output.SystemError(err.Error(), nil) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 9072a6d5..0a075068 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -3,6 +3,7 @@ package cmd_test import ( + "os" "testing" "github.com/pingidentity/pingcli/internal/configuration/options" @@ -104,6 +105,9 @@ func TestRootCmd_InvalidColorFlag(t *testing.T) { // Test Root Command Executes when provided the --config flag func TestRootCmd_ConfigFlag(t *testing.T) { + // Add the --config args to os.Args + os.Args = append(os.Args, "--"+options.RootConfigOption.CobraParamName, "config.yaml") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName, "config.yaml") testutils.CheckExpectedError(t, err, nil) } @@ -115,6 +119,13 @@ func TestRootCmd_NoValueConfigFlag(t *testing.T) { testutils.CheckExpectedError(t, err, &expectedErrorPattern) } +// Test Root Command fails on non-existent configuration file +func TestRootCmd_NonExistentConfigFile(t *testing.T) { + expectedErrorPattern := `^Configuration file '.*' does not exist. Use the default configuration file location or specify a valid configuration file location with the --config flag\.$` + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName, "non_existent.yaml") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + // Test Root Command Executes when provided the --profile flag func TestRootCmd_ProfileFlag(t *testing.T) { err := testutils_cobra.ExecutePingcli(t, "--"+options.RootProfileOption.CobraParamName, "default") @@ -128,7 +139,7 @@ func TestRootCmd_NoValueProfileFlag(t *testing.T) { testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// // Test Root Command Detailed Exit Code Flag +// Test Root Command Detailed Exit Code Flag func TestRootCmd_DetailedExitCodeFlag(t *testing.T) { err := testutils_cobra.ExecutePingcli(t, "--"+options.RootDetailedExitCodeOption.CobraParamName) testutils.CheckExpectedError(t, err, nil) @@ -137,7 +148,7 @@ func TestRootCmd_DetailedExitCodeFlag(t *testing.T) { testutils.CheckExpectedError(t, err, nil) } -// // Test Root Command Detailed Exit Code Flag with output Warn +// Test Root Command Detailed Exit Code Flag with output Warn func TestRootCmd_DetailedExitCodeWarnLoggedFunc(t *testing.T) { testutils_koanf.InitKoanfs(t) t.Setenv(options.RootDetailedExitCodeOption.EnvVar, "true") diff --git a/internal/autocompletion/config_args.go b/internal/autocompletion/config_args.go index 237a9bd5..43fbb73e 100644 --- a/internal/autocompletion/config_args.go +++ b/internal/autocompletion/config_args.go @@ -12,11 +12,21 @@ import ( ) func ConfigViewProfileFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return profiles.GetKoanfConfig().ProfileNames(), cobra.ShellCompDirectiveNoFileComp + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + } + + return koanfConfig.ProfileNames(), cobra.ShellCompDirectiveNoFileComp } func ConfigReturnNonActiveProfilesFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - profileNames := profiles.GetKoanfConfig().ProfileNames() + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + } + + profileNames := koanfConfig.ProfileNames() if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/internal/autocompletion/root_flags.go b/internal/autocompletion/root_flags.go index bea293f2..dc9ac1ee 100644 --- a/internal/autocompletion/root_flags.go +++ b/internal/autocompletion/root_flags.go @@ -3,13 +3,21 @@ package autocompletion import ( + "fmt" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" "github.com/spf13/cobra" ) func RootProfileFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return profiles.GetKoanfConfig().ProfileNames(), cobra.ShellCompDirectiveNoFileComp + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + } + + return koanfConfig.ProfileNames(), cobra.ShellCompDirectiveNoFileComp } func RootOutputFormatFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/internal/commands/config/add_profile_internal.go b/internal/commands/config/add_profile_internal.go index 0d9403bc..49a0760b 100644 --- a/internal/commands/config/add_profile_internal.go +++ b/internal/commands/config/add_profile_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "io" "strconv" @@ -14,15 +15,45 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + ErrProfileNameNotProvided = errors.New("unable to determine profile name") + ErrSetActiveInvalid = errors.New("invalid value for set-active flag. must be 'true' or 'false'") +) + +type AddProfileError struct { + Err error +} + +func (e *AddProfileError) Error() string { + var err *AddProfileError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to add profile: %s", e.Err.Error()) +} + +func (e *AddProfileError) Unwrap() error { + var err *AddProfileError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { newProfileName, newDescription, setActive, err := readConfigAddProfileOptions(rc) if err != nil { - return fmt.Errorf("failed to add profile: %w", err) + return &AddProfileError{Err: err} + } + + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &AddProfileError{Err: err} } - err = profiles.GetKoanfConfig().ValidateNewProfileName(newProfileName) + err = koanfConfig.ValidateNewProfileName(newProfileName) if err != nil { - return fmt.Errorf("failed to add profile: %w", err) + return &AddProfileError{Err: err} } output.Message(fmt.Sprintf("Adding new profile '%s'...", newProfileName), nil) @@ -30,26 +61,26 @@ func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { subKoanf := koanf.New(".") err = subKoanf.Set(options.ProfileDescriptionOption.KoanfKey, newDescription) if err != nil { - return fmt.Errorf("failed to add profile: %w", err) + return &AddProfileError{Err: err} } - if err = profiles.GetKoanfConfig().SaveProfile(newProfileName, subKoanf); err != nil { - return fmt.Errorf("failed to add profile: %w", err) + if err = koanfConfig.SaveProfile(newProfileName, subKoanf); err != nil { + return &AddProfileError{Err: err} } - output.Success(fmt.Sprintf("Profile created. Update additional profile attributes via 'pingcli config set' or directly within the config file at '%s'", profiles.GetKoanfConfig().GetKoanfConfigFile()), nil) + output.Success(fmt.Sprintf("Profile created. Update additional profile attributes via 'pingcli config set' or directly within the config file at '%s'", koanfConfig.GetKoanfConfigFile()), nil) if setActive { - if err = profiles.GetKoanfConfig().ChangeActiveProfile(newProfileName); err != nil { - return fmt.Errorf("failed to set active profile: %w", err) + if err = koanfConfig.ChangeActiveProfile(newProfileName); err != nil { + return &AddProfileError{Err: err} } output.Success(fmt.Sprintf("Profile '%s' set as active.", newProfileName), nil) } - err = profiles.GetKoanfConfig().DefaultMissingKoanfKeys() + err = koanfConfig.DefaultMissingKoanfKeys() if err != nil { - return fmt.Errorf("failed to add profile: %w", err) + return &AddProfileError{Err: err} } return nil @@ -57,15 +88,15 @@ func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { func readConfigAddProfileOptions(rc io.ReadCloser) (newProfileName, newDescription string, setActive bool, err error) { if newProfileName, err = readConfigAddProfileNameOption(rc); err != nil { - return newProfileName, newDescription, setActive, err + return newProfileName, newDescription, setActive, &AddProfileError{Err: err} } if newDescription, err = readConfigAddProfileDescriptionOption(rc); err != nil { - return newProfileName, newDescription, setActive, err + return newProfileName, newDescription, setActive, &AddProfileError{Err: err} } if setActive, err = readConfigAddProfileSetActiveOption(rc); err != nil { - return newProfileName, newDescription, setActive, err + return newProfileName, newDescription, setActive, &AddProfileError{Err: err} } return newProfileName, newDescription, setActive, nil @@ -73,22 +104,27 @@ func readConfigAddProfileOptions(rc io.ReadCloser) (newProfileName, newDescripti func readConfigAddProfileNameOption(rc io.ReadCloser) (newProfileName string, err error) { if !options.ConfigAddProfileNameOption.Flag.Changed { - newProfileName, err = input.RunPrompt("New profile name", profiles.GetKoanfConfig().ValidateNewProfileName, rc) + koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return newProfileName, err + return newProfileName, &AddProfileError{Err: err} + } + + newProfileName, err = input.RunPrompt("New profile name", koanfConfig.ValidateNewProfileName, rc) + if err != nil { + return newProfileName, &AddProfileError{Err: err} } if newProfileName == "" { - return newProfileName, fmt.Errorf("unable to determine profile name") + return newProfileName, &AddProfileError{Err: ErrProfileNameNotProvided} } } else { newProfileName, err = profiles.GetOptionValue(options.ConfigAddProfileNameOption) if err != nil { - return newProfileName, err + return newProfileName, &AddProfileError{Err: err} } if newProfileName == "" { - return newProfileName, fmt.Errorf("unable to determine profile name") + return newProfileName, &AddProfileError{Err: ErrProfileNameNotProvided} } } @@ -97,21 +133,37 @@ func readConfigAddProfileNameOption(rc io.ReadCloser) (newProfileName string, er func readConfigAddProfileDescriptionOption(rc io.ReadCloser) (newDescription string, err error) { if !options.ConfigAddProfileDescriptionOption.Flag.Changed { - return input.RunPrompt("New profile description: ", nil, rc) + newDescription, err = input.RunPrompt("New profile description: ", nil, rc) + if err != nil { + return newDescription, &AddProfileError{Err: err} + } } else { - return profiles.GetOptionValue(options.ConfigAddProfileDescriptionOption) + newDescription, err = profiles.GetOptionValue(options.ConfigAddProfileDescriptionOption) + if err != nil { + return newDescription, &AddProfileError{Err: err} + } } + + return newDescription, nil } func readConfigAddProfileSetActiveOption(rc io.ReadCloser) (setActive bool, err error) { if !options.ConfigAddProfileSetActiveOption.Flag.Changed { - return input.RunPromptConfirm("Set new profile as active: ", rc) + setActive, err = input.RunPromptConfirm("Set new profile as active: ", rc) + if err != nil { + return setActive, &AddProfileError{Err: err} + } } else { boolStr, err := profiles.GetOptionValue(options.ConfigAddProfileSetActiveOption) if err != nil { - return setActive, err + return setActive, &AddProfileError{Err: err} } - return strconv.ParseBool(boolStr) + setActive, err = strconv.ParseBool(boolStr) + if err != nil { + return setActive, &AddProfileError{Err: ErrSetActiveInvalid} + } } + + return setActive, nil } diff --git a/internal/commands/config/add_profile_internal_test.go b/internal/commands/config/add_profile_internal_test.go index 0c792186..1444f5c5 100644 --- a/internal/commands/config/add_profile_internal_test.go +++ b/internal/commands/config/add_profile_internal_test.go @@ -3,130 +3,79 @@ package config_internal import ( + "errors" "os" "testing" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigAddProfile function func Test_RunInternalConfigAddProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("test-profile") - description = customtypes.String("test-description") - setActive = customtypes.Bool(false) - ) - - options.ConfigAddProfileNameOption.Flag.Changed = true - options.ConfigAddProfileNameOption.CobraParamValue = &profileName - - options.ConfigAddProfileDescriptionOption.Flag.Changed = true - options.ConfigAddProfileDescriptionOption.CobraParamValue = &description - - options.ConfigAddProfileSetActiveOption.Flag.Changed = true - options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive - - err := RunInternalConfigAddProfile(os.Stdin) - if err != nil { - t.Errorf("RunInternalConfigAddProfile returned error: %v", err) + testCases := []struct { + name string + profileName customtypes.String + description customtypes.String + setActive customtypes.Bool + expectedError error + }{ + { + name: "Create New Profile and Not Set it as the Active Profile", + profileName: "test-profile", + description: "test-description", + setActive: customtypes.Bool(false), + }, + { + name: "Create New Profile and Set it as the Active Profile", + profileName: "test-profile-active", + description: "test-description-active", + setActive: customtypes.Bool(true), + }, + { + name: "Invalid Profile Name: Already Exists", + profileName: "default", + description: "test-description", + setActive: customtypes.Bool(false), + expectedError: profiles.ErrProfileNameAlreadyExists, + }, + { + name: "Invalid Profile Name: None Provided", + profileName: "", + description: "test-description", + setActive: customtypes.Bool(false), + expectedError: ErrProfileNameNotProvided, + }, } -} - -// Test RunInternalConfigAddProfile function fails when existing profile name is provided -func Test_RunInternalConfigAddProfile_ExistingProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("default") - description = customtypes.String("test-description") - setActive = customtypes.Bool(false) - ) - - options.ConfigAddProfileNameOption.Flag.Changed = true - options.ConfigAddProfileNameOption.CobraParamValue = &profileName - - options.ConfigAddProfileDescriptionOption.Flag.Changed = true - options.ConfigAddProfileDescriptionOption.CobraParamValue = &description - - options.ConfigAddProfileSetActiveOption.Flag.Changed = true - options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive - - expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'. profile already exists$` - err := RunInternalConfigAddProfile(os.Stdin) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigAddProfile function fails when profile name is not provided -func Test_RunInternalConfigAddProfile_NoProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - var ( - profileName = customtypes.String("") - description = customtypes.String("test-description") - setActive = customtypes.Bool(false) - ) - - options.ConfigAddProfileNameOption.Flag.Changed = true - options.ConfigAddProfileNameOption.CobraParamValue = &profileName - options.ConfigAddProfileDescriptionOption.Flag.Changed = true - options.ConfigAddProfileDescriptionOption.CobraParamValue = &description - - options.ConfigAddProfileSetActiveOption.Flag.Changed = true - options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive - - expectedErrorPattern := `^failed to add profile: unable to determine profile name$` - err := RunInternalConfigAddProfile(os.Stdin) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigAddProfile function succeeds with set active flag set to true -func Test_RunInternalConfigAddProfile_SetActive(t *testing.T) { - testutils_koanf.InitKoanfs(t) - var ( - profileName = customtypes.String("test-profile-active") - description = customtypes.String("test-description") - setActive = customtypes.Bool(true) - ) - - options.ConfigAddProfileNameOption.Flag.Changed = true - options.ConfigAddProfileNameOption.CobraParamValue = &profileName - - options.ConfigAddProfileDescriptionOption.Flag.Changed = true - options.ConfigAddProfileDescriptionOption.CobraParamValue = &description - - options.ConfigAddProfileSetActiveOption.Flag.Changed = true - options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive - - err := RunInternalConfigAddProfile(os.Stdin) - if err != nil { - t.Errorf("RunInternalConfigAddProfile returned error: %v", err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &tc.profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &tc.description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &tc.setActive + + err := RunInternalConfigAddProfile(os.Stdin) + + if tc.expectedError != nil { + assert.Error(t, err) + var addProfileErr *AddProfileError + if errors.As(err, &addProfileErr) { + assert.ErrorIs(t, addProfileErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type AddProfileError") + } + } else { + assert.NoError(t, err) + } + }) } } - -// Test RunInternalConfigAddProfile function fails with invalid set active flag -func Test_RunInternalConfigAddProfile_InvalidSetActive(t *testing.T) { - testutils_koanf.InitKoanfs(t) - var ( - profileName = customtypes.String("test-profile") - description = customtypes.String("test-description") - setActive = customtypes.String("invalid") - ) - - options.ConfigAddProfileNameOption.Flag.Changed = true - options.ConfigAddProfileNameOption.CobraParamValue = &profileName - - options.ConfigAddProfileDescriptionOption.Flag.Changed = true - options.ConfigAddProfileDescriptionOption.CobraParamValue = &description - - options.ConfigAddProfileSetActiveOption.Flag.Changed = true - options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive - - expectedErrorPattern := `^failed to add profile: strconv.ParseBool: parsing ".*": invalid syntax$` - err := RunInternalConfigAddProfile(os.Stdin) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} diff --git a/internal/commands/config/delete_profile_internal.go b/internal/commands/config/delete_profile_internal.go index 0b69422f..0b9fe210 100644 --- a/internal/commands/config/delete_profile_internal.go +++ b/internal/commands/config/delete_profile_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "io" @@ -12,6 +13,26 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +type DeleteProfileError struct { + Err error +} + +func (e *DeleteProfileError) Error() string { + var err *DeleteProfileError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to delete profile: %s", e.Err.Error()) +} + +func (e *DeleteProfileError) Unwrap() error { + var err *DeleteProfileError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) { var pName string if len(args) == 1 { @@ -19,17 +40,22 @@ func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) } else { pName, err = promptUserToDeleteProfile(rc) if err != nil { - return fmt.Errorf("failed to delete profile: %w", err) + return &DeleteProfileError{Err: err} } } - if err = profiles.GetKoanfConfig().ValidateExistingProfileName(pName); err != nil { - return fmt.Errorf("failed to delete profile: %w", err) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &DeleteProfileError{Err: err} + } + + if err = koanfConfig.ValidateExistingProfileName(pName); err != nil { + return &DeleteProfileError{Err: err} } confirmed, err := promptUserToConfirmDelete(pName, rc) if err != nil { - return fmt.Errorf("failed to delete profile: %w", err) + return &DeleteProfileError{Err: err} } if !confirmed { @@ -40,17 +66,21 @@ func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) err = deleteProfile(pName) if err != nil { - return fmt.Errorf("failed to delete profile: %w", err) + return &DeleteProfileError{Err: err} } return nil } func promptUserToDeleteProfile(rc io.ReadCloser) (pName string, err error) { - pName, err = input.RunPromptSelect("Select profile to delete", profiles.GetKoanfConfig().ProfileNames(), rc) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return pName, &DeleteProfileError{Err: err} + } + pName, err = input.RunPromptSelect("Select profile to delete", koanfConfig.ProfileNames(), rc) if err != nil { - return pName, err + return pName, &DeleteProfileError{Err: err} } return pName, nil @@ -61,7 +91,7 @@ func promptUserToConfirmDelete(pName string, rc io.ReadCloser) (confirmed bool, if options.ConfigDeleteAutoAcceptOption.Flag.Changed { autoAccept, err = profiles.GetOptionValue(options.ConfigDeleteAutoAcceptOption) if err != nil { - return false, err + return false, &DeleteProfileError{Err: err} } } @@ -69,14 +99,23 @@ func promptUserToConfirmDelete(pName string, rc io.ReadCloser) (confirmed bool, return true, nil } - return input.RunPromptConfirm(fmt.Sprintf("Are you sure you want to delete profile '%s'", pName), rc) + confirmed, err = input.RunPromptConfirm(fmt.Sprintf("Are you sure you want to delete profile '%s'", pName), rc) + if err != nil { + return false, &DeleteProfileError{Err: err} + } + return confirmed, nil } func deleteProfile(pName string) (err error) { output.Message(fmt.Sprintf("Deleting profile '%s'...", pName), nil) - if err = profiles.GetKoanfConfig().DeleteProfile(pName); err != nil { - return err + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &DeleteProfileError{Err: err} + } + + if err = koanfConfig.DeleteProfile(pName); err != nil { + return &DeleteProfileError{Err: err} } output.Success(fmt.Sprintf("Profile '%s' deleted.", pName), nil) diff --git a/internal/commands/config/delete_profile_internal_test.go b/internal/commands/config/delete_profile_internal_test.go index 14d08e83..ba0281ea 100644 --- a/internal/commands/config/delete_profile_internal_test.go +++ b/internal/commands/config/delete_profile_internal_test.go @@ -3,52 +3,75 @@ package config_internal import ( + "errors" + "os" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test deleteProfile function -func Test_deleteProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) +func Test_RunInternalConfigDeleteProfile(t *testing.T) { + testCases := []struct { + name string + profileName string + autoConfirmDelete customtypes.Bool + expectedError error + }{ + { + name: "Delete Existing Profile", + profileName: "production", + autoConfirmDelete: true, + }, + { + name: "Invalid Profile Name: Active Profile", + profileName: "default", + autoConfirmDelete: true, + expectedError: profiles.ErrDeleteActiveProfile, + }, + { + name: "Invalid Profile Name: Non-Existent Profile", + profileName: "non-existent", + autoConfirmDelete: false, + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Invalid Profile Name: Empty Profile Name", + profileName: "", + autoConfirmDelete: false, + expectedError: profiles.ErrProfileNameEmpty, + }, + { + name: "Invalid Profile Name: Special Characters", + profileName: "(*#&)", + autoConfirmDelete: false, + expectedError: profiles.ErrProfileNameNotExist, + }, + } - err := deleteProfile("production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test deleteProfile function fails with active profile -func Test_deleteProfile_ActiveProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^'.*' is the active profile and cannot be deleted$` - err := deleteProfile("default") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test deleteProfile function fails with invalid profile name -func Test_deleteProfile_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^invalid profile name: '.*' profile does not exist$` - err := deleteProfile("(*#&)") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test deleteProfile function fails with empty profile name -func Test_deleteProfile_EmptyProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid profile name: profile name cannot be empty$` - err := deleteProfile("") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + options.ConfigDeleteAutoAcceptOption.Flag.Changed = true + options.ConfigDeleteAutoAcceptOption.CobraParamValue = &tc.autoConfirmDelete -// Test deleteProfile function fails with non-existent profile name -func Test_deleteProfile_NonExistentProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) + err := RunInternalConfigDeleteProfile([]string{tc.profileName}, os.Stdin) - expectedErrorPattern := `^invalid profile name: '.*' profile does not exist$` - err := deleteProfile("non-existent") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + assert.Error(t, err) + var deleteProfileErr *DeleteProfileError + if errors.As(err, &deleteProfileErr) { + assert.ErrorIs(t, deleteProfileErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type DeleteProfileError") + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/commands/config/get_internal.go b/internal/commands/config/get_internal.go index a481b394..753057de 100644 --- a/internal/commands/config/get_internal.go +++ b/internal/commands/config/get_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "strings" @@ -12,14 +13,36 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +var ErrUndeterminedProfile = errors.New("unable to determine profile to get configuration from") + +type GetError struct { + Err error +} + +func (e *GetError) Error() string { + var err *GetError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to get configuration: %s", e.Err.Error()) +} + +func (e *GetError) Unwrap() error { + var err *GetError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigGet(koanfKey string) (err error) { if err = configuration.ValidateParentKoanfKey(koanfKey); err != nil { - return fmt.Errorf("failed to get configuration: %w", err) + return &GetError{Err: err} } pName, err := readConfigGetOptions() if err != nil { - return fmt.Errorf("failed to get configuration: %w", err) + return &GetError{Err: err} } msgStr := fmt.Sprintf("Configuration values for profile '%s' and key '%s':\n", pName, koanfKey) @@ -31,7 +54,7 @@ func RunInternalConfigGet(koanfKey string) (err error) { vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return fmt.Errorf("failed to get configuration: %w", err) + return &GetError{Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -59,11 +82,11 @@ func readConfigGetOptions() (pName string, err error) { } if err != nil { - return "", err + return pName, &GetError{Err: err} } if pName == "" { - return "", fmt.Errorf("unable to determine profile to get configuration from") + return pName, &GetError{Err: ErrUndeterminedProfile} } return pName, nil diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index 4df92f2a..1942c7d2 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -3,62 +3,68 @@ package config_internal import ( + "errors" "testing" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigGet function func Test_RunInternalConfigGet(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalConfigGet("service") - if err != nil { - t.Errorf("RunInternalConfigGet returned error: %v", err) + testCases := []struct { + name string + profileName customtypes.String + koanfKey string + expectedError error + }{ + { + name: "Get configuration for existing key", + profileName: "default", + koanfKey: "service", + }, + { + name: "Get configuration for invalid key", + profileName: "default", + koanfKey: "invalid-key", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Get configuration with a different profile", + profileName: "production", + koanfKey: "service", + }, + { + name: "Get configuration with a non-existent profile", + profileName: "non-existent", + koanfKey: "service", + expectedError: profiles.ErrProfileNameNotExist, + }, } -} - -// Test RunInternalConfigGet function fails with invalid key -func Test_RunInternalConfigGet_InvalidKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `(?s)^failed to get configuration: key '.*' is not recognized as a valid configuration key\.\s*Use 'pingcli config list-keys' to view all available keys` - err := RunInternalConfigGet("invalid-key") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} -// Test RunInternalConfigGet function with different profile -func Test_RunInternalConfigGet_DifferentProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - var ( - profileName = customtypes.String("production") - ) + options.RootProfileOption.Flag.Changed = true + options.RootProfileOption.CobraParamValue = &tc.profileName - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName + err := RunInternalConfigGet(tc.koanfKey) - err := RunInternalConfigGet("service") - if err != nil { - t.Errorf("RunInternalConfigGet returned error: %v", err) + if tc.expectedError != nil { + assert.Error(t, err) + var getErr *GetError + if errors.As(err, &getErr) { + assert.ErrorIs(t, getErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type GetError") + } + } else { + assert.NoError(t, err) + } + }) } } - -// Test RunInternalConfigGet function fails with invalid profile name -func Test_RunInternalConfigGet_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("invalid") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - - expectedErrorPattern := `^failed to get configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigGet("service") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} diff --git a/internal/commands/config/list_keys_internal.go b/internal/commands/config/list_keys_internal.go index f0499630..56953c68 100644 --- a/internal/commands/config/list_keys_internal.go +++ b/internal/commands/config/list_keys_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "strings" @@ -13,12 +14,37 @@ import ( "gopkg.in/yaml.v3" ) -func returnKeysYamlString() (string, error) { - var err error +var ( + ErrRetrieveKeys = errors.New("failed to retrieve configuration keys") + ErrNestedMap = errors.New("failed to create nested map for key") + ErrMarshalKeys = errors.New("failed to marshal keys to YAML format") +) + +type ListKeysError struct { + Err error +} + +func (e *ListKeysError) Error() string { + var err *ListKeysError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to get configuration keys list: %s", e.Err.Error()) +} + +func (e *ListKeysError) Unwrap() error { + var err *ListKeysError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + +func returnKeysYamlString() (keysYamlStr string, err error) { koanfKeys := configuration.KoanfKeys() if len(koanfKeys) == 0 { - return "", fmt.Errorf("unable to retrieve valid keys") + return keysYamlStr, &ListKeysError{Err: ErrRetrieveKeys} } // Split the input string into individual keys @@ -48,7 +74,7 @@ func returnKeysYamlString() (string, error) { } currentMap, currentMapOk = currentMap[k].(map[string]interface{}) if !currentMapOk { - return "", fmt.Errorf("failed to get configuration keys list: error creating nested map for key %s", koanfKey) + return keysYamlStr, &ListKeysError{Err: ErrNestedMap} } } } @@ -57,18 +83,18 @@ func returnKeysYamlString() (string, error) { // Marshal the result into YAML yamlData, err := yaml.Marshal(keyMap) if err != nil { - return "", fmt.Errorf("error marshaling keys to YAML format") + return keysYamlStr, &ListKeysError{Err: ErrMarshalKeys} } - return string(yamlData), nil + keysYamlStr = string(yamlData) + return keysYamlStr, nil } func returnKeysString() (string, error) { - // var err error validKeys := configuration.KoanfKeys() if len(validKeys) == 0 { - return "", fmt.Errorf("unable to retrieve valid keys") + return "", &ListKeysError{Err: ErrRetrieveKeys} } else { validKeysJoined := strings.Join(validKeys, "\n- ") @@ -80,19 +106,19 @@ func RunInternalConfigListKeys() (err error) { var outputMessageString string yamlFlagStr, err := profiles.GetOptionValue(options.ConfigListKeysYamlOption) if err != nil { - return err + return &ListKeysError{Err: err} } if yamlFlagStr == "true" { // Output the YAML data as a string outputMessageString, err = returnKeysYamlString() if err != nil { - return err + return &ListKeysError{Err: err} } } else { // Output data list string outputMessageString, err = returnKeysString() if err != nil { - return err + return &ListKeysError{Err: err} } } diff --git a/internal/commands/config/list_keys_internal_test.go b/internal/commands/config/list_keys_internal_test.go index b4150377..d6847e24 100644 --- a/internal/commands/config/list_keys_internal_test.go +++ b/internal/commands/config/list_keys_internal_test.go @@ -3,16 +3,40 @@ package config_internal import ( + "errors" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigListKeys function func Test_RunInternalConfigListKeys(t *testing.T) { - testutils_koanf.InitKoanfs(t) + testCases := []struct { + name string + expectedError error + }{ + { + name: "Get List of Keys", + }, + } - err := RunInternalConfigListKeys() - testutils.CheckExpectedError(t, err, nil) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := RunInternalConfigListKeys() + + if tc.expectedError != nil { + assert.Error(t, err) + var listKeysErr *ListKeysError + if errors.As(err, &listKeysErr) { + assert.ErrorIs(t, listKeysErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type ListKeysError") + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/commands/config/list_profiles_internal.go b/internal/commands/config/list_profiles_internal.go index 3faa053f..95f7fbfe 100644 --- a/internal/commands/config/list_profiles_internal.go +++ b/internal/commands/config/list_profiles_internal.go @@ -3,17 +3,45 @@ package config_internal import ( + "errors" + "fmt" + "github.com/fatih/color" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) +type ListProfilesError struct { + Err error +} + +func (e *ListProfilesError) Error() string { + var err *ListProfilesError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to list profiles: %s", e.Err.Error()) +} + +func (e *ListProfilesError) Unwrap() error { + var err *ListProfilesError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigListProfiles() (err error) { - profileNames := profiles.GetKoanfConfig().ProfileNames() + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &ListProfilesError{Err: err} + } + + profileNames := koanfConfig.ProfileNames() activeProfileName, err := profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - return err + return &ListProfilesError{Err: err} } listStr := "Profiles:\n" @@ -29,7 +57,7 @@ func RunInternalConfigListProfiles() (err error) { listStr += "- " + profileName + "\n" } - description := profiles.GetKoanfConfig().KoanfInstance().String(profileName + "." + "description") + description := koanfConfig.KoanfInstance().String(profileName + "." + options.ProfileDescriptionOption.KoanfKey) if description != "" { listStr += " " + description } diff --git a/internal/commands/config/list_profiles_internal_test.go b/internal/commands/config/list_profiles_internal_test.go index cd55163f..f167e67c 100644 --- a/internal/commands/config/list_profiles_internal_test.go +++ b/internal/commands/config/list_profiles_internal_test.go @@ -3,16 +3,40 @@ package config_internal import ( + "errors" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigListProfiles function func Test_RunInternalConfigListProfiles(t *testing.T) { - testutils_koanf.InitKoanfs(t) + testCases := []struct { + name string + expectedError error + }{ + { + name: "Get List of Profiles", + }, + } - err := RunInternalConfigListProfiles() - testutils.CheckExpectedError(t, err, nil) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := RunInternalConfigListProfiles() + + if tc.expectedError != nil { + assert.Error(t, err) + var listProfilesErr *ListProfilesError + if errors.As(err, &listProfilesErr) { + assert.ErrorIs(t, listProfilesErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type ListProfilesError") + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/commands/config/set_active_profile_internal.go b/internal/commands/config/set_active_profile_internal.go index 436bbecb..3c3e4d1b 100644 --- a/internal/commands/config/set_active_profile_internal.go +++ b/internal/commands/config/set_active_profile_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "io" @@ -11,6 +12,26 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +type SetActiveProfileError struct { + Err error +} + +func (e *SetActiveProfileError) Error() string { + var err *SetActiveProfileError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to set active profile: %s", e.Err.Error()) +} + +func (e *SetActiveProfileError) Unwrap() error { + var err *SetActiveProfileError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err error) { var pName string if len(args) == 1 { @@ -18,14 +39,19 @@ func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err err } else { pName, err = promptUserToSelectActiveProfile(rc) if err != nil { - return fmt.Errorf("failed to set active profile: %w", err) + return &SetActiveProfileError{Err: err} } } output.Message(fmt.Sprintf("Setting active profile to '%s'...", pName), nil) - if err = profiles.GetKoanfConfig().ChangeActiveProfile(pName); err != nil { - return fmt.Errorf("failed to set active profile: %w", err) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &SetActiveProfileError{Err: err} + } + + if err = koanfConfig.ChangeActiveProfile(pName); err != nil { + return &SetActiveProfileError{Err: err} } output.Success(fmt.Sprintf("Active profile set to '%s'", pName), nil) @@ -34,10 +60,14 @@ func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err err } func promptUserToSelectActiveProfile(rc io.ReadCloser) (pName string, err error) { - pName, err = input.RunPromptSelect("Select profile to set as active: ", profiles.GetKoanfConfig().ProfileNames(), rc) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return "", &SetActiveProfileError{Err: err} + } + pName, err = input.RunPromptSelect("Select profile to set as active: ", koanfConfig.ProfileNames(), rc) if err != nil { - return pName, err + return pName, &SetActiveProfileError{Err: err} } return pName, nil diff --git a/internal/commands/config/set_active_profile_internal_test.go b/internal/commands/config/set_active_profile_internal_test.go index b27128c8..134ea53d 100644 --- a/internal/commands/config/set_active_profile_internal_test.go +++ b/internal/commands/config/set_active_profile_internal_test.go @@ -3,35 +3,63 @@ package config_internal import ( + "errors" "os" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigSetActiveProfile function func Test_RunInternalConfigSetActiveProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) + testCases := []struct { + name string + profileName string + expectedError error + }{ + { + name: "Set different profile as active", + profileName: "production", + }, + { + name: "Set invalid profile name as active", + profileName: "(*#&)", + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Set non-existent profile as active", + profileName: "non-existent", + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Set empty profile name as active", + profileName: "", + expectedError: profiles.ErrProfileNameEmpty, + }, + { + name: "Set current active profile as active", + profileName: "default", + }, + } - err := RunInternalConfigSetActiveProfile([]string{"production"}, os.Stdin) - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalConfigSetActiveProfile function fails with invalid profile name -func Test_RunInternalConfigSetActiveProfile_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigSetActiveProfile([]string{"(*#&)"}, os.Stdin) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test RunInternalConfigSetActiveProfile function fails with non-existent profile -func Test_RunInternalConfigSetActiveProfile_NonExistentProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) + err := RunInternalConfigSetActiveProfile([]string{tc.profileName}, os.Stdin) - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigSetActiveProfile([]string{"non-existent"}, os.Stdin) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + assert.Error(t, err) + var setActiveProfileErr *SetActiveProfileError + if errors.As(err, &setActiveProfileErr) { + assert.ErrorIs(t, setActiveProfileErr.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type SetActiveProfileError") + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index 7a1bd464..5bf98056 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "strings" @@ -14,44 +15,94 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + ErrEmptyValue = errors.New("the set value provided is empty. Use 'pingcli config unset %s' to unset a key's configuration") + ErrDetermineProfileSet = errors.New("unable to determine profile to set configuration to") + ErrKeyAssignmentFormat = errors.New("invalid key-value assignment. Expect 'key=value' format") + ErrActiveProfileAssignment = errors.New("invalid active profile assignment. Please use the 'pingcli config set active-profile ' command to set the active profile") + ErrSetKey = errors.New("unable to set key in configuration profile") + ErrMustBeBoolean = errors.New("the value assignment must be a boolean. Allowed [true, false]") + ErrMustBeExportFormat = errors.New(fmt.Sprintf("the value assignment must be a valid export format. Allowed [%s]", strings.Join(customtypes.ExportFormatValidValues(), ", "))) + ErrMustBeExportServiceGroup = errors.New(fmt.Sprintf("the value assignment must be a valid export service group. Allowed [%s]", strings.Join(customtypes.ExportServiceGroupValidValues(), ", "))) + ErrMustBeExportService = errors.New(fmt.Sprintf("the value assignment must be valid export service(s). Allowed [%s]", strings.Join(customtypes.ExportServicesValidValues(), ", "))) + ErrMustBeOutputFormat = errors.New(fmt.Sprintf("the value assignment must be a valid output format. Allowed [%s]", strings.Join(customtypes.OutputFormatValidValues(), ", "))) + ErrMustBePingoneRegionCode = errors.New(fmt.Sprintf("the value assignment must be a valid PingOne region code. Allowed [%s]", strings.Join(customtypes.PingOneRegionCodeValidValues(), ", "))) + ErrMustBeString = errors.New("the value assignment must be a string") + ErrMustBeStringSlice = errors.New("the value assignment must be a string slice") + ErrMustBeUUID = errors.New("the value assignment must be a valid UUID") + ErrMustBePingoneAuthType = errors.New(fmt.Sprintf("the value assignment must be a valid PingOne Authentication Type. Allowed [%s]", strings.Join(customtypes.PingOneAuthenticationTypeValidValues(), ", "))) + ErrMustBePingfederateAuthType = errors.New(fmt.Sprintf("the value assignment must be a valid PingFederate Authentication Type. Allowed [%s]", strings.Join(customtypes.PingFederateAuthenticationTypeValidValues(), ", "))) + ErrMustBeInteger = errors.New("the value assignment must be an integer") + ErrMustBeHttpMethod = errors.New(fmt.Sprintf("the value assignment must be a valid HTTP method. Allowed [%s]", strings.Join(customtypes.HTTPMethodValidValues(), ", "))) + ErrMustBeRequestService = errors.New(fmt.Sprintf("the value assignment must be a valid request service. Allowed [%s]", strings.Join(customtypes.RequestServiceValidValues(), ", "))) + ErrMustBeLicenseProduct = errors.New(fmt.Sprintf("the value assignment must be a valid license product. Allowed [%s]", strings.Join(customtypes.LicenseProductValidValues(), ", "))) + ErrMustBeLicenseVersion = errors.New("the value assignment must be a valid license version. Must be of the form 'major.minor'") + ErrTypeNotRecognized = errors.New("the variable type for the configuration key is not recognized or supported") +) + +type SetError struct { + Err error +} + +func (e *SetError) Error() string { + var err *SetError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to set configuration: %s", e.Err.Error()) +} + +func (e *SetError) Unwrap() error { + var err *SetError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigSet(kvPair string) (err error) { pName, vKey, vValue, err := readConfigSetOptions(kvPair) if err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } if err = configuration.ValidateKoanfKey(vKey); err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } // Make sure value is not empty, and suggest unset command if it is if vValue == "" { - return fmt.Errorf("failed to set configuration: value for key '%s' is empty. Use 'pingcli config unset %s' to unset the key", vKey, vKey) + return &SetError{Err: ErrEmptyValue} + } + + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &SetError{Err: err} } - subKoanf, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } opt, err := configuration.OptionFromKoanfKey(vKey) if err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } if err = setValue(subKoanf, vKey, vValue, opt.Type); err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } - if err = profiles.GetKoanfConfig().SaveProfile(pName, subKoanf); err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { + return &SetError{Err: err} } msgStr := "Configuration set successfully:\n" vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return fmt.Errorf("failed to set configuration: %w", err) + return &SetError{Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -72,11 +123,11 @@ func RunInternalConfigSet(kvPair string) (err error) { func readConfigSetOptions(kvPair string) (pName string, vKey string, vValue string, err error) { if pName, err = readConfigSetProfileName(); err != nil { - return pName, vKey, vValue, err + return pName, vKey, vValue, &SetError{Err: err} } if vKey, vValue, err = parseKeyValuePair(kvPair); err != nil { - return pName, vKey, vValue, err + return pName, vKey, vValue, &SetError{Err: err} } return pName, vKey, vValue, nil @@ -90,24 +141,24 @@ func readConfigSetProfileName() (pName string, err error) { } if err != nil { - return pName, err + return pName, &SetError{Err: err} } if pName == "" { - return pName, fmt.Errorf("unable to determine profile to set configuration to") + return pName, &SetError{Err: ErrDetermineProfileSet} } return pName, nil } -func parseKeyValuePair(kvPair string) (string, string, error) { +func parseKeyValuePair(kvPair string) (key string, value string, err error) { parsedInput := strings.SplitN(kvPair, "=", 2) if len(parsedInput) < 2 { - return "", "", fmt.Errorf("invalid assignment format '%s'. Expect 'key=value' format", kvPair) + return key, value, &SetError{Err: ErrKeyAssignmentFormat} } if strings.EqualFold(parsedInput[0], options.RootActiveProfileOption.KoanfKey) { - return "", "", fmt.Errorf("invalid assignment. Please use the 'pingcli config set active-profile ' command to set the active profile") + return key, value, &SetError{Err: ErrActiveProfileAssignment} } return parsedInput[0], parsedInput[1], nil @@ -118,149 +169,149 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. case options.BOOL: b := new(customtypes.Bool) if err = b.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a boolean. Allowed [true, false]: %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeBoolean, err) } err = profileKoanf.Set(vKey, b) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.EXPORT_FORMAT: exportFormat := new(customtypes.ExportFormat) if err = exportFormat.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid export format. Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportFormatValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeExportFormat, err) } err = profileKoanf.Set(vKey, exportFormat) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.EXPORT_SERVICE_GROUP: exportServiceGroup := new(customtypes.ExportServiceGroup) if err = exportServiceGroup.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be valid export service group. Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportServiceGroupValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeExportServiceGroup, err) } err = profileKoanf.Set(vKey, exportServiceGroup) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.EXPORT_SERVICES: exportServices := new(customtypes.ExportServices) if err = exportServices.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be valid export service(s). Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportServicesValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeExportService, err) } err = profileKoanf.Set(vKey, exportServices) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.OUTPUT_FORMAT: outputFormat := new(customtypes.OutputFormat) if err = outputFormat.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid output format. Allowed [%s]: %w", vKey, strings.Join(customtypes.OutputFormatValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeOutputFormat, err) } err = profileKoanf.Set(vKey, outputFormat) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.PINGONE_REGION_CODE: region := new(customtypes.PingOneRegionCode) if err = region.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid PingOne Region Code. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingOneRegionCodeValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBePingoneRegionCode, err) } err = profileKoanf.Set(vKey, region) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.STRING: str := new(customtypes.String) if err = str.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a string: %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeString, err) } err = profileKoanf.Set(vKey, str) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.STRING_SLICE: strSlice := new(customtypes.StringSlice) if err = strSlice.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a string slice: %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeStringSlice, err) } err = profileKoanf.Set(vKey, strSlice) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.UUID: uuid := new(customtypes.UUID) if err = uuid.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid UUID: %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeUUID, err) } err = profileKoanf.Set(vKey, uuid) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.PINGONE_AUTH_TYPE: authType := new(customtypes.PingOneAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid PingOne Authentication Type. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingOneAuthenticationTypeValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBePingoneAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.PINGFEDERATE_AUTH_TYPE: authType := new(customtypes.PingFederateAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid PingFederate Authentication Type. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingFederateAuthenticationTypeValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBePingfederateAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.INT: intValue := new(customtypes.Int) if err = intValue.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be an integer: %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeInteger, err) } err = profileKoanf.Set(vKey, intValue) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.REQUEST_HTTP_METHOD: httpMethod := new(customtypes.HTTPMethod) if err = httpMethod.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid HTTP method. Allowed [%s]: %w", vKey, strings.Join(customtypes.HTTPMethodValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeHttpMethod, err) } err = profileKoanf.Set(vKey, httpMethod) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.REQUEST_SERVICE: service := new(customtypes.RequestService) if err = service.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid request service. Allowed [%s]: %w", vKey, strings.Join(customtypes.RequestServiceValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeRequestService, err) } err = profileKoanf.Set(vKey, service) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.LICENSE_PRODUCT: licenseProduct := new(customtypes.LicenseProduct) if err = licenseProduct.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid license product. Allowed [%s]: %w", vKey, strings.Join(customtypes.LicenseProductValidValues(), ", "), err) + return fmt.Errorf("%w: %v", ErrMustBeLicenseProduct, err) } err = profileKoanf.Set(vKey, licenseProduct) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } case options.LICENSE_VERSION: licenseVersion := new(customtypes.LicenseVersion) if err = licenseVersion.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid license version. Must be of the form 'major.minor': %w", vKey, err) + return fmt.Errorf("%w: %v", ErrMustBeLicenseVersion, err) } err = profileKoanf.Set(vKey, licenseVersion) if err != nil { - return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + return fmt.Errorf("%w: %v", ErrSetKey, err) } default: - return fmt.Errorf("failed to set configuration: variable type for key '%s' is not recognized", vKey) + return &SetError{Err: ErrTypeNotRecognized} } return nil diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index b30e8450..bf1ee6b0 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -3,120 +3,102 @@ package config_internal import ( + "errors" + "fmt" "testing" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigSet function func Test_RunInternalConfigSet(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := RunInternalConfigSet("noColor=true") - if err != nil { - t.Errorf("RunInternalConfigSet returned error: %v", err) + testCases := []struct { + name string + profileName customtypes.String + kvPair string + expectedError error + }{ + { + name: "Set noColor to True", + kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), + }, + { + name: "Set active profile", + kvPair: fmt.Sprintf("%s=production", options.RootActiveProfileOption.KoanfKey), + expectedError: ErrActiveProfileAssignment, + }, + { + name: "Set non-existant key", + kvPair: "nonExistantKey=true", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Set boolean key with invalid variable type", + kvPair: fmt.Sprintf("%s=invalid", options.RootColorOption.KoanfKey), + expectedError: ErrMustBeBoolean, + }, + { + name: "Set key on non-existent profile", + profileName: "non-existent", + kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Set noColor to True on different profile", + profileName: "production", + kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), + }, + { + name: "Set key on invalid profile name format", + profileName: "(*#&)", + kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Set key with empty value", + kvPair: fmt.Sprintf("%s=", options.RootColorOption.KoanfKey), + expectedError: ErrEmptyValue, + }, + { + name: "Run set command with no key-value pair provided", + kvPair: "", + expectedError: ErrKeyAssignmentFormat, + }, + { + name: "Run set with invalid key-value assignment format", + kvPair: "key::value", + expectedError: ErrKeyAssignmentFormat, + }, } -} - -// Test RunInternalConfigSet function fails when active profile is set -func Test_RunInternalConfigSet_InvalidActiveProfileUse(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("default") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - expectedErrorPattern := `^failed to set configuration: invalid assignment. Please use the 'pingcli config set active-profile ' command to set the active profile` - err := RunInternalConfigSet("activeProfile=myNewProfile") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function fails with invalid key -func Test_RunInternalConfigSet_InvalidKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to set configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := RunInternalConfigSet("invalid-key=false") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function fails with invalid value -func Test_RunInternalConfigSet_InvalidValue(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to set configuration: value for key '.*' must be a boolean. Allowed .*: strconv.ParseBool: parsing ".*": invalid syntax$` - err := RunInternalConfigSet("noColor=invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function fails with non-existent profile name -func Test_RunInternalConfigSet_NonExistentProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("non-existent") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - - expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigSet("noColor=true") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function with different profile -func Test_RunInternalConfigSet_DifferentProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("production") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - err := RunInternalConfigSet("noColor=true") - if err != nil { - t.Errorf("RunInternalConfigSet returned error: %v", err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + if tc.profileName != "" { + options.RootProfileOption.Flag.Changed = true + options.RootProfileOption.CobraParamValue = &tc.profileName + } + + err := RunInternalConfigSet(tc.kvPair) + + if tc.expectedError != nil { + assert.Error(t, err) + var setError *SetError + if errors.As(err, &setError) { + assert.ErrorIs(t, setError.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type SetError") + } + } else { + assert.NoError(t, err) + } + }) } } - -// Test RunInternalConfigSet function fails with invalid profile name -func Test_RunInternalConfigSet_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("*&%*&") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - - expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigSet("noColor=true") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function fails with no value provided -func Test_RunInternalConfigSet_NoValue(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to set configuration: value for key '.*' is empty. Use 'pingcli config unset .*' to unset the key$` - err := RunInternalConfigSet("noColor=") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigSet function fails with no keyValue provided -func Test_RunInternalConfigSet_NoKeyValue(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to set configuration: invalid assignment format ''\. Expect 'key=value' format$` - err := RunInternalConfigSet("") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} diff --git a/internal/commands/config/unset_internal.go b/internal/commands/config/unset_internal.go index 374f4d0b..d02ccb92 100644 --- a/internal/commands/config/unset_internal.go +++ b/internal/commands/config/unset_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "strings" @@ -12,40 +13,67 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +var ErrDetermineProfileUnset = errors.New("unable to determine profile to unset configuration from") + +type UnsetError struct { + Err error +} + +func (e *UnsetError) Error() string { + var err *UnsetError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to unset configuration: %s", e.Err.Error()) +} + +func (e *UnsetError) Unwrap() error { + var err *UnsetError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigUnset(koanfKey string) (err error) { if err = configuration.ValidateKoanfKey(koanfKey); err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} } pName, err := readConfigUnsetOptions() if err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} + } + + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &UnsetError{Err: err} } - subKoanf, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} } opt, err := configuration.OptionFromKoanfKey(koanfKey) if err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} } err = subKoanf.Set(koanfKey, opt.DefaultValue) if err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} } - if err = profiles.GetKoanfConfig().SaveProfile(pName, subKoanf); err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { + return &UnsetError{Err: err} } msgStr := "Configuration unset successfully:\n" vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return fmt.Errorf("failed to unset configuration: %w", err) + return &UnsetError{Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -72,11 +100,11 @@ func readConfigUnsetOptions() (pName string, err error) { } if err != nil { - return pName, err + return pName, &UnsetError{Err: err} } if pName == "" { - return pName, fmt.Errorf("unable to determine profile to unset configuration from") + return pName, &UnsetError{Err: ErrDetermineProfileUnset} } return pName, nil diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index 1a793788..28ac4a10 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -3,62 +3,75 @@ package config_internal import ( + "errors" "testing" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigUnset function func Test_RunInternalConfigUnset(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := RunInternalConfigUnset("noColor") - if err != nil { - t.Errorf("RunInternalConfigUnset returned error: %v", err) + testCases := []struct { + name string + profileName customtypes.String + koanfKey string + expectedError error + }{ + { + name: "Unset noColor", + koanfKey: options.RootColorOption.KoanfKey, + }, + { + name: "Unset on non-existant key", + koanfKey: "nonExistantKey", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Unset key on a different profile", + profileName: customtypes.String("production"), + koanfKey: options.RootColorOption.KoanfKey, + }, + { + name: "Unset key with a non-existant profile", + profileName: customtypes.String("nonExistant"), + koanfKey: options.RootColorOption.KoanfKey, + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "Run Unset with no key provided", + koanfKey: "", + expectedError: configuration.ErrInvalidConfigurationKey, + }, } -} - -// Test RunInternalConfigUnset function fails with invalid key -func Test_RunInternalConfigUnset_InvalidKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := RunInternalConfigUnset("invalid-key") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigUnset function with different profile -func Test_RunInternalConfigUnset_DifferentProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - var ( - profileName = customtypes.String("production") - ) + if tc.profileName != "" { + options.RootProfileOption.Flag.Changed = true + options.RootProfileOption.CobraParamValue = &tc.profileName + } - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName + err := RunInternalConfigUnset(tc.koanfKey) - err := RunInternalConfigUnset("noColor") - if err != nil { - t.Errorf("RunInternalConfigUnset returned error: %v", err) + if tc.expectedError != nil { + assert.Error(t, err) + var unsetError *UnsetError + if errors.As(err, &unsetError) { + assert.ErrorIs(t, unsetError.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type UnsetError") + } + } else { + assert.NoError(t, err) + } + }) } } - -// Test RunInternalConfigUnset function fails with invalid profile name -func Test_RunInternalConfigUnset_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - var ( - profileName = customtypes.String("invalid") - ) - - options.RootProfileOption.Flag.Changed = true - options.RootProfileOption.CobraParamValue = &profileName - - expectedErrorPattern := `^failed to unset configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigUnset("noColor") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} diff --git a/internal/commands/config/view_profile_internal.go b/internal/commands/config/view_profile_internal.go index 54cea55f..4acb0828 100644 --- a/internal/commands/config/view_profile_internal.go +++ b/internal/commands/config/view_profile_internal.go @@ -3,6 +3,7 @@ package config_internal import ( + "errors" "fmt" "strings" @@ -11,6 +12,26 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +type ViewProfileError struct { + Err error +} + +func (e *ViewProfileError) Error() string { + var err *ViewProfileError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to view profile: %s", e.Err.Error()) +} + +func (e *ViewProfileError) Unwrap() error { + var err *ViewProfileError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalConfigViewProfile(args []string) (err error) { var msgStr, pName string if len(args) == 1 { @@ -18,20 +39,25 @@ func RunInternalConfigViewProfile(args []string) (err error) { } else { pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - return fmt.Errorf("failed to view profile: %w", err) + return &ViewProfileError{Err: err} } } + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return &ViewProfileError{Err: err} + } + // Validate the profile name - err = profiles.GetKoanfConfig().ValidateExistingProfileName(pName) + err = koanfConfig.ValidateExistingProfileName(pName) if err != nil { - return fmt.Errorf("failed to view profile: %w", err) + return &ViewProfileError{Err: err} } // Get the Koanf configuration for the specified profile - koanfProfile, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + koanfProfile, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return fmt.Errorf("failed to get config from profile: %w", err) + return &ViewProfileError{Err: err} } // Iterate over the options in profile and print them @@ -46,7 +72,7 @@ func RunInternalConfigViewProfile(args []string) (err error) { } if err != nil { - return fmt.Errorf("failed to get koanf value from option: %w", err) + return &ViewProfileError{Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) diff --git a/internal/commands/config/view_profile_internal_test.go b/internal/commands/config/view_profile_internal_test.go index 23224ca7..16f5478a 100644 --- a/internal/commands/config/view_profile_internal_test.go +++ b/internal/commands/config/view_profile_internal_test.go @@ -3,33 +3,57 @@ package config_internal import ( + "errors" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test RunInternalConfigViewProfile function func Test_RunInternalConfigViewProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalConfigViewProfile([]string{}) - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalConfigViewProfile function fails with invalid profile name -func Test_RunInternalConfigViewProfile_InvalidProfileName(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to view profile: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigViewProfile([]string{"invalid"}) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigViewProfile function with different profile -func Test_RunInternalConfigViewProfile_DifferentProfile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalConfigViewProfile([]string{"production"}) - testutils.CheckExpectedError(t, err, nil) + testCases := []struct { + name string + profileName []string + expectedError error + }{ + { + name: "View active profile by providing no profile", + profileName: []string{}, + }, + { + name: "View non-existent profile", + profileName: []string{"nonexistent"}, + expectedError: profiles.ErrProfileNameNotExist, + }, + { + name: "View profile by providing one", + profileName: []string{"production"}, + }, + { + name: "View empty name profile", + profileName: []string{""}, + expectedError: profiles.ErrProfileNameEmpty, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := RunInternalConfigViewProfile(tc.profileName) + + if tc.expectedError != nil { + assert.Error(t, err) + var viewError *ViewProfileError + if errors.As(err, &viewError) { + assert.ErrorIs(t, viewError.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type ViewProfileError") + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go index d4c72d0c..6a503cbd 100644 --- a/internal/commands/license/license_internal.go +++ b/internal/commands/license/license_internal.go @@ -14,20 +14,50 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + ErrLicenseDataEmpty = errors.New("returned license data is empty. please check your request parameters") + ErrGetProduct = errors.New("failed to get product option value") + ErrGetVersion = errors.New("failed to get version option value") + ErrGetDevopsUser = errors.New("failed to get devops user option value") + ErrGetDevopsKey = errors.New("failed to get devops key option value") + ErrRequiredValues = errors.New("product, version, devops user, and devops key must be specified for license request") + ErrLicenseRequest = errors.New("license request failed") +) + +type LicenseError struct { + Err error +} + +func (e *LicenseError) Error() string { + var err *LicenseError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("failed to run license request: %s", e.Err.Error()) +} + +func (e *LicenseError) Unwrap() error { + var err *LicenseError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func RunInternalLicense() (err error) { product, version, devopsUser, devopsKey, err := readLicenseOptionValues() if err != nil { - return fmt.Errorf("failed to run license request: %w", err) + return &LicenseError{Err: err} } ctx := context.Background() licenseData, err := runLicenseRequest(ctx, product, version, devopsUser, devopsKey) if err != nil { - return fmt.Errorf("failed to run license request: %w", err) + return &LicenseError{Err: err} } if licenseData == "" { - return fmt.Errorf("failed to run license request: returned license data is empty, please check your request parameters") + return &LicenseError{Err: ErrLicenseDataEmpty} } output.Message(licenseData, nil) @@ -38,26 +68,26 @@ func RunInternalLicense() (err error) { func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, err error) { product, err = profiles.GetOptionValue(options.LicenseProductOption) if err != nil { - return "", "", "", "", fmt.Errorf("failed to get product option: %w", err) + return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetProduct, err)} } version, err = profiles.GetOptionValue(options.LicenseVersionOption) if err != nil { - return "", "", "", "", fmt.Errorf("failed to get version option: %w", err) + return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetVersion, err)} } devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) if err != nil { - return "", "", "", "", fmt.Errorf("failed to get devops user option: %w", err) + return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetDevopsUser, err)} } devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) if err != nil { - return "", "", "", "", fmt.Errorf("failed to get devops key option: %w", err) + return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetDevopsKey, err)} } if product == "" || version == "" || devopsUser == "" || devopsKey == "" { - return "", "", "", "", fmt.Errorf("product, version, devops user, and devops key must be specified for license request") + return product, version, devopsUser, devopsKey, &LicenseError{Err: ErrRequiredValues} } return product, version, devopsUser, devopsKey, nil @@ -66,7 +96,7 @@ func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, func runLicenseRequest(ctx context.Context, product, version, devopsUser, devopsKey string) (licenseData string, err error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://license.pingidentity.com/devops/license", nil) if err != nil { - return "", fmt.Errorf("failed to create license request: %w", err) + return licenseData, &LicenseError{Err: err} } req.Header.Set("Devops-User", devopsUser) @@ -79,20 +109,23 @@ func runLicenseRequest(ctx context.Context, product, version, devopsUser, devops client := &http.Client{} res, err := client.Do(req) if err != nil { - return "", fmt.Errorf("failed to execute license request: %w", err) + return licenseData, &LicenseError{Err: err} } defer func() { cErr := res.Body.Close() err = errors.Join(err, cErr) + if err != nil { + err = &LicenseError{Err: err} + } }() body, err := io.ReadAll(res.Body) if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) + return licenseData, &LicenseError{Err: err} } if res.StatusCode < 200 || res.StatusCode >= 300 { - return "", fmt.Errorf("license request failed with status %d: %s", res.StatusCode, string(body)) + return "", &LicenseError{Err: fmt.Errorf("%w with status %d: %s", ErrLicenseRequest, res.StatusCode, string(body))} } return string(body), nil diff --git a/internal/commands/license/license_internal_test.go b/internal/commands/license/license_internal_test.go index fa3a2282..13a104a1 100644 --- a/internal/commands/license/license_internal_test.go +++ b/internal/commands/license/license_internal_test.go @@ -3,127 +3,90 @@ package license_internal import ( - "os" + "errors" "testing" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// setLicenseProductAndVersion sets up product and version options -func setLicenseProductAndVersion(product, version string) { - if product != "" { - productVal := customtypes.LicenseProduct(product) - options.LicenseProductOption.CobraParamValue = &productVal - options.LicenseProductOption.Flag.Changed = true +func Test_RunInternalLicense(t *testing.T) { + testCases := []struct { + name string + product customtypes.LicenseProduct + version customtypes.LicenseVersion + devopsUser customtypes.String + devopsKey customtypes.String + expectedError error + }{ + { + name: "Request PingFederate 13.0 License", + product: customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE), + version: "13.0", + }, + { + name: "Request license with empty product", + product: "", + version: "13.0", + expectedError: ErrRequiredValues, + }, + { + name: "Request license with empty version", + product: customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE), + version: "", + expectedError: ErrRequiredValues, + }, + { + name: "Request license with invalid devops key", + product: customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE), + version: "13.0", + devopsKey: "invalid-key", + expectedError: ErrLicenseRequest, + }, + { + name: "Request license with invalid devops user", + product: customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE), + version: "13.0", + devopsUser: "invalid-user", + expectedError: ErrLicenseRequest, + }, } - if version != "" { - versionVal := customtypes.LicenseVersion(version) - options.LicenseVersionOption.CobraParamValue = &versionVal - options.LicenseVersionOption.Flag.Changed = true - } -} - -// Test RunInternalLicense function with valid options -func Test_RunInternalLicense_Success(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "13.0") - - err := RunInternalLicense() - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalLicense with missing product option -func Test_RunInternalLicense_MissingProduct(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - // Set up test data with missing product - setLicenseProductAndVersion("", "13.0") - - // Run the function - expectedErrorPattern := `^failed to run license request: product, version, devops user, and devops key must be specified for license request$` - err := RunInternalLicense() - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalLicense with missing version option -func Test_RunInternalLicense_MissingVersion(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "") - - // Run the function - expectedErrorPattern := `^failed to run license request: product, version, devops user, and devops key must be specified for license request$` - err := RunInternalLicense() - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test readLicenseOptionValues function -func Test_readLicenseOptionValues(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - // Set up test data with all options - setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "13.0") - - // Run the function - product, version, devopsUser, devopsKey, err := readLicenseOptionValues() - - testutils.CheckExpectedError(t, err, nil) - if product != customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE { - t.Errorf("expected product %q, got %q", customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, product) - } - if version != "13.0" { - t.Errorf("expected version %q, got %q", "13.0", version) - } - if devopsUser == "" { - t.Error("expected devops user to be set, but it was empty") - } - if devopsKey == "" { - t.Error("expected devops key to be set, but it was empty") - } -} - -func Test_readLicenseOptionValues_EmptyValues(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "") - - expectedErrorPattern := `^product, version, devops user, and devops key must be specified for license request$` - _, _, _, _, err := readLicenseOptionValues() - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test runLicenseRequest function success -func Test_runLicenseRequest_Success(t *testing.T) { - licenseData, err := runLicenseRequest( - t.Context(), - customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, - "13.0", - os.Getenv("TEST_PINGCLI_DEVOPS_USER"), - os.Getenv("TEST_PINGCLI_DEVOPS_KEY")) - - testutils.CheckExpectedError(t, err, nil) - if licenseData == "" { - t.Error("expected license data to be non-empty, but it was empty") - } -} - -// Test runLicenseRequest with an invalid devops key -func Test_runLicenseRequest_InvalidDevopsKey(t *testing.T) { - licenseData, err := runLicenseRequest( - t.Context(), - customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, - "13.0", - os.Getenv("TEST_PINGCLI_DEVOPS_USER"), - "invalid-key") - - expectedErrorPattern := `^license request failed with status 401\: \{ "error"\: "Invalid devops-key header" \}$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) - if licenseData != "" { - t.Error("expected license data to be empty, but it was not") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + options.LicenseProductOption.Flag.Changed = true + options.LicenseProductOption.CobraParamValue = &tc.product + + options.LicenseVersionOption.Flag.Changed = true + options.LicenseVersionOption.CobraParamValue = &tc.version + + if tc.devopsUser != "" { + options.LicenseDevopsUserOption.Flag.Changed = true + options.LicenseDevopsUserOption.CobraParamValue = &tc.devopsUser + } + + if tc.devopsKey != "" { + options.LicenseDevopsKeyOption.Flag.Changed = true + options.LicenseDevopsKeyOption.CobraParamValue = &tc.devopsKey + } + + err := RunInternalLicense() + + if tc.expectedError != nil { + assert.Error(t, err) + var licenseError *LicenseError + if errors.As(err, &licenseError) { + assert.ErrorIs(t, licenseError.Unwrap(), tc.expectedError) + } else { + assert.Fail(t, "Expected error to be of type LicenseError") + } + } else { + assert.NoError(t, err) + } + }) } } diff --git a/internal/commands/plugin/add_internal.go b/internal/commands/plugin/add_internal.go index 9bce2a2a..44bcabf4 100644 --- a/internal/commands/plugin/add_internal.go +++ b/internal/commands/plugin/add_internal.go @@ -41,7 +41,12 @@ func addPluginExecutable(pluginExecutable string) error { return fmt.Errorf("failed to read profile name: %w", err) } - subKoanf, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return fmt.Errorf("failed to get koanf config: %w", err) + } + + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { return fmt.Errorf("failed to get profile: %w", err) } @@ -72,7 +77,7 @@ func addPluginExecutable(pluginExecutable string) error { return err } - if err = profiles.GetKoanfConfig().SaveProfile(pName, subKoanf); err != nil { + if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { return err } diff --git a/internal/commands/plugin/remove_internal.go b/internal/commands/plugin/remove_internal.go index 9a641761..d7cd9e7e 100644 --- a/internal/commands/plugin/remove_internal.go +++ b/internal/commands/plugin/remove_internal.go @@ -34,7 +34,12 @@ func removePluginExecutable(pluginExecutable string) (bool, error) { return false, fmt.Errorf("failed to read profile name: %w", err) } - subKoanf, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return false, fmt.Errorf("failed to get koanf config: %w", err) + } + + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { return false, fmt.Errorf("failed to get profile: %w", err) } @@ -64,7 +69,7 @@ func removePluginExecutable(pluginExecutable string) (bool, error) { return false, err } - if err = profiles.GetKoanfConfig().SaveProfile(pName, subKoanf); err != nil { + if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { return false, err } diff --git a/internal/commands/request/request_internal.go b/internal/commands/request/request_internal.go index 019d61c2..dab4b990 100644 --- a/internal/commands/request/request_internal.go +++ b/internal/commands/request/request_internal.go @@ -308,7 +308,12 @@ func pingoneAuth() (accessToken string, err error) { } } - subKoanf, err := profiles.GetKoanfConfig().GetProfileKoanf(pName) + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return "", err + } + + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { return "", err } @@ -321,7 +326,7 @@ func pingoneAuth() (accessToken string, err error) { if err != nil { return "", err } - err = profiles.GetKoanfConfig().SaveProfile(pName, subKoanf) + err = koanfConfig.SaveProfile(pName, subKoanf) if err != nil { return "", err } diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index d388c12e..053b1b07 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -3,6 +3,7 @@ package configuration import ( + "errors" "fmt" "slices" "strings" @@ -18,6 +19,31 @@ import ( configuration_services "github.com/pingidentity/pingcli/internal/configuration/services" ) +var ( + ErrInvalidConfigurationKey = errors.New("provided key is not recognized as a valid configuration key.\nuse 'pingcli config list-keys' to view all available keys") + ErrNoOptionForKey = errors.New("no option found for the provided configuration key") +) + +type ConfigurationError struct { + Err error +} + +func (e *ConfigurationError) Error() string { + var err *ConfigurationError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("configuration options error: %s", e.Err.Error()) +} + +func (e *ConfigurationError) Unwrap() error { + var err *ConfigurationError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func KoanfKeys() (keys []string) { for _, opt := range options.Options() { if opt.KoanfKey != "" { @@ -38,7 +64,7 @@ func ValidateKoanfKey(koanfKey string) error { } } - return fmt.Errorf("key '%s' is not recognized as a valid configuration key.\nUse 'pingcli config list-keys' to view all available keys", koanfKey) + return &ConfigurationError{Err: ErrInvalidConfigurationKey} } // Return a list of all koanf keys from Options @@ -72,7 +98,7 @@ func ValidateParentKoanfKey(koanfKey string) error { } } - return fmt.Errorf("key '%s' is not recognized as a valid configuration key.\nUse 'pingcli config list-keys' to view all available keys", koanfKey) + return &ConfigurationError{Err: ErrInvalidConfigurationKey} } func OptionFromKoanfKey(koanfKey string) (opt options.Option, err error) { @@ -82,7 +108,7 @@ func OptionFromKoanfKey(koanfKey string) (opt options.Option, err error) { } } - return opt, fmt.Errorf("failed to get option: no option found for koanf key: %s", koanfKey) + return opt, &ConfigurationError{Err: ErrNoOptionForKey} } func InitAllOptions() { diff --git a/internal/input/input.go b/internal/input/input.go index 1b3116ec..862523b6 100644 --- a/internal/input/input.go +++ b/internal/input/input.go @@ -4,11 +4,24 @@ package input import ( "errors" + "fmt" "io" "github.com/manifoldco/promptui" ) +type InputPromptError struct { + Err error +} + +func (e *InputPromptError) Error() string { + return fmt.Sprintf("input prompt failed: %s", e.Err.Error()) +} + +func (e *InputPromptError) Unwrap() error { + return e.Err +} + func RunPrompt(message string, validateFunc func(string) error, rc io.ReadCloser) (string, error) { p := promptui.Prompt{ Label: message, @@ -16,7 +29,12 @@ func RunPrompt(message string, validateFunc func(string) error, rc io.ReadCloser Stdin: rc, } - return p.Run() + userInput, err := p.Run() + if err != nil { + return "", &InputPromptError{Err: err} + } + + return userInput, nil } func RunPromptConfirm(message string, rc io.ReadCloser) (bool, error) { @@ -34,7 +52,7 @@ func RunPromptConfirm(message string, rc io.ReadCloser) (bool, error) { return false, nil } - return false, err + return false, &InputPromptError{Err: err} } return true, nil @@ -49,6 +67,9 @@ func RunPromptSelect(message string, items []string, rc io.ReadCloser) (selectio } _, selection, err = p.Run() + if err != nil { + return "", &InputPromptError{Err: err} + } - return selection, err + return selection, nil } diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 16ef763d..72767c7d 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -3,6 +3,7 @@ package profiles import ( + "errors" "fmt" "os" "regexp" @@ -17,6 +18,22 @@ import ( var ( k *KoanfConfig + + ErrNoOptionValue = errors.New("no option value found") + ErrKoanfNotInitialized = errors.New("koanf instance is not initialized") + ErrProfileNameEmpty = errors.New("invalid profile name: profile name cannot be empty") + ErrProfileNameFormat = errors.New("invalid profile name: profile name must contain only alphanumeric characters, underscores, and dashes") + ErrProfileNameSameAsActiveProfileKey = errors.New("invalid profile name: profile name cannot be the same as the active profile key") + ErrSetActiveProfile = errors.New("error setting active profile") + ErrWriteKoanfFile = errors.New("failed to write configuration file to disk") + ErrProfileNameNotExist = errors.New("invalid profile name: profile name does not exist") + ErrProfileNameAlreadyExists = errors.New("invalid profile name: profile name already exists") + ErrKoanfProfileExtractAndLoad = errors.New("failed to extract and load profile configuration") + ErrSetKoanfKeyValue = errors.New("failed to set koanf key value") + ErrMarshalKoanf = errors.New("failed to marshal koanf configuration") + ErrKoanfMerge = errors.New("failed to merge koanf configuration") + ErrDeleteActiveProfile = errors.New("the active profile cannot be deleted") + ErrSetKoanfKeyDefaultValue = errors.New("failed to set koanf key default value") ) type KoanfConfig struct { @@ -24,6 +41,26 @@ type KoanfConfig struct { configFilePath *string } +type KoanfError struct { + Err error +} + +func (e *KoanfError) Error() string { + var err *KoanfError + if errors.As(e.Err, &err) { + return err.Error() + } + return fmt.Sprintf("profile configuration error: %s", e.Err.Error()) +} + +func (e *KoanfError) Unwrap() error { + var err *KoanfError + if errors.As(e.Err, &err) { + return err.Unwrap() + } + return e.Err +} + func NewKoanfConfig(cnfFilePath string) *KoanfConfig { k = &KoanfConfig{ koanfInstance: koanf.New("."), @@ -33,8 +70,11 @@ func NewKoanfConfig(cnfFilePath string) *KoanfConfig { return k } -func GetKoanfConfig() *KoanfConfig { - return k +func GetKoanfConfig() (*KoanfConfig, error) { + if k == nil || k.KoanfInstance == nil { + return nil, &KoanfError{Err: ErrKoanfNotInitialized} + } + return k, nil } func (k KoanfConfig) GetKoanfConfigFile() string { @@ -67,13 +107,11 @@ func GetActiveProfileName(k *koanf.Koanf) string { func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bool, err error) { if opt.KoanfKey != "" { - var ( - kValue any - mainKoanfInstance = GetKoanfConfig() - ) + var kValue any - if mainKoanfInstance == nil || mainKoanfInstance.KoanfInstance() == nil { - return "", false, fmt.Errorf("failed to get option value: koanf instance is not initialized") + mainKoanfInstance, err := GetKoanfConfig() + if err != nil { + return "", false, &KoanfError{Err: err} } // Case 1: Koanf Key is the ActiveProfile Key, get value from main koanf instance @@ -89,12 +127,12 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo if pName == "" { pName, err = GetOptionValue(options.RootProfileOption) if err != nil { - return "", false, err + return "", false, &KoanfError{Err: err} } if pName == "" { pName, err = GetOptionValue(options.RootActiveProfileOption) if err != nil { - return "", false, err + return "", false, &KoanfError{Err: err} } } } @@ -102,7 +140,7 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo // Get the sub koanf instance for the profile subKoanf, err := mainKoanfInstance.GetProfileKoanf(pName) if err != nil { - return "", false, err + return "", false, &KoanfError{Err: err} } kValue = subKoanf.Get(opt.KoanfKey) @@ -158,16 +196,16 @@ func (k KoanfConfig) ProfileNames() (profileNames []string) { // The profile name cannot be empty func (k KoanfConfig) ValidateProfileNameFormat(pName string) (err error) { if pName == "" { - return fmt.Errorf("invalid profile name: profile name cannot be empty") + return &KoanfError{Err: ErrProfileNameEmpty} } re := regexp.MustCompile(`^[a-zA-Z0-9\_\-]+$`) if !re.MatchString(pName) { - return fmt.Errorf("invalid profile name: '%s'. name must contain only alphanumeric characters, underscores, and dashes", pName) + return &KoanfError{Err: ErrProfileNameFormat} } if strings.EqualFold(pName, options.RootActiveProfileOption.KoanfKey) { - return fmt.Errorf("invalid profile name: '%s'. name cannot be the same as the active profile key", pName) + return &KoanfError{Err: ErrProfileNameSameAsActiveProfileKey} } return nil @@ -175,16 +213,16 @@ func (k KoanfConfig) ValidateProfileNameFormat(pName string) (err error) { func (k KoanfConfig) ChangeActiveProfile(pName string) (err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return err + return &KoanfError{Err: err} } err = k.KoanfInstance().Set(options.RootActiveProfileOption.KoanfKey, pName) if err != nil { - return fmt.Errorf("error setting active profile: %w", err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetActiveProfile, err)} } if err = k.WriteFile(); err != nil { - return fmt.Errorf("failed to write config file for set active profile: %w", err) + return &KoanfError{Err: err} } return nil @@ -193,7 +231,7 @@ func (k KoanfConfig) ChangeActiveProfile(pName string) (err error) { // The profile name must exist func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { if pName == "" { - return fmt.Errorf("invalid profile name: profile name cannot be empty") + return &KoanfError{Err: ErrProfileNameEmpty} } pNames := k.ProfileNames() @@ -201,7 +239,7 @@ func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { if !slices.ContainsFunc(pNames, func(n string) bool { return n == pName }) { - return fmt.Errorf("invalid profile name: '%s' profile does not exist", pName) + return &KoanfError{Err: ErrProfileNameNotExist} } return nil @@ -211,7 +249,7 @@ func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { // The new profile name must be unique func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { if err = k.ValidateProfileNameFormat(pName); err != nil { - return err + return &KoanfError{Err: err} } pNames := k.ProfileNames() @@ -219,7 +257,7 @@ func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { if slices.ContainsFunc(pNames, func(n string) bool { return n == pName }) { - return fmt.Errorf("invalid profile name: '%s'. profile already exists", pName) + return &KoanfError{Err: ErrProfileNameAlreadyExists} } return nil @@ -227,14 +265,14 @@ func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { func (k KoanfConfig) GetProfileKoanf(pName string) (subKoanf *koanf.Koanf, err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return nil, err + return nil, &KoanfError{Err: err} } // Create a new koanf instance for the profile subKoanf = koanf.New(".") err = subKoanf.Load(confmap.Provider(k.KoanfInstance().Cut(pName).All(), "."), nil) if err != nil { - return nil, fmt.Errorf("error marshalling koanf: %w", err) + return nil, &KoanfError{Err: fmt.Errorf("%w: %v", ErrKoanfProfileExtractAndLoad, err)} } return subKoanf, nil @@ -255,7 +293,7 @@ func (k KoanfConfig) WriteFile() (err error) { if strings.ToLower(fullKoanfKeyValue) == key { err = k.KoanfInstance().Set(fullKoanfKeyValue, val) if err != nil { - return fmt.Errorf("error setting koanf key %s: %w", fullKoanfKeyValue, err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyValue, err)} } k.KoanfInstance().Delete(key) } @@ -271,12 +309,12 @@ func (k KoanfConfig) WriteFile() (err error) { encodedConfig, err := k.KoanfInstance().Marshal(yaml.Parser()) if err != nil { - return fmt.Errorf("error marshalling koanf: %w", err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrMarshalKoanf, err)} } err = os.WriteFile(k.GetKoanfConfigFile(), encodedConfig, 0600) if err != nil { - return fmt.Errorf("error opening file (%s): %w", k.GetKoanfConfigFile(), err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrWriteKoanfFile, err)} } return nil @@ -285,12 +323,12 @@ func (k KoanfConfig) WriteFile() (err error) { func (k KoanfConfig) SaveProfile(pName string, subKoanf *koanf.Koanf) (err error) { err = k.KoanfInstance().MergeAt(subKoanf, pName) if err != nil { - return fmt.Errorf("error merging koanf: %w", err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrKoanfMerge, err)} } err = k.WriteFile() if err != nil { - return fmt.Errorf("failed to save profile '%s': %w", pName, err) + return &KoanfError{Err: err} } return nil @@ -298,16 +336,16 @@ func (k KoanfConfig) SaveProfile(pName string, subKoanf *koanf.Koanf) (err error func (k KoanfConfig) DeleteProfile(pName string) (err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return err + return &KoanfError{Err: err} } activeProfileName, err := GetOptionValue(options.RootActiveProfileOption) if err != nil { - return err + return &KoanfError{Err: err} } if activeProfileName == pName { - return fmt.Errorf("'%s' is the active profile and cannot be deleted", pName) + return &KoanfError{Err: ErrDeleteActiveProfile} } // Delete the profile from the main koanf @@ -315,7 +353,7 @@ func (k KoanfConfig) DeleteProfile(pName string) (err error) { err = k.WriteFile() if err != nil { - return fmt.Errorf("failed to delete profile '%s': %w", pName, err) + return &KoanfError{Err: err} } return nil @@ -326,7 +364,7 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { for _, pName := range k.ProfileNames() { subKoanf, err := k.GetProfileKoanf(pName) if err != nil { - return err + return &KoanfError{Err: err} } for _, opt := range options.Options() { @@ -337,13 +375,13 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { if !subKoanf.Exists(opt.KoanfKey) { err = subKoanf.Set(opt.KoanfKey, opt.DefaultValue) if err != nil { - return fmt.Errorf("error setting default value for koanf key %s: %w", opt.KoanfKey, err) + return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyDefaultValue, err)} } } } err = k.SaveProfile(pName, subKoanf) if err != nil { - return fmt.Errorf("failed to save profile '%s': %w", pName, err) + return &KoanfError{Err: err} } } @@ -372,9 +410,9 @@ func GetOptionValue(opt options.Option) (string, error) { return opt.DefaultValue.String(), nil } - // This is a error, as it means the option is not configured internally to contain one of the 4 values above. + // This is an error, as it means the option is not configured internally to contain one of the 4 values above. // This should never happen, as all options should at least have a default value. - return "", fmt.Errorf("failed to get option value: no value found: %v", opt) + return "", &KoanfError{Err: ErrNoOptionValue} } func MaskValue(value any) string { diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index c45f5d70..1e37a23f 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -14,8 +14,13 @@ import ( ) func Validate() (err error) { + koanfConfig, err := GetKoanfConfig() + if err != nil { + return fmt.Errorf("failed to get koanf config: %w", err) + } + // Get a slice of all profile names configured in the config.yaml file - profileNames := GetKoanfConfig().ProfileNames() + profileNames := koanfConfig.ProfileNames() // Validate profile names if err = validateProfileNames(profileNames); err != nil { @@ -31,7 +36,7 @@ func Validate() (err error) { // Make sure selected profile is in the configuration file if !slices.Contains(profileNames, profileName) { return fmt.Errorf("failed to validate Ping CLI configuration: '%s' profile not found in configuration "+ - "file %s", profileName, GetKoanfConfig().GetKoanfConfigFile()) + "file %s", profileName, koanfConfig.GetKoanfConfigFile()) } } @@ -43,12 +48,12 @@ func Validate() (err error) { // Make sure selected active profile is in the configuration file if !slices.Contains(profileNames, activeProfileName) { return fmt.Errorf("failed to validate Ping CLI configuration: active profile '%s' not found in configuration "+ - "file %s", activeProfileName, GetKoanfConfig().GetKoanfConfigFile()) + "file %s", activeProfileName, koanfConfig.GetKoanfConfigFile()) } // for each profile key, validate the profile koanf for _, pName := range profileNames { - subKoanf, err := GetKoanfConfig().GetProfileKoanf(pName) + subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) } @@ -66,8 +71,13 @@ func Validate() (err error) { } func validateProfileNames(profileNames []string) error { + koanfConfig, err := GetKoanfConfig() + if err != nil { + return fmt.Errorf("failed to get koanf config: %w", err) + } + for _, profileName := range profileNames { - if err := GetKoanfConfig().ValidateProfileNameFormat(profileName); err != nil { + if err := koanfConfig.ValidateProfileNameFormat(profileName); err != nil { return err } } From ac175296aae2cf364ab3181cb8082a736b88f0ae Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Wed, 10 Sep 2025 15:40:55 -0600 Subject: [PATCH 02/14] Update Dependencies --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ed2146d9..31c80e8e 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 golang.org/x/mod v0.28.0 google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.9 @@ -200,7 +201,6 @@ require ( github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tdakkota/asciicheck v0.4.1 // indirect github.com/tetafro/godot v1.5.1 // indirect diff --git a/go.sum b/go.sum index d88dd941..6c4acb9b 100644 --- a/go.sum +++ b/go.sum @@ -623,8 +623,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= From c34324e437193fb5ad740701aee03d9b4ad7bbd4 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Wed, 10 Sep 2025 15:56:08 -0600 Subject: [PATCH 03/14] Update case-insensitive tests to new framework --- internal/commands/config/get_internal_test.go | 15 ++---- internal/commands/config/set_internal_test.go | 47 ++++++++++--------- .../commands/config/unset_internal_test.go | 41 ++++++++-------- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index 154ef4ba..6e0dda6c 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -43,6 +43,11 @@ func Test_RunInternalConfigGet(t *testing.T) { koanfKey: "service", expectedError: profiles.ErrProfileNameNotExist, }, + { + name: "Get configuration with a case-insensitive key", + profileName: "default", + koanfKey: "SeRvIcE", + }, } for _, tc := range testCases { @@ -68,13 +73,3 @@ func Test_RunInternalConfigGet(t *testing.T) { }) } } - -// Test RunInternalConfigGet function with case-insensitive keys -func Test_RunInternalConfigGet_CaseInsensitiveKeys(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalConfigGet("SeRvIcE") - if err != nil { - t.Errorf("RunInternalConfigGet returned error: %v", err) - } -} diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index 4b23ddfa..9965ef2e 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -21,12 +21,16 @@ func Test_RunInternalConfigSet(t *testing.T) { testCases := []struct { name string profileName customtypes.String + checkOption *options.Option + checkValue string kvPair string expectedError error }{ { - name: "Set noColor to True", - kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), + name: "Set noColor to True", + checkOption: &options.RootColorOption, + checkValue: "true", + kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), }, { name: "Set active profile", @@ -34,7 +38,7 @@ func Test_RunInternalConfigSet(t *testing.T) { expectedError: ErrActiveProfileAssignment, }, { - name: "Set non-existant key", + name: "Set non-existent key", kvPair: "nonExistantKey=true", expectedError: configuration.ErrInvalidConfigurationKey, }, @@ -52,6 +56,8 @@ func Test_RunInternalConfigSet(t *testing.T) { { name: "Set noColor to True on different profile", profileName: "production", + checkOption: &options.RootColorOption, + checkValue: "true", kvPair: fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey), }, { @@ -75,6 +81,12 @@ func Test_RunInternalConfigSet(t *testing.T) { kvPair: "key::value", expectedError: ErrKeyAssignmentFormat, }, + { + name: "Set value with case-insensitive key", + kvPair: "nOcOlOr=true", + checkOption: &options.RootColorOption, + checkValue: "true", + }, } for _, tc := range testCases { @@ -99,26 +111,17 @@ func Test_RunInternalConfigSet(t *testing.T) { } else { assert.NoError(t, err) } - }) - } -} - -// Test Test_RunInternalConfigSet function succeeds with case-insensitive keys -func Test_RunInternalConfigSet_CaseInsensitiveKeys(t *testing.T) { - testutils_koanf.InitKoanfs(t) - err := RunInternalConfigSet("NoCoLoR=true") - if err != nil { - t.Errorf("RunInternalConfigSet returned error: %v", err) - } - - // Make sure the actual correct key was set, not the case-insensitive one - vVal, err := profiles.GetOptionValue(options.RootColorOption) - if err != nil { - t.Errorf("GetOptionValue returned error: %v", err) - } + if tc.checkOption != nil { + vVal, err := profiles.GetOptionValue(*tc.checkOption) + if err != nil { + assert.Fail(t, "GetOptionValue returned error: %v", err) + } - if vVal != "true" { - t.Errorf("Expected %s to be true, got %v", options.RootColorOption.KoanfKey, vVal) + if vVal != tc.checkValue { + assert.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkValue, vVal) + } + } + }) } } diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index 9c6cb1a7..ab7f44db 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -21,11 +21,13 @@ func Test_RunInternalConfigUnset(t *testing.T) { name string profileName customtypes.String koanfKey string + checkOption *options.Option expectedError error }{ { - name: "Unset noColor", - koanfKey: options.RootColorOption.KoanfKey, + name: "Unset noColor", + koanfKey: options.RootColorOption.KoanfKey, + checkOption: &options.RootColorOption, }, { name: "Unset on non-existant key", @@ -36,6 +38,7 @@ func Test_RunInternalConfigUnset(t *testing.T) { name: "Unset key on a different profile", profileName: customtypes.String("production"), koanfKey: options.RootColorOption.KoanfKey, + checkOption: &options.RootColorOption, }, { name: "Unset key with a non-existant profile", @@ -48,6 +51,11 @@ func Test_RunInternalConfigUnset(t *testing.T) { koanfKey: "", expectedError: configuration.ErrInvalidConfigurationKey, }, + { + name: "Unset with case-insensitive key", + koanfKey: "nOcOlOr", + checkOption: &options.RootColorOption, + }, } for _, tc := range testCases { @@ -72,26 +80,17 @@ func Test_RunInternalConfigUnset(t *testing.T) { } else { assert.NoError(t, err) } - }) - } -} - -// Test RunInternalConfigUnset function succeeds with case-insensitive key -func Test_RunInternalConfigUnset_CaseInsensitiveKeys(t *testing.T) { - testutils_koanf.InitKoanfs(t) - err := RunInternalConfigUnset("NoCoLoR") - if err != nil { - t.Errorf("RunInternalConfigUnset returned error: %v", err) - } - - // Make sure the actual correct key was unset, not the case-insensitive one - vVal, err := profiles.GetOptionValue(options.RootColorOption) - if err != nil { - t.Errorf("GetOptionValue returned error: %v", err) - } + if tc.checkOption != nil { + vVal, err := profiles.GetOptionValue(*tc.checkOption) + if err != nil { + assert.Fail(t, "GetOptionValue returned error: %v", err) + } - if vVal != options.RootColorOption.DefaultValue.String() { - t.Errorf("Expected %s to be %s, got %v", options.RootColorOption.KoanfKey, options.RootColorOption.DefaultValue.String(), vVal) + if vVal != tc.checkOption.DefaultValue.String() { + assert.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkOption.DefaultValue.String(), vVal) + } + } + }) } } From 242e2f5a8544d7215b868cc4481a68bef4057c7a Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Mon, 22 Sep 2025 20:52:24 -0600 Subject: [PATCH 04/14] Move to centralized pingcli error type --- Makefile | 5 +- .../commands/config/add_profile_internal.go | 72 +-- .../config/add_profile_internal_test.go | 30 +- internal/commands/config/common_errors.go | 7 + .../config/delete_profile_internal.go | 46 +- .../config/delete_profile_internal_test.go | 11 +- internal/commands/config/get_internal.go | 36 +- internal/commands/config/get_internal_test.go | 11 +- .../commands/config/list_keys_internal.go | 59 +- .../config/list_keys_internal_test.go | 74 ++- .../commands/config/list_profiles_internal.go | 30 +- .../config/list_profiles_internal_test.go | 11 +- .../config/set_active_profile_internal.go | 34 +- .../set_active_profile_internal_test.go | 11 +- internal/commands/config/set_internal.go | 55 +- internal/commands/config/set_internal_test.go | 18 +- internal/commands/config/unset_internal.go | 46 +- .../commands/config/unset_internal_test.go | 18 +- .../commands/config/view_profile_internal.go | 34 +- .../config/view_profile_internal_test.go | 11 +- internal/commands/license/license_internal.go | 48 +- .../commands/license/license_internal_test.go | 11 +- internal/commands/platform/export_internal.go | 175 +++--- .../commands/platform/export_internal_test.go | 564 +++++++++--------- internal/commands/plugin/add_internal.go | 43 +- internal/commands/plugin/add_internal_test.go | 86 ++- internal/commands/plugin/common_errors.go | 9 + internal/commands/plugin/list_internal.go | 13 +- .../commands/plugin/list_internal_test.go | 28 +- internal/commands/plugin/remove_internal.go | 33 +- .../commands/plugin/remove_internal_test.go | 79 ++- internal/errs/pingcli_error.go | 28 + internal/profiles/koanf.go | 84 +-- .../testing/testutils_koanf/koanf_utils.go | 12 +- 34 files changed, 894 insertions(+), 938 deletions(-) create mode 100644 internal/commands/config/common_errors.go create mode 100644 internal/commands/plugin/common_errors.go create mode 100644 internal/errs/pingcli_error.go diff --git a/Makefile b/Makefile index d21b2c80..046a393f 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # This simplifies complex commands and makes the file more readable. .ONESHELL: SHELL := $(shell which bash || which sh) +.SHELLFLAGS := -ec # ==================================================================================== # VARIABLES @@ -90,7 +91,6 @@ protogen: ## Generate Go code from .proto files test: _check_ping_env ## Run all tests @echo " > Test: Running all Go tests..." - set -e for dir in $(TEST_DIRS); do echo " -> $$dir" $(GOTEST) $$dir @@ -142,7 +142,7 @@ _check_ping_env: _check_docker: @echo " > Docker: Checking if the Docker daemon is running..." - $(DOCKER) info > /dev/null 2>&1 + $(DOCKER) info > /dev/null echo "✅ Docker daemon is running." _run_pf_container: @@ -159,7 +159,6 @@ _run_pf_container: _wait_for_pf: @echo " > Docker: Waiting for container to become healthy (up to 4 minutes)..." - set -e timeout=240 while test $$timeout -gt 0; do status=$$(docker inspect --format='{{json .State.Health.Status}}' $(CONTAINER_NAME) 2>/dev/null || echo "") diff --git a/internal/commands/config/add_profile_internal.go b/internal/commands/config/add_profile_internal.go index 49a0760b..f69b6948 100644 --- a/internal/commands/config/add_profile_internal.go +++ b/internal/commands/config/add_profile_internal.go @@ -10,50 +10,32 @@ import ( "github.com/knadh/koanf/v2" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/input" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) var ( - ErrProfileNameNotProvided = errors.New("unable to determine profile name") - ErrSetActiveInvalid = errors.New("invalid value for set-active flag. must be 'true' or 'false'") + addProfileErrorPrefix = "failed to add profile" + ErrNoProfileProvided = errors.New("unable to determine profile name") + ErrSetActiveFlagInvalid = errors.New("invalid value for set-active flag. must be 'true' or 'false'") + ErrKoanfNotInitialized = errors.New("koanf configuration not initialized") ) -type AddProfileError struct { - Err error -} - -func (e *AddProfileError) Error() string { - var err *AddProfileError - if errors.As(e.Err, &err) { - return err.Error() +func RunInternalConfigAddProfile(rc io.ReadCloser, koanfConfig *profiles.KoanfConfig) (err error) { + if koanfConfig == nil { + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: ErrKoanfNotInitialized} } - return fmt.Sprintf("failed to add profile: %s", e.Err.Error()) -} -func (e *AddProfileError) Unwrap() error { - var err *AddProfileError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - -func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { newProfileName, newDescription, setActive, err := readConfigAddProfileOptions(rc) if err != nil { - return &AddProfileError{Err: err} - } - - koanfConfig, err := profiles.GetKoanfConfig() - if err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } err = koanfConfig.ValidateNewProfileName(newProfileName) if err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } output.Message(fmt.Sprintf("Adding new profile '%s'...", newProfileName), nil) @@ -61,18 +43,18 @@ func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { subKoanf := koanf.New(".") err = subKoanf.Set(options.ProfileDescriptionOption.KoanfKey, newDescription) if err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } if err = koanfConfig.SaveProfile(newProfileName, subKoanf); err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Profile created. Update additional profile attributes via 'pingcli config set' or directly within the config file at '%s'", koanfConfig.GetKoanfConfigFile()), nil) if setActive { if err = koanfConfig.ChangeActiveProfile(newProfileName); err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Profile '%s' set as active.", newProfileName), nil) @@ -80,7 +62,7 @@ func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { err = koanfConfig.DefaultMissingKoanfKeys() if err != nil { - return &AddProfileError{Err: err} + return &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } return nil @@ -88,15 +70,15 @@ func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { func readConfigAddProfileOptions(rc io.ReadCloser) (newProfileName, newDescription string, setActive bool, err error) { if newProfileName, err = readConfigAddProfileNameOption(rc); err != nil { - return newProfileName, newDescription, setActive, &AddProfileError{Err: err} + return newProfileName, newDescription, setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } if newDescription, err = readConfigAddProfileDescriptionOption(rc); err != nil { - return newProfileName, newDescription, setActive, &AddProfileError{Err: err} + return newProfileName, newDescription, setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } if setActive, err = readConfigAddProfileSetActiveOption(rc); err != nil { - return newProfileName, newDescription, setActive, &AddProfileError{Err: err} + return newProfileName, newDescription, setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } return newProfileName, newDescription, setActive, nil @@ -106,25 +88,25 @@ func readConfigAddProfileNameOption(rc io.ReadCloser) (newProfileName string, er if !options.ConfigAddProfileNameOption.Flag.Changed { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return newProfileName, &AddProfileError{Err: err} + return newProfileName, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } newProfileName, err = input.RunPrompt("New profile name", koanfConfig.ValidateNewProfileName, rc) if err != nil { - return newProfileName, &AddProfileError{Err: err} + return newProfileName, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } if newProfileName == "" { - return newProfileName, &AddProfileError{Err: ErrProfileNameNotProvided} + return newProfileName, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: ErrNoProfileProvided} } } else { newProfileName, err = profiles.GetOptionValue(options.ConfigAddProfileNameOption) if err != nil { - return newProfileName, &AddProfileError{Err: err} + return newProfileName, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } if newProfileName == "" { - return newProfileName, &AddProfileError{Err: ErrProfileNameNotProvided} + return newProfileName, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: ErrNoProfileProvided} } } @@ -135,12 +117,12 @@ func readConfigAddProfileDescriptionOption(rc io.ReadCloser) (newDescription str if !options.ConfigAddProfileDescriptionOption.Flag.Changed { newDescription, err = input.RunPrompt("New profile description: ", nil, rc) if err != nil { - return newDescription, &AddProfileError{Err: err} + return newDescription, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } } else { newDescription, err = profiles.GetOptionValue(options.ConfigAddProfileDescriptionOption) if err != nil { - return newDescription, &AddProfileError{Err: err} + return newDescription, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } } @@ -151,17 +133,17 @@ func readConfigAddProfileSetActiveOption(rc io.ReadCloser) (setActive bool, err if !options.ConfigAddProfileSetActiveOption.Flag.Changed { setActive, err = input.RunPromptConfirm("Set new profile as active: ", rc) if err != nil { - return setActive, &AddProfileError{Err: err} + return setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } } else { boolStr, err := profiles.GetOptionValue(options.ConfigAddProfileSetActiveOption) if err != nil { - return setActive, &AddProfileError{Err: err} + return setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: err} } setActive, err = strconv.ParseBool(boolStr) if err != nil { - return setActive, &AddProfileError{Err: ErrSetActiveInvalid} + return setActive, &errs.PingCLIError{Prefix: addProfileErrorPrefix, Err: ErrSetActiveFlagInvalid} } } diff --git a/internal/commands/config/add_profile_internal_test.go b/internal/commands/config/add_profile_internal_test.go index 1444f5c5..e259713e 100644 --- a/internal/commands/config/add_profile_internal_test.go +++ b/internal/commands/config/add_profile_internal_test.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "os" "testing" @@ -12,6 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigAddProfile(t *testing.T) { @@ -20,6 +20,7 @@ func Test_RunInternalConfigAddProfile(t *testing.T) { profileName customtypes.String description customtypes.String setActive customtypes.Bool + setKoanfNil bool expectedError error }{ { @@ -46,13 +47,25 @@ func Test_RunInternalConfigAddProfile(t *testing.T) { profileName: "", description: "test-description", setActive: customtypes.Bool(false), - expectedError: ErrProfileNameNotProvided, + expectedError: ErrNoProfileProvided, + }, + { + name: "Koanf Not Initialized", + profileName: "new-profile", + description: "test-description", + setActive: customtypes.Bool(false), + setKoanfNil: true, + expectedError: ErrKoanfNotInitialized, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - testutils_koanf.InitKoanfs(t) + koanfConfig := testutils_koanf.InitKoanfs(t) + + if tc.setKoanfNil { + koanfConfig = nil + } options.ConfigAddProfileNameOption.Flag.Changed = true options.ConfigAddProfileNameOption.CobraParamValue = &tc.profileName @@ -63,16 +76,11 @@ func Test_RunInternalConfigAddProfile(t *testing.T) { options.ConfigAddProfileSetActiveOption.Flag.Changed = true options.ConfigAddProfileSetActiveOption.CobraParamValue = &tc.setActive - err := RunInternalConfigAddProfile(os.Stdin) + err := RunInternalConfigAddProfile(os.Stdin, koanfConfig) if tc.expectedError != nil { - assert.Error(t, err) - var addProfileErr *AddProfileError - if errors.As(err, &addProfileErr) { - assert.ErrorIs(t, addProfileErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type AddProfileError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/config/common_errors.go b/internal/commands/config/common_errors.go new file mode 100644 index 00000000..324d690f --- /dev/null +++ b/internal/commands/config/common_errors.go @@ -0,0 +1,7 @@ +package config_internal + +import "errors" + +var ( + ErrUndeterminedProfile = errors.New("unable to determine configuration profile") +) diff --git a/internal/commands/config/delete_profile_internal.go b/internal/commands/config/delete_profile_internal.go index 0b9fe210..39a068e9 100644 --- a/internal/commands/config/delete_profile_internal.go +++ b/internal/commands/config/delete_profile_internal.go @@ -3,35 +3,19 @@ package config_internal import ( - "errors" "fmt" "io" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/input" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -type DeleteProfileError struct { - Err error -} - -func (e *DeleteProfileError) Error() string { - var err *DeleteProfileError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to delete profile: %s", e.Err.Error()) -} - -func (e *DeleteProfileError) Unwrap() error { - var err *DeleteProfileError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + deleteProfileErrorPrefix = "failed to delete profile" +) func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) { var pName string @@ -40,22 +24,22 @@ func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) } else { pName, err = promptUserToDeleteProfile(rc) if err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } if err = koanfConfig.ValidateExistingProfileName(pName); err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } confirmed, err := promptUserToConfirmDelete(pName, rc) if err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } if !confirmed { @@ -66,7 +50,7 @@ func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) err = deleteProfile(pName) if err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } return nil @@ -75,12 +59,12 @@ func RunInternalConfigDeleteProfile(args []string, rc io.ReadCloser) (err error) func promptUserToDeleteProfile(rc io.ReadCloser) (pName string, err error) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return pName, &DeleteProfileError{Err: err} + return pName, &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } pName, err = input.RunPromptSelect("Select profile to delete", koanfConfig.ProfileNames(), rc) if err != nil { - return pName, &DeleteProfileError{Err: err} + return pName, &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } return pName, nil @@ -91,7 +75,7 @@ func promptUserToConfirmDelete(pName string, rc io.ReadCloser) (confirmed bool, if options.ConfigDeleteAutoAcceptOption.Flag.Changed { autoAccept, err = profiles.GetOptionValue(options.ConfigDeleteAutoAcceptOption) if err != nil { - return false, &DeleteProfileError{Err: err} + return false, &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } } @@ -101,7 +85,7 @@ func promptUserToConfirmDelete(pName string, rc io.ReadCloser) (confirmed bool, confirmed, err = input.RunPromptConfirm(fmt.Sprintf("Are you sure you want to delete profile '%s'", pName), rc) if err != nil { - return false, &DeleteProfileError{Err: err} + return false, &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } return confirmed, nil } @@ -111,11 +95,11 @@ func deleteProfile(pName string) (err error) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } if err = koanfConfig.DeleteProfile(pName); err != nil { - return &DeleteProfileError{Err: err} + return &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Profile '%s' deleted.", pName), nil) diff --git a/internal/commands/config/delete_profile_internal_test.go b/internal/commands/config/delete_profile_internal_test.go index ba0281ea..f6ac9df9 100644 --- a/internal/commands/config/delete_profile_internal_test.go +++ b/internal/commands/config/delete_profile_internal_test.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "os" "testing" @@ -12,6 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigDeleteProfile(t *testing.T) { @@ -62,13 +62,8 @@ func Test_RunInternalConfigDeleteProfile(t *testing.T) { err := RunInternalConfigDeleteProfile([]string{tc.profileName}, os.Stdin) if tc.expectedError != nil { - assert.Error(t, err) - var deleteProfileErr *DeleteProfileError - if errors.As(err, &deleteProfileErr) { - assert.ErrorIs(t, deleteProfileErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type DeleteProfileError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/config/get_internal.go b/internal/commands/config/get_internal.go index 84dc1bbd..485090a8 100644 --- a/internal/commands/config/get_internal.go +++ b/internal/commands/config/get_internal.go @@ -3,46 +3,28 @@ package config_internal import ( - "errors" "fmt" "strings" "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -var ErrUndeterminedProfile = errors.New("unable to determine profile to get configuration from") - -type GetError struct { - Err error -} - -func (e *GetError) Error() string { - var err *GetError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to get configuration: %s", e.Err.Error()) -} - -func (e *GetError) Unwrap() error { - var err *GetError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + getErrorPrefix = "failed to get configuration" +) func RunInternalConfigGet(koanfKey string) (err error) { if err = configuration.ValidateParentKoanfKey(koanfKey); err != nil { - return &GetError{Err: err} + return &errs.PingCLIError{Prefix: getErrorPrefix, Err: err} } pName, err := readConfigGetOptions() if err != nil { - return &GetError{Err: err} + return &errs.PingCLIError{Prefix: getErrorPrefix, Err: err} } msgStr := fmt.Sprintf("Configuration values for profile '%s' and key '%s':\n", pName, koanfKey) @@ -60,7 +42,7 @@ func RunInternalConfigGet(koanfKey string) (err error) { vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return &GetError{Err: err} + return &errs.PingCLIError{Prefix: getErrorPrefix, Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -88,11 +70,11 @@ func readConfigGetOptions() (pName string, err error) { } if err != nil { - return pName, &GetError{Err: err} + return pName, &errs.PingCLIError{Prefix: getErrorPrefix, Err: err} } if pName == "" { - return pName, &GetError{Err: ErrUndeterminedProfile} + return pName, &errs.PingCLIError{Prefix: getErrorPrefix, Err: ErrUndeterminedProfile} } return pName, nil diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index 6e0dda6c..32b65b27 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "testing" "github.com/pingidentity/pingcli/internal/configuration" @@ -12,6 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigGet(t *testing.T) { @@ -60,13 +60,8 @@ func Test_RunInternalConfigGet(t *testing.T) { err := RunInternalConfigGet(tc.koanfKey) if tc.expectedError != nil { - assert.Error(t, err) - var getErr *GetError - if errors.As(err, &getErr) { - assert.ErrorIs(t, getErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type GetError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/config/list_keys_internal.go b/internal/commands/config/list_keys_internal.go index 56953c68..f0b10a8b 100644 --- a/internal/commands/config/list_keys_internal.go +++ b/internal/commands/config/list_keys_internal.go @@ -4,47 +4,29 @@ package config_internal import ( "errors" - "fmt" + "slices" "strings" "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" "gopkg.in/yaml.v3" ) var ( - ErrRetrieveKeys = errors.New("failed to retrieve configuration keys") - ErrNestedMap = errors.New("failed to create nested map for key") - ErrMarshalKeys = errors.New("failed to marshal keys to YAML format") + ErrRetrieveKeys = errors.New("failed to retrieve configuration keys") + ErrNestedMap = errors.New("failed to create nested map for key") + ErrMarshalKeys = errors.New("failed to marshal keys to YAML format") + listKeysErrorPrefix = "failed to get configuration keys list" ) -type ListKeysError struct { - Err error -} - -func (e *ListKeysError) Error() string { - var err *ListKeysError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to get configuration keys list: %s", e.Err.Error()) -} - -func (e *ListKeysError) Unwrap() error { - var err *ListKeysError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - func returnKeysYamlString() (keysYamlStr string, err error) { koanfKeys := configuration.KoanfKeys() if len(koanfKeys) == 0 { - return keysYamlStr, &ListKeysError{Err: ErrRetrieveKeys} + return keysYamlStr, &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: ErrRetrieveKeys} } // Split the input string into individual keys @@ -53,7 +35,7 @@ func returnKeysYamlString() (keysYamlStr string, err error) { // Iterate over each koanf key for _, koanfKey := range koanfKeys { // Skip the "activeProfile" key - if koanfKey == "activeProfile" { + if koanfKey == options.RootActiveProfileOption.KoanfKey { continue } @@ -74,7 +56,7 @@ func returnKeysYamlString() (keysYamlStr string, err error) { } currentMap, currentMapOk = currentMap[k].(map[string]interface{}) if !currentMapOk { - return keysYamlStr, &ListKeysError{Err: ErrNestedMap} + return keysYamlStr, &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: ErrNestedMap} } } } @@ -83,7 +65,7 @@ func returnKeysYamlString() (keysYamlStr string, err error) { // Marshal the result into YAML yamlData, err := yaml.Marshal(keyMap) if err != nil { - return keysYamlStr, &ListKeysError{Err: ErrMarshalKeys} + return keysYamlStr, &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: ErrMarshalKeys} } keysYamlStr = string(yamlData) @@ -94,31 +76,36 @@ func returnKeysString() (string, error) { validKeys := configuration.KoanfKeys() if len(validKeys) == 0 { - return "", &ListKeysError{Err: ErrRetrieveKeys} - } else { - validKeysJoined := strings.Join(validKeys, "\n- ") - - return "Valid Keys:\n- " + validKeysJoined, nil + return "", &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: ErrRetrieveKeys} } + + // Remove the "activeProfile" key from the list + validKeys = slices.DeleteFunc(validKeys, func(s string) bool { + return s == options.RootActiveProfileOption.KoanfKey + }) + + validKeysJoined := strings.Join(validKeys, "\n- ") + + return "Valid Keys:\n- " + validKeysJoined, nil } func RunInternalConfigListKeys() (err error) { var outputMessageString string yamlFlagStr, err := profiles.GetOptionValue(options.ConfigListKeysYamlOption) if err != nil { - return &ListKeysError{Err: err} + return &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: err} } if yamlFlagStr == "true" { // Output the YAML data as a string outputMessageString, err = returnKeysYamlString() if err != nil { - return &ListKeysError{Err: err} + return &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: err} } } else { // Output data list string outputMessageString, err = returnKeysString() if err != nil { - return &ListKeysError{Err: err} + return &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: err} } } diff --git a/internal/commands/config/list_keys_internal_test.go b/internal/commands/config/list_keys_internal_test.go index d6847e24..4c0a1d3a 100644 --- a/internal/commands/config/list_keys_internal_test.go +++ b/internal/commands/config/list_keys_internal_test.go @@ -3,20 +3,56 @@ package config_internal import ( - "errors" + "io" + "os" + "strings" "testing" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigListKeys(t *testing.T) { + testutils_koanf.InitKoanfs(t) + testCases := []struct { name string + contains []string + notContains []string + enableYaml customtypes.Bool expectedError error }{ { name: "Get List of Keys", + contains: []string{ + options.RootColorOption.KoanfKey, + options.RootOutputFormatOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + options.PlatformExportServiceGroupOption.KoanfKey, + options.PingFederateAdminAPIPathOption.KoanfKey, + options.PingOneAuthenticationWorkerClientIDOption.KoanfKey, + }, + notContains: []string{ + options.RootActiveProfileOption.KoanfKey, + }, + }, + { + name: "Get List of Keys in YAML format", + enableYaml: true, + contains: []string{ + strings.Split(options.PlatformExportServiceGroupOption.KoanfKey, ".")[0] + ":", + strings.Split(options.PingFederateAdminAPIPathOption.KoanfKey, ".")[0] + ":", + strings.Split(options.PingOneAuthenticationWorkerClientIDOption.KoanfKey, ".")[0] + ":", + }, + notContains: []string{ + options.PlatformExportServiceGroupOption.KoanfKey, + options.PingFederateAdminAPIPathOption.KoanfKey, + options.PingOneAuthenticationWorkerClientIDOption.KoanfKey, + options.RootActiveProfileOption.KoanfKey, + }, }, } @@ -24,18 +60,38 @@ func Test_RunInternalConfigListKeys(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testutils_koanf.InitKoanfs(t) + if tc.enableYaml { + options.ConfigListKeysYamlOption.Flag.Changed = true + options.ConfigListKeysYamlOption.CobraParamValue = &tc.enableYaml + } + + originalStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + t.Cleanup(func() { + os.Stdout = originalStdout + }) + err := RunInternalConfigListKeys() if tc.expectedError != nil { - assert.Error(t, err) - var listKeysErr *ListKeysError - if errors.As(err, &listKeysErr) { - assert.ErrorIs(t, listKeysErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type ListKeysError") - } + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) } else { - assert.NoError(t, err) + require.NoError(t, err) + } + + w.Close() + capturedOutputBytes, _ := io.ReadAll(r) + capturedOutput := string(capturedOutputBytes) + + for _, expected := range tc.contains { + assert.Contains(t, capturedOutput, expected) + } + + for _, notExpected := range tc.notContains { + assert.NotContains(t, capturedOutput, notExpected) } }) } diff --git a/internal/commands/config/list_profiles_internal.go b/internal/commands/config/list_profiles_internal.go index 95f7fbfe..e39d6fc4 100644 --- a/internal/commands/config/list_profiles_internal.go +++ b/internal/commands/config/list_profiles_internal.go @@ -3,45 +3,27 @@ package config_internal import ( - "errors" - "fmt" - "github.com/fatih/color" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -type ListProfilesError struct { - Err error -} - -func (e *ListProfilesError) Error() string { - var err *ListProfilesError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to list profiles: %s", e.Err.Error()) -} - -func (e *ListProfilesError) Unwrap() error { - var err *ListProfilesError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + listProfilesErrorPrefix = "failed to list profiles" +) func RunInternalConfigListProfiles() (err error) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &ListProfilesError{Err: err} + return &errs.PingCLIError{Prefix: listProfilesErrorPrefix, Err: err} } profileNames := koanfConfig.ProfileNames() activeProfileName, err := profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - return &ListProfilesError{Err: err} + return &errs.PingCLIError{Prefix: listProfilesErrorPrefix, Err: err} } listStr := "Profiles:\n" diff --git a/internal/commands/config/list_profiles_internal_test.go b/internal/commands/config/list_profiles_internal_test.go index f167e67c..9d0279b8 100644 --- a/internal/commands/config/list_profiles_internal_test.go +++ b/internal/commands/config/list_profiles_internal_test.go @@ -3,11 +3,11 @@ package config_internal import ( - "errors" "testing" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigListProfiles(t *testing.T) { @@ -27,13 +27,8 @@ func Test_RunInternalConfigListProfiles(t *testing.T) { err := RunInternalConfigListProfiles() if tc.expectedError != nil { - assert.Error(t, err) - var listProfilesErr *ListProfilesError - if errors.As(err, &listProfilesErr) { - assert.ErrorIs(t, listProfilesErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type ListProfilesError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/config/set_active_profile_internal.go b/internal/commands/config/set_active_profile_internal.go index 3c3e4d1b..823ec014 100644 --- a/internal/commands/config/set_active_profile_internal.go +++ b/internal/commands/config/set_active_profile_internal.go @@ -3,34 +3,18 @@ package config_internal import ( - "errors" "fmt" "io" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/input" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -type SetActiveProfileError struct { - Err error -} - -func (e *SetActiveProfileError) Error() string { - var err *SetActiveProfileError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to set active profile: %s", e.Err.Error()) -} - -func (e *SetActiveProfileError) Unwrap() error { - var err *SetActiveProfileError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + setActiveProfileErrorPrefix = "failed to set active profile" +) func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err error) { var pName string @@ -39,7 +23,7 @@ func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err err } else { pName, err = promptUserToSelectActiveProfile(rc) if err != nil { - return &SetActiveProfileError{Err: err} + return &errs.PingCLIError{Prefix: setActiveProfileErrorPrefix, Err: err} } } @@ -47,11 +31,11 @@ func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err err koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &SetActiveProfileError{Err: err} + return &errs.PingCLIError{Prefix: setActiveProfileErrorPrefix, Err: err} } if err = koanfConfig.ChangeActiveProfile(pName); err != nil { - return &SetActiveProfileError{Err: err} + return &errs.PingCLIError{Prefix: setActiveProfileErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Active profile set to '%s'", pName), nil) @@ -62,12 +46,12 @@ func RunInternalConfigSetActiveProfile(args []string, rc io.ReadCloser) (err err func promptUserToSelectActiveProfile(rc io.ReadCloser) (pName string, err error) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return "", &SetActiveProfileError{Err: err} + return "", &errs.PingCLIError{Prefix: setActiveProfileErrorPrefix, Err: err} } pName, err = input.RunPromptSelect("Select profile to set as active: ", koanfConfig.ProfileNames(), rc) if err != nil { - return pName, &SetActiveProfileError{Err: err} + return pName, &errs.PingCLIError{Prefix: setActiveProfileErrorPrefix, Err: err} } return pName, nil diff --git a/internal/commands/config/set_active_profile_internal_test.go b/internal/commands/config/set_active_profile_internal_test.go index 134ea53d..16bdfe8a 100644 --- a/internal/commands/config/set_active_profile_internal_test.go +++ b/internal/commands/config/set_active_profile_internal_test.go @@ -3,13 +3,13 @@ package config_internal import ( - "errors" "os" "testing" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigSetActiveProfile(t *testing.T) { @@ -50,13 +50,8 @@ func Test_RunInternalConfigSetActiveProfile(t *testing.T) { err := RunInternalConfigSetActiveProfile([]string{tc.profileName}, os.Stdin) if tc.expectedError != nil { - assert.Error(t, err) - var setActiveProfileErr *SetActiveProfileError - if errors.As(err, &setActiveProfileErr) { - assert.ErrorIs(t, setActiveProfileErr.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type SetActiveProfileError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index 2ab05959..6102e33f 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -11,13 +11,13 @@ import ( "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) var ( ErrEmptyValue = errors.New("the set value provided is empty. Use 'pingcli config unset %s' to unset a key's configuration") - ErrDetermineProfileSet = errors.New("unable to determine profile to set configuration to") ErrKeyAssignmentFormat = errors.New("invalid key-value assignment. Expect 'key=value' format") ErrActiveProfileAssignment = errors.New("invalid active profile assignment. Please use the 'pingcli config set active-profile ' command to set the active profile") ErrSetKey = errors.New("unable to set key in configuration profile") @@ -38,71 +38,52 @@ var ( ErrMustBeLicenseProduct = fmt.Errorf("the value assignment must be a valid license product. Allowed [%s]", strings.Join(customtypes.LicenseProductValidValues(), ", ")) ErrMustBeLicenseVersion = errors.New("the value assignment must be a valid license version. Must be of the form 'major.minor'") ErrTypeNotRecognized = errors.New("the variable type for the configuration key is not recognized or supported") + setErrorPrefix = "failed to set configuration" ) -type SetError struct { - Err error -} - -func (e *SetError) Error() string { - var err *SetError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to set configuration: %s", e.Err.Error()) -} - -func (e *SetError) Unwrap() error { - var err *SetError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - func RunInternalConfigSet(kvPair string) (err error) { pName, vKey, vValue, err := readConfigSetOptions(kvPair) if err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } if err = configuration.ValidateKoanfKey(vKey); err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } // Make sure value is not empty, and suggest unset command if it is if vValue == "" { - return &SetError{Err: ErrEmptyValue} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrEmptyValue} } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } opt, err := configuration.OptionFromKoanfKey(vKey) if err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } if err = setValue(subKoanf, opt.KoanfKey, vValue, opt.Type); err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } msgStr := "Configuration set successfully:\n" vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return &SetError{Err: err} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -123,11 +104,11 @@ func RunInternalConfigSet(kvPair string) (err error) { func readConfigSetOptions(kvPair string) (pName string, vKey string, vValue string, err error) { if pName, err = readConfigSetProfileName(); err != nil { - return pName, vKey, vValue, &SetError{Err: err} + return pName, vKey, vValue, &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } if vKey, vValue, err = parseKeyValuePair(kvPair); err != nil { - return pName, vKey, vValue, &SetError{Err: err} + return pName, vKey, vValue, &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } return pName, vKey, vValue, nil @@ -141,11 +122,11 @@ func readConfigSetProfileName() (pName string, err error) { } if err != nil { - return pName, &SetError{Err: err} + return pName, &errs.PingCLIError{Prefix: setErrorPrefix, Err: err} } if pName == "" { - return pName, &SetError{Err: ErrDetermineProfileSet} + return pName, &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrUndeterminedProfile} } return pName, nil @@ -154,11 +135,11 @@ func readConfigSetProfileName() (pName string, err error) { func parseKeyValuePair(kvPair string) (key string, value string, err error) { parsedInput := strings.SplitN(kvPair, "=", 2) if len(parsedInput) < 2 { - return key, value, &SetError{Err: ErrKeyAssignmentFormat} + return key, value, &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrKeyAssignmentFormat} } if strings.EqualFold(parsedInput[0], options.RootActiveProfileOption.KoanfKey) { - return key, value, &SetError{Err: ErrActiveProfileAssignment} + return key, value, &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrActiveProfileAssignment} } return parsedInput[0], parsedInput[1], nil @@ -311,7 +292,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. return fmt.Errorf("%w: %v", ErrSetKey, err) } default: - return &SetError{Err: ErrTypeNotRecognized} + return &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrTypeNotRecognized} } return nil diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index 9965ef2e..87e4a2a3 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "fmt" "testing" @@ -12,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigSet(t *testing.T) { @@ -101,25 +100,20 @@ func Test_RunInternalConfigSet(t *testing.T) { err := RunInternalConfigSet(tc.kvPair) if tc.expectedError != nil { - assert.Error(t, err) - var setError *SetError - if errors.As(err, &setError) { - assert.ErrorIs(t, setError.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type SetError") - } + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) } else { - assert.NoError(t, err) + require.NoError(t, err) } if tc.checkOption != nil { vVal, err := profiles.GetOptionValue(*tc.checkOption) if err != nil { - assert.Fail(t, "GetOptionValue returned error: %v", err) + require.Fail(t, "GetOptionValue returned error: %v", err) } if vVal != tc.checkValue { - assert.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkValue, vVal) + require.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkValue, vVal) } } }) diff --git a/internal/commands/config/unset_internal.go b/internal/commands/config/unset_internal.go index 262c1d28..5859f116 100644 --- a/internal/commands/config/unset_internal.go +++ b/internal/commands/config/unset_internal.go @@ -3,77 +3,59 @@ package config_internal import ( - "errors" "fmt" "strings" "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -var ErrDetermineProfileUnset = errors.New("unable to determine profile to unset configuration from") - -type UnsetError struct { - Err error -} - -func (e *UnsetError) Error() string { - var err *UnsetError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to unset configuration: %s", e.Err.Error()) -} - -func (e *UnsetError) Unwrap() error { - var err *UnsetError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + unsetErrorPrefix = "failed to unset configuration" +) func RunInternalConfigUnset(koanfKey string) (err error) { if err = configuration.ValidateKoanfKey(koanfKey); err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } pName, err := readConfigUnsetOptions() if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } opt, err := configuration.OptionFromKoanfKey(koanfKey) if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } err = subKoanf.Set(opt.KoanfKey, opt.DefaultValue) if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } msgStr := "Configuration unset successfully:\n" vVal, _, err := profiles.KoanfValueFromOption(opt, pName) if err != nil { - return &UnsetError{Err: err} + return &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) @@ -100,11 +82,11 @@ func readConfigUnsetOptions() (pName string, err error) { } if err != nil { - return pName, &UnsetError{Err: err} + return pName, &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: err} } if pName == "" { - return pName, &UnsetError{Err: ErrDetermineProfileUnset} + return pName, &errs.PingCLIError{Prefix: unsetErrorPrefix, Err: ErrUndeterminedProfile} } return pName, nil diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index ab7f44db..80aee488 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "testing" "github.com/pingidentity/pingcli/internal/configuration" @@ -11,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigUnset(t *testing.T) { @@ -70,25 +69,20 @@ func Test_RunInternalConfigUnset(t *testing.T) { err := RunInternalConfigUnset(tc.koanfKey) if tc.expectedError != nil { - assert.Error(t, err) - var unsetError *UnsetError - if errors.As(err, &unsetError) { - assert.ErrorIs(t, unsetError.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type UnsetError") - } + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) } else { - assert.NoError(t, err) + require.NoError(t, err) } if tc.checkOption != nil { vVal, err := profiles.GetOptionValue(*tc.checkOption) if err != nil { - assert.Fail(t, "GetOptionValue returned error: %v", err) + require.Fail(t, "GetOptionValue returned error: %v", err) } if vVal != tc.checkOption.DefaultValue.String() { - assert.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkOption.DefaultValue.String(), vVal) + require.Fail(t, "Expected %s to be %s, got %v", tc.checkOption.KoanfKey, tc.checkOption.DefaultValue.String(), vVal) } } }) diff --git a/internal/commands/config/view_profile_internal.go b/internal/commands/config/view_profile_internal.go index 4acb0828..24763bc1 100644 --- a/internal/commands/config/view_profile_internal.go +++ b/internal/commands/config/view_profile_internal.go @@ -3,34 +3,18 @@ package config_internal import ( - "errors" "fmt" "strings" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) -type ViewProfileError struct { - Err error -} - -func (e *ViewProfileError) Error() string { - var err *ViewProfileError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to view profile: %s", e.Err.Error()) -} - -func (e *ViewProfileError) Unwrap() error { - var err *ViewProfileError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} +var ( + viewProfileErrorPrefix = "failed to view profile" +) func RunInternalConfigViewProfile(args []string) (err error) { var msgStr, pName string @@ -39,25 +23,25 @@ func RunInternalConfigViewProfile(args []string) (err error) { } else { pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - return &ViewProfileError{Err: err} + return &errs.PingCLIError{Prefix: viewProfileErrorPrefix, Err: err} } } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return &ViewProfileError{Err: err} + return &errs.PingCLIError{Prefix: viewProfileErrorPrefix, Err: err} } // Validate the profile name err = koanfConfig.ValidateExistingProfileName(pName) if err != nil { - return &ViewProfileError{Err: err} + return &errs.PingCLIError{Prefix: viewProfileErrorPrefix, Err: err} } // Get the Koanf configuration for the specified profile koanfProfile, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return &ViewProfileError{Err: err} + return &errs.PingCLIError{Prefix: viewProfileErrorPrefix, Err: err} } // Iterate over the options in profile and print them @@ -72,7 +56,7 @@ func RunInternalConfigViewProfile(args []string) (err error) { } if err != nil { - return &ViewProfileError{Err: err} + return &errs.PingCLIError{Prefix: viewProfileErrorPrefix, Err: err} } unmaskOptionVal, err := profiles.GetOptionValue(options.ConfigUnmaskSecretValueOption) diff --git a/internal/commands/config/view_profile_internal_test.go b/internal/commands/config/view_profile_internal_test.go index 16f5478a..bbfa73fc 100644 --- a/internal/commands/config/view_profile_internal_test.go +++ b/internal/commands/config/view_profile_internal_test.go @@ -3,12 +3,12 @@ package config_internal import ( - "errors" "testing" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalConfigViewProfile(t *testing.T) { @@ -44,13 +44,8 @@ func Test_RunInternalConfigViewProfile(t *testing.T) { err := RunInternalConfigViewProfile(tc.profileName) if tc.expectedError != nil { - assert.Error(t, err) - var viewError *ViewProfileError - if errors.As(err, &viewError) { - assert.ErrorIs(t, viewError.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type ViewProfileError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go index 6a503cbd..6e1e17f1 100644 --- a/internal/commands/license/license_internal.go +++ b/internal/commands/license/license_internal.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) @@ -22,42 +23,23 @@ var ( ErrGetDevopsKey = errors.New("failed to get devops key option value") ErrRequiredValues = errors.New("product, version, devops user, and devops key must be specified for license request") ErrLicenseRequest = errors.New("license request failed") + licenseErrorPrefix = "failed to run license request" ) -type LicenseError struct { - Err error -} - -func (e *LicenseError) Error() string { - var err *LicenseError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("failed to run license request: %s", e.Err.Error()) -} - -func (e *LicenseError) Unwrap() error { - var err *LicenseError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - func RunInternalLicense() (err error) { product, version, devopsUser, devopsKey, err := readLicenseOptionValues() if err != nil { - return &LicenseError{Err: err} + return &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } ctx := context.Background() licenseData, err := runLicenseRequest(ctx, product, version, devopsUser, devopsKey) if err != nil { - return &LicenseError{Err: err} + return &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } if licenseData == "" { - return &LicenseError{Err: ErrLicenseDataEmpty} + return &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: ErrLicenseDataEmpty} } output.Message(licenseData, nil) @@ -68,26 +50,26 @@ func RunInternalLicense() (err error) { func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, err error) { product, err = profiles.GetOptionValue(options.LicenseProductOption) if err != nil { - return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetProduct, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetProduct, err)} } version, err = profiles.GetOptionValue(options.LicenseVersionOption) if err != nil { - return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetVersion, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetVersion, err)} } devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) if err != nil { - return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetDevopsUser, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetDevopsUser, err)} } devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) if err != nil { - return product, version, devopsUser, devopsKey, &LicenseError{Err: fmt.Errorf("%w: %v", ErrGetDevopsKey, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetDevopsKey, err)} } if product == "" || version == "" || devopsUser == "" || devopsKey == "" { - return product, version, devopsUser, devopsKey, &LicenseError{Err: ErrRequiredValues} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: ErrRequiredValues} } return product, version, devopsUser, devopsKey, nil @@ -96,7 +78,7 @@ func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, func runLicenseRequest(ctx context.Context, product, version, devopsUser, devopsKey string) (licenseData string, err error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://license.pingidentity.com/devops/license", nil) if err != nil { - return licenseData, &LicenseError{Err: err} + return licenseData, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } req.Header.Set("Devops-User", devopsUser) @@ -109,23 +91,23 @@ func runLicenseRequest(ctx context.Context, product, version, devopsUser, devops client := &http.Client{} res, err := client.Do(req) if err != nil { - return licenseData, &LicenseError{Err: err} + return licenseData, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } defer func() { cErr := res.Body.Close() err = errors.Join(err, cErr) if err != nil { - err = &LicenseError{Err: err} + err = &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } }() body, err := io.ReadAll(res.Body) if err != nil { - return licenseData, &LicenseError{Err: err} + return licenseData, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } if res.StatusCode < 200 || res.StatusCode >= 300 { - return "", &LicenseError{Err: fmt.Errorf("%w with status %d: %s", ErrLicenseRequest, res.StatusCode, string(body))} + return "", &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w with status %d: %s", ErrLicenseRequest, res.StatusCode, string(body))} } return string(body), nil diff --git a/internal/commands/license/license_internal_test.go b/internal/commands/license/license_internal_test.go index 13a104a1..fc09f2f5 100644 --- a/internal/commands/license/license_internal_test.go +++ b/internal/commands/license/license_internal_test.go @@ -3,13 +3,13 @@ package license_internal import ( - "errors" "testing" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_RunInternalLicense(t *testing.T) { @@ -77,13 +77,8 @@ func Test_RunInternalLicense(t *testing.T) { err := RunInternalLicense() if tc.expectedError != nil { - assert.Error(t, err) - var licenseError *LicenseError - if errors.As(err, &licenseError) { - assert.ErrorIs(t, licenseError.Unwrap(), tc.expectedError) - } else { - assert.Fail(t, "Expected error to be of type LicenseError") - } + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) } else { assert.NoError(t, err) } diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index 8f25cf70..1bd1c446 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -6,6 +6,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "net/http" "os" @@ -25,6 +26,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingone/protect" "github.com/pingidentity/pingcli/internal/connector/pingone/sso" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" @@ -41,61 +43,91 @@ var ( pingoneContext context.Context ) +var ( + exportErrorPrefix = "failed to export service(s)" + ErrNilContext = errors.New("context is nil") + ErrReadCaCertPemFile = errors.New("failed to read CA certificate PEM file") + ErrAppendToCertPool = errors.New("failed to append to certificate pool from PEM file") + ErrBasicAuthEmpty = errors.New("failed to initialize PingFederate service. Basic authentication username and/or password is not set") + ErrAccessTokenEmpty = errors.New("failed to initialize PingFederate service. Access token is not set") + ErrClientCredentialsEmpty = errors.New("failed to initialize PingFederate service. Client ID, Client Secret, and/or Token URL is not set") + ErrPingFederateAuthType = errors.New("failed to initialize PingFederate service. Unrecognized authentication type") + ErrPingFederateInit = errors.New("failed to initialize PingFederate service. Check authentication type and credentials") + ErrHttpTransportNil = errors.New("failed to initialize PingFederate service. http transport is nil") + ErrHttpsHostEmpty = errors.New("failed to initialize PingFederate service. HTTPS host is not set") + ErrPingOneConfigValuesEmpty = errors.New("failed to initialize pingone API client. one of worker client ID, worker client secret, " + + "pingone region code, and/or worker environment ID is not set. configure these properties via parameter flags, " + + "environment variables, or the tool's configuration file (default: $HOME/.pingcli/config.yaml)") + ErrPingOneInit = errors.New("failed to initialize pingone API client. Check worker client ID, worker client secret," + + " worker environment ID, and pingone region code configuration values") + ErrOutputDirectoryEmpty = errors.New("output directory is not set") + ErrGetPresentWorkingDirectory = errors.New("failed to get present working directory") + ErrCreateOutputDirectory = errors.New("failed to create output directory") + ErrReadOutputDirectory = errors.New("failed to read contents of output directory") + ErrOutputDirectoryNotEmpty = errors.New("output directory is not empty. use '--overwrite' to overwrite existing files and export data") + ErrDeterminePingOneExportEnv = errors.New("failed to determine pingone export environment ID") + ErrPingOneClientNil = errors.New("pingone API client is nil") + ErrValidatePingOneEnvId = errors.New("failed to validate pingone environment ID") + ErrPingOneEnvNotExist = errors.New("pingone environment does not exist") + ErrConnectorListNil = errors.New("exportable connectors list is nil") + ErrExportService = errors.New("failed to export service") +) + func RunInternalExport(ctx context.Context, commandVersion string) (err error) { if ctx == nil { - return fmt.Errorf("failed to run 'platform export' command. context is nil") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrNilContext} } exportFormat, err := profiles.GetOptionValue(options.PlatformExportExportFormatOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } exportServiceGroup, err := profiles.GetOptionValue(options.PlatformExportServiceGroupOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } exportServices, err := profiles.GetOptionValue(options.PlatformExportServiceOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } outputDir, err := profiles.GetOptionValue(options.PlatformExportOutputDirectoryOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } overwriteExport, err := profiles.GetOptionValue(options.PlatformExportOverwriteOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } var exportableConnectors *[]connector.Exportable es := new(customtypes.ExportServices) if err = es.Set(exportServices); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } esg := new(customtypes.ExportServiceGroup) if err = esg.Set(exportServiceGroup); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } es2 := new(customtypes.ExportServices) if err = es2.SetServicesByServiceGroup(esg); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if err = es.Merge(*es2); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if es.ContainsPingOneService() { if err = initPingOneServices(ctx, commandVersion); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } } if es.ContainsPingFederateService() { if err = initPingFederateServices(ctx, commandVersion); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } } @@ -103,14 +135,14 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { overwriteExportBool, err := strconv.ParseBool(overwriteExport) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if outputDir, err = createOrValidateOutputDir(outputDir, overwriteExportBool); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if err := exportConnectors(exportableConnectors, exportFormat, outputDir, overwriteExportBool); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Export to directory '%s' complete.", outputDir), nil) @@ -120,16 +152,16 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { func initPingFederateServices(ctx context.Context, pingcliVersion string) (err error) { if ctx == nil { - return fmt.Errorf("failed to initialize PingFederate services. context is nil") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrNilContext} } pfInsecureTrustAllTLS, err := profiles.GetOptionValue(options.PingFederateInsecureTrustAllTLSOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } caCertPemFiles, err := profiles.GetOptionValue(options.PingFederateCACertificatePemFilesOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } caCertPool := x509.NewCertPool() @@ -140,18 +172,24 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e caCertPemFile := filepath.Clean(caCertPemFile) caCert, err := os.ReadFile(caCertPemFile) if err != nil { - return fmt.Errorf("failed to read CA certificate PEM file '%s': %w", caCertPemFile, err) + return &errs.PingCLIError{ + Prefix: exportErrorPrefix, + Err: fmt.Errorf("%w '%s': %v", ErrReadCaCertPemFile, caCertPemFile, err), + } } ok := caCertPool.AppendCertsFromPEM(caCert) if !ok { - return fmt.Errorf("failed to parse CA certificate PEM file '%s' to certificate pool", caCertPemFile) + return &errs.PingCLIError{ + Prefix: exportErrorPrefix, + Err: fmt.Errorf("%w '%s': %v", ErrAppendToCertPool, caCertPemFile, err), + } } } pfInsecureTrustAllTLSBool, err := strconv.ParseBool(pfInsecureTrustAllTLS) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } tr := &http.Transport{ @@ -162,28 +200,28 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e } if err = initPingFederateApiClient(tr, pingcliVersion); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } // Create context based on pingfederate authentication type authType, err := profiles.GetOptionValue(options.PingFederateAuthenticationTypeOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } switch { case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC): pfUsername, err := profiles.GetOptionValue(options.PingFederateBasicAuthUsernameOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } pfPassword, err := profiles.GetOptionValue(options.PingFederateBasicAuthPasswordOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pfUsername == "" || pfPassword == "" { - return fmt.Errorf("failed to initialize PingFederate services. Basic authentication username or password is empty") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrBasicAuthEmpty} } pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextBasicAuth, pingfederateGoClient.BasicAuth{ @@ -193,34 +231,34 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN): pfAccessToken, err := profiles.GetOptionValue(options.PingFederateAccessTokenAuthAccessTokenOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pfAccessToken == "" { - return fmt.Errorf("failed to initialize PingFederate services. Access token is empty") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrAccessTokenEmpty} } pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextAccessToken, pfAccessToken) case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS): pfClientID, err := profiles.GetOptionValue(options.PingFederateClientCredentialsAuthClientIDOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } pfClientSecret, err := profiles.GetOptionValue(options.PingFederateClientCredentialsAuthClientSecretOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } pfTokenUrl, err := profiles.GetOptionValue(options.PingFederateClientCredentialsAuthTokenURLOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } pfScopes, err := profiles.GetOptionValue(options.PingFederateClientCredentialsAuthScopesOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pfClientID == "" || pfClientSecret == "" || pfTokenUrl == "" { - return fmt.Errorf("failed to initialize PingFederate services. Client ID, Client Secret, or Token URL is empty") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrClientCredentialsEmpty} } pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextOAuth2, pingfederateGoClient.OAuthValues{ @@ -231,14 +269,14 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e Scopes: strings.Split(pfScopes, ","), }) default: - return fmt.Errorf("failed to initialize PingFederate services. unrecognized authentication type '%s'", authType) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s'", ErrPingFederateAuthType, authType)} } // Test PF API client with create Context Auth _, response, err := pingfederateApiClient.VersionAPI.GetVersion(pingfederateContext).Execute() ok, err := common.HandleClientResponse(response, err, "GetVersion", "pingfederate_client_init") if err != nil || !ok { - return fmt.Errorf("failed to initialize PingFederate Go Client. Check authentication type and credentials") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrPingFederateInit} } return nil @@ -246,15 +284,15 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e func initPingOneServices(ctx context.Context, cmdVersion string) (err error) { if err = initPingOneApiClient(ctx, cmdVersion); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if err = getPingOneExportEnvID(); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if err := validatePingOneExportEnvID(ctx); err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } pingoneContext = ctx @@ -267,20 +305,20 @@ func initPingFederateApiClient(tr *http.Transport, pingcliVersion string) (err e l.Debug().Msgf("Initializing PingFederate API client.") if tr == nil { - return fmt.Errorf("failed to initialize pingfederate API client. http transport is nil") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrHttpTransportNil} } httpsHost, err := profiles.GetOptionValue(options.PingFederateHTTPSHostOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } adminApiPath, err := profiles.GetOptionValue(options.PingFederateAdminAPIPathOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } xBypassExternalValidationHeader, err := profiles.GetOptionValue(options.PingFederateXBypassExternalValidationHeaderOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } // default adminApiPath to /pf-admin-api/v1 if not set @@ -289,7 +327,7 @@ func initPingFederateApiClient(tr *http.Transport, pingcliVersion string) (err e } if httpsHost == "" { - return fmt.Errorf(`failed to initialize pingfederate API client. the pingfederate https host configuration value is not set: configure this property via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingcli/config.yaml)`) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrHttpsHostEmpty} } userAgent := fmt.Sprintf("pingcli/%s", pingcliVersion) @@ -320,30 +358,28 @@ func initPingOneApiClient(ctx context.Context, pingcliVersion string) (err error l.Debug().Msgf("Initializing PingOne API client.") if ctx == nil { - return fmt.Errorf("failed to initialize pingone API client. context is nil") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrNilContext} } pingoneApiClientId, err = profiles.GetOptionValue(options.PingOneAuthenticationWorkerClientIDOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } clientSecret, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerClientSecretOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } environmentID, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerEnvironmentIDOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } regionCode, err := profiles.GetOptionValue(options.PingOneRegionCodeOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pingoneApiClientId == "" || clientSecret == "" || environmentID == "" || regionCode == "" { - return fmt.Errorf("failed to initialize pingone API client. one of worker client ID, worker client secret, " + - "pingone region code, and/or worker environment ID is empty. configure these properties via parameter flags, " + - "environment variables, or the tool's configuration file (default: $HOME/.pingcli/config.yaml)") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrPingOneConfigValuesEmpty} } userAgent := fmt.Sprintf("pingcli/%s", pingcliVersion) @@ -364,8 +400,7 @@ func initPingOneApiClient(ctx context.Context, pingcliVersion string) (err error pingoneApiClient, err = apiConfig.APIClient(ctx) if err != nil { - return fmt.Errorf("failed to initialize pingone API client. Check worker client ID, worker client secret,"+ - " worker environment ID, and pingone region code configuration values. %v", err) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %v", ErrPingOneInit, err)} } return nil @@ -376,18 +411,14 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved // Check if outputDir is empty if outputDir == "" { - return "", fmt.Errorf("failed to export services. The output directory is not set. Specify the output directory "+ - "via the '--%s' flag, '%s' environment variable, or key '%s' in the configuration file", - options.PlatformExportOutputDirectoryOption.CobraParamName, - options.PlatformExportOutputDirectoryOption.EnvVar, - options.PlatformExportOutputDirectoryOption.KoanfKey) + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrOutputDirectoryEmpty} } // Check if path is absolute. If not, make it absolute using the present working directory if !filepath.IsAbs(outputDir) { pwd, err := os.Getwd() if err != nil { - return "", fmt.Errorf("failed to get present working directory: %w", err) + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetPresentWorkingDirectory, err)} } outputDir = filepath.Join(pwd, outputDir) @@ -402,7 +433,7 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved err = os.MkdirAll(outputDir, os.FileMode(0700)) if err != nil { - return "", fmt.Errorf("failed to create output directory '%s': %s", outputDir, err.Error()) + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrCreateOutputDirectory, outputDir, err)} } output.Success(fmt.Sprintf("Output directory '%s' created", outputDir), nil) @@ -412,11 +443,11 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved // This can be changed with the --overwrite export parameter dirEntries, err := os.ReadDir(outputDir) if err != nil { - return "", fmt.Errorf("failed to read contents of output directory '%s': %w", outputDir, err) + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrReadOutputDirectory, outputDir, err)} } if len(dirEntries) > 0 { - return "", fmt.Errorf("output directory '%s' is not empty. Use --overwrite to overwrite existing export data", outputDir) + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrOutputDirectoryNotEmpty} } } @@ -426,16 +457,16 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved func getPingOneExportEnvID() (err error) { pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingOneEnvironmentIDOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pingoneExportEnvID == "" { pingoneExportEnvID, err = profiles.GetOptionValue(options.PingOneAuthenticationWorkerEnvironmentIDOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if pingoneExportEnvID == "" { - return fmt.Errorf("failed to determine pingone export environment ID") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrDeterminePingOneExportEnv} } output.Message("No target PingOne export environment ID specified. Defaulting export environment ID to the Worker App environment ID.", nil) @@ -449,24 +480,24 @@ func validatePingOneExportEnvID(ctx context.Context) (err error) { l.Debug().Msgf("Validating export environment ID...") if ctx == nil { - return fmt.Errorf("failed to validate pingone environment ID '%s'. context is nil", pingoneExportEnvID) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("failed to validate pingone environment ID '%s'. %w", pingoneExportEnvID, ErrNilContext)} } if pingoneApiClient == nil { - return fmt.Errorf("failed to validate pingone environment ID '%s'. apiClient is nil", pingoneExportEnvID) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("failed to validate pingone environment ID '%s'. %w", pingoneExportEnvID, ErrPingOneClientNil)} } environment, response, err := pingoneApiClient.ManagementAPIClient.EnvironmentsApi.ReadOneEnvironment(ctx, pingoneExportEnvID).Execute() ok, err := common.HandleClientResponse(response, err, "ReadOneEnvironment", "pingone_environment") if err != nil { - return err + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if !ok { - return fmt.Errorf("failed to validate pingone environment ID '%s'", pingoneExportEnvID) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s'", ErrValidatePingOneEnvId, pingoneExportEnvID)} } if environment == nil { - return fmt.Errorf("failed to validate pingone environment ID '%s'. environment matching ID does not exist", pingoneExportEnvID) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrPingOneEnvNotExist, pingoneExportEnvID)} } return nil @@ -504,7 +535,7 @@ func getExportableConnectors(exportServices *customtypes.ExportServices) (export func exportConnectors(exportableConnectors *[]connector.Exportable, exportFormat, outputDir string, overwriteExport bool) (err error) { if exportableConnectors == nil { - return fmt.Errorf("failed to export services. exportable connectors list is nil") + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: ErrConnectorListNil} } // Loop through user defined exportable connectors and export them @@ -513,7 +544,7 @@ func exportConnectors(exportableConnectors *[]connector.Exportable, exportFormat err := connector.Export(exportFormat, outputDir, overwriteExport) if err != nil { - return fmt.Errorf("failed to export '%s' service: %s", connector.ConnectorServiceName(), err.Error()) + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrExportService, connector.ConnectorServiceName(), err)} } } diff --git a/internal/commands/platform/export_internal_test.go b/internal/commands/platform/export_internal_test.go index 5d98e224..328900d6 100644 --- a/internal/commands/platform/export_internal_test.go +++ b/internal/commands/platform/export_internal_test.go @@ -3,357 +3,351 @@ package platform_internal import ( - "crypto/tls" - "net/http" + "fmt" "os" "regexp" "testing" "github.com/pingidentity/pingcli/internal/configuration/options" - "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" - pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + "github.com/stretchr/testify/require" ) -// Test RunInternalExport function -func TestRunInternalExport(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalExport(t.Context(), "v1.2.3") - testutils.CheckExpectedError(t, err, nil) - - // Check if there are terraform files in the export directory - outputDir, err := profiles.GetOptionValue(options.PlatformExportOutputDirectoryOption) - if err != nil { - t.Fatalf("profiles.GetOptionValue() error = %v", err) - } - - files, err := os.ReadDir(outputDir) - if err != nil { - t.Fatalf("os.ReadDir() error = %v", err) - } - - // Check the number of files in the directory - if len(files) == 0 { - t.Errorf("RunInternalExport() num files = %v, want non-zero", len(files)) - } - - // Check the file type is .tf - re := regexp.MustCompile(`^.*\.tf$`) - for _, file := range files { - if file.IsDir() { - t.Errorf("RunInternalExport() file = %v, want file", file) - } - - if !re.MatchString(file.Name()) { - t.Errorf("RunInternalExport() file = %v, want .tf file", file.Name()) - } - } +type testCase struct { + name string + services customtypes.ExportServices + checkTfFiles bool + nilContext bool + cACertPemFiles customtypes.StringSlice + pfAuthType customtypes.PingFederateAuthenticationType + pfAccessToken customtypes.String + pfClientId customtypes.String + pfClientSecret customtypes.String + pfTokenURL customtypes.String + outputDir customtypes.String + overwriteOutputDirLocation bool + changeWorkingDir bool + overwriteOnExport customtypes.Bool + expectedError error } -// Test RunInternalExport function fails with nil context -func TestRunInternalExportNilContext(t *testing.T) { - expectedErrorPattern := `^failed to run 'platform export' command\. context is nil$` - err := RunInternalExport(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test initPingFederateServices function -func TestInitPingFederateServices(t *testing.T) { +func Test_RunInternalExport(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := initPingFederateServices(t.Context(), "v1.2.3") - testutils.CheckExpectedError(t, err, nil) - - // make sure pf context is not nil - if pingfederateContext == nil { - t.Errorf("initPingFederateServices() pingfederateContext = %v, want non-nil", pingfederateContext) - } - - // check pf context has auth values included - if pingfederateContext.Value(pingfederateGoClient.ContextBasicAuth) == nil { - t.Errorf("initPingFederateServices() pingfederateContext.Value = %v, want non-nil", pingfederateContext.Value(pingfederateGoClient.ContextBasicAuth)) - } -} - -// Test initPingFederateServices function fails with nil context -func TestInitPingFederateServicesNilContext(t *testing.T) { - expectedErrorPattern := `^failed to initialize PingFederate services\. context is nil$` - err := initPingFederateServices(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test initPingOneServices function -func TestInitPingOneServices(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := initPingOneServices(t.Context(), "v1.2.3") - testutils.CheckExpectedError(t, err, nil) - - // make sure po context is not nil - if pingoneContext == nil { - t.Errorf("initPingOneServices() pingoneContext = %v, want non-nil", pingoneContext) - } -} - -// Test initPingFederateApiClient function -func TestInitPingFederateApiClient(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //#nosec G402 -- This is a test + goldenCACertPemFile := createGoldenCACertPemFile(t) + malformedCaCertPemFile := createMalformedCACertPemFile(t) + unwriteableDir := createUnwriteableDir(t) + unreadableDir := createUnreadableDir(t) + nonEmptyDir := createNonEmptyDir(t) + + testCases := []testCase{ + { + name: "Test Happy Path - All Services", + services: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + }, + checkTfFiles: true, + }, + { + name: "Test export with no services selected", + services: []string{}, + }, + // TODO - The PF Container used for testing needs to support Access Token Auth + // { + // name: "Test Happy Path - Access Token", + // services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + // checkTfFiles: true, + // pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN), + // }, + // TODO - The PF Container used for testing needs to support Client Credentials Auth + // { + // name: "Test Happy Path - PingFederate Client Credentials", + // services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + // checkTfFiles: true, + // pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS), + // }, + { + name: "Test with empty access token - PingFederate Access Token Auth", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN), + pfAccessToken: "", + expectedError: ErrAccessTokenEmpty, + }, + { + name: "Test with invalid access token - PingFederate Access Token Auth", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN), + pfAccessToken: "invalid-token", + expectedError: ErrPingFederateInit, + }, + { + name: "Test empty client credentials - PingFederate Client Credentials Auth", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS), + pfClientId: "", + expectedError: ErrClientCredentialsEmpty, + }, + { + name: "Test invalid client credentials - PingFederate Client Credentials Auth", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + pfAuthType: customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS), + pfClientId: "invalid-client-id", + pfClientSecret: "invalid-client-secret", + pfTokenURL: "http://localhost:9031/pf-admin-api/v1/oauth/token", + expectedError: ErrPingFederateInit, + }, + { + name: "Test Happy Path With PEM file - PingFederate", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + checkTfFiles: true, + cACertPemFiles: *goldenCACertPemFile, + }, + { + name: "Test with nil context", + nilContext: true, + expectedError: ErrNilContext, + }, + { + name: "Test with invalid PEM filepath - PingFederate", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + cACertPemFiles: []string{"/invalid/file/path.pem"}, + expectedError: ErrReadCaCertPemFile, + }, + { + name: "Test with malformed PEM file - PingFederate", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + cACertPemFiles: *malformedCaCertPemFile, + expectedError: ErrAppendToCertPool, + }, + { + name: "Test invalid PingFederate Auth Type", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + pfAuthType: "invalid-auth-type", + expectedError: ErrPingFederateAuthType, + }, + { + name: "Test empty output directory", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: "", + overwriteOutputDirLocation: true, + expectedError: ErrOutputDirectoryEmpty, + }, + { + name: "Test non-writable output directory", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: customtypes.String(unwriteableDir), + overwriteOutputDirLocation: true, + expectedError: ErrCreateOutputDirectory, + }, + { + name: "Test Happy Path with relative output directory", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: "relative-dir", + overwriteOutputDirLocation: true, + changeWorkingDir: true, + checkTfFiles: true, + }, + { + name: "Test unreable output directory", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: customtypes.String(unreadableDir), + overwriteOutputDirLocation: true, + expectedError: ErrReadOutputDirectory, + }, + { + name: "Test non-empty output directory without overwrite", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: customtypes.String(nonEmptyDir), + overwriteOutputDirLocation: true, + expectedError: ErrOutputDirectoryNotEmpty, + }, + { + name: "Test non-empty output directory with overwrite", + services: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT}, + outputDir: customtypes.String(nonEmptyDir), + overwriteOutputDirLocation: true, + overwriteOnExport: true, + checkTfFiles: true, }, } - err := initPingFederateApiClient(tr, "v1.2.3") - testutils.CheckExpectedError(t, err, nil) - - // make sure pf client is not nil - if pingfederateApiClient == nil { - t.Errorf("initPingFederateApiClient() pingfederateApiClient = %v, want non-nil", pingfederateApiClient) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + setupTestCase(t, tc) + + ctx := t.Context() + if tc.nilContext { + ctx = nil + } + + err := RunInternalExport(ctx, "v1.2.3") + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + if tc.checkTfFiles { + outputDir, err := profiles.GetOptionValue(options.PlatformExportOutputDirectoryOption) + require.NoError(t, err) + + files, err := os.ReadDir(outputDir) + require.NoError(t, err) + require.NotZero(t, len(files), "Expected non-zero number of files in output directory") + + re := regexp.MustCompile(`^.*\.tf$`) + for _, file := range files { + require.False(t, file.IsDir(), "Expected file, got directory: %v", file.Name()) + require.True(t, re.MatchString(file.Name()), "Expected .tf file, got: %v", file.Name()) + } + } + }) } } -// Test initPingFederateApiClient function fails with nil transport -func TestInitPingFederateApiClientNilTransport(t *testing.T) { - expectedErrorPattern := `^failed to initialize pingfederate API client\. http transport is nil$` - err := initPingFederateApiClient(nil, "v1.2.3") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func setupTestCase(t *testing.T, tc testCase) { + t.Helper() -// Test initPingOneApiClient function -func TestInitPingOneApiClient(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := initPingOneApiClient(t.Context(), "v1.2.3") - testutils.CheckExpectedError(t, err, nil) - - // make sure po client is not nil - if pingoneApiClient == nil { - t.Errorf("initPingOneApiClient() pingoneApiClient = %v, want non-nil", pingoneApiClient) + if tc.services != nil { + options.PlatformExportServiceOption.Flag.Changed = true + options.PlatformExportServiceOption.CobraParamValue = &tc.services } -} - -// Test initPingOneApiClient function fails with nil context -func TestInitPingOneApiClientNilContext(t *testing.T) { - expectedErrorPattern := `^failed to initialize pingone API client\. context is nil$` - err := initPingOneApiClient(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} -// Test createOrValidateOutputDir function with non-existent directory -func TestCreateOrValidateOutputDir(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - outputDir := os.TempDir() + "/nonexistantdir" - - _, err := createOrValidateOutputDir(outputDir, false) - testutils.CheckExpectedError(t, err, nil) -} - -// Test createOrValidateOutputDir function with existent directory -func TestCreateOrValidateOutputDirExistentDir(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - outputDir := t.TempDir() - - _, err := createOrValidateOutputDir(outputDir, false) - testutils.CheckExpectedError(t, err, nil) -} - -// Test createOrValidateOutputDir function with existent directory and overwrite flag -// when there is a file in the directory -func TestCreateOrValidateOutputDirExistentDirWithFile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - outputDir := t.TempDir() - - file, err := os.Create(outputDir + "/file") //#nosec G304 -- This is a test - if err != nil { - t.Fatalf("os.Create() error = %v", err) + if tc.cACertPemFiles != nil { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "cACertPemFiles is only applicable to PingFederate service export") + options.PingFederateCACertificatePemFilesOption.Flag.Changed = true + options.PingFederateCACertificatePemFilesOption.CobraParamValue = &tc.cACertPemFiles } - err = file.Close() - if err != nil { - t.Fatalf("file.Close() error = %v", err) - } - - _, err = createOrValidateOutputDir(outputDir, true) - testutils.CheckExpectedError(t, err, nil) -} - -// Test createOrValidateOutputDir function fails with existent directory and no overwrite flag -// when there is a file in the directory -func TestCreateOrValidateOutputDirExistentDirWithFileNoOverwrite(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - outputDir := t.TempDir() - file, err := os.Create(outputDir + "/file") //#nosec G304 -- this is a test - if err != nil { - t.Fatalf("os.Create() error = %v", err) + if tc.pfAuthType != "" { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "pfAuthType is only applicable to PingFederate service export") + options.PingFederateAuthenticationTypeOption.Flag.Changed = true + options.PingFederateAuthenticationTypeOption.CobraParamValue = &tc.pfAuthType } - err = file.Close() - if err != nil { - t.Fatalf("file.Close() error = %v", err) - } - - expectedErrorPattern := `^output directory '.*' is not empty\. Use --overwrite to overwrite existing export data$` - _, err = createOrValidateOutputDir(outputDir, false) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test getPingOneExportEnvID function -func TestGetPingOneExportEnvID(t *testing.T) { - testutils_koanf.InitKoanfs(t) - if err := getPingOneExportEnvID(); err != nil { - t.Errorf("getPingOneExportEnvID() error = %v, want nil", err) + if tc.pfAccessToken != "" { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "pfAccessToken is only applicable to PingFederate service export") + options.PingFederateAccessTokenAuthAccessTokenOption.Flag.Changed = true + options.PingFederateAccessTokenAuthAccessTokenOption.CobraParamValue = &tc.pfAccessToken } - // Check pingoneExportEnvID is not empty - if pingoneExportEnvID == "" { - t.Errorf("getPingOneExportEnvID() pingoneExportEnvID = %v, want non-empty", pingoneExportEnvID) + if tc.pfClientId != "" { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "pfClientId is only applicable to PingFederate service export") + options.PingFederateClientCredentialsAuthClientIDOption.Flag.Changed = true + options.PingFederateClientCredentialsAuthClientIDOption.CobraParamValue = &tc.pfClientId } -} - -// Test validatePingOneExportEnvID function -func TestValidatePingOneExportEnvID(t *testing.T) { - testutils_koanf.InitKoanfs(t) - if err := initPingOneApiClient(t.Context(), "v1.2.3"); err != nil { - t.Errorf("initPingOneApiClient() error = %v, want nil", err) + if tc.pfClientSecret != "" { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "pfClientSecret is only applicable to PingFederate service export") + options.PingFederateClientCredentialsAuthClientSecretOption.Flag.Changed = true + options.PingFederateClientCredentialsAuthClientSecretOption.CobraParamValue = &tc.pfClientSecret } - if err := getPingOneExportEnvID(); err != nil { - t.Errorf("getPingOneExportEnvID() error = %v, want nil", err) + if tc.pfTokenURL != "" { + require.Contains(t, tc.services, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, "pfTokenURL is only applicable to PingFederate service export") + options.PingFederateClientCredentialsAuthTokenURLOption.Flag.Changed = true + options.PingFederateClientCredentialsAuthTokenURLOption.CobraParamValue = &tc.pfTokenURL } - err := validatePingOneExportEnvID(t.Context()) - testutils.CheckExpectedError(t, err, nil) -} - -// Test validatePingOneExportEnvID function fails with nil context -func TestValidatePingOneExportEnvIDNilContext(t *testing.T) { - expectedErrorPattern := `^failed to validate pingone environment ID '.*'\. context is nil$` - err := validatePingOneExportEnvID(nil) //nolint:staticcheck // SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test getExportableConnectors function -func TestGetExportableConnectors(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - es := new(customtypes.ExportServices) - err := es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - if err != nil { - t.Fatalf("ms.Set() error = %v", err) + if tc.overwriteOutputDirLocation { + options.PlatformExportOutputDirectoryOption.Flag.Changed = true + options.PlatformExportOutputDirectoryOption.CobraParamValue = &tc.outputDir } - expectedConnectors := len(es.GetServices()) + if tc.changeWorkingDir { + originalWd, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(t.TempDir()) + require.NoError(t, err) - exportableConnectors := getExportableConnectors(es) - if len(*exportableConnectors) == 0 { - t.Errorf("getExportableConnectors() exportableConnectors = %v, want non-empty", exportableConnectors) + t.Cleanup(func() { + require.NoError(t, os.Chdir(originalWd)) + }) } - if len(*exportableConnectors) != expectedConnectors { - t.Errorf("getExportableConnectors() exportableConnectors = %v, want %v", len(*exportableConnectors), expectedConnectors) + if tc.overwriteOnExport { + options.PlatformExportOverwriteOption.Flag.Changed = true + options.PlatformExportOverwriteOption.CobraParamValue = &tc.overwriteOnExport } } -// Test getExportableConnectors function with nil MultiService -func TestGetExportableConnectorsNilMultiService(t *testing.T) { - exportableConnectors := getExportableConnectors(nil) +func createCaCertPemFile(t *testing.T, certStr string) *customtypes.StringSlice { + t.Helper() - expectedConnectors := 0 - if len(*exportableConnectors) != expectedConnectors { - t.Errorf("getExportableConnectors() exportableConnectors = %v, want %v", len(*exportableConnectors), expectedConnectors) - } -} + testCACertPemFiles := new(customtypes.StringSlice) -// Test exportConnectors function -func TestExportConnectors(t *testing.T) { - testutils_koanf.InitKoanfs(t) + caCertFile, err := os.CreateTemp(t.TempDir(), "caCert-*.pem") + require.NoError(t, err) - err := initPingOneServices(t.Context(), "v1.2.3") - if err != nil { - t.Fatalf("initPingOneServices() error = %v", err) - } + _, err = caCertFile.WriteString(certStr) + require.NoError(t, err) - es := new(customtypes.ExportServices) - err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - if err != nil { - t.Fatalf("ms.Set() error = %v", err) - } + err = caCertFile.Close() + require.NoError(t, err) - exportableConnectors := getExportableConnectors(es) + err = testCACertPemFiles.Set(caCertFile.Name()) + require.NoError(t, err) - err = exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) - testutils.CheckExpectedError(t, err, nil) + return testCACertPemFiles } -// Test exportConnectors function with nil exportable connectors -func TestExportConnectorsNilExportableConnectors(t *testing.T) { - err := exportConnectors(nil, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) +func createGoldenCACertPemFile(t *testing.T) *customtypes.StringSlice { + t.Helper() - expectedErrorPattern := `^failed to export services\. exportable connectors list is nil$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + certStr, err := testutils.CreateX509Certificate() + require.NoError(t, err) + + return createCaCertPemFile(t, certStr) } -// Test exportConnectors function with empty exportable connectors -func TestExportConnectorsEmptyExportableConnectors(t *testing.T) { - exportableConnectors := &[]connector.Exportable{} +func createMalformedCACertPemFile(t *testing.T) *customtypes.StringSlice { + t.Helper() - err := exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) - testutils.CheckExpectedError(t, err, nil) + return createCaCertPemFile(t, "malformed-cert") } -// Test exportConnectors function with invalid export format -func TestExportConnectorsInvalidExportFormat(t *testing.T) { - testutils_koanf.InitKoanfs(t) +func createUnwriteableDir(t *testing.T) string { + t.Helper() - err := initPingOneServices(t.Context(), "v1.2.3") - if err != nil { - t.Fatalf("initPingOneServices() error = %v", err) - } + dir := t.TempDir() + err := os.Chmod(dir, 0444) // read-only + require.NoError(t, err) - es := new(customtypes.ExportServices) - err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - if err != nil { - t.Fatalf("ms.Set() error = %v", err) - } + return fmt.Sprintf("%s/subdir", dir) +} - exportableConnectors := getExportableConnectors(es) +func createUnreadableDir(t *testing.T) string { + t.Helper() - err = exportConnectors(exportableConnectors, "invalid", t.TempDir(), true) + dir := t.TempDir() + err := os.Chmod(dir, 0000) // no permissions + require.NoError(t, err) - expectedErrorPattern := `^failed to export '.*' service: unrecognized export format ".*"\. Must be one of: .*$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + return dir } -// Test exportConnectors function with invalid output directory -func TestExportConnectorsInvalidOutputDir(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := initPingOneServices(t.Context(), "v1.2.3") - if err != nil { - t.Fatalf("initPingOneServices() error = %v", err) - } +func createNonEmptyDir(t *testing.T) string { + t.Helper() - es := new(customtypes.ExportServices) - err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - if err != nil { - t.Fatalf("ms.Set() error = %v", err) - } - - exportableConnectors := getExportableConnectors(es) + dir := t.TempDir() + file, err := os.CreateTemp(dir, "file-*.tf") + require.NoError(t, err) - err = exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, "/invalid", true) + err = file.Close() + require.NoError(t, err) - expectedErrorPattern := `^failed to export '.*' service: failed to create export file ".*". err: open .*: no such file or directory$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + return dir } diff --git a/internal/commands/plugin/add_internal.go b/internal/commands/plugin/add_internal.go index 44bcabf4..3b7488bc 100644 --- a/internal/commands/plugin/add_internal.go +++ b/internal/commands/plugin/add_internal.go @@ -3,31 +3,37 @@ package plugin_internal import ( + "errors" "fmt" "os/exec" "strings" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + addErrorPrefix = "failed to add plugin" + ErrPluginAlreadyExists = errors.New("plugin executable already exists in configuration") + ErrPluginNotFound = errors.New("plugin executable not found in system PATH") +) + func RunInternalPluginAdd(pluginExecutable string) error { if pluginExecutable == "" { - return fmt.Errorf("plugin executable is required") + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: ErrPluginNameEmpty} } - // Check if plugin executable is in PATH _, err := exec.LookPath(pluginExecutable) if err != nil { - // exec error contains executable name and $PATH error message - return fmt.Errorf("failed to add plugin: %w", err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %v", ErrPluginNotFound, err)} } err = addPluginExecutable(pluginExecutable) if err != nil { - return fmt.Errorf("failed to add plugin: %w", err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } output.Success(fmt.Sprintf("Plugin '%s' added.", pluginExecutable), nil) @@ -38,47 +44,50 @@ func RunInternalPluginAdd(pluginExecutable string) error { func addPluginExecutable(pluginExecutable string) error { pName, err := readPluginAddProfileName() if err != nil { - return fmt.Errorf("failed to read profile name: %w", err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return fmt.Errorf("failed to get koanf config: %w", err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return fmt.Errorf("failed to get profile: %w", err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } - existingPluginExectuables, _, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, pName) + existingPluginExectuables, ok, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, pName) if err != nil { - return fmt.Errorf("failed to get existing plugin configuration from profile '%s': %w", pName, err) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} + } + if !ok { + existingPluginExectuables = "" } strSlice := new(customtypes.StringSlice) if err = strSlice.Set(existingPluginExectuables); err != nil { - return err + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } // Check if the plugin is already added for _, existingPlugin := range strSlice.StringSlice() { if strings.EqualFold(existingPlugin, pluginExecutable) { - return fmt.Errorf("plugin '%s' is already added to profile '%s'", pluginExecutable, pName) + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrPluginAlreadyExists, pluginExecutable)} } } if err = strSlice.Set(pluginExecutable); err != nil { - return err + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } err = subKoanf.Set(options.PluginExecutablesOption.KoanfKey, strSlice) if err != nil { - return err + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { - return err + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } return nil @@ -92,11 +101,11 @@ func readPluginAddProfileName() (pName string, err error) { } if err != nil { - return pName, err + return pName, &errs.PingCLIError{Prefix: addErrorPrefix, Err: err} } if pName == "" { - return pName, fmt.Errorf("unable to determine active profile") + return pName, &errs.PingCLIError{Prefix: addErrorPrefix, Err: ErrUndeterminedProfile} } return pName, nil diff --git a/internal/commands/plugin/add_internal_test.go b/internal/commands/plugin/add_internal_test.go index 59ca3d79..65655d6b 100644 --- a/internal/commands/plugin/add_internal_test.go +++ b/internal/commands/plugin/add_internal_test.go @@ -6,54 +6,74 @@ import ( "os" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test RunInternalPluginAdd function func Test_RunInternalPluginAdd(t *testing.T) { testutils_koanf.InitKoanfs(t) - // Create a temporary $PATH for a test plugin + goldenPluginFileName := createGoldenPlugin(t) + + testCases := []struct { + name string + pluginName string + expectedError error + }{ + { + name: "Happy path - Add plugin", + pluginName: goldenPluginFileName, + }, + { + name: "Test non-existent plugin", + pluginName: "non-existent-plugin", + expectedError: ErrPluginNotFound, + }, + { + name: "Test empty plugin name", + pluginName: "", + expectedError: ErrPluginNameEmpty, + }, + // TODO - In testutils_koanf.InitKoanfs(t), create a valid plugin executable and add it to the config and path similar to below + // { + // name: "Test adding a plugin that already exists", + // pluginName: "existing-plugin", + // expectedError: ErrPluginAlreadyExists, + // }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := RunInternalPluginAdd(tc.pluginName) + + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} + +func createGoldenPlugin(t *testing.T) string { pathDir := t.TempDir() t.Setenv("PATH", pathDir) testPlugin, err := os.CreateTemp(pathDir, "test-plugin-*.sh") - if err != nil { - t.Fatalf("Failed to create temporary plugin file: %v", err) - } - - defer func() { - err = os.Remove(testPlugin.Name()) - if err != nil { - t.Fatalf("Failed to remove temporary plugin file: %v", err) - } - }() + require.NoError(t, err) _, err = testPlugin.WriteString("#!/usr/bin/env sh\necho \"Hello, world!\"\nexit 0\n") - if err != nil { - t.Fatalf("Failed to write to temporary plugin file: %v", err) - } + require.NoError(t, err) err = testPlugin.Chmod(0755) - if err != nil { - t.Fatalf("Failed to set permissions on temporary plugin file: %v", err) - } + require.NoError(t, err) err = testPlugin.Close() - if err != nil { - t.Fatalf("Failed to close temporary plugin file: %v", err) - } - - err = RunInternalPluginAdd(testPlugin.Name()) - if err != nil { - t.Errorf("RunInternalPluginAdd returned error: %v", err) - } -} + require.NoError(t, err) -// Test RunInternalPluginAdd function fails with non-existent plugin -func Test_RunInternalPluginAdd_NonExistentPlugin(t *testing.T) { - expectedErrorPattern := `^failed to add plugin: exec: .*: executable file not found in \$PATH$` - err := RunInternalPluginAdd("non-existent-plugin") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + return testPlugin.Name() } diff --git a/internal/commands/plugin/common_errors.go b/internal/commands/plugin/common_errors.go new file mode 100644 index 00000000..a35e08cc --- /dev/null +++ b/internal/commands/plugin/common_errors.go @@ -0,0 +1,9 @@ +package plugin_internal + +import "errors" + +var ( + ErrPluginNameEmpty = errors.New("plugin executable name is empty") + ErrReadPluginNamesConfig = errors.New("failed to read configured plugin executable names") + ErrUndeterminedProfile = errors.New("unable to determine configuration profile") +) diff --git a/internal/commands/plugin/list_internal.go b/internal/commands/plugin/list_internal.go index a4f90fb4..66b69e40 100644 --- a/internal/commands/plugin/list_internal.go +++ b/internal/commands/plugin/list_internal.go @@ -7,14 +7,23 @@ import ( "strings" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + listErrorPrefix = "failed to list plugins" +) + func RunInternalPluginList() error { - existingPluginExectuables, _, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, "") + existingPluginExectuables, ok, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, "") if err != nil { - return fmt.Errorf("failed to get existing plugin configuration: %w", err) + return &errs.PingCLIError{Prefix: listErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} + } + if !ok { + output.Message("No plugins configured.", nil) + return nil } listStr := "Plugins:\n" diff --git a/internal/commands/plugin/list_internal_test.go b/internal/commands/plugin/list_internal_test.go index 3b206529..ef6ef0d5 100644 --- a/internal/commands/plugin/list_internal_test.go +++ b/internal/commands/plugin/list_internal_test.go @@ -6,14 +6,34 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test RunInternalPluginList function func Test_RunInternalPluginList(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := RunInternalPluginList() - if err != nil { - t.Errorf("RunInternalPluginList returned error: %v", err) + testCases := []struct { + name string + expectedError error + }{ + { + name: "Happy path - List plugins", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := RunInternalPluginList() + + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + }) } } diff --git a/internal/commands/plugin/remove_internal.go b/internal/commands/plugin/remove_internal.go index d7cd9e7e..ea9a3033 100644 --- a/internal/commands/plugin/remove_internal.go +++ b/internal/commands/plugin/remove_internal.go @@ -7,22 +7,29 @@ import ( "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + removeErrorPrefix = "failed to remove plugin" +) + func RunInternalPluginRemove(pluginExecutable string) error { if pluginExecutable == "" { - return fmt.Errorf("plugin executable is required") + return &errs.PingCLIError{Prefix: removeErrorPrefix, Err: ErrPluginNameEmpty} } ok, err := removePluginExecutable(pluginExecutable) if err != nil { - return fmt.Errorf("failed to remove plugin: %w", err) + return &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } if ok { output.Success(fmt.Sprintf("Plugin '%s' removed.", pluginExecutable), nil) + } else { + output.Warn(fmt.Sprintf("Plugin '%s' not found in configuration and was not removed.", pluginExecutable), nil) } return nil @@ -31,46 +38,44 @@ func RunInternalPluginRemove(pluginExecutable string) error { func removePluginExecutable(pluginExecutable string) (bool, error) { pName, err := readPluginRemoveProfileName() if err != nil { - return false, fmt.Errorf("failed to read profile name: %w", err) + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return false, fmt.Errorf("failed to get koanf config: %w", err) + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return false, fmt.Errorf("failed to get profile: %w", err) + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } existingPluginExectuables, _, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, pName) if err != nil { - return false, fmt.Errorf("failed to get existing plugin configuration from profile '%s': %w", pName, err) + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} } strSlice := new(customtypes.StringSlice) if err = strSlice.Set(existingPluginExectuables); err != nil { - return false, err + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } removed, err := strSlice.Remove(pluginExecutable) if err != nil { - return false, err + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } if !removed { - output.Warn(fmt.Sprintf("plugin executable '%s' not found in profile '%s' plugins", pluginExecutable, pName), nil) - return false, nil } err = subKoanf.Set(options.PluginExecutablesOption.KoanfKey, strSlice) if err != nil { - return false, err + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } if err = koanfConfig.SaveProfile(pName, subKoanf); err != nil { - return false, err + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } return true, nil @@ -84,11 +89,11 @@ func readPluginRemoveProfileName() (pName string, err error) { } if err != nil { - return pName, err + return pName, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: err} } if pName == "" { - return pName, fmt.Errorf("unable to determine active profile") + return pName, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: ErrUndeterminedProfile} } return pName, nil diff --git a/internal/commands/plugin/remove_internal_test.go b/internal/commands/plugin/remove_internal_test.go index 98baafcc..3697932b 100644 --- a/internal/commands/plugin/remove_internal_test.go +++ b/internal/commands/plugin/remove_internal_test.go @@ -3,63 +3,52 @@ package plugin_internal import ( - "os" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test RunInternalPluginRemove function func Test_RunInternalPluginRemove(t *testing.T) { testutils_koanf.InitKoanfs(t) - // Create a temporary $PATH for a test plugin - pathDir := t.TempDir() - t.Setenv("PATH", pathDir) - - testPlugin, err := os.CreateTemp(pathDir, "test-plugin-*.sh") - if err != nil { - t.Fatalf("Failed to create temporary plugin file: %v", err) + goldenPluginFileName := createGoldenPlugin(t) + + testCases := []struct { + name string + pluginName string + createPluginFirst bool + expectedError error + }{ + { + name: "Happy path - List plugins", + pluginName: goldenPluginFileName, + createPluginFirst: true, + }, + { + name: "Test non-existent plugin", + pluginName: "non-existent-plugin", + }, } - defer func() { - err = os.Remove(testPlugin.Name()) - if err != nil { - t.Fatalf("Failed to remove temporary plugin file: %v", err) - } - }() - - _, err = testPlugin.WriteString("#!/usr/bin/env sh\necho \"Hello, world!\"\nexit 0\n") - if err != nil { - t.Fatalf("Failed to write to temporary plugin file: %v", err) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - err = testPlugin.Chmod(0755) - if err != nil { - t.Fatalf("Failed to set permissions on temporary plugin file: %v", err) - } + if tc.createPluginFirst { + err := RunInternalPluginAdd(tc.pluginName) + require.NoError(t, err) + } - err = testPlugin.Close() - if err != nil { - t.Fatalf("Failed to close temporary plugin file: %v", err) - } + err := RunInternalPluginRemove(tc.pluginName) - err = RunInternalPluginAdd(testPlugin.Name()) - if err != nil { - t.Errorf("RunInternalPluginAdd returned error: %v", err) + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + }) } - - err = RunInternalPluginRemove(testPlugin.Name()) - if err != nil { - t.Errorf("RunInternalPluginRemove returned error: %v", err) - } -} - -// Test RunInternalPluginRemove function succeeds with non-existent plugin -func Test_RunInternalPluginRemove_NonExistentPlugin(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := RunInternalPluginRemove("non-existent-plugin") - testutils.CheckExpectedError(t, err, nil) } diff --git a/internal/errs/pingcli_error.go b/internal/errs/pingcli_error.go new file mode 100644 index 00000000..650b1edf --- /dev/null +++ b/internal/errs/pingcli_error.go @@ -0,0 +1,28 @@ +package errs + +import ( + "errors" + "fmt" + "strings" +) + +type PingCLIError struct { + Err error + Prefix string +} + +func (e *PingCLIError) Error() string { + // Check if the wrapped error is also a PingCLIError to avoid redundant prefixes + var err *PingCLIError + if errors.As(e.Err, &err) { + if strings.EqualFold(err.Prefix, e.Prefix) { + return err.Error() + } + } + + return fmt.Sprintf("%s: %s", e.Prefix, e.Err.Error()) +} + +func (e *PingCLIError) Unwrap() error { + return e.Err +} diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 72767c7d..128e99ff 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -14,6 +14,7 @@ import ( "github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/v2" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" ) var ( @@ -34,6 +35,7 @@ var ( ErrKoanfMerge = errors.New("failed to merge koanf configuration") ErrDeleteActiveProfile = errors.New("the active profile cannot be deleted") ErrSetKoanfKeyDefaultValue = errors.New("failed to set koanf key default value") + koanfErrorPrefix = "profile configuration error" ) type KoanfConfig struct { @@ -41,26 +43,6 @@ type KoanfConfig struct { configFilePath *string } -type KoanfError struct { - Err error -} - -func (e *KoanfError) Error() string { - var err *KoanfError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("profile configuration error: %s", e.Err.Error()) -} - -func (e *KoanfError) Unwrap() error { - var err *KoanfError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - func NewKoanfConfig(cnfFilePath string) *KoanfConfig { k = &KoanfConfig{ koanfInstance: koanf.New("."), @@ -71,8 +53,8 @@ func NewKoanfConfig(cnfFilePath string) *KoanfConfig { } func GetKoanfConfig() (*KoanfConfig, error) { - if k == nil || k.KoanfInstance == nil { - return nil, &KoanfError{Err: ErrKoanfNotInitialized} + if k == nil || k.koanfInstance == nil { + return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrKoanfNotInitialized} } return k, nil } @@ -111,7 +93,7 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo mainKoanfInstance, err := GetKoanfConfig() if err != nil { - return "", false, &KoanfError{Err: err} + return "", false, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } // Case 1: Koanf Key is the ActiveProfile Key, get value from main koanf instance @@ -127,12 +109,12 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo if pName == "" { pName, err = GetOptionValue(options.RootProfileOption) if err != nil { - return "", false, &KoanfError{Err: err} + return "", false, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } if pName == "" { pName, err = GetOptionValue(options.RootActiveProfileOption) if err != nil { - return "", false, &KoanfError{Err: err} + return "", false, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } } } @@ -140,7 +122,7 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo // Get the sub koanf instance for the profile subKoanf, err := mainKoanfInstance.GetProfileKoanf(pName) if err != nil { - return "", false, &KoanfError{Err: err} + return "", false, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } kValue = subKoanf.Get(opt.KoanfKey) @@ -196,16 +178,16 @@ func (k KoanfConfig) ProfileNames() (profileNames []string) { // The profile name cannot be empty func (k KoanfConfig) ValidateProfileNameFormat(pName string) (err error) { if pName == "" { - return &KoanfError{Err: ErrProfileNameEmpty} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameEmpty} } re := regexp.MustCompile(`^[a-zA-Z0-9\_\-]+$`) if !re.MatchString(pName) { - return &KoanfError{Err: ErrProfileNameFormat} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameFormat} } if strings.EqualFold(pName, options.RootActiveProfileOption.KoanfKey) { - return &KoanfError{Err: ErrProfileNameSameAsActiveProfileKey} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameSameAsActiveProfileKey} } return nil @@ -213,16 +195,16 @@ func (k KoanfConfig) ValidateProfileNameFormat(pName string) (err error) { func (k KoanfConfig) ChangeActiveProfile(pName string) (err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } err = k.KoanfInstance().Set(options.RootActiveProfileOption.KoanfKey, pName) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetActiveProfile, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetActiveProfile, err)} } if err = k.WriteFile(); err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } return nil @@ -231,7 +213,7 @@ func (k KoanfConfig) ChangeActiveProfile(pName string) (err error) { // The profile name must exist func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { if pName == "" { - return &KoanfError{Err: ErrProfileNameEmpty} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameEmpty} } pNames := k.ProfileNames() @@ -239,7 +221,7 @@ func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { if !slices.ContainsFunc(pNames, func(n string) bool { return n == pName }) { - return &KoanfError{Err: ErrProfileNameNotExist} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameNotExist} } return nil @@ -249,7 +231,7 @@ func (k KoanfConfig) ValidateExistingProfileName(pName string) (err error) { // The new profile name must be unique func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { if err = k.ValidateProfileNameFormat(pName); err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } pNames := k.ProfileNames() @@ -257,7 +239,7 @@ func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { if slices.ContainsFunc(pNames, func(n string) bool { return n == pName }) { - return &KoanfError{Err: ErrProfileNameAlreadyExists} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrProfileNameAlreadyExists} } return nil @@ -265,14 +247,14 @@ func (k KoanfConfig) ValidateNewProfileName(pName string) (err error) { func (k KoanfConfig) GetProfileKoanf(pName string) (subKoanf *koanf.Koanf, err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return nil, &KoanfError{Err: err} + return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } // Create a new koanf instance for the profile subKoanf = koanf.New(".") err = subKoanf.Load(confmap.Provider(k.KoanfInstance().Cut(pName).All(), "."), nil) if err != nil { - return nil, &KoanfError{Err: fmt.Errorf("%w: %v", ErrKoanfProfileExtractAndLoad, err)} + return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrKoanfProfileExtractAndLoad, err)} } return subKoanf, nil @@ -293,7 +275,7 @@ func (k KoanfConfig) WriteFile() (err error) { if strings.ToLower(fullKoanfKeyValue) == key { err = k.KoanfInstance().Set(fullKoanfKeyValue, val) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyValue, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyValue, err)} } k.KoanfInstance().Delete(key) } @@ -309,12 +291,12 @@ func (k KoanfConfig) WriteFile() (err error) { encodedConfig, err := k.KoanfInstance().Marshal(yaml.Parser()) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrMarshalKoanf, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrMarshalKoanf, err)} } err = os.WriteFile(k.GetKoanfConfigFile(), encodedConfig, 0600) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrWriteKoanfFile, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrWriteKoanfFile, err)} } return nil @@ -323,12 +305,12 @@ func (k KoanfConfig) WriteFile() (err error) { func (k KoanfConfig) SaveProfile(pName string, subKoanf *koanf.Koanf) (err error) { err = k.KoanfInstance().MergeAt(subKoanf, pName) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrKoanfMerge, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrKoanfMerge, err)} } err = k.WriteFile() if err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } return nil @@ -336,16 +318,16 @@ func (k KoanfConfig) SaveProfile(pName string, subKoanf *koanf.Koanf) (err error func (k KoanfConfig) DeleteProfile(pName string) (err error) { if err = k.ValidateExistingProfileName(pName); err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } activeProfileName, err := GetOptionValue(options.RootActiveProfileOption) if err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } if activeProfileName == pName { - return &KoanfError{Err: ErrDeleteActiveProfile} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrDeleteActiveProfile} } // Delete the profile from the main koanf @@ -353,7 +335,7 @@ func (k KoanfConfig) DeleteProfile(pName string) (err error) { err = k.WriteFile() if err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } return nil @@ -364,7 +346,7 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { for _, pName := range k.ProfileNames() { subKoanf, err := k.GetProfileKoanf(pName) if err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } for _, opt := range options.Options() { @@ -375,13 +357,13 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { if !subKoanf.Exists(opt.KoanfKey) { err = subKoanf.Set(opt.KoanfKey, opt.DefaultValue) if err != nil { - return &KoanfError{Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyDefaultValue, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyDefaultValue, err)} } } } err = k.SaveProfile(pName, subKoanf) if err != nil { - return &KoanfError{Err: err} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: err} } } @@ -412,7 +394,7 @@ func GetOptionValue(opt options.Option) (string, error) { // This is an error, as it means the option is not configured internally to contain one of the 4 values above. // This should never happen, as all options should at least have a default value. - return "", &KoanfError{Err: ErrNoOptionValue} + return "", &errs.PingCLIError{Err: ErrNoOptionValue} } func MaskValue(value any) string { diff --git a/internal/testing/testutils_koanf/koanf_utils.go b/internal/testing/testutils_koanf/koanf_utils.go index a4e0ef56..bf007ade 100644 --- a/internal/testing/testutils_koanf/koanf_utils.go +++ b/internal/testing/testutils_koanf/koanf_utils.go @@ -114,25 +114,27 @@ func CreateConfigFile(t *testing.T) string { return configFilePath } -func configureMainKoanf(t *testing.T) { +func configureMainKoanf(t *testing.T) *profiles.KoanfConfig { t.Helper() configFilePath = CreateConfigFile(t) - mainKoanf := profiles.NewKoanfConfig(configFilePath) + koanfConfig := profiles.NewKoanfConfig(configFilePath) - if err := mainKoanf.KoanfInstance().Load(file.Provider(configFilePath), yaml.Parser()); err != nil { + if err := koanfConfig.KoanfInstance().Load(file.Provider(configFilePath), yaml.Parser()); err != nil { t.Fatalf("Failed to load configuration from file '%s': %v", configFilePath, err) } + + return koanfConfig } -func InitKoanfs(t *testing.T) { +func InitKoanfs(t *testing.T) *profiles.KoanfConfig { t.Helper() configuration.InitAllOptions() configFileContents = strings.Replace(getDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) - configureMainKoanf(t) + return configureMainKoanf(t) } func InitKoanfsCustomFile(t *testing.T, fileContents string) { From e1323d2216e487dde55c929ee807d131d3684903 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Wed, 24 Sep 2025 10:25:01 -0600 Subject: [PATCH 05/14] WIP --- cmd/config/add_profile.go | 8 +- .../config/delete_profile_internal.go | 1 + .../commands/config/list_keys_internal.go | 1 + .../config/list_keys_internal_test.go | 4 +- internal/commands/config/set_internal.go | 64 +-- .../commands/config/unset_internal_test.go | 4 +- internal/commands/license/license_internal.go | 8 +- internal/commands/platform/export_internal.go | 16 +- .../commands/platform/export_internal_test.go | 7 +- internal/commands/plugin/add_internal.go | 4 +- internal/commands/plugin/add_internal_test.go | 2 + internal/commands/plugin/list_internal.go | 3 +- internal/commands/plugin/remove_internal.go | 2 +- internal/commands/request/request_internal.go | 110 +++-- .../commands/request/request_internal_test.go | 390 ++++++++++-------- internal/configuration/configuration.go | 29 +- internal/profiles/koanf.go | 15 +- 17 files changed, 361 insertions(+), 307 deletions(-) diff --git a/cmd/config/add_profile.go b/cmd/config/add_profile.go index 0c586f11..063198b3 100644 --- a/cmd/config/add_profile.go +++ b/cmd/config/add_profile.go @@ -9,6 +9,7 @@ import ( config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/logger" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/spf13/cobra" ) @@ -47,7 +48,12 @@ func configAddProfileRunE(cmd *cobra.Command, args []string) error { l := logger.Get() l.Debug().Msgf("Config add-profile Subcommand Called.") - if err := config_internal.RunInternalConfigAddProfile(os.Stdin); err != nil { + koanfConfig, err := profiles.GetKoanfConfig() + if err != nil { + return err + } + + if err := config_internal.RunInternalConfigAddProfile(os.Stdin, koanfConfig); err != nil { return err } diff --git a/internal/commands/config/delete_profile_internal.go b/internal/commands/config/delete_profile_internal.go index 39a068e9..425ff538 100644 --- a/internal/commands/config/delete_profile_internal.go +++ b/internal/commands/config/delete_profile_internal.go @@ -87,6 +87,7 @@ func promptUserToConfirmDelete(pName string, rc io.ReadCloser) (confirmed bool, if err != nil { return false, &errs.PingCLIError{Prefix: deleteProfileErrorPrefix, Err: err} } + return confirmed, nil } diff --git a/internal/commands/config/list_keys_internal.go b/internal/commands/config/list_keys_internal.go index f0b10a8b..6e47cadf 100644 --- a/internal/commands/config/list_keys_internal.go +++ b/internal/commands/config/list_keys_internal.go @@ -69,6 +69,7 @@ func returnKeysYamlString() (keysYamlStr string, err error) { } keysYamlStr = string(yamlData) + return keysYamlStr, nil } diff --git a/internal/commands/config/list_keys_internal_test.go b/internal/commands/config/list_keys_internal_test.go index 4c0a1d3a..f6890ac7 100644 --- a/internal/commands/config/list_keys_internal_test.go +++ b/internal/commands/config/list_keys_internal_test.go @@ -82,7 +82,9 @@ func Test_RunInternalConfigListKeys(t *testing.T) { require.NoError(t, err) } - w.Close() + err = w.Close() + require.NoError(t, err) + capturedOutputBytes, _ := io.ReadAll(r) capturedOutput := string(capturedOutputBytes) diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index 6102e33f..0e6b47fc 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -150,146 +150,146 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. case options.BOOL: b := new(customtypes.Bool) if err = b.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeBoolean, err) + return fmt.Errorf("%w: %w", ErrMustBeBoolean, err) } err = profileKoanf.Set(vKey, b) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.EXPORT_FORMAT: exportFormat := new(customtypes.ExportFormat) if err = exportFormat.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeExportFormat, err) + return fmt.Errorf("%w: %w", ErrMustBeExportFormat, err) } err = profileKoanf.Set(vKey, exportFormat) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.EXPORT_SERVICE_GROUP: exportServiceGroup := new(customtypes.ExportServiceGroup) if err = exportServiceGroup.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeExportServiceGroup, err) + return fmt.Errorf("%w: %w", ErrMustBeExportServiceGroup, err) } err = profileKoanf.Set(vKey, exportServiceGroup) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.EXPORT_SERVICES: exportServices := new(customtypes.ExportServices) if err = exportServices.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeExportService, err) + return fmt.Errorf("%w: %w", ErrMustBeExportService, err) } err = profileKoanf.Set(vKey, exportServices) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.OUTPUT_FORMAT: outputFormat := new(customtypes.OutputFormat) if err = outputFormat.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeOutputFormat, err) + return fmt.Errorf("%w: %w", ErrMustBeOutputFormat, err) } err = profileKoanf.Set(vKey, outputFormat) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.PINGONE_REGION_CODE: region := new(customtypes.PingOneRegionCode) if err = region.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBePingoneRegionCode, err) + return fmt.Errorf("%w: %w", ErrMustBePingoneRegionCode, err) } err = profileKoanf.Set(vKey, region) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.STRING: str := new(customtypes.String) if err = str.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeString, err) + return fmt.Errorf("%w: %w", ErrMustBeString, err) } err = profileKoanf.Set(vKey, str) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.STRING_SLICE: strSlice := new(customtypes.StringSlice) if err = strSlice.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeStringSlice, err) + return fmt.Errorf("%w: %w", ErrMustBeStringSlice, err) } err = profileKoanf.Set(vKey, strSlice) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.UUID: uuid := new(customtypes.UUID) if err = uuid.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeUUID, err) + return fmt.Errorf("%w: %w", ErrMustBeUUID, err) } err = profileKoanf.Set(vKey, uuid) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.PINGONE_AUTH_TYPE: authType := new(customtypes.PingOneAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBePingoneAuthType, err) + return fmt.Errorf("%w: %w", ErrMustBePingoneAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.PINGFEDERATE_AUTH_TYPE: authType := new(customtypes.PingFederateAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBePingfederateAuthType, err) + return fmt.Errorf("%w: %w", ErrMustBePingfederateAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.INT: intValue := new(customtypes.Int) if err = intValue.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeInteger, err) + return fmt.Errorf("%w: %w", ErrMustBeInteger, err) } err = profileKoanf.Set(vKey, intValue) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.REQUEST_HTTP_METHOD: httpMethod := new(customtypes.HTTPMethod) if err = httpMethod.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeHttpMethod, err) + return fmt.Errorf("%w: %w", ErrMustBeHttpMethod, err) } err = profileKoanf.Set(vKey, httpMethod) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.REQUEST_SERVICE: service := new(customtypes.RequestService) if err = service.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeRequestService, err) + return fmt.Errorf("%w: %w", ErrMustBeRequestService, err) } err = profileKoanf.Set(vKey, service) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.LICENSE_PRODUCT: licenseProduct := new(customtypes.LicenseProduct) if err = licenseProduct.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeLicenseProduct, err) + return fmt.Errorf("%w: %w", ErrMustBeLicenseProduct, err) } err = profileKoanf.Set(vKey, licenseProduct) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } case options.LICENSE_VERSION: licenseVersion := new(customtypes.LicenseVersion) if err = licenseVersion.Set(vValue); err != nil { - return fmt.Errorf("%w: %v", ErrMustBeLicenseVersion, err) + return fmt.Errorf("%w: %w", ErrMustBeLicenseVersion, err) } err = profileKoanf.Set(vKey, licenseVersion) if err != nil { - return fmt.Errorf("%w: %v", ErrSetKey, err) + return fmt.Errorf("%w: %w", ErrSetKey, err) } default: return &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrTypeNotRecognized} diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index 80aee488..3daa833f 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -29,7 +29,7 @@ func Test_RunInternalConfigUnset(t *testing.T) { checkOption: &options.RootColorOption, }, { - name: "Unset on non-existant key", + name: "Unset on non-existent key", koanfKey: "nonExistantKey", expectedError: configuration.ErrInvalidConfigurationKey, }, @@ -40,7 +40,7 @@ func Test_RunInternalConfigUnset(t *testing.T) { checkOption: &options.RootColorOption, }, { - name: "Unset key with a non-existant profile", + name: "Unset key with a non-existent profile", profileName: customtypes.String("nonExistant"), koanfKey: options.RootColorOption.KoanfKey, expectedError: profiles.ErrProfileNameNotExist, diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go index 6e1e17f1..00d969eb 100644 --- a/internal/commands/license/license_internal.go +++ b/internal/commands/license/license_internal.go @@ -50,22 +50,22 @@ func RunInternalLicense() (err error) { func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, err error) { product, err = profiles.GetOptionValue(options.LicenseProductOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetProduct, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetProduct, err)} } version, err = profiles.GetOptionValue(options.LicenseVersionOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetVersion, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetVersion, err)} } devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetDevopsUser, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsUser, err)} } devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetDevopsKey, err)} + return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsKey, err)} } if product == "" || version == "" || devopsUser == "" || devopsKey == "" { diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index 1bd1c446..b5f4ccae 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -174,7 +174,7 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e if err != nil { return &errs.PingCLIError{ Prefix: exportErrorPrefix, - Err: fmt.Errorf("%w '%s': %v", ErrReadCaCertPemFile, caCertPemFile, err), + Err: fmt.Errorf("%w '%s': %w", ErrReadCaCertPemFile, caCertPemFile, err), } } @@ -182,7 +182,7 @@ func initPingFederateServices(ctx context.Context, pingcliVersion string) (err e if !ok { return &errs.PingCLIError{ Prefix: exportErrorPrefix, - Err: fmt.Errorf("%w '%s': %v", ErrAppendToCertPool, caCertPemFile, err), + Err: fmt.Errorf("%w '%s': %w", ErrAppendToCertPool, caCertPemFile, err), } } } @@ -400,7 +400,7 @@ func initPingOneApiClient(ctx context.Context, pingcliVersion string) (err error pingoneApiClient, err = apiConfig.APIClient(ctx) if err != nil { - return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %v", ErrPingOneInit, err)} + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %w", ErrPingOneInit, err)} } return nil @@ -418,7 +418,7 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved if !filepath.IsAbs(outputDir) { pwd, err := os.Getwd() if err != nil { - return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %v", ErrGetPresentWorkingDirectory, err)} + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetPresentWorkingDirectory, err)} } outputDir = filepath.Join(pwd, outputDir) @@ -433,7 +433,7 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved err = os.MkdirAll(outputDir, os.FileMode(0700)) if err != nil { - return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrCreateOutputDirectory, outputDir, err)} + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrCreateOutputDirectory, outputDir, err)} } output.Success(fmt.Sprintf("Output directory '%s' created", outputDir), nil) @@ -443,7 +443,7 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved // This can be changed with the --overwrite export parameter dirEntries, err := os.ReadDir(outputDir) if err != nil { - return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrReadOutputDirectory, outputDir, err)} + return resolvedOutputDir, &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrReadOutputDirectory, outputDir, err)} } if len(dirEntries) > 0 { @@ -493,7 +493,7 @@ func validatePingOneExportEnvID(ctx context.Context) (err error) { return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } if !ok { - return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s'", ErrValidatePingOneEnvId, pingoneExportEnvID)} + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrValidatePingOneEnvId, pingoneExportEnvID)} } if environment == nil { @@ -544,7 +544,7 @@ func exportConnectors(exportableConnectors *[]connector.Exportable, exportFormat err := connector.Export(exportFormat, outputDir, overwriteExport) if err != nil { - return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %v", ErrExportService, connector.ConnectorServiceName(), err)} + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrExportService, connector.ConnectorServiceName(), err)} } } diff --git a/internal/commands/platform/export_internal_test.go b/internal/commands/platform/export_internal_test.go index 328900d6..af6efd6b 100644 --- a/internal/commands/platform/export_internal_test.go +++ b/internal/commands/platform/export_internal_test.go @@ -270,11 +270,10 @@ func setupTestCase(t *testing.T, tc testCase) { originalWd, err := os.Getwd() require.NoError(t, err) - err = os.Chdir(t.TempDir()) - require.NoError(t, err) + t.Chdir(t.TempDir()) t.Cleanup(func() { - require.NoError(t, os.Chdir(originalWd)) + t.Chdir(originalWd) }) } @@ -323,7 +322,7 @@ func createUnwriteableDir(t *testing.T) string { t.Helper() dir := t.TempDir() - err := os.Chmod(dir, 0444) // read-only + err := os.Chmod(dir, 0400) // read-only require.NoError(t, err) return fmt.Sprintf("%s/subdir", dir) diff --git a/internal/commands/plugin/add_internal.go b/internal/commands/plugin/add_internal.go index 3b7488bc..2c6eb0ff 100644 --- a/internal/commands/plugin/add_internal.go +++ b/internal/commands/plugin/add_internal.go @@ -28,7 +28,7 @@ func RunInternalPluginAdd(pluginExecutable string) error { _, err := exec.LookPath(pluginExecutable) if err != nil { - return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %v", ErrPluginNotFound, err)} + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %w", ErrPluginNotFound, err)} } err = addPluginExecutable(pluginExecutable) @@ -59,7 +59,7 @@ func addPluginExecutable(pluginExecutable string) error { existingPluginExectuables, ok, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, pName) if err != nil { - return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} + return &errs.PingCLIError{Prefix: addErrorPrefix, Err: fmt.Errorf("%w: %w", ErrReadPluginNamesConfig, err)} } if !ok { existingPluginExectuables = "" diff --git a/internal/commands/plugin/add_internal_test.go b/internal/commands/plugin/add_internal_test.go index 65655d6b..b1166f03 100644 --- a/internal/commands/plugin/add_internal_test.go +++ b/internal/commands/plugin/add_internal_test.go @@ -60,6 +60,8 @@ func Test_RunInternalPluginAdd(t *testing.T) { } func createGoldenPlugin(t *testing.T) string { + t.Helper() + pathDir := t.TempDir() t.Setenv("PATH", pathDir) diff --git a/internal/commands/plugin/list_internal.go b/internal/commands/plugin/list_internal.go index 66b69e40..ced1fedc 100644 --- a/internal/commands/plugin/list_internal.go +++ b/internal/commands/plugin/list_internal.go @@ -19,10 +19,11 @@ var ( func RunInternalPluginList() error { existingPluginExectuables, ok, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, "") if err != nil { - return &errs.PingCLIError{Prefix: listErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} + return &errs.PingCLIError{Prefix: listErrorPrefix, Err: fmt.Errorf("%w: %w", ErrReadPluginNamesConfig, err)} } if !ok { output.Message("No plugins configured.", nil) + return nil } diff --git a/internal/commands/plugin/remove_internal.go b/internal/commands/plugin/remove_internal.go index ea9a3033..243b60e2 100644 --- a/internal/commands/plugin/remove_internal.go +++ b/internal/commands/plugin/remove_internal.go @@ -53,7 +53,7 @@ func removePluginExecutable(pluginExecutable string) (bool, error) { existingPluginExectuables, _, err := profiles.KoanfValueFromOption(options.PluginExecutablesOption, pName) if err != nil { - return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: fmt.Errorf("%w: %v", ErrReadPluginNamesConfig, err)} + return false, &errs.PingCLIError{Prefix: removeErrorPrefix, Err: fmt.Errorf("%w: %w", ErrReadPluginNamesConfig, err)} } strSlice := new(customtypes.StringSlice) diff --git a/internal/commands/request/request_internal.go b/internal/commands/request/request_internal.go index dab4b990..5ddf8020 100644 --- a/internal/commands/request/request_internal.go +++ b/internal/commands/request/request_internal.go @@ -12,16 +12,31 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "time" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" ) +var ( + requestErrorPrefix = "failed to send custom request" + ErrServiceEmpty = errors.New("service is not set") + ErrUnrecognizedService = errors.New("unrecognized service") + ErrHttpMethodEmpty = errors.New("http method is not set") + ErrUnrecognizedHttpMethod = errors.New("unrecognized http method") + ErrPingOneRegionCodeEmpty = errors.New("PingOne region code is not set") + ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized PingOne region code") + ErrPingOneWorkerEnvIDEmpty = errors.New("PingOne worker environment ID is not set") + ErrPingOneClientIDAndSecretEmpty = errors.New("PingOne client ID and/or client secret is not set") + ErrPingOneAuthenticate = errors.New("failed to authenticate with PingOne") +) + type PingOneAuthResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` @@ -31,21 +46,21 @@ type PingOneAuthResponse struct { func RunInternalRequest(uri string) (err error) { service, err := profiles.GetOptionValue(options.RequestServiceOption) if err != nil { - return fmt.Errorf("failed to send custom request: %w", err) + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if service == "" { - return fmt.Errorf("failed to send custom request: service is required") + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: ErrServiceEmpty} } switch service { case customtypes.ENUM_REQUEST_SERVICE_PINGONE: err = runInternalPingOneRequest(uri) if err != nil { - return fmt.Errorf("failed to send custom request: %w", err) + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } default: - return fmt.Errorf("failed to send custom request: unrecognized service '%s'", service) + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrUnrecognizedService, service)} } return nil @@ -54,39 +69,43 @@ func RunInternalRequest(uri string) (err error) { func runInternalPingOneRequest(uri string) (err error) { accessToken, err := pingoneAccessToken() if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } topLevelDomain, err := getTopLevelDomain() if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } failOption, err := profiles.GetOptionValue(options.RequestFailOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } apiURL := fmt.Sprintf("https://api.pingone.%s/v1/%s", topLevelDomain, uri) httpMethod, err := profiles.GetOptionValue(options.RequestHTTPMethodOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if httpMethod == "" { - return fmt.Errorf("http method is required") + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: ErrHttpMethodEmpty} + } + + if !slices.Contains(customtypes.HTTPMethodValidValues(), httpMethod) { + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrUnrecognizedHttpMethod, httpMethod)} } data, err := getDataRaw() if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if data == "" { data, err = getDataFile() if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } } @@ -95,18 +114,18 @@ func runInternalPingOneRequest(uri string) (err error) { client := &http.Client{} req, err := http.NewRequestWithContext(context.Background(), httpMethod, apiURL, payload) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } headers, err := profiles.GetOptionValue(options.RequestHeaderOption) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } requestHeaders := new(customtypes.HeaderSlice) err = requestHeaders.Set(headers) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } requestHeaders.SetHttpRequestHeaders(req) @@ -121,19 +140,20 @@ func runInternalPingOneRequest(uri string) (err error) { res, err := client.Do(req) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } defer func() { cErr := res.Body.Close() if cErr != nil { err = errors.Join(err, cErr) + err = &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } }() body, err := io.ReadAll(res.Body) if err != nil { - return err + return &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } fields := map[string]any{ @@ -159,11 +179,11 @@ func runInternalPingOneRequest(uri string) (err error) { func getTopLevelDomain() (topLevelDomain string, err error) { pingoneRegionCode, err := profiles.GetOptionValue(options.PingOneRegionCodeOption) if err != nil { - return "", err + return topLevelDomain, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if pingoneRegionCode == "" { - return "", fmt.Errorf("PingOne region code is required") + return topLevelDomain, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: ErrPingOneRegionCodeEmpty} } switch pingoneRegionCode { @@ -178,7 +198,7 @@ func getTopLevelDomain() (topLevelDomain string, err error) { case customtypes.ENUM_PINGONE_REGION_CODE_NA: topLevelDomain = customtypes.ENUM_PINGONE_TLD_NA default: - return "", fmt.Errorf("unrecognized PingOne region code: '%s'", pingoneRegionCode) + return topLevelDomain, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: fmt.Errorf("%w: '%s'", ErrUnrecognizedPingOneRegionCode, pingoneRegionCode)} } return topLevelDomain, nil @@ -188,13 +208,13 @@ func pingoneAccessToken() (accessToken string, err error) { // Check if existing access token is available accessToken, err = profiles.GetOptionValue(options.RequestAccessTokenOption) if err != nil { - return "", err + return accessToken, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if accessToken != "" { accessTokenExpiry, err := profiles.GetOptionValue(options.RequestAccessTokenExpiryOption) if err != nil { - return "", err + return accessToken, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if accessTokenExpiry == "" { @@ -204,7 +224,7 @@ func pingoneAccessToken() (accessToken string, err error) { // convert expiry string to int tokenExpiryInt, err := strconv.ParseInt(accessTokenExpiry, 10, 64) if err != nil { - return "", err + return accessToken, &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } // Get current Unix epoch time in seconds @@ -225,31 +245,31 @@ func pingoneAccessToken() (accessToken string, err error) { func pingoneAuth() (accessToken string, err error) { topLevelDomain, err := getTopLevelDomain() if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } workerEnvId, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerEnvironmentIDOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if workerEnvId == "" { - return "", fmt.Errorf("PingOne worker environment ID is required") + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: ErrPingOneWorkerEnvIDEmpty} } authURL := fmt.Sprintf("https://auth.pingone.%s/%s/as/token", topLevelDomain, workerEnvId) clientId, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerClientIDOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } clientSecret, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerClientSecretOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if clientId == "" || clientSecret == "" { - return "", fmt.Errorf("PingOne client ID and secret are required") + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: ErrPingOneClientIDAndSecretEmpty} } basicAuthBase64 := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) @@ -259,7 +279,7 @@ func pingoneAuth() (accessToken string, err error) { client := &http.Client{} req, err := http.NewRequestWithContext(context.Background(), customtypes.ENUM_HTTP_METHOD_POST, authURL, payload) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthBase64)) @@ -267,29 +287,33 @@ func pingoneAuth() (accessToken string, err error) { res, err := client.Do(req) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } defer func() { cErr := res.Body.Close() if cErr != nil { err = errors.Join(err, cErr) + err = &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } }() responseBodyBytes, err := io.ReadAll(res.Body) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if res.StatusCode < 200 || res.StatusCode >= 300 { - return "", fmt.Errorf("failed to authenticate with PingOne: Response Status %s: Response Body %s", res.Status, string(responseBodyBytes)) + return "", &errs.PingCLIError{ + Prefix: requestErrorPrefix, + Err: fmt.Errorf("%w: Response Status %s: Response Body %s", ErrPingOneAuthenticate, res.Status, string(responseBodyBytes)), + } } pingoneAuthResponse := new(PingOneAuthResponse) err = json.Unmarshal(responseBodyBytes, pingoneAuthResponse) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } currentTime := time.Now().Unix() @@ -298,37 +322,37 @@ func pingoneAuth() (accessToken string, err error) { // Store access token and expiry pName, err := profiles.GetOptionValue(options.RootProfileOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if pName == "" { pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } } koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } err = subKoanf.Set(options.RequestAccessTokenOption.KoanfKey, pingoneAuthResponse.AccessToken) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } err = subKoanf.Set(options.RequestAccessTokenExpiryOption.KoanfKey, tokenExpiry) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } err = koanfConfig.SaveProfile(pName, subKoanf) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } return pingoneAuthResponse.AccessToken, nil @@ -337,14 +361,14 @@ func pingoneAuth() (accessToken string, err error) { func getDataFile() (data string, err error) { dataFilepath, err := profiles.GetOptionValue(options.RequestDataOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } if dataFilepath != "" { dataFilepath = filepath.Clean(dataFilepath) contents, err := os.ReadFile(dataFilepath) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } return string(contents), nil @@ -356,7 +380,7 @@ func getDataFile() (data string, err error) { func getDataRaw() (data string, err error) { data, err = profiles.GetOptionValue(options.RequestDataRawOption) if err != nil { - return "", err + return "", &errs.PingCLIError{Prefix: requestErrorPrefix, Err: err} } return data, nil diff --git a/internal/commands/request/request_internal_test.go b/internal/commands/request/request_internal_test.go index 91dba74b..00125af2 100644 --- a/internal/commands/request/request_internal_test.go +++ b/internal/commands/request/request_internal_test.go @@ -3,7 +3,6 @@ package request_internal import ( - "errors" "fmt" "os" "os/exec" @@ -11,229 +10,266 @@ import ( "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test RunInternalRequest function func Test_RunInternalRequest(t *testing.T) { testutils_koanf.InitKoanfs(t) - t.Setenv(options.RequestServiceOption.EnvVar, "pingone") - - err := RunInternalRequest(fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar))) - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalRequest function with fail -func Test_RunInternalRequestWithFail(t *testing.T) { - if os.Getenv("RUN_INTERNAL_FAIL_TEST") == "true" { - testutils_koanf.InitKoanfs(t) - t.Setenv(options.RequestServiceOption.EnvVar, "pingone") - options.RequestFailOption.Flag.Changed = true - err := options.RequestFailOption.Flag.Value.Set("true") - if err != nil { - t.Fatal(err) - } - _ = RunInternalRequest("environments/failTest") - t.Fatal("This should never run due to internal request resulting in os.Exit(1)") - } else { - cmdName := os.Args[0] - cmd := exec.CommandContext(t.Context(), cmdName, "-test.run=Test_RunInternalRequestWithFail") //#nosec G204 -- This is a test - cmd.Env = append(os.Environ(), "RUN_INTERNAL_FAIL_TEST=true") - err := cmd.Run() - - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - if !exitErr.Success() { - return - } - } - - t.Fatalf("The process did not exit with a non-zero: %s", err) + workerEnvId, err := profiles.GetOptionValue(options.PingOneAuthenticationWorkerEnvironmentIDOption) + require.NoError(t, err) + + defaultService := customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE) + defaultHttpMethod := customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET) + defaultRegionCode := customtypes.PingOneRegionCode(customtypes.ENUM_PINGONE_REGION_CODE_NA) + + testCases := []struct { + name string + uri string + service *customtypes.RequestService + httpMethod *customtypes.HTTPMethod + regionCode *customtypes.PingOneRegionCode + workerEnvId *customtypes.String + workerClientId *customtypes.String + runTwiceToSetAccessToken bool + expectedError error + }{ + { + name: "Happy path - Run internal request", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + }, + { + name: "Test request with empty service", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + service: utils.Pointer(customtypes.RequestService("")), + expectedError: ErrServiceEmpty, + }, + { + name: "Test with invalid service", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + service: utils.Pointer(customtypes.RequestService("invalid-service")), + expectedError: ErrUnrecognizedService, + }, + { + name: "Happy Path - Test with invalid URI", + uri: "invalid-uri", + }, + { + name: "Test with empty HTTP method", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + httpMethod: utils.Pointer(customtypes.HTTPMethod("")), + expectedError: ErrHttpMethodEmpty, + }, + { + name: "Test with invalid HTTP method", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + httpMethod: utils.Pointer(customtypes.HTTPMethod("invalid-http-method")), + expectedError: ErrUnrecognizedHttpMethod, + }, + { + name: "Test with empty pingone region code", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + regionCode: utils.Pointer(customtypes.PingOneRegionCode("")), + expectedError: ErrPingOneRegionCodeEmpty, + }, + { + name: "Test with invalid pingone region code", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + regionCode: utils.Pointer(customtypes.PingOneRegionCode("invalid-region-code")), + expectedError: ErrUnrecognizedPingOneRegionCode, + }, + { + name: "Test with empty worker environment ID", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + workerEnvId: utils.Pointer(customtypes.String("")), + expectedError: ErrPingOneWorkerEnvIDEmpty, + }, + { + name: "Test with empty worker client ID", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + workerClientId: utils.Pointer(customtypes.String("")), + expectedError: ErrPingOneClientIDAndSecretEmpty, + }, + { + name: "Test with invalid worker client ID", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + workerClientId: utils.Pointer(customtypes.String("invalid-client-id")), + expectedError: ErrPingOneAuthenticate, + }, + { + name: "Happy path - Run internal request twice to set access token", + uri: fmt.Sprintf("environments/%s/populations", workerEnvId), + runTwiceToSetAccessToken: true, + }, } -} -// Test RunInternalRequest function with empty service -func Test_RunInternalRequest_EmptyService(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - err := os.Unsetenv(options.RequestServiceOption.EnvVar) - if err != nil { - t.Fatalf("failed to unset environment variable: %v", err) - } - - err = RunInternalRequest("environments") - expectedErrorPattern := "failed to send custom request: service is required" - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalRequest function with unrecognized service -func Test_RunInternalRequest_UnrecognizedService(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - t.Setenv(options.RequestServiceOption.EnvVar, "invalid-service") - - err := RunInternalRequest("environments") - expectedErrorPattern := "failed to send custom request: unrecognized service 'invalid-service'" - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalRequest function with valid service but invalid URI -// This should not error, but rather print a failure message with Body and status of response -func Test_RunInternalRequest_ValidService_InvalidURI(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - t.Setenv(options.RequestServiceOption.EnvVar, "pingone") - - err := RunInternalRequest("invalid-uri") - testutils.CheckExpectedError(t, err, nil) -} + options.RequestServiceOption.Flag.Changed = true + if tc.service != nil { + options.RequestServiceOption.CobraParamValue = tc.service + } else { + options.RequestServiceOption.CobraParamValue = &defaultService + } -// Test runInternalPingOneRequest function -func Test_runInternalPingOneRequest(t *testing.T) { - testutils_koanf.InitKoanfs(t) + options.RequestHTTPMethodOption.Flag.Changed = true + if tc.httpMethod != nil { + options.RequestHTTPMethodOption.CobraParamValue = tc.httpMethod + } else { + options.RequestHTTPMethodOption.CobraParamValue = &defaultHttpMethod + } - err := runInternalPingOneRequest("environments") - testutils.CheckExpectedError(t, err, nil) -} + options.PingOneRegionCodeOption.Flag.Changed = true + if tc.regionCode != nil { + options.PingOneRegionCodeOption.CobraParamValue = tc.regionCode + } else { + options.PingOneRegionCodeOption.CobraParamValue = &defaultRegionCode + } -// Test runInternalPingOneRequest function with invalid URI -// This should not error, but rather print a failure message with Body and status of response -func Test_runInternalPingOneRequest_InvalidURI(t *testing.T) { - testutils_koanf.InitKoanfs(t) + if tc.workerEnvId != nil { + options.PingOneAuthenticationWorkerEnvironmentIDOption.Flag.Changed = true + options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamValue = tc.workerEnvId + } - err := runInternalPingOneRequest("invalid-uri") - testutils.CheckExpectedError(t, err, nil) -} + if tc.workerClientId != nil { + options.PingOneAuthenticationWorkerClientIDOption.Flag.Changed = true + options.PingOneAuthenticationWorkerClientIDOption.CobraParamValue = tc.workerClientId + } -// Test getTopLevelDomain function -func Test_getTopLevelDomain(t *testing.T) { - testutils_koanf.InitKoanfs(t) + err := RunInternalRequest(tc.uri) - t.Setenv(options.PingOneRegionCodeOption.EnvVar, customtypes.ENUM_PINGONE_REGION_CODE_CA) + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } - domain, err := getTopLevelDomain() - testutils.CheckExpectedError(t, err, nil) + if tc.runTwiceToSetAccessToken { + err = RunInternalRequest(tc.uri) - expectedDomain := customtypes.ENUM_PINGONE_TLD_CA - if domain != expectedDomain { - t.Errorf("expected %s, got %s", expectedDomain, domain) + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + } + }) } } -// Test getTopLevelDomain function with invalid region code -func Test_getTopLevelDomain_InvalidRegionCode(t *testing.T) { - testutils_koanf.InitKoanfs(t) +// Test RunInternalRequest function with fail +func Test_RunInternalRequestWithFail(t *testing.T) { + if os.Getenv("RUN_INTERNAL_FAIL_TEST") == "true" { + testutils_koanf.InitKoanfs(t) - t.Setenv(options.PingOneRegionCodeOption.EnvVar, "invalid-region") + service := customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE) + fail := customtypes.String("true") - _, err := getTopLevelDomain() - expectedErrorPattern := "unrecognized PingOne region code: 'invalid-region'" - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + options.RequestServiceOption.Flag.Changed = true + options.RequestServiceOption.CobraParamValue = &service -// Test pingoneAccessToken function -func Test_pingoneAccessToken(t *testing.T) { - testutils_koanf.InitKoanfs(t) + options.RequestFailOption.Flag.Changed = true + options.RequestFailOption.CobraParamValue = &fail - firstToken, err := pingoneAccessToken() - testutils.CheckExpectedError(t, err, nil) + _ = RunInternalRequest("environments/failTest") + t.Fatal("This should never run due to internal request resulting in os.Exit(1)") + } else { + cmdName := os.Args[0] + cmd := exec.CommandContext(t.Context(), cmdName, "-test.run=Test_RunInternalRequestWithFail") //#nosec G204 -- This is a test + cmd.Env = append(os.Environ(), "RUN_INTERNAL_FAIL_TEST=true") + output, err := cmd.CombinedOutput() - // Run the function again to test caching - secondToken, err := pingoneAccessToken() - testutils.CheckExpectedError(t, err, nil) + require.Contains(t, string(output), "ERROR: Failed Custom Request") + require.NotContains(t, string(output), "This should never run due to internal request resulting in os.Exit(1)") - if firstToken != secondToken { - t.Errorf("expected access token to be cached, got different tokens: %s and %s", firstToken, secondToken) + var exitErr *exec.ExitError + require.ErrorAs(t, err, &exitErr) + require.False(t, exitErr.Success(), "Process should exit with a non-zero") } } -// Test pingoneAuth function -func Test_pingoneAuth(t *testing.T) { +func Test_getData(t *testing.T) { testutils_koanf.InitKoanfs(t) - firstToken, err := pingoneAuth() - testutils.CheckExpectedError(t, err, nil) - - // Check token was cached - secondToken, err := pingoneAccessToken() - testutils.CheckExpectedError(t, err, nil) - - if firstToken != secondToken { - t.Errorf("expected access token to be cached, got different tokens: %s and %s", firstToken, secondToken) + dataFileContents := `{data: 'json from file'}` + dataRawContents := `{data: 'json from raw'}` + + dataFile := createDataJSONFile(t, dataFileContents) + + testCases := []struct { + name string + rawData *customtypes.String + dataFile *customtypes.String + expectedError error + }{ + { + name: "Happy path - get data from rawData", + rawData: utils.Pointer(customtypes.String(dataRawContents)), + }, + { + name: "Happy path - get data from dataFile", + dataFile: utils.Pointer(customtypes.String(dataFile)), + }, } -} -// Test pingoneAuth function with invalid credentials -func Test_pingoneAuth_InvalidCredentials(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - t.Setenv(options.PingOneAuthenticationWorkerClientIDOption.EnvVar, "invalid") - - _, err := pingoneAuth() - expectedErrorPattern := `(?s)^failed to authenticate with PingOne: Response Status 401 Unauthorized: Response Body .*$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + require.True(t, (tc.rawData != nil) != (tc.dataFile != nil), "Either rawData or dataFile must be set, but not both") -// Test getData function -func Test_getDataRaw(t *testing.T) { - testutils_koanf.InitKoanfs(t) + var ( + dataStr string + err error + ) - expectedData := "{data: 'json'}" - t.Setenv(options.RequestDataRawOption.EnvVar, expectedData) + if tc.rawData != nil { + options.RequestDataRawOption.Flag.Changed = true + options.RequestDataRawOption.CobraParamValue = tc.rawData - data, err := getDataRaw() - testutils.CheckExpectedError(t, err, nil) + dataStr, err = getDataRaw() - if data != expectedData { - t.Errorf("expected %s, got %s", expectedData, data) - } -} + require.Equal(t, dataStr, dataRawContents) + } -// Test getData function with empty data -func Test_getDataRaw_EmptyData(t *testing.T) { - testutils_koanf.InitKoanfs(t) + if tc.dataFile != nil { + options.RequestDataOption.Flag.Changed = true + options.RequestDataOption.CobraParamValue = tc.dataFile - t.Setenv(options.RequestDataRawOption.EnvVar, "") + dataStr, err = getDataFile() - data, err := getDataRaw() - testutils.CheckExpectedError(t, err, nil) + require.Equal(t, dataStr, dataFileContents) + } - if data != "" { - t.Errorf("expected empty data, got %s", data) + if tc.expectedError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + }) } } -// Test getData function with file input -func Test_getDataFile_FileInput(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedData := "{data: 'json from file'}" - testDir := t.TempDir() - testFile := testDir + "/test.json" - err := os.WriteFile(testFile, []byte(expectedData), 0600) - if err != nil { - t.Fatalf("failed to write test file: %v", err) - } - - t.Setenv(options.RequestDataOption.EnvVar, testFile) +func createDataJSONFile(t *testing.T, data string) string { + t.Helper() - data, err := getDataFile() - testutils.CheckExpectedError(t, err, nil) + file, err := os.CreateTemp(t.TempDir(), "data-*.json") + require.NoError(t, err) - if data != expectedData { - t.Errorf("expected %s, got %s", expectedData, data) - } -} - -// Test getData function with non-existent file input -func Test_getDataFile_NonExistentFileInput(t *testing.T) { - testutils_koanf.InitKoanfs(t) + _, err = file.WriteString(data) + require.NoError(t, err) - t.Setenv(options.RequestDataOption.EnvVar, "non_existent_file.json") + err = file.Close() + require.NoError(t, err) - _, err := getDataFile() - expectedErrorPattern := `^open .*: no such file or directory$` - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + return file.Name() } diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 758bcc44..584d6cd0 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -4,7 +4,6 @@ package configuration import ( "errors" - "fmt" "slices" "strings" @@ -17,33 +16,15 @@ import ( configuration_request "github.com/pingidentity/pingcli/internal/configuration/request" configuration_root "github.com/pingidentity/pingcli/internal/configuration/root" configuration_services "github.com/pingidentity/pingcli/internal/configuration/services" + "github.com/pingidentity/pingcli/internal/errs" ) var ( + configurationErrorPrefix = "configuration options error" ErrInvalidConfigurationKey = errors.New("provided key is not recognized as a valid configuration key.\nuse 'pingcli config list-keys' to view all available keys") ErrNoOptionForKey = errors.New("no option found for the provided configuration key") ) -type ConfigurationError struct { - Err error -} - -func (e *ConfigurationError) Error() string { - var err *ConfigurationError - if errors.As(e.Err, &err) { - return err.Error() - } - return fmt.Sprintf("configuration options error: %s", e.Err.Error()) -} - -func (e *ConfigurationError) Unwrap() error { - var err *ConfigurationError - if errors.As(e.Err, &err) { - return err.Unwrap() - } - return e.Err -} - func KoanfKeys() (keys []string) { for _, opt := range options.Options() { if opt.KoanfKey != "" { @@ -64,7 +45,7 @@ func ValidateKoanfKey(koanfKey string) error { } } - return &ConfigurationError{Err: ErrInvalidConfigurationKey} + return &errs.PingCLIError{Prefix: configurationErrorPrefix, Err: ErrInvalidConfigurationKey} } // Return a list of all koanf keys from Options @@ -98,7 +79,7 @@ func ValidateParentKoanfKey(koanfKey string) error { } } - return &ConfigurationError{Err: ErrInvalidConfigurationKey} + return &errs.PingCLIError{Prefix: configurationErrorPrefix, Err: ErrInvalidConfigurationKey} } func OptionFromKoanfKey(koanfKey string) (opt options.Option, err error) { @@ -108,7 +89,7 @@ func OptionFromKoanfKey(koanfKey string) (opt options.Option, err error) { } } - return opt, &ConfigurationError{Err: ErrNoOptionForKey} + return opt, &errs.PingCLIError{Prefix: configurationErrorPrefix, Err: ErrNoOptionForKey} } func InitAllOptions() { diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 128e99ff..2fd7a51d 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -56,6 +56,7 @@ func GetKoanfConfig() (*KoanfConfig, error) { if k == nil || k.koanfInstance == nil { return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: ErrKoanfNotInitialized} } + return k, nil } @@ -200,7 +201,7 @@ func (k KoanfConfig) ChangeActiveProfile(pName string) (err error) { err = k.KoanfInstance().Set(options.RootActiveProfileOption.KoanfKey, pName) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetActiveProfile, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrSetActiveProfile, err)} } if err = k.WriteFile(); err != nil { @@ -254,7 +255,7 @@ func (k KoanfConfig) GetProfileKoanf(pName string) (subKoanf *koanf.Koanf, err e subKoanf = koanf.New(".") err = subKoanf.Load(confmap.Provider(k.KoanfInstance().Cut(pName).All(), "."), nil) if err != nil { - return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrKoanfProfileExtractAndLoad, err)} + return nil, &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrKoanfProfileExtractAndLoad, err)} } return subKoanf, nil @@ -275,7 +276,7 @@ func (k KoanfConfig) WriteFile() (err error) { if strings.ToLower(fullKoanfKeyValue) == key { err = k.KoanfInstance().Set(fullKoanfKeyValue, val) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyValue, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrSetKoanfKeyValue, err)} } k.KoanfInstance().Delete(key) } @@ -291,12 +292,12 @@ func (k KoanfConfig) WriteFile() (err error) { encodedConfig, err := k.KoanfInstance().Marshal(yaml.Parser()) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrMarshalKoanf, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrMarshalKoanf, err)} } err = os.WriteFile(k.GetKoanfConfigFile(), encodedConfig, 0600) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrWriteKoanfFile, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrWriteKoanfFile, err)} } return nil @@ -305,7 +306,7 @@ func (k KoanfConfig) WriteFile() (err error) { func (k KoanfConfig) SaveProfile(pName string, subKoanf *koanf.Koanf) (err error) { err = k.KoanfInstance().MergeAt(subKoanf, pName) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrKoanfMerge, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrKoanfMerge, err)} } err = k.WriteFile() @@ -357,7 +358,7 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { if !subKoanf.Exists(opt.KoanfKey) { err = subKoanf.Set(opt.KoanfKey, opt.DefaultValue) if err != nil { - return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %v", ErrSetKoanfKeyDefaultValue, err)} + return &errs.PingCLIError{Prefix: koanfErrorPrefix, Err: fmt.Errorf("%w: %w", ErrSetKoanfKeyDefaultValue, err)} } } } From 493cc2c8041544df968971ce5c8e68299e7c0c13 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Mon, 29 Sep 2025 18:18:38 -0600 Subject: [PATCH 06/14] Implement custom types --- internal/configuration/configuration.go | 5 + internal/configuration/configuration_test.go | 182 ++++--- .../connector/exportable_resource_test.go | 48 +- internal/customtypes/bool.go | 29 +- internal/customtypes/bool_test.go | 191 +++++-- internal/customtypes/common_errors.go | 7 + internal/customtypes/export_format.go | 21 +- internal/customtypes/export_format_test.go | 148 ++++-- internal/customtypes/export_service_group.go | 53 +- .../customtypes/export_service_group_test.go | 184 ++++++- internal/customtypes/export_services.go | 148 +++--- internal/customtypes/export_services_test.go | 478 +++++++++++++++--- internal/customtypes/headers.go | 62 ++- internal/customtypes/headers_test.go | 237 ++++++++- internal/customtypes/http_method.go | 20 +- internal/customtypes/http_method_test.go | 150 +++++- internal/customtypes/int.go | 27 +- internal/customtypes/int_test.go | 158 +++++- internal/customtypes/license_product.go | 20 +- internal/customtypes/license_product_test.go | 128 +++++ internal/customtypes/license_version.go | 29 +- internal/customtypes/license_version_test.go | 128 +++++ internal/customtypes/output_format.go | 20 +- internal/customtypes/output_format_test.go | 129 ++++- .../customtypes/pingfederate_auth_type.go | 21 +- .../pingfederate_auth_type_test.go | 131 ++++- internal/customtypes/pingone_auth_type.go | 20 +- .../customtypes/pingone_auth_type_test.go | 131 ++++- internal/customtypes/pingone_region_code.go | 20 +- .../customtypes/pingone_region_code_test.go | 129 ++++- internal/customtypes/request_services.go | 20 +- internal/customtypes/request_services_test.go | 131 ++++- internal/customtypes/string.go | 19 +- internal/customtypes/string_slice.go | 22 +- internal/customtypes/string_slice_test.go | 214 +++++++- internal/customtypes/string_test.go | 122 +++++ internal/customtypes/uuid.go | 11 +- internal/customtypes/uuid_test.go | 130 ++++- 38 files changed, 3038 insertions(+), 685 deletions(-) create mode 100644 internal/customtypes/common_errors.go create mode 100644 internal/customtypes/license_product_test.go create mode 100644 internal/customtypes/license_version_test.go create mode 100644 internal/customtypes/string_test.go diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 584d6cd0..1e28bdd8 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -23,6 +23,7 @@ var ( configurationErrorPrefix = "configuration options error" ErrInvalidConfigurationKey = errors.New("provided key is not recognized as a valid configuration key.\nuse 'pingcli config list-keys' to view all available keys") ErrNoOptionForKey = errors.New("no option found for the provided configuration key") + ErrEmptyKeyForOptionSearch = errors.New("empty key provided for option search, too many matches with options not configured with a koanf key") ) func KoanfKeys() (keys []string) { @@ -83,6 +84,10 @@ func ValidateParentKoanfKey(koanfKey string) error { } func OptionFromKoanfKey(koanfKey string) (opt options.Option, err error) { + if koanfKey == "" { + return opt, &errs.PingCLIError{Prefix: configurationErrorPrefix, Err: ErrEmptyKeyForOptionSearch} + } + for _, opt := range options.Options() { if strings.EqualFold(opt.KoanfKey, koanfKey) { return opt, nil diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go index 3796c9bc..678a8508 100644 --- a/internal/configuration/configuration_test.go +++ b/internal/configuration/configuration_test.go @@ -3,114 +3,148 @@ package configuration_test import ( + "strings" "testing" "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/require" ) -// Test ValidateKoanfKey function func Test_ValidateKoanfKey(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := configuration.ValidateKoanfKey("noColor") - if err != nil { - t.Errorf("ValidateKoanfKey returned error: %v", err) + testCases := []struct { + name string + koanfKey string + expectedError error + }{ + { + name: "Happy path - valid key", + koanfKey: options.RootColorOption.KoanfKey, + }, + { + name: "Invalid key", + koanfKey: "invalid-key", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Empty key", + koanfKey: "", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Happy Path - case insensitive key", + koanfKey: strings.ToUpper(options.RootColorOption.KoanfKey), + }, } -} - -// Test ValidateKoanfKey function fails with invalid key -func Test_ValidateKoanfKey_InvalidKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := configuration.ValidateKoanfKey("invalid-key") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test ValidateKoanfKey function fails with empty key -func Test_ValidateKoanfKey_EmptyKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) + err := configuration.ValidateKoanfKey(tc.koanfKey) - expectedErrorPattern := `^key '' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := configuration.ValidateKoanfKey("") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test ValidateKoanfKey supports case-insensitive keys -func Test_ValidateKoanfKey_CaseInsensitive(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := configuration.ValidateKoanfKey("NoCoLoR") - if err != nil { - t.Errorf("ValidateKoanfKey returned error: %v", err) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test ValidateParentKoanfKey function func Test_ValidateParentKoanfKey(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := configuration.ValidateParentKoanfKey("service") - if err != nil { - t.Errorf("ValidateParentKoanfKey returned error: %v", err) + testCases := []struct { + name string + koanfKey string + expectedError error + }{ + { + name: "Happy path - valid parent key", + koanfKey: strings.SplitN(options.PingOneAuthenticationTypeOption.KoanfKey, ".", 2)[0], + }, + { + name: "Invalid key", + koanfKey: "invalid-parent-key", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Empty key", + koanfKey: "", + expectedError: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Happy Path - case insensitive parent key", + koanfKey: strings.ToUpper(strings.SplitN(options.PingOneAuthenticationTypeOption.KoanfKey, ".", 2)[0]), + }, } -} - -// Test ValidateParentKoanfKey function fails with invalid key -func Test_ValidateParentKoanfKey_InvalidKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := configuration.ValidateParentKoanfKey("invalid-key") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} -// Test ValidateParentKoanfKey function fails with empty key -func Test_ValidateParentKoanfKey_EmptyKey(t *testing.T) { - testutils_koanf.InitKoanfs(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^key '' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := configuration.ValidateParentKoanfKey("") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := configuration.ValidateParentKoanfKey(tc.koanfKey) -// Test ValidateParentKoanfKey supports case-insensitive keys -func Test_ValidateParentKoanfKey_CaseInsensitive(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := configuration.ValidateParentKoanfKey("SeRvIcE") - if err != nil { - t.Errorf("ValidateParentKoanfKey returned error: %v", err) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test OptionFromKoanfKey function func Test_OptionFromKoanfKey(t *testing.T) { testutils_koanf.InitKoanfs(t) - opt, err := configuration.OptionFromKoanfKey("noColor") - if err != nil { - t.Errorf("OptionFromKoanfKey returned error: %v", err) + testCases := []struct { + name string + koanfKey string + expectedOption options.Option + expectedError error + }{ + { + name: "Happy path - valid key", + koanfKey: options.RootColorOption.KoanfKey, + expectedOption: options.RootColorOption, + }, + { + name: "Happy path - case insensitive key", + koanfKey: strings.ToUpper(options.RootColorOption.KoanfKey), + expectedOption: options.RootColorOption, + }, + { + name: "Invalid key", + koanfKey: "invalid-key", + expectedError: configuration.ErrNoOptionForKey, + }, + { + name: "Empty key", + koanfKey: "", + expectedError: configuration.ErrEmptyKeyForOptionSearch, + }, } - if opt.KoanfKey != "noColor" { - t.Errorf("OptionFromKoanfKey returned invalid option: %v", opt) - } -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test OptionFromKoanfKey supports case-insensitive keys -func Test_OptionFromKoanfKey_CaseInsensitive(t *testing.T) { - testutils_koanf.InitKoanfs(t) + opt, err := configuration.OptionFromKoanfKey(tc.koanfKey) - opt, err := configuration.OptionFromKoanfKey("NoCoLoR") - if err != nil { - t.Errorf("OptionFromKoanfKey returned error: %v", err) - } + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } - if opt.KoanfKey != options.RootColorOption.KoanfKey { - t.Errorf("OptionFromKoanfKey returned invalid option: %v", opt) + require.Equal(t, tc.expectedOption, opt) + }) } } diff --git a/internal/connector/exportable_resource_test.go b/internal/connector/exportable_resource_test.go index f3ee7411..9c67878f 100644 --- a/internal/connector/exportable_resource_test.go +++ b/internal/connector/exportable_resource_test.go @@ -6,19 +6,51 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/connector" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/require" ) -// Test sanitization of resource name -func TestSanitize(t *testing.T) { - sanitizedResourceName := "pingcli__Customer-0020-HTML-0020-Form-0020--0028-PF-0029-" +func Test_Sanitize(t *testing.T) { + testutils_koanf.InitKoanfs(t) - importBlock := connector.ImportBlock{ - ResourceName: "Customer HTML Form (PF)", + testCases := []struct { + name string + resourceName string + expectedSanitizedResourceName string + }{ + { + name: "Happy path - Simple", + resourceName: "Customer", + expectedSanitizedResourceName: "pingcli__Customer", + }, + { + name: "Happy path - Alphanumeric", + resourceName: "CustomerHTMLFormPF", + expectedSanitizedResourceName: "pingcli__CustomerHTMLFormPF", + }, + { + name: "Happy path - Spaces and Parentheses", + resourceName: "Customer HTML Form (PF)", + expectedSanitizedResourceName: "pingcli__Customer-0020-HTML-0020-Form-0020--0028-PF-0029-", + }, + { + name: "Happy path - Special Characters", + resourceName: "Customer@HTML#Form$PF%", + expectedSanitizedResourceName: "pingcli__Customer-0040-HTML-0023-Form-0024-PF-0025-", + }, } - importBlock.Sanitize() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - if importBlock.ResourceName != sanitizedResourceName { - t.Errorf("Sanitize function test failed") + importBlock := connector.ImportBlock{ + ResourceName: tc.resourceName, + } + + importBlock.Sanitize() + + require.Equal(t, importBlock.ResourceName, tc.expectedSanitizedResourceName) + }) } } diff --git a/internal/customtypes/bool.go b/internal/customtypes/bool.go index ab9a1841..1a3077df 100644 --- a/internal/customtypes/bool.go +++ b/internal/customtypes/bool.go @@ -3,12 +3,19 @@ package customtypes import ( + "errors" "fmt" "strconv" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + boolErrorPrefix = "custom type bool error" + ErrParseBool = errors.New("failed to parse value as bool") +) + type Bool bool // Verify that the custom type satisfies the pflag.Value interface @@ -16,26 +23,34 @@ var _ pflag.Value = (*Bool)(nil) func (b *Bool) Set(val string) error { if b == nil { - return fmt.Errorf("failed to set Bool value: %s. Bool is nil", val) + return &errs.PingCLIError{Prefix: boolErrorPrefix, Err: ErrCustomTypeNil} } parsedBool, err := strconv.ParseBool(val) if err != nil { - return err + return &errs.PingCLIError{Prefix: boolErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrParseBool, val, err)} } *b = Bool(parsedBool) return nil } -func (b Bool) Type() string { +func (b *Bool) Type() string { return "bool" } -func (b Bool) String() string { - return strconv.FormatBool(bool(b)) +func (b *Bool) String() string { + if b == nil { + return "false" + } + + return strconv.FormatBool(bool(*b)) } -func (b Bool) Bool() bool { - return bool(b) +func (b *Bool) Bool() bool { + if b == nil { + return false + } + + return bool(*b) } diff --git a/internal/customtypes/bool_test.go b/internal/customtypes/bool_test.go index a1c4098d..fc96012c 100644 --- a/internal/customtypes/bool_test.go +++ b/internal/customtypes/bool_test.go @@ -6,81 +6,170 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test Bool Set function func Test_Bool_Set(t *testing.T) { - b := new(customtypes.Bool) - val := "true" - - err := b.Set(val) - if err != nil { - t.Errorf("Set returned error: %v", err) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Bool + boolStr string + expectedError error + }{ + { + name: "Happy path - true", + cType: new(customtypes.Bool), + boolStr: "true", + }, + { + name: "Happy path - false", + cType: new(customtypes.Bool), + boolStr: "false", + }, + { + name: "Invalid value", + cType: new(customtypes.Bool), + boolStr: "invalid", + expectedError: customtypes.ErrParseBool, + }, + { + name: "Empty value", + cType: new(customtypes.Bool), + boolStr: "", + expectedError: customtypes.ErrParseBool, + }, + { + name: "Nil custom bool type", + cType: nil, + boolStr: "true", + expectedError: customtypes.ErrCustomTypeNil, + }, } - val = "false" + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.boolStr) - err = b.Set(val) - if err != nil { - t.Errorf("Set returned error: %v", err) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test Set function fails with invalid value -func Test_Bool_Set_InvalidValue(t *testing.T) { - b := new(customtypes.Bool) - val := "invalid" +func Test_Bool_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Bool + expectedType string + }{ + { + name: "Happy path - true", + cType: utils.Pointer(customtypes.Bool(true)), + expectedType: "bool", + }, + { + name: "Happy path - false", + cType: utils.Pointer(customtypes.Bool(false)), + expectedType: "bool", + }, + { + name: "Nil custom bool type", + cType: nil, + expectedType: "bool", + }, + } - expectedErrorPattern := `^strconv.ParseBool: parsing ".*": invalid syntax$` - err := b.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Set function fails with nil -func Test_Bool_Set_Nil(t *testing.T) { - var b *customtypes.Bool - val := "true" + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set Bool value: .* Bool is nil$` - err := b.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_Bool_String(t *testing.T) { - b := customtypes.Bool(true) - - expected := "true" - actual := b.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Bool + expectedStr string + }{ + { + name: "Happy path - true", + cType: utils.Pointer(customtypes.Bool(true)), + expectedStr: "true", + }, + { + name: "Happy path - false", + cType: utils.Pointer(customtypes.Bool(false)), + expectedStr: "false", + }, + { + name: "Nil custom bool type", + cType: nil, + expectedStr: "false", + }, } - b = customtypes.Bool(false) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected = "false" - actual = b.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } -// Test Bool function func Test_Bool_Bool(t *testing.T) { - b := customtypes.Bool(true) - - expected := true - actual := b.Bool() - if actual != expected { - t.Errorf("Bool returned: %t, expected: %t", actual, expected) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Bool + expectedBool bool + }{ + { + name: "Happy path - true", + cType: utils.Pointer(customtypes.Bool(true)), + expectedBool: true, + }, + { + name: "Happy path - false", + cType: utils.Pointer(customtypes.Bool(false)), + expectedBool: false, + }, + { + name: "Nil custom bool type", + cType: nil, + expectedBool: false, + }, } - b = customtypes.Bool(false) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualBool := tc.cType.Bool() - expected = false - actual = b.Bool() - if actual != expected { - t.Errorf("Bool returned: %t, expected: %t", actual, expected) + require.Equal(t, tc.expectedBool, actualBool) + }) } } diff --git a/internal/customtypes/common_errors.go b/internal/customtypes/common_errors.go new file mode 100644 index 00000000..217cbb1e --- /dev/null +++ b/internal/customtypes/common_errors.go @@ -0,0 +1,7 @@ +package customtypes + +import "errors" + +var ( + ErrCustomTypeNil = errors.New("failed to set value. custom type is nil") +) diff --git a/internal/customtypes/export_format.go b/internal/customtypes/export_format.go index 856768b1..537e777f 100644 --- a/internal/customtypes/export_format.go +++ b/internal/customtypes/export_format.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -14,6 +16,11 @@ const ( ENUM_EXPORT_FORMAT_HCL string = "HCL" ) +var ( + exportFormatErrorPrefix = "custom type export format error" + ErrUnrecognisedFormat = errors.New("unrecognized export format") +) + type ExportFormat string // Verify that the custom type satisfies the pflag.Value interface @@ -23,7 +30,7 @@ var _ pflag.Value = (*ExportFormat)(nil) func (ef *ExportFormat) Set(format string) error { if ef == nil { - return fmt.Errorf("failed to set Export Format value: %s. Export Format is nil", format) + return &errs.PingCLIError{Prefix: exportFormatErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -32,18 +39,22 @@ func (ef *ExportFormat) Set(format string) error { case strings.EqualFold(format, ""): *ef = ExportFormat("") default: - return fmt.Errorf("unrecognized export format '%s'. Must be one of: %s", format, strings.Join(ExportFormatValidValues(), ", ")) + return &errs.PingCLIError{Prefix: exportFormatErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedFormat, format, strings.Join(ExportFormatValidValues(), ", "))} } return nil } -func (ef ExportFormat) Type() string { +func (ef *ExportFormat) Type() string { return "string" } -func (ef ExportFormat) String() string { - return string(ef) +func (ef *ExportFormat) String() string { + if ef == nil { + return "" + } + + return string(*ef) } func ExportFormatValidValues() []string { diff --git a/internal/customtypes/export_format_test.go b/internal/customtypes/export_format_test.go index 206886d7..031a9651 100644 --- a/internal/customtypes/export_format_test.go +++ b/internal/customtypes/export_format_test.go @@ -6,50 +6,140 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test ExportFormat Set function func Test_ExportFormat_Set(t *testing.T) { - // Create a new ExportFormat - exportFormat := new(customtypes.ExportFormat) + testutils_koanf.InitKoanfs(t) - err := exportFormat.Set(customtypes.ENUM_EXPORT_FORMAT_HCL) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.ExportFormat + formatStr string + expectedError error + }{ + { + name: "Happy path - HCL", + cType: new(customtypes.ExportFormat), + formatStr: customtypes.ENUM_EXPORT_FORMAT_HCL, + }, + { + name: "Happy path - empty", + cType: new(customtypes.ExportFormat), + formatStr: "", + }, + { + name: "Invalid value", + cType: new(customtypes.ExportFormat), + formatStr: "invalid", + expectedError: customtypes.ErrUnrecognisedFormat, + }, + { + name: "Nil custom type", + cType: nil, + formatStr: customtypes.ENUM_EXPORT_FORMAT_HCL, + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_ExportFormat_Set_InvalidValue(t *testing.T) { - // Create a new ExportFormat - exportFormat := new(customtypes.ExportFormat) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" + err := tc.cType.Set(tc.formatStr) - expectedErrorPattern := `^unrecognized export format '.*'. Must be one of: .*$` - err := exportFormat.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_ExportFormat_Set_Nil(t *testing.T) { - var exportFormat *customtypes.ExportFormat +func Test_ExportFormat_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportFormat + expectedType string + }{ + { + name: "Happy path - HCL", + cType: utils.Pointer(customtypes.ExportFormat(customtypes.ENUM_EXPORT_FORMAT_HCL)), + expectedType: "string", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportFormat("")), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := customtypes.ENUM_EXPORT_FORMAT_HCL + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set Export Format value: .* Export Format is nil$` - err := exportFormat.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_ExportFormat_String(t *testing.T) { - exportFormat := customtypes.ExportFormat(customtypes.ENUM_EXPORT_FORMAT_HCL) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportFormat + expectedStr string + }{ + { + name: "Happy path - HCL", + cType: utils.Pointer(customtypes.ExportFormat(customtypes.ENUM_EXPORT_FORMAT_HCL)), + expectedStr: customtypes.ENUM_EXPORT_FORMAT_HCL, + }, + { + name: "Happy path - Empty", + cType: utils.Pointer(customtypes.ExportFormat("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } - expected := customtypes.ENUM_EXPORT_FORMAT_HCL - actual := exportFormat.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} + +func Test_ExportFormatValidValues(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + expectedValues := []string{ + customtypes.ENUM_EXPORT_FORMAT_HCL, } + + actualValues := customtypes.ExportFormatValidValues() + + require.Equal(t, expectedValues, actualValues) } diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go index 3f580a91..741d7a66 100644 --- a/internal/customtypes/export_service_group.go +++ b/internal/customtypes/export_service_group.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -14,6 +16,11 @@ const ( ENUM_EXPORT_SERVICE_GROUP_PINGONE string = "pingone" ) +var ( + exportServiceGroupErrorPrefix = "custom type export service group error" + ErrUnrecognisedServiceGroup = errors.New("unrecognized service group") +) + type ExportServiceGroup string // Verify that the custom type satisfies the pflag.Value interface @@ -21,29 +28,57 @@ var _ pflag.Value = (*ExportServiceGroup)(nil) func (esg *ExportServiceGroup) Set(serviceGroup string) error { if esg == nil { - return fmt.Errorf("failed to set ExportServiceGroup value: %s. ExportServiceGroup is nil", serviceGroup) + return &errs.PingCLIError{Prefix: exportServiceGroupErrorPrefix, Err: ErrCustomTypeNil} } if serviceGroup == "" { return nil } - switch { - case strings.EqualFold(ENUM_EXPORT_SERVICE_GROUP_PINGONE, serviceGroup): - *esg = ExportServiceGroup(ENUM_EXPORT_SERVICE_GROUP_PINGONE) - default: - return fmt.Errorf("unrecognized service group '%s'. Must be one of: %s", serviceGroup, strings.Join(ExportServiceGroupValidValues(), ", ")) + // Create a map of valid service groups to check the user provided group against + validServiceGroups := ExportServiceGroupValidValues() + validServiceGroupMap := make(map[string]struct{}, len(validServiceGroups)) + for _, s := range validServiceGroups { + validServiceGroupMap[s] = struct{}{} + } + + if _, ok := validServiceGroupMap[serviceGroup]; !ok { + return &errs.PingCLIError{Prefix: exportServiceGroupErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedServiceGroup, serviceGroup, strings.Join(validServiceGroups, ", "))} } + *esg = ExportServiceGroup(serviceGroup) + return nil } -func (esg ExportServiceGroup) Type() string { +func (esg *ExportServiceGroup) Type() string { return "string" } -func (esg ExportServiceGroup) String() string { - return string(esg) +func (esg *ExportServiceGroup) String() string { + if esg == nil { + return "" + } + return string(*esg) +} + +func (esg *ExportServiceGroup) GetServicesInGroup() []string { + if esg == nil { + return []string{} + } + + switch esg.String() { + case ENUM_EXPORT_SERVICE_GROUP_PINGONE: + return []string{ + ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, + ENUM_EXPORT_SERVICE_PINGONE_SSO, + ENUM_EXPORT_SERVICE_PINGONE_MFA, + ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + } + default: + return []string{} + } } func ExportServiceGroupValidValues() []string { diff --git a/internal/customtypes/export_service_group_test.go b/internal/customtypes/export_service_group_test.go index ce887811..79607b2b 100644 --- a/internal/customtypes/export_service_group_test.go +++ b/internal/customtypes/export_service_group_test.go @@ -6,49 +6,179 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test ExportServiceGroup Set function func Test_ExportServiceGroup_Set(t *testing.T) { - // Create a new ExportServiceGroup - esg := new(customtypes.ExportServiceGroup) + testutils_koanf.InitKoanfs(t) - err := esg.Set(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.ExportServiceGroup + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.ExportServiceGroup), + value: customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE, + }, + { + name: "Invalid value", + cType: new(customtypes.ExportServiceGroup), + value: "invalid", + expectedError: customtypes.ErrUnrecognisedServiceGroup, + }, + { + name: "Happy path - empty", + cType: new(customtypes.ExportServiceGroup), + value: "", + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE, + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test ExportServiceGroup Set function fails with invalid value -func Test_ExportServiceGroup_Set_InvalidValue(t *testing.T) { - // Create a new ExportServiceGroup - esg := new(customtypes.ExportServiceGroup) +func Test_ExportServiceGroup_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServiceGroup + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.ExportServiceGroup(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)), + expectedType: "string", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServiceGroup("")), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" + actualType := tc.cType.Type() - expectedErrorPattern := `^unrecognized service group .*\. Must be one of: .*$` - err := esg.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test ExportServiceGroup Set function fails with nil -func Test_ExportServiceGroup_Set_Nil(t *testing.T) { - var esg *customtypes.ExportServiceGroup +func Test_ExportServiceGroup_String(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServiceGroup + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.ExportServiceGroup(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)), + expectedStr: customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServiceGroup("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE + actualStr := tc.cType.String() - expectedErrorPattern := `^failed to set ExportServiceGroup value: .* ExportServiceGroup is nil$` - err := esg.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedStr, actualStr) + }) + } } -// Test ExportServiceGroup Valid Values returns expected amount func Test_ExportServiceGroupValidValues(t *testing.T) { - serviceGroupEnum := customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE + expectedServiceGroups := []string{ + customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE, + } + + actualServiceGroupValidValues := customtypes.ExportServiceGroupValidValues() + require.Equal(t, actualServiceGroupValidValues, expectedServiceGroups) +} + +func Test_ExportServiceGroup_GetServicesInGroup(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServiceGroup + expectedStrs []string + }{ + { + name: "Happy path - pingone", + cType: utils.Pointer(customtypes.ExportServiceGroup(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)), + expectedStrs: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + }, + }, + { + name: "non existant group", + cType: utils.Pointer(customtypes.ExportServiceGroup("non-existant")), + expectedStrs: []string{}, + }, + { + name: "Nil custom type", + cType: nil, + expectedStrs: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStrs := tc.cType.GetServicesInGroup() - serviceGroupValidValues := customtypes.ExportServiceGroupValidValues() - if serviceGroupValidValues[0] != serviceGroupEnum { - t.Errorf("ExportServiceGroupValidValues returned: %v, expected: %v", serviceGroupValidValues, serviceGroupEnum) + require.Equal(t, tc.expectedStrs, actualStrs) + }) } } diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go index 13cb6aac..68d1808a 100644 --- a/internal/customtypes/export_services.go +++ b/internal/customtypes/export_services.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -19,78 +21,94 @@ const ( ENUM_EXPORT_SERVICE_PINGFEDERATE string = "pingfederate" ) +var ( + exportServicesErrorPrefix = "custom type export services error" + ErrUnrecognisedExportService = errors.New("unrecognized service") +) + type ExportServices []string // Verify that the custom type satisfies the pflag.Value interface var _ pflag.Value = (*ExportServices)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter -func (es ExportServices) GetServices() []string { - return []string(es) +func (es *ExportServices) GetServices() []string { + if es == nil { + return []string{} + } + + return []string(*es) } -func (es *ExportServices) Set(services string) error { +func (es *ExportServices) Set(servicesStr string) error { if es == nil { - return fmt.Errorf("failed to set ExportServices value: %s. ExportServices is nil", services) + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: ErrCustomTypeNil} } - if services == "" || services == "[]" { + if servicesStr == "" || servicesStr == "[]" { return nil } + // Create a map of valid service values to check against user-provided services validServices := ExportServicesValidValues() - serviceList := strings.Split(services, ",") - returnServiceList := *es + validServiceMap := make(map[string]string, len(validServices)) + for _, s := range validServices { + validServiceMap[strings.ToLower(s)] = s + } + + // Create a map of existing services set in the ExportServices object + existingServices := make(map[string]struct{}, len(*es)) + for _, s := range *es { + existingServices[s] = struct{}{} + } - for _, service := range serviceList { - if !slices.ContainsFunc(validServices, func(validService string) bool { - if strings.EqualFold(validService, service) { - if !slices.Contains(returnServiceList, validService) { - returnServiceList = append(returnServiceList, validService) - } + // Loop through user-provided services + // check for valid value in map + // check the service does not already exist + for service := range strings.SplitSeq(servicesStr, ",") { + service = strings.ToLower(strings.TrimSpace(service)) - return true - } + enumService, ok := validServiceMap[service] + if !ok { + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedExportService, service, strings.Join(validServices, ", "))} + } - return false - }) { - return fmt.Errorf("failed to set ExportServices: Invalid service: %s. Allowed services: %s", service, strings.Join(validServices, ", ")) + if _, ok := existingServices[enumService]; ok { + continue } - } - slices.Sort(returnServiceList) + *es = append(*es, enumService) + } - *es = returnServiceList + slices.Sort(*es) return nil } func (es *ExportServices) SetServicesByServiceGroup(serviceGroup *ExportServiceGroup) error { if es == nil { - return fmt.Errorf("failed to set ExportServices value: %s. ExportServices is nil", serviceGroup) + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: ErrCustomTypeNil} } - if serviceGroup.String() == "" { + if serviceGroup == nil || serviceGroup.String() == "" { return nil } - switch { - case strings.EqualFold(ENUM_EXPORT_SERVICE_GROUP_PINGONE, serviceGroup.String()): - return es.Set(strings.Join(ExportServicesPingOneValidValues(), ",")) - default: - return fmt.Errorf("failed to SetServicesByServiceGroup: Invalid service group: %s. Allowed services: %s", serviceGroup.String(), strings.Join(ExportServiceGroupValidValues(), ", ")) - } + es.Set(strings.Join(serviceGroup.GetServicesInGroup(), ",")) + + return nil } -func (es ExportServices) ContainsPingOneService() bool { - if es == nil { +func (es *ExportServices) ContainsPingOneService() bool { + if es == nil || len(*es) == 0 { return false } - pingoneServices := ExportServicesPingOneValidValues() + esg := ExportServiceGroup(ENUM_EXPORT_SERVICE_GROUP_PINGONE) + servicesInGroup := esg.GetServicesInGroup() - for _, service := range es { - if slices.ContainsFunc(pingoneServices, func(s string) bool { + for _, service := range *es { + if slices.ContainsFunc(servicesInGroup, func(s string) bool { return strings.EqualFold(s, service) }) { return true @@ -100,52 +118,39 @@ func (es ExportServices) ContainsPingOneService() bool { return false } -func (es ExportServices) ContainsPingFederateService() bool { - if es == nil { +func (es *ExportServices) ContainsPingFederateService() bool { + if es == nil || len(*es) == 0 { return false } - return slices.Contains(es, ENUM_EXPORT_SERVICE_PINGFEDERATE) + return slices.ContainsFunc(*es, func(s string) bool { + return strings.EqualFold(s, ENUM_EXPORT_SERVICE_PINGFEDERATE) + }) } -func (es ExportServices) Type() string { +func (es *ExportServices) Type() string { return "[]string" } -func (es ExportServices) String() string { - return strings.Join(es, ",") -} - -func ExportServicesValidValues() []string { - allServices := []string{ - ENUM_EXPORT_SERVICE_PINGFEDERATE, - ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, - ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, - ENUM_EXPORT_SERVICE_PINGONE_SSO, - ENUM_EXPORT_SERVICE_PINGONE_MFA, - ENUM_EXPORT_SERVICE_PINGONE_PROTECT, +func (es *ExportServices) String() string { + if es == nil { + return "" } - slices.Sort(allServices) + slices.Sort(*es) - return allServices + return strings.Join(*es, ",") } -func ExportServicesPingOneValidValues() []string { - pingOneServices := []string{ - ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, - ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, - ENUM_EXPORT_SERVICE_PINGONE_SSO, - ENUM_EXPORT_SERVICE_PINGONE_MFA, - ENUM_EXPORT_SERVICE_PINGONE_PROTECT, +func (es *ExportServices) Merge(es2 *ExportServices) error { + if es == nil { + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: ErrCustomTypeNil} } - slices.Sort(pingOneServices) - - return pingOneServices -} + if es2 == nil { + return nil + } -func (es *ExportServices) Merge(es2 ExportServices) error { mergedServices := []string{} for _, service := range append(es.GetServices(), es2.GetServices()...) { @@ -158,3 +163,18 @@ func (es *ExportServices) Merge(es2 ExportServices) error { return es.Set(strings.Join(mergedServices, ",")) } + +func ExportServicesValidValues() []string { + allServices := []string{ + ENUM_EXPORT_SERVICE_PINGFEDERATE, + ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, + ENUM_EXPORT_SERVICE_PINGONE_SSO, + ENUM_EXPORT_SERVICE_PINGONE_MFA, + ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + } + + slices.Sort(allServices) + + return allServices +} diff --git a/internal/customtypes/export_services_test.go b/internal/customtypes/export_services_test.go index f78df857..83226548 100644 --- a/internal/customtypes/export_services_test.go +++ b/internal/customtypes/export_services_test.go @@ -3,103 +3,463 @@ package customtypes_test import ( + "fmt" "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test ExportServices Set function -func Test_ExportServices_Set(t *testing.T) { - es := new(customtypes.ExportServices) +func Test_ExportServices_GetServices(t *testing.T) { + testutils_koanf.InitKoanfs(t) - service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA - err := es.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.ExportServices + expectedStrs []string + }{ + { + name: "Happy path", + cType: new(customtypes.ExportServices), + expectedStrs: nil, + }, + { + name: "Happy path - multiple services", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO})), + expectedStrs: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + }, + }, + { + name: "Nil custom type", + cType: nil, + expectedStrs: []string{}, + }, } - services := es.GetServices() - if len(services) != 1 { - t.Errorf("GetServices returned: %v, expected: %v", services, service) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStrs := tc.cType.GetServices() - if services[0] != service { - t.Errorf("GetServices returned: %v, expected: %v", services, service) + require.Equal(t, tc.expectedStrs, actualStrs) + }) } } -// Test ExportServices Set function with invalid value -func Test_ExportServices_Set_InvalidValue(t *testing.T) { - es := new(customtypes.ExportServices) +func Test_ExportServices_Set(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServices + servicesStrs []string + expectedNumServices int + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.ExportServices), + servicesStrs: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA}, + expectedNumServices: 1, + }, + { + name: "Happy path - multiple services", + cType: new(customtypes.ExportServices), + servicesStrs: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO}, + expectedNumServices: 2, + }, + { + name: "Happy path - duplicate services", + cType: new(customtypes.ExportServices), + servicesStrs: []string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE}, + expectedNumServices: 2, + }, + { + name: "Happy path - empty", + cType: new(customtypes.ExportServices), + servicesStrs: []string{""}, + expectedNumServices: 0, + }, + { + name: "Invalid value", + cType: new(customtypes.ExportServices), + servicesStrs: []string{"invalid"}, + expectedNumServices: 0, + expectedError: customtypes.ErrUnrecognisedExportService, + }, + { + name: "Nil custom type", + cType: nil, + servicesStrs: []string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA}, + expectedNumServices: 0, + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + for _, servicesStr := range tc.servicesStrs { + err := tc.cType.Set(servicesStr) - invalidValue := "invalid" - expectedErrorPattern := `^failed to set ExportServices: Invalid service: .*\. Allowed services: .*$` - err := es.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + } + + require.Equal(t, tc.expectedNumServices, len(tc.cType.GetServices())) + }) + } } -// Test ExportServices Set function with nil -func Test_ExportServices_Set_Nil(t *testing.T) { - var es *customtypes.ExportServices +func Test_ExportServices_SetServicesByServiceGroup(t *testing.T) { + testutils_koanf.InitKoanfs(t) - service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA - expectedErrorPattern := `^failed to set ExportServices value: .* ExportServices is nil$` - err := es.Set(service) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + testCases := []struct { + name string + cType *customtypes.ExportServices + serviceGroup *customtypes.ExportServiceGroup + expectedNumServices int + expectedError error + }{ + { + name: "Happy path - pingone", + cType: new(customtypes.ExportServices), + serviceGroup: utils.Pointer(customtypes.ExportServiceGroup(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)), + expectedNumServices: 5, + }, + { + name: "Happy path - empty service group", + cType: new(customtypes.ExportServices), + serviceGroup: utils.Pointer(customtypes.ExportServiceGroup("")), + expectedNumServices: 0, + }, + { + name: "Nil custom type", + cType: nil, + serviceGroup: utils.Pointer(customtypes.ExportServiceGroup(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)), + expectedNumServices: 0, + expectedError: customtypes.ErrCustomTypeNil, + }, + { + name: "Nil service group", + cType: new(customtypes.ExportServices), + serviceGroup: nil, + expectedNumServices: 0, + }, + { + name: "Invalid service group", + cType: new(customtypes.ExportServices), + serviceGroup: utils.Pointer(customtypes.ExportServiceGroup("invalid")), + expectedNumServices: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.SetServicesByServiceGroup(tc.serviceGroup) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.expectedNumServices, len(tc.cType.GetServices())) + }) + } } -// Test ExportServices ContainsPingOneService function func Test_ExportServices_ContainsPingOneService(t *testing.T) { - es := new(customtypes.ExportServices) + testutils_koanf.InitKoanfs(t) - service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA - err := es.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.ExportServices + expectedBool bool + }{ + { + name: "Happy path - pingone mfa", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA})), + expectedBool: true, + }, + { + name: "Happy path - pingone sso", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO})), + expectedBool: true, + }, + { + name: "Happy path - pingone platform", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PLATFORM})), + expectedBool: true, + }, + { + name: "Happy path - pingone authorize", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE})), + expectedBool: true, + }, + { + name: "Happy path - pingone protect", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT})), + expectedBool: true, + }, + { + name: "Happy path - pingfederate", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE})), + expectedBool: false, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServices([]string{""})), + expectedBool: false, + }, + { + name: "Nil custom type", + cType: nil, + expectedBool: false, + }, } - if !es.ContainsPingOneService() { - t.Errorf("ContainsPingOneService returned false, expected true") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualBool := tc.cType.ContainsPingOneService() + + require.Equal(t, tc.expectedBool, actualBool) + }) } } -// Test ExportServices ContainsPingFederateService function func Test_ExportServices_ContainsPingFederateService(t *testing.T) { - es := new(customtypes.ExportServices) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServices + expectedBool bool + }{ + { + name: "Happy path - pingfederate", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE})), + expectedBool: true, + }, + { + name: "Happy path - pingone mfa", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA})), + expectedBool: false, + }, + { + name: "Nil custom type", + cType: nil, + expectedBool: false, + }, + } - service := customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE - err := es.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualBool := tc.cType.ContainsPingFederateService() + + require.Equal(t, tc.expectedBool, actualBool) + }) + } +} + +func Test_ExportServices_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServices + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.ExportServices([]string{customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA})), + expectedType: "[]string", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServices([]string{""})), + expectedType: "[]string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "[]string", + }, } - if !es.ContainsPingFederateService() { - t.Errorf("ContainsPingFederateService returned false, expected true") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) } } -// Test ExportServices String function func Test_ExportServices_String(t *testing.T) { - es := new(customtypes.ExportServices) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServices + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + })), + expectedStr: customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + }, + { + name: "Test ordering", + cType: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + })), + expectedStr: fmt.Sprintf("%s,%s", customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO), + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServices([]string{""})), + expectedStr: "", + }, + } - service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA - err := es.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} + +func Test_ExportServices_Merge(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.ExportServices + es2 *customtypes.ExportServices + expectedNumServices int + expectedServices []string + expectedError error + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + })), + es2: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + })), + expectedNumServices: 2, + expectedServices: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + }, + }, + { + name: "Test ordering", + cType: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + })), + es2: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + })), + expectedNumServices: 2, + expectedServices: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + }, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.ExportServices([]string{})), + es2: utils.Pointer(customtypes.ExportServices([]string{})), + expectedNumServices: 0, + expectedServices: []string{}, + }, + { + name: "Nil custom type", + cType: nil, + es2: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + })), + expectedNumServices: 0, + expectedServices: []string{}, + expectedError: customtypes.ErrCustomTypeNil, + }, + { + name: "Nil es2", + cType: utils.Pointer(customtypes.ExportServices([]string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + })), + es2: nil, + expectedNumServices: 1, + expectedServices: []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, + }, + }, } - expected := service - actual := es.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Merge(tc.es2) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.expectedNumServices, len(tc.cType.GetServices())) + require.Equal(t, tc.expectedServices, tc.cType.GetServices()) + }) } } -// Test ExportServicePingOneValidValues -func Test_ExportServicesPingOneValidValues(t *testing.T) { - pingOneServiceGroupValidValues := customtypes.ExportServicesPingOneValidValues() - if len(pingOneServiceGroupValidValues) != 5 { - t.Errorf("ExportServicesPingOneValidValues returned: %v, expected: %v", len(pingOneServiceGroupValidValues), 5) +func Test_ExportServicesValidValues(t *testing.T) { + expectedServices := []string{ + customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO, } + + actualServices := customtypes.ExportServicesValidValues() + require.Equal(t, actualServices, expectedServices) + require.Equal(t, len(actualServices), len(expectedServices)) } diff --git a/internal/customtypes/headers.go b/internal/customtypes/headers.go index e21b1847..3f839e21 100644 --- a/internal/customtypes/headers.go +++ b/internal/customtypes/headers.go @@ -3,15 +3,24 @@ package customtypes import ( + "errors" "fmt" "net/http" "regexp" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + headerErrorPrefix = "custom type header error" + ErrInvalidHeaderFormat = errors.New("invalid header format. must be in `key:value` format") + ErrDisallowedAuthHeader = errors.New("authorization header is not allowed") + headerRegex = regexp.MustCompile(`(^[^\s:]+):(.*)`) +) + type Header struct { Key string Value string @@ -22,66 +31,71 @@ type HeaderSlice []Header // Verify that the custom type satisfies the pflag.Value interface var _ pflag.Value = (*HeaderSlice)(nil) -func NewHeader(header string) (Header, error) { - regexPattern := `(^[^\s]+):[\t ]{0,1}(.*)$` - headerNameRegex := regexp.MustCompile(regexPattern) - matches := headerNameRegex.FindStringSubmatch(header) +func newHeader(header string) (Header, error) { + matches := headerRegex.FindStringSubmatch(header) if len(matches) != 3 { - return Header{}, fmt.Errorf("failed to set Headers: Invalid header: %s. Headers must be in the proper format. Expected regex pattern: %s", header, regexPattern) + return Header{}, fmt.Errorf("%w: %s", ErrInvalidHeaderFormat, header) } - if matches[1] == "Authorization" { - return Header{}, fmt.Errorf("failed to set Headers: Invalid header: %s. Authorization header is not allowed", matches[1]) + key := matches[1] + if strings.EqualFold(key, "Authorization") { + return Header{}, fmt.Errorf("%w: %s", ErrDisallowedAuthHeader, key) } return Header{ - Key: matches[1], - Value: matches[2], + Key: key, + Value: strings.TrimSpace(matches[2]), // Trim space as tabs and spaces are allowed after the colon in Header format }, nil } func (h *HeaderSlice) Set(val string) error { if h == nil { - return fmt.Errorf("failed to set Headers value: %s. Headers is nil", val) + return &errs.PingCLIError{Prefix: headerErrorPrefix, Err: ErrCustomTypeNil} } if val == "" || val == "[]" { return nil - } else { - valH := strings.SplitSeq(val, ",") - for header := range valH { - headerVal, err := NewHeader(header) - if err != nil { - return err - } - *h = append(*h, headerVal) + } + + for header := range strings.SplitSeq(val, ",") { + headerVal, err := newHeader(header) + if err != nil { + return &errs.PingCLIError{Prefix: headerErrorPrefix, Err: err} } + *h = append(*h, headerVal) } return nil } -func (h HeaderSlice) SetHttpRequestHeaders(request *http.Request) { - for _, header := range h { +func (h *HeaderSlice) SetHttpRequestHeaders(request *http.Request) { + if h == nil { + return + } + + for _, header := range *h { request.Header.Add(header.Key, header.Value) } } -func (h HeaderSlice) Type() string { +func (h *HeaderSlice) Type() string { return "[]string" } -func (h HeaderSlice) String() string { +func (h *HeaderSlice) String() string { + if h == nil { + return "" + } return strings.Join(h.StringSlice(), ",") } -func (h HeaderSlice) StringSlice() []string { +func (h *HeaderSlice) StringSlice() []string { if h == nil { return []string{} } headers := []string{} - for _, header := range h { + for _, header := range *h { headers = append(headers, fmt.Sprintf("%s:%s", header.Key, header.Value)) } diff --git a/internal/customtypes/headers_test.go b/internal/customtypes/headers_test.go index c5fc0764..a89246ab 100644 --- a/internal/customtypes/headers_test.go +++ b/internal/customtypes/headers_test.go @@ -3,38 +3,233 @@ package customtypes_test import ( + "net/http" "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test Headers Set function -func Test_Headers_Set(t *testing.T) { - hs := new(customtypes.HeaderSlice) +func Test_HeaderSlice_Set(t *testing.T) { + testutils_koanf.InitKoanfs(t) - service := "key: value" - err := hs.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.HeaderSlice + headerStr string + expectedError error + }{ + { + name: "Happy path - single header", + cType: new(customtypes.HeaderSlice), + headerStr: "key:value", + }, + { + name: "Happy path - multiple headers", + cType: new(customtypes.HeaderSlice), + headerStr: "key1:value1,key2:value2", + }, + { + name: "Happy path - empty", + cType: new(customtypes.HeaderSlice), + headerStr: "", + }, + { + name: "Invalid value", + cType: new(customtypes.HeaderSlice), + headerStr: "invalid-value", + expectedError: customtypes.ErrInvalidHeaderFormat, + }, + { + name: "Disallowed auth header", + cType: new(customtypes.HeaderSlice), + headerStr: "Authorization:some-token", + expectedError: customtypes.ErrDisallowedAuthHeader, + }, + { + name: "Nil custom header type", + cType: nil, + headerStr: "key:value", + expectedError: customtypes.ErrCustomTypeNil, + }, + { + name: "valid header with space after colon", + cType: new(customtypes.HeaderSlice), + headerStr: "key: value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.headerStr) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test Headers Set function with invalid value -func Test_Headers_Set_InvalidValue(t *testing.T) { - hs := new(customtypes.HeaderSlice) +func Test_HeaderSlice_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.HeaderSlice + expectedType string + }{ + { + name: "Happy path", + cType: new(customtypes.HeaderSlice), + expectedType: "[]string", + }, + { + name: "Nil custom header type", + cType: nil, + expectedType: "[]string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() - invalidValue := "invalid=value" - expectedErrorPattern := `^failed to set Headers: Invalid header: .*\. Headers must be in the proper format. Expected regex pattern: .*$` - err := hs.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test Headers Set function with nil -func Test_Headers_Set_Nil(t *testing.T) { - var hs *customtypes.HeaderSlice +func Test_HeaderSlice_String(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.HeaderSlice + expectedStr string + }{ + { + name: "Happy path - single header", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key", Value: "value"}}), + expectedStr: "key:value", + }, + { + name: "Happy path - multiple headers", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}), + expectedStr: "key1:value1,key2:value2", + }, + { + name: "Happy path - empty", + cType: new(customtypes.HeaderSlice), + expectedStr: "", + }, + { + name: "Nil custom header type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} - expectedErrorPattern := `^failed to set Headers value: .* Headers is nil$` - err := hs.Set("key: value") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) +func Test_HeaderSlice_StringSlice(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.HeaderSlice + expectedStrSlice []string + }{ + { + name: "Happy path - single header", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key", Value: "value"}}), + expectedStrSlice: []string{"key:value"}, + }, + { + name: "Happy path - multiple headers", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key2", Value: "value2"}, {Key: "key1", Value: "value1"}}), + expectedStrSlice: []string{"key1:value1", "key2:value2"}, + }, + { + name: "Happy path - empty", + cType: new(customtypes.HeaderSlice), + expectedStrSlice: []string{}, + }, + { + name: "Nil custom header type", + cType: nil, + expectedStrSlice: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStrSlice := tc.cType.StringSlice() + + require.Equal(t, tc.expectedStrSlice, actualStrSlice) + }) + } +} + +func Test_HeaderSlice_SetHttpRequestHeaders(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.HeaderSlice + expectedHeader http.Header + }{ + { + name: "Happy path - single header", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key", Value: "value"}}), + expectedHeader: http.Header{"Key": []string{"value"}}, + }, + { + name: "Happy path - multiple headers", + cType: utils.Pointer(customtypes.HeaderSlice{{Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}}), + expectedHeader: http.Header{"Key1": []string{"value1"}, "Key2": []string{"value2"}}, + }, + { + name: "Happy path - empty", + cType: new(customtypes.HeaderSlice), + expectedHeader: http.Header{}, + }, + { + name: "Nil custom header type", + cType: nil, + expectedHeader: http.Header{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + req, err := http.NewRequest(http.MethodGet, "http://localhost", nil) + require.NoError(t, err) + + tc.cType.SetHttpRequestHeaders(req) + + require.Equal(t, tc.expectedHeader, req.Header) + }) + } } diff --git a/internal/customtypes/http_method.go b/internal/customtypes/http_method.go index 6d379448..a5b6e6c9 100644 --- a/internal/customtypes/http_method.go +++ b/internal/customtypes/http_method.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -18,6 +20,11 @@ const ( ENUM_HTTP_METHOD_PATCH string = "PATCH" ) +var ( + httpMethodErrorPrefix = "custom type http method error" + ErrUnrecognizedMethod = errors.New("unrecognized http method") +) + type HTTPMethod string // Verify that the custom type satisfies the pflag.Value interface @@ -27,7 +34,7 @@ var _ pflag.Value = (*HTTPMethod)(nil) func (hm *HTTPMethod) Set(httpMethod string) error { if hm == nil { - return fmt.Errorf("failed to set HTTP Method value: %s. HTTPMethod is nil", httpMethod) + return &errs.PingCLIError{Prefix: httpMethodErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -44,18 +51,21 @@ func (hm *HTTPMethod) Set(httpMethod string) error { case strings.EqualFold(httpMethod, ""): *hm = HTTPMethod("") default: - return fmt.Errorf("unrecognized HTTP Method: '%s'. Must be one of: %s", httpMethod, strings.Join(HTTPMethodValidValues(), ", ")) + return &errs.PingCLIError{Prefix: httpMethodErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedMethod, httpMethod, strings.Join(HTTPMethodValidValues(), ", "))} } return nil } -func (hm HTTPMethod) Type() string { +func (hm *HTTPMethod) Type() string { return "string" } -func (hm HTTPMethod) String() string { - return string(hm) +func (hm *HTTPMethod) String() string { + if hm == nil { + return "" + } + return string(*hm) } func HTTPMethodValidValues() []string { diff --git a/internal/customtypes/http_method_test.go b/internal/customtypes/http_method_test.go index 3ebd326f..16fc2309 100644 --- a/internal/customtypes/http_method_test.go +++ b/internal/customtypes/http_method_test.go @@ -3,49 +3,145 @@ package customtypes_test import ( + "slices" "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test HTTP Method Set function func Test_HTTPMethod_Set(t *testing.T) { - // Create a new HTTPMethod - httpMethod := new(customtypes.HTTPMethod) + testutils_koanf.InitKoanfs(t) - err := httpMethod.Set(customtypes.ENUM_HTTP_METHOD_GET) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.HTTPMethod + value string + expectedError error + }{ + { + name: "Happy path - GET", + cType: new(customtypes.HTTPMethod), + value: customtypes.ENUM_HTTP_METHOD_GET, + }, + { + name: "Happy path - empty", + cType: new(customtypes.HTTPMethod), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.HTTPMethod), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedMethod, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_HTTP_METHOD_GET, + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_HTTPMethod_Set_InvalidValue(t *testing.T) { - httpMethod := new(customtypes.HTTPMethod) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) - invalidValue := "invalid" - expectedErrorPattern := `^unrecognized HTTP Method: '.*'. Must be one of: .*$` - err := httpMethod.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_HTTPMethod_Set_Nil(t *testing.T) { - var httpMethod *customtypes.HTTPMethod +func Test_HTTPMethod_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.HTTPMethod + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set HTTP Method value: .*\. HTTPMethod is nil$` - err := httpMethod.Set(customtypes.ENUM_HTTP_METHOD_GET) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_HTTPMethod_String(t *testing.T) { - httpMethod := customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET) + testutils_koanf.InitKoanfs(t) - expected := customtypes.ENUM_HTTP_METHOD_GET - actual := httpMethod.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + testCases := []struct { + name string + cType *customtypes.HTTPMethod + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET)), + expectedStr: customtypes.ENUM_HTTP_METHOD_GET, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.HTTPMethod("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} + +func Test_HTTPMethodValidValues(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + expectedValues := []string{ + customtypes.ENUM_HTTP_METHOD_DELETE, + customtypes.ENUM_HTTP_METHOD_GET, + customtypes.ENUM_HTTP_METHOD_PATCH, + customtypes.ENUM_HTTP_METHOD_POST, + customtypes.ENUM_HTTP_METHOD_PUT, + } + + slices.Sort(expectedValues) + + actualValues := customtypes.HTTPMethodValidValues() + + require.Equal(t, expectedValues, actualValues) } diff --git a/internal/customtypes/int.go b/internal/customtypes/int.go index 1aab2502..fa320c7a 100644 --- a/internal/customtypes/int.go +++ b/internal/customtypes/int.go @@ -3,12 +3,19 @@ package customtypes import ( + "errors" "fmt" "strconv" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + intErrorPrefix = "custom type int error" + ErrParseInt = errors.New("failed to parse value as int") +) + type Int int64 // Verify that the custom type satisfies the pflag.Value interface @@ -16,26 +23,32 @@ var _ pflag.Value = (*Int)(nil) func (i *Int) Set(val string) error { if i == nil { - return fmt.Errorf("failed to set Int value: %s. Int is nil", val) + return &errs.PingCLIError{Prefix: intErrorPrefix, Err: ErrCustomTypeNil} } parsedInt, err := strconv.ParseInt(val, 10, 64) if err != nil { - return err + return &errs.PingCLIError{Prefix: intErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrParseInt, val, err)} } *i = Int(parsedInt) return nil } -func (i Int) Type() string { +func (i *Int) Type() string { return "int64" } -func (i Int) String() string { - return strconv.FormatInt(int64(i), 10) +func (i *Int) String() string { + if i == nil { + return "0" + } + return strconv.FormatInt(int64(*i), 10) } -func (i Int) Int64() int64 { - return int64(i) +func (i *Int) Int64() int64 { + if i == nil { + return 0 + } + return int64(*i) } diff --git a/internal/customtypes/int_test.go b/internal/customtypes/int_test.go index 69552b41..43ee468b 100644 --- a/internal/customtypes/int_test.go +++ b/internal/customtypes/int_test.go @@ -6,46 +6,150 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test Int Set function func Test_Int_Set(t *testing.T) { - i := new(customtypes.Int) + testutils_koanf.InitKoanfs(t) - err := i.Set("42") - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.Int + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.Int), + value: "42", + }, + { + name: "Invalid value", + cType: new(customtypes.Int), + value: "invalid", + expectedError: customtypes.ErrParseInt, + }, + { + name: "Empty value", + cType: new(customtypes.Int), + value: "", + expectedError: customtypes.ErrParseInt, + }, + { + name: "Nil custom type", + cType: nil, + value: "42", + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_Int_Set_InvalidValue(t *testing.T) { - i := new(customtypes.Int) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) - invalidValue := "invalid" - expectedErrorPattern := `^strconv.ParseInt: parsing ".*": invalid syntax$` - err := i.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_Int_Set_Nil(t *testing.T) { - var i *customtypes.Int - val := "42" +func Test_Int_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Int + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.Int(42)), + expectedType: "int64", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "int64", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set Int value: .* Int is nil$` - err := i.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_Int_String(t *testing.T) { - i := customtypes.Int(42) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Int + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.Int(42)), + expectedStr: "42", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} + +func Test_Int_Int64(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.Int + expectedInt int64 + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.Int(42)), + expectedInt: 42, + }, + { + name: "Nil custom type", + cType: nil, + expectedInt: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualInt := tc.cType.Int64() - expected := "42" - actual := i.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedInt, actualInt) + }) } } diff --git a/internal/customtypes/license_product.go b/internal/customtypes/license_product.go index 7de3e3bc..dfb51a74 100644 --- a/internal/customtypes/license_product.go +++ b/internal/customtypes/license_product.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -20,6 +22,11 @@ const ( ENUM_LICENSE_PRODUCT_PING_FEDERATE string = "pingfederate" ) +var ( + licenseProductErrorPrefix = "custom type license product error" + ErrUnrecognizedProduct = errors.New("unrecognized license product") +) + type LicenseProduct string // Verify that the custom type satisfies the pflag.Value interface @@ -28,7 +35,7 @@ var _ pflag.Value = (*LicenseProduct)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter func (lp *LicenseProduct) Set(product string) error { if lp == nil { - return fmt.Errorf("failed to set LicenseProduct value: %s. LicenseProduct is nil", product) + return &errs.PingCLIError{Prefix: licenseProductErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -49,18 +56,21 @@ func (lp *LicenseProduct) Set(product string) error { case strings.EqualFold(product, ""): // Allow empty string to be set *lp = LicenseProduct("") default: - return fmt.Errorf("unrecognized License Product: '%s'. Must be one of: %s", product, strings.Join(LicenseProductValidValues(), ", ")) + return &errs.PingCLIError{Prefix: licenseProductErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedProduct, product, strings.Join(LicenseProductValidValues(), ", "))} } return nil } -func (lp LicenseProduct) Type() string { +func (lp *LicenseProduct) Type() string { return "string" } -func (lp LicenseProduct) String() string { - return string(lp) +func (lp *LicenseProduct) String() string { + if lp == nil { + return "" + } + return string(*lp) } func LicenseProductValidValues() []string { diff --git a/internal/customtypes/license_product_test.go b/internal/customtypes/license_product_test.go new file mode 100644 index 00000000..6fc08a8b --- /dev/null +++ b/internal/customtypes/license_product_test.go @@ -0,0 +1,128 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" +) + +func Test_LicenseProduct_Set(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseProduct + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.LicenseProduct), + value: customtypes.ENUM_LICENSE_PRODUCT_PING_ACCESS, + }, + { + name: "Happy path - empty", + cType: new(customtypes.LicenseProduct), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.LicenseProduct), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedProduct, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_LICENSE_PRODUCT_PING_ACCESS, + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_LicenseProduct_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseProduct + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_ACCESS)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } +} + +func Test_LicenseProduct_String(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseProduct + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.LicenseProduct(customtypes.ENUM_LICENSE_PRODUCT_PING_ACCESS)), + expectedStr: customtypes.ENUM_LICENSE_PRODUCT_PING_ACCESS, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.LicenseProduct("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} diff --git a/internal/customtypes/license_version.go b/internal/customtypes/license_version.go index 278f3aa7..3666dfb2 100644 --- a/internal/customtypes/license_version.go +++ b/internal/customtypes/license_version.go @@ -3,44 +3,55 @@ package customtypes import ( + "errors" "fmt" "regexp" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + licenseVersionErrorPrefix = "custom type license version error" + ErrInvalidVersionFormat = errors.New("invalid version format, must be 'major.minor'") +) + type LicenseVersion string // Verify that the custom type satisfies the pflag.Value interface var _ pflag.Value = (*LicenseVersion)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter -func (lp *LicenseVersion) Set(version string) error { - if lp == nil { - return fmt.Errorf("failed to set LicenseVersion value: %s. LicenseVersion is nil", version) +func (lv *LicenseVersion) Set(version string) error { + if lv == nil { + return &errs.PingCLIError{Prefix: licenseVersionErrorPrefix, Err: ErrCustomTypeNil} } // The license version must be of the form "major.minor" or empty if version == "" { - *lp = LicenseVersion("") + *lv = LicenseVersion("") return nil } // Validate the format of the version string via regex if !regexp.MustCompile(`^\d+\.\d+$`).MatchString(version) { - return fmt.Errorf("failed to set LicenseVersion value: %s. Invalid version format, must be 'major.minor'. Example: '12.3'", version) + return &errs.PingCLIError{Prefix: licenseVersionErrorPrefix, Err: fmt.Errorf("%w: %s. Example: '12.3'", ErrInvalidVersionFormat, version)} } - *lp = LicenseVersion(version) + *lv = LicenseVersion(version) return nil } -func (lp LicenseVersion) Type() string { +func (lv *LicenseVersion) Type() string { return "string" } -func (lp LicenseVersion) String() string { - return string(lp) +func (lv *LicenseVersion) String() string { + if lv == nil { + return "" + } + + return string(*lv) } diff --git a/internal/customtypes/license_version_test.go b/internal/customtypes/license_version_test.go new file mode 100644 index 00000000..94ad1f0d --- /dev/null +++ b/internal/customtypes/license_version_test.go @@ -0,0 +1,128 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" +) + +func Test_LicenseVersion_Set(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseVersion + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.LicenseVersion), + value: "12.3", + }, + { + name: "Happy path - empty", + cType: new(customtypes.LicenseVersion), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.LicenseVersion), + value: "invalid", + expectedError: customtypes.ErrInvalidVersionFormat, + }, + { + name: "Nil custom type", + cType: nil, + value: "12.3", + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_LicenseVersion_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseVersion + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.LicenseVersion("12.3")), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } +} + +func Test_LicenseVersion_String(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.LicenseVersion + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.LicenseVersion("12.3")), + expectedStr: "12.3", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.LicenseVersion("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} diff --git a/internal/customtypes/output_format.go b/internal/customtypes/output_format.go index 0c4e3467..b1d18abb 100644 --- a/internal/customtypes/output_format.go +++ b/internal/customtypes/output_format.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -15,6 +17,11 @@ const ( ENUM_OUTPUT_FORMAT_JSON string = "json" ) +var ( + outputFormatErrorPrefix = "custom type output format error" + ErrUnrecognizedOutputFormat = errors.New("unrecognized output format") +) + type OutputFormat string // Verify that the custom type satisfies the pflag.Value interface @@ -24,7 +31,7 @@ var _ pflag.Value = (*OutputFormat)(nil) func (o *OutputFormat) Set(outputFormat string) error { if o == nil { - return fmt.Errorf("failed to set Output Format value: %s. Output Format is nil", outputFormat) + return &errs.PingCLIError{Prefix: outputFormatErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -35,18 +42,21 @@ func (o *OutputFormat) Set(outputFormat string) error { case strings.EqualFold(outputFormat, ""): *o = OutputFormat("") default: - return fmt.Errorf("unrecognized Output Format: '%s'. Must be one of: %s", outputFormat, strings.Join(OutputFormatValidValues(), ", ")) + return &errs.PingCLIError{Prefix: outputFormatErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedOutputFormat, outputFormat, strings.Join(OutputFormatValidValues(), ", "))} } return nil } -func (o OutputFormat) Type() string { +func (o *OutputFormat) Type() string { return "string" } -func (o OutputFormat) String() string { - return string(o) +func (o *OutputFormat) String() string { + if o == nil { + return "" + } + return string(*o) } func OutputFormatValidValues() []string { diff --git a/internal/customtypes/output_format_test.go b/internal/customtypes/output_format_test.go index 018ff6cf..a2b12080 100644 --- a/internal/customtypes/output_format_test.go +++ b/internal/customtypes/output_format_test.go @@ -6,48 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test OutputFormat Set function func Test_OutputFormat_Set(t *testing.T) { - outputFormat := new(customtypes.OutputFormat) + testutils_koanf.InitKoanfs(t) - err := outputFormat.Set(customtypes.ENUM_OUTPUT_FORMAT_JSON) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.OutputFormat + value string + expectedError error + }{ + { + name: "Happy path - JSON", + cType: new(customtypes.OutputFormat), + value: customtypes.ENUM_OUTPUT_FORMAT_JSON, + }, + { + name: "Happy path - empty", + cType: new(customtypes.OutputFormat), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.OutputFormat), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedOutputFormat, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_OUTPUT_FORMAT_JSON, + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_OutputFormat_Set_InvalidValue(t *testing.T) { - outputFormat := new(customtypes.OutputFormat) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" + err := tc.cType.Set(tc.value) - expectedErrorPattern := `^unrecognized Output Format: '.*'\. Must be one of: .*$` - err := outputFormat.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_OutputFormat_Set_Nil(t *testing.T) { - var outputFormat *customtypes.OutputFormat +func Test_OutputFormat_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.OutputFormat + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_JSON)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := customtypes.ENUM_OUTPUT_FORMAT_JSON + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set Output Format value: .* Output Format is nil$` - err := outputFormat.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_OutputFormat_String(t *testing.T) { - outputFormat := customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_JSON) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.OutputFormat + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_JSON)), + expectedStr: customtypes.ENUM_OUTPUT_FORMAT_JSON, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.OutputFormat("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := customtypes.ENUM_OUTPUT_FORMAT_JSON - actual := outputFormat.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } diff --git a/internal/customtypes/pingfederate_auth_type.go b/internal/customtypes/pingfederate_auth_type.go index c61b32f2..6e9fead5 100644 --- a/internal/customtypes/pingfederate_auth_type.go +++ b/internal/customtypes/pingfederate_auth_type.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -16,6 +18,11 @@ const ( ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS string = "clientCredentialsAuth" ) +var ( + pingFederateAuthTypeErrorPrefix = "custom type pingfederate authentication type error" + ErrUnrecognizedPingFederateAuth = errors.New("unrecognized pingfederate authentication type") +) + type PingFederateAuthenticationType string // Verify that the custom type satisfies the pflag.Value interface @@ -24,7 +31,7 @@ var _ pflag.Value = (*PingFederateAuthenticationType)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter func (pat *PingFederateAuthenticationType) Set(authType string) error { if pat == nil { - return fmt.Errorf("failed to set PingFederate Authentication Type value: %s. PingFederate Authentication Type is nil", authType) + return &errs.PingCLIError{Prefix: pingFederateAuthTypeErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -37,18 +44,22 @@ func (pat *PingFederateAuthenticationType) Set(authType string) error { case strings.EqualFold(authType, ""): *pat = PingFederateAuthenticationType("") default: - return fmt.Errorf("unrecognized PingFederate Authentication Type: '%s'. Must be one of: %s", authType, strings.Join(PingFederateAuthenticationTypeValidValues(), ", ")) + return &errs.PingCLIError{Prefix: pingFederateAuthTypeErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedPingFederateAuth, authType, strings.Join(PingFederateAuthenticationTypeValidValues(), ", "))} } return nil } -func (pat PingFederateAuthenticationType) Type() string { +func (pat *PingFederateAuthenticationType) Type() string { return "string" } -func (pat PingFederateAuthenticationType) String() string { - return string(pat) +func (pat *PingFederateAuthenticationType) String() string { + if pat == nil { + return "" + } + + return string(*pat) } func PingFederateAuthenticationTypeValidValues() []string { diff --git a/internal/customtypes/pingfederate_auth_type_test.go b/internal/customtypes/pingfederate_auth_type_test.go index 6534cddd..c793d1b8 100644 --- a/internal/customtypes/pingfederate_auth_type_test.go +++ b/internal/customtypes/pingfederate_auth_type_test.go @@ -6,44 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test PingFederateAuthType Set function func Test_PingFederateAuthType_Set(t *testing.T) { - // Create a new PingFederateAuthType - pingAuthType := new(customtypes.PingFederateAuthenticationType) + testutils_koanf.InitKoanfs(t) - err := pingAuthType.Set(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) - testutils.CheckExpectedError(t, err, nil) -} + testCases := []struct { + name string + cType *customtypes.PingFederateAuthenticationType + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.PingFederateAuthenticationType), + value: customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + { + name: "Happy path - empty", + cType: new(customtypes.PingFederateAuthenticationType), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.PingFederateAuthenticationType), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedPingFederateAuth, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + expectedError: customtypes.ErrCustomTypeNil, + }, + } -// Test Set function fails with invalid value -func Test_PingFederateAuthType_Set_InvalidValue(t *testing.T) { - pingAuthType := new(customtypes.PingFederateAuthenticationType) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" - expectedErrorPattern := `^unrecognized PingFederate Authentication Type: '.*'\. Must be one of: .*$` - err := pingAuthType.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_PingFederateAuthType_Set_Nil(t *testing.T) { - var pingAuthType *customtypes.PingFederateAuthenticationType +func Test_PingFederateAuthType_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingFederateAuthenticationType + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^failed to set PingFederate Authentication Type value: .*\. PingFederate Authentication Type is nil$` - err := pingAuthType.Set(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_PingFederateAuthType_String(t *testing.T) { - pingAuthType := customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingFederateAuthenticationType + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingFederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC)), + expectedStr: customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.PingFederateAuthenticationType("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC - actual := pingAuthType.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } diff --git a/internal/customtypes/pingone_auth_type.go b/internal/customtypes/pingone_auth_type.go index 63f71bfb..4765b74d 100644 --- a/internal/customtypes/pingone_auth_type.go +++ b/internal/customtypes/pingone_auth_type.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -14,6 +16,11 @@ const ( ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER string = "worker" ) +var ( + pingOneAuthTypeErrorPrefix = "custom type pingone auth type error" + ErrUnrecognizedPingOneAuth = errors.New("unrecognized pingone authentication type") +) + type PingOneAuthenticationType string // Verify that the custom type satisfies the pflag.Value interface @@ -22,7 +29,7 @@ var _ pflag.Value = (*PingOneAuthenticationType)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter func (pat *PingOneAuthenticationType) Set(authType string) error { if pat == nil { - return fmt.Errorf("failed to set PingOne Authentication Type value: %s. PingOne Authentication Type is nil", authType) + return &errs.PingCLIError{Prefix: pingOneAuthTypeErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -31,18 +38,21 @@ func (pat *PingOneAuthenticationType) Set(authType string) error { case strings.EqualFold(authType, ""): *pat = PingOneAuthenticationType("") default: - return fmt.Errorf("unrecognized PingOne Authentication Type: '%s'. Must be one of: %s", authType, strings.Join(PingOneAuthenticationTypeValidValues(), ", ")) + return &errs.PingCLIError{Prefix: pingOneAuthTypeErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedPingOneAuth, authType, strings.Join(PingOneAuthenticationTypeValidValues(), ", "))} } return nil } -func (pat PingOneAuthenticationType) Type() string { +func (pat *PingOneAuthenticationType) Type() string { return "string" } -func (pat PingOneAuthenticationType) String() string { - return string(pat) +func (pat *PingOneAuthenticationType) String() string { + if pat == nil { + return "" + } + return string(*pat) } func PingOneAuthenticationTypeValidValues() []string { diff --git a/internal/customtypes/pingone_auth_type_test.go b/internal/customtypes/pingone_auth_type_test.go index 130bb37f..1e985440 100644 --- a/internal/customtypes/pingone_auth_type_test.go +++ b/internal/customtypes/pingone_auth_type_test.go @@ -6,44 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test PingOne Authentication Type Set function func Test_PingOneAuthType_Set(t *testing.T) { - // Create a new PingOneAuthType - pingAuthType := new(customtypes.PingOneAuthenticationType) + testutils_koanf.InitKoanfs(t) - err := pingAuthType.Set(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) - testutils.CheckExpectedError(t, err, nil) -} + testCases := []struct { + name string + cType *customtypes.PingOneAuthenticationType + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.PingOneAuthenticationType), + value: customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER, + }, + { + name: "Happy path - empty", + cType: new(customtypes.PingOneAuthenticationType), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.PingOneAuthenticationType), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedPingOneAuth, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER, + expectedError: customtypes.ErrCustomTypeNil, + }, + } -// Test Set function fails with invalid value -func Test_PingOneAuthType_Set_InvalidValue(t *testing.T) { - pingAuthType := new(customtypes.PingOneAuthenticationType) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" - expectedErrorPattern := `^unrecognized PingOne Authentication Type: '.*'\. Must be one of: .*$` - err := pingAuthType.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_PingOneAuthType_Set_Nil(t *testing.T) { - var pingAuthType *customtypes.PingOneAuthenticationType +func Test_PingOneAuthType_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingOneAuthenticationType + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingOneAuthenticationType(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^failed to set PingOne Authentication Type value: .*\. PingOne Authentication Type is nil$` - err := pingAuthType.Set(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_PingOneAuthType_String(t *testing.T) { - pingAuthType := customtypes.PingOneAuthenticationType(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingOneAuthenticationType + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingOneAuthenticationType(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER)), + expectedStr: customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.PingOneAuthenticationType("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER - actual := pingAuthType.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } diff --git a/internal/customtypes/pingone_region_code.go b/internal/customtypes/pingone_region_code.go index eeba73da..12236794 100644 --- a/internal/customtypes/pingone_region_code.go +++ b/internal/customtypes/pingone_region_code.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -24,6 +26,11 @@ const ( ENUM_PINGONE_TLD_NA string = "com" ) +var ( + pingOneRegionCodeErrorPrefix = "custom type pingone region code error" + ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized pingone region code") +) + type PingOneRegionCode string // Verify that the custom type satisfies the pflag.Value interface @@ -33,7 +40,7 @@ var _ pflag.Value = (*PingOneRegionCode)(nil) func (prc *PingOneRegionCode) Set(regionCode string) error { if prc == nil { - return fmt.Errorf("failed to set PingOne Region Code value: %s. PingOne Region Code is nil", regionCode) + return &errs.PingCLIError{Prefix: pingOneRegionCodeErrorPrefix, Err: ErrCustomTypeNil} } switch { case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_AP): @@ -49,18 +56,21 @@ func (prc *PingOneRegionCode) Set(regionCode string) error { case strings.EqualFold(regionCode, ""): *prc = PingOneRegionCode("") default: - return fmt.Errorf("unrecognized PingOne Region Code: '%s'. Must be one of: %s", regionCode, strings.Join(PingOneRegionCodeValidValues(), ", ")) + return &errs.PingCLIError{Prefix: pingOneRegionCodeErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedPingOneRegionCode, regionCode, strings.Join(PingOneRegionCodeValidValues(), ", "))} } return nil } -func (prc PingOneRegionCode) Type() string { +func (prc *PingOneRegionCode) Type() string { return "string" } -func (prc PingOneRegionCode) String() string { - return string(prc) +func (prc *PingOneRegionCode) String() string { + if prc == nil { + return "" + } + return string(*prc) } func PingOneRegionCodeValidValues() []string { diff --git a/internal/customtypes/pingone_region_code_test.go b/internal/customtypes/pingone_region_code_test.go index cf1a711c..11e2f970 100644 --- a/internal/customtypes/pingone_region_code_test.go +++ b/internal/customtypes/pingone_region_code_test.go @@ -6,48 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test PingOneRegion Set function func Test_PingOneRegion_Set(t *testing.T) { - prc := new(customtypes.PingOneRegionCode) + testutils_koanf.InitKoanfs(t) - err := prc.Set(customtypes.ENUM_PINGONE_REGION_CODE_AP) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.PingOneRegionCode + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.PingOneRegionCode), + value: customtypes.ENUM_PINGONE_REGION_CODE_AP, + }, + { + name: "Happy path - empty", + cType: new(customtypes.PingOneRegionCode), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.PingOneRegionCode), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedPingOneRegionCode, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_PINGONE_REGION_CODE_AP, + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_PingOneRegion_Set_InvalidValue(t *testing.T) { - prc := new(customtypes.PingOneRegionCode) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" + err := tc.cType.Set(tc.value) - expectedErrorPattern := `^unrecognized PingOne Region Code: '.*'\. Must be one of: .*$` - err := prc.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_PingOneRegion_Set_Nil(t *testing.T) { - var prc *customtypes.PingOneRegionCode +func Test_PingOneRegion_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingOneRegionCode + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingOneRegionCode(customtypes.ENUM_PINGONE_REGION_CODE_AP)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := customtypes.ENUM_PINGONE_REGION_CODE_AP + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set PingOne Region Code value: .* PingOne Region Code is nil$` - err := prc.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_PingOneRegion_String(t *testing.T) { - pingoneRegion := customtypes.PingOneRegionCode(customtypes.ENUM_PINGONE_REGION_CODE_CA) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.PingOneRegionCode + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.PingOneRegionCode(customtypes.ENUM_PINGONE_REGION_CODE_CA)), + expectedStr: customtypes.ENUM_PINGONE_REGION_CODE_CA, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.PingOneRegionCode("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := customtypes.ENUM_PINGONE_REGION_CODE_CA - actual := pingoneRegion.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } diff --git a/internal/customtypes/request_services.go b/internal/customtypes/request_services.go index 38a13453..c68e490b 100644 --- a/internal/customtypes/request_services.go +++ b/internal/customtypes/request_services.go @@ -3,10 +3,12 @@ package customtypes import ( + "errors" "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) @@ -14,6 +16,11 @@ const ( ENUM_REQUEST_SERVICE_PINGONE string = "pingone" ) +var ( + requestServiceErrorPrefix = "custom type request service error" + ErrUnrecognizedService = errors.New("unrecognized request service") +) + type RequestService string // Verify that the custom type satisfies the pflag.Value interface @@ -22,7 +29,7 @@ var _ pflag.Value = (*RequestService)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter func (rs *RequestService) Set(service string) error { if rs == nil { - return fmt.Errorf("failed to set RequestService value: %s. RequestService is nil", service) + return &errs.PingCLIError{Prefix: requestServiceErrorPrefix, Err: ErrCustomTypeNil} } switch { @@ -31,18 +38,21 @@ func (rs *RequestService) Set(service string) error { case strings.EqualFold(service, ""): *rs = RequestService("") default: - return fmt.Errorf("unrecognized Request Service: '%s'. Must be one of: %s", service, strings.Join(RequestServiceValidValues(), ", ")) + return &errs.PingCLIError{Prefix: requestServiceErrorPrefix, Err: fmt.Errorf("%w: '%s'. Must be one of: %s", ErrUnrecognizedService, service, strings.Join(RequestServiceValidValues(), ", "))} } return nil } -func (rs RequestService) Type() string { +func (rs *RequestService) Type() string { return "string" } -func (rs RequestService) String() string { - return string(rs) +func (rs *RequestService) String() string { + if rs == nil { + return "" + } + return string(*rs) } func RequestServiceValidValues() []string { diff --git a/internal/customtypes/request_services_test.go b/internal/customtypes/request_services_test.go index ddac9e04..42ee4db9 100644 --- a/internal/customtypes/request_services_test.go +++ b/internal/customtypes/request_services_test.go @@ -6,44 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test Request Services Set function func Test_RequestServices_Set(t *testing.T) { - rs := new(customtypes.RequestService) + testutils_koanf.InitKoanfs(t) - service := customtypes.ENUM_REQUEST_SERVICE_PINGONE - err := rs.Set(service) - testutils.CheckExpectedError(t, err, nil) -} + testCases := []struct { + name string + cType *customtypes.RequestService + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.RequestService), + value: customtypes.ENUM_REQUEST_SERVICE_PINGONE, + }, + { + name: "Happy path - empty", + cType: new(customtypes.RequestService), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.RequestService), + value: "invalid", + expectedError: customtypes.ErrUnrecognizedService, + }, + { + name: "Nil custom type", + cType: nil, + value: customtypes.ENUM_REQUEST_SERVICE_PINGONE, + expectedError: customtypes.ErrCustomTypeNil, + }, + } -// Test Set function fails with invalid value -func Test_RequestServices_Set_InvalidValue(t *testing.T) { - rs := new(customtypes.RequestService) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" - expectedErrorPattern := `^unrecognized Request Service: '.*'\. Must be one of: .*$` - err := rs.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_RequestServices_Set_Nil(t *testing.T) { - var rs *customtypes.RequestService +func Test_RequestServices_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.RequestService + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE)), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - expectedErrorPattern := `^failed to set RequestService value: .*\. RequestService is nil$` - err := rs.Set(customtypes.ENUM_REQUEST_SERVICE_PINGONE) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_RequestServices_String(t *testing.T) { - rs := customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.RequestService + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE)), + expectedStr: customtypes.ENUM_REQUEST_SERVICE_PINGONE, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.RequestService("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := customtypes.ENUM_REQUEST_SERVICE_PINGONE - actual := rs.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } diff --git a/internal/customtypes/string.go b/internal/customtypes/string.go index 1718ac98..f16aec37 100644 --- a/internal/customtypes/string.go +++ b/internal/customtypes/string.go @@ -3,11 +3,14 @@ package customtypes import ( - "fmt" - + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + stringErrorPrefix = "custom type string error" +) + type String string // Verify that the custom type satisfies the pflag.Value interface @@ -15,7 +18,7 @@ var _ pflag.Value = (*String)(nil) func (s *String) Set(val string) error { if s == nil { - return fmt.Errorf("failed to set String value: %s. String is nil", val) + return &errs.PingCLIError{Prefix: stringErrorPrefix, Err: ErrCustomTypeNil} } *s = String(val) @@ -23,10 +26,14 @@ func (s *String) Set(val string) error { return nil } -func (s String) Type() string { +func (s *String) Type() string { return "string" } -func (s String) String() string { - return string(s) +func (s *String) String() string { + if s == nil { + return "" + } + + return string(*s) } diff --git a/internal/customtypes/string_slice.go b/internal/customtypes/string_slice.go index 9af9fef5..ee62ffda 100644 --- a/internal/customtypes/string_slice.go +++ b/internal/customtypes/string_slice.go @@ -3,13 +3,17 @@ package customtypes import ( - "fmt" "slices" "strings" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + stringSliceErrorPrefix = "custom type string slice error" +) + type StringSlice []string // Verify that the custom type satisfies the pflag.Value interface @@ -17,7 +21,7 @@ var _ pflag.Value = (*StringSlice)(nil) func (ss *StringSlice) Set(val string) error { if ss == nil { - return fmt.Errorf("failed to set StringSlice value: %s. StringSlice is nil", val) + return &errs.PingCLIError{Prefix: stringSliceErrorPrefix, Err: ErrCustomTypeNil} } if val == "" || val == "[]" { @@ -32,7 +36,7 @@ func (ss *StringSlice) Set(val string) error { func (ss *StringSlice) Remove(val string) (bool, error) { if ss == nil { - return false, fmt.Errorf("failed to remove StringSlice value: %s. StringSlice is nil", val) + return false, &errs.PingCLIError{Prefix: stringSliceErrorPrefix, Err: ErrCustomTypeNil} } if val == "" || val == "[]" { @@ -50,18 +54,22 @@ func (ss *StringSlice) Remove(val string) (bool, error) { return false, nil } -func (ss StringSlice) Type() string { +func (ss *StringSlice) Type() string { return "[]string" } -func (ss StringSlice) String() string { +func (ss *StringSlice) String() string { + if ss == nil { + return "" + } + return strings.Join(ss.StringSlice(), ",") } -func (ss StringSlice) StringSlice() []string { +func (ss *StringSlice) StringSlice() []string { if ss == nil { return []string{} } - return []string(ss) + return []string(*ss) } diff --git a/internal/customtypes/string_slice_test.go b/internal/customtypes/string_slice_test.go index aae33ccf..0c490dd7 100644 --- a/internal/customtypes/string_slice_test.go +++ b/internal/customtypes/string_slice_test.go @@ -6,48 +6,208 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test StringSlice Set function func Test_StringSlice_Set(t *testing.T) { - ss := new(customtypes.StringSlice) + testutils_koanf.InitKoanfs(t) - val := "value1,value2" - err := ss.Set(val) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.StringSlice + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.StringSlice), + value: "value1,value2", + }, + { + name: "Happy path - empty", + cType: new(customtypes.StringSlice), + value: "", + }, + { + name: "Nil custom type", + cType: nil, + value: "value1,value2", + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) } } -// Test Set function fails with nil -func Test_StringSlice_Set_Nil(t *testing.T) { - var ss *customtypes.StringSlice +func Test_StringSlice_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.StringSlice + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + expectedType: "[]string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "[]string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := "value1,value2" - expectedErrorPattern := `^failed to set StringSlice value: .* StringSlice is nil$` - err := ss.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_StringSlice_String(t *testing.T) { - ss := customtypes.StringSlice([]string{"value1", "value2"}) + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.StringSlice + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + expectedStr: "value1,value2", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.StringSlice([]string{})), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := "value1,value2" - actual := ss.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } -// Test StringSlice String function with empty slice -func Test_StringSlice_String_Empty(t *testing.T) { - ss := customtypes.StringSlice([]string{}) +func Test_StringSlice_StringSlice(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.StringSlice + expectedStrSlice []string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + expectedStrSlice: []string{"value1", "value2"}, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.StringSlice([]string{})), + expectedStrSlice: []string{}, + }, + { + name: "Nil custom type", + cType: nil, + expectedStrSlice: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStrSlice := tc.cType.StringSlice() + + require.Equal(t, tc.expectedStrSlice, actualStrSlice) + }) + } +} + +func Test_StringSlice_Remove(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.StringSlice + value string + expectedBool bool + expectedError error + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + value: "value1", + expectedBool: true, + }, + { + name: "Happy path - not found", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + value: "value3", + expectedBool: false, + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.StringSlice([]string{"value1", "value2"})), + value: "", + expectedBool: false, + }, + { + name: "Nil custom type", + cType: nil, + value: "value1", + expectedBool: false, + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualBool, err := tc.cType.Remove(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } - expected := "" - actual := ss.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedBool, actualBool) + }) } } diff --git a/internal/customtypes/string_test.go b/internal/customtypes/string_test.go new file mode 100644 index 00000000..cc16c608 --- /dev/null +++ b/internal/customtypes/string_test.go @@ -0,0 +1,122 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" +) + +func Test_String_Set(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.String + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.String), + value: "value", + }, + { + name: "Happy path - empty", + cType: new(customtypes.String), + value: "", + }, + { + name: "Nil custom type", + cType: nil, + value: "value", + expectedError: customtypes.ErrCustomTypeNil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := tc.cType.Set(tc.value) + + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_String_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.String + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.String("value")), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualType := tc.cType.Type() + + require.Equal(t, tc.expectedType, actualType) + }) + } +} + +func Test_String_String(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.String + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.String("value")), + expectedStr: "value", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.String("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() + + require.Equal(t, tc.expectedStr, actualStr) + }) + } +} diff --git a/internal/customtypes/uuid.go b/internal/customtypes/uuid.go index 8aae001b..a60e2c8a 100644 --- a/internal/customtypes/uuid.go +++ b/internal/customtypes/uuid.go @@ -3,12 +3,19 @@ package customtypes import ( + "errors" "fmt" "github.com/hashicorp/go-uuid" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/pflag" ) +var ( + uuidErrorPrefix = "custom type uuid error" + ErrInvalidUUID = errors.New("invalid uuid") +) + type UUID string // Verify that the custom type satisfies the pflag.Value interface @@ -16,7 +23,7 @@ var _ pflag.Value = (*UUID)(nil) func (u *UUID) Set(val string) error { if u == nil { - return fmt.Errorf("failed to set UUID value: %s. UUID is nil", val) + return &errs.PingCLIError{Prefix: uuidErrorPrefix, Err: ErrCustomTypeNil} } if val == "" { @@ -27,7 +34,7 @@ func (u *UUID) Set(val string) error { _, err := uuid.ParseUUID(val) if err != nil { - return err + return &errs.PingCLIError{Prefix: uuidErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrInvalidUUID, val, err)} } *u = UUID(val) diff --git a/internal/customtypes/uuid_test.go b/internal/customtypes/uuid_test.go index e46815cd..dad65b76 100644 --- a/internal/customtypes/uuid_test.go +++ b/internal/customtypes/uuid_test.go @@ -6,49 +6,123 @@ import ( "testing" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/pingidentity/pingcli/internal/utils" + "github.com/stretchr/testify/require" ) -// Test UUID Set function func Test_UUID_Set(t *testing.T) { - uuid := new(customtypes.UUID) + testutils_koanf.InitKoanfs(t) - val := "123e4567-e89b-12d3-a456-426614174000" - err := uuid.Set(val) - if err != nil { - t.Errorf("Set returned error: %v", err) + testCases := []struct { + name string + cType *customtypes.UUID + value string + expectedError error + }{ + { + name: "Happy path", + cType: new(customtypes.UUID), + value: "123e4567-e89b-12d3-a456-426614174000", + }, + { + name: "Happy path - empty", + cType: new(customtypes.UUID), + value: "", + }, + { + name: "Invalid value", + cType: new(customtypes.UUID), + value: "invalid", + expectedError: customtypes.ErrInvalidUUID, + }, + { + name: "Nil custom type", + cType: nil, + value: "123e4567-e89b-12d3-a456-426614174000", + expectedError: customtypes.ErrCustomTypeNil, + }, } -} -// Test Set function fails with invalid value -func Test_UUID_Set_InvalidValue(t *testing.T) { - uuid := new(customtypes.UUID) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - invalidValue := "invalid" + err := tc.cType.Set(tc.value) - expectedErrorPattern := `^uuid string is wrong length$` - err := uuid.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + }) + } } -// Test Set function fails with nil -func Test_UUID_Set_Nil(t *testing.T) { - var uuid *customtypes.UUID +func Test_UUID_Type(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.UUID + expectedType string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.UUID("123e4567-e89b-12d3-a456-426614174000")), + expectedType: "string", + }, + { + name: "Nil custom type", + cType: nil, + expectedType: "string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - val := "123e4567-e89b-12d3-a456-426614174000" + actualType := tc.cType.Type() - expectedErrorPattern := `^failed to set UUID value: .* UUID is nil$` - err := uuid.Set(val) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.Equal(t, tc.expectedType, actualType) + }) + } } -// Test String function func Test_UUID_String(t *testing.T) { - uuid := customtypes.UUID("123e4567-e89b-12d3-a456-426614174000") + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + cType *customtypes.UUID + expectedStr string + }{ + { + name: "Happy path", + cType: utils.Pointer(customtypes.UUID("123e4567-e89b-12d3-a456-426614174000")), + expectedStr: "123e4567-e89b-12d3-a456-426614174000", + }, + { + name: "Happy path - empty", + cType: utils.Pointer(customtypes.UUID("")), + expectedStr: "", + }, + { + name: "Nil custom type", + cType: nil, + expectedStr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + actualStr := tc.cType.String() - expected := "123e4567-e89b-12d3-a456-426614174000" - actual := uuid.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) + require.Equal(t, tc.expectedStr, actualStr) + }) } } From f209a0f5ad823b89dcb1cfafec2fde85e423daf1 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 30 Sep 2025 15:25:39 -0600 Subject: [PATCH 07/14] Complete new test framework --- cmd/common/cobra_utils.go | 12 +- cmd/completion/cmd.go | 21 +- cmd/completion/cmd_test.go | 68 +- cmd/config/add_profile.go | 5 +- cmd/config/add_profile_test.go | 195 +++-- cmd/config/config_test.go | 105 ++- cmd/config/delete_profile.go | 3 +- cmd/config/delete_profile_test.go | 101 ++- cmd/config/get.go | 3 +- cmd/config/get_test.go | 172 ++--- cmd/config/list_keys.go | 3 +- cmd/config/list_keys_test.go | 141 ++-- cmd/config/list_profiles.go | 3 +- cmd/config/list_profiles_test.go | 78 +- cmd/config/set.go | 3 +- cmd/config/set_active_profile.go | 3 +- cmd/config/set_active_profile_test.go | 114 +-- cmd/config/set_test.go | 158 ++-- cmd/config/unset.go | 3 +- cmd/config/unset_test.go | 134 ++-- cmd/config/view_profile.go | 3 +- cmd/config/view_profile_test.go | 103 ++- cmd/feedback/feedback_test.go | 91 ++- cmd/license/license.go | 3 +- cmd/license/license_test.go | 163 ++-- cmd/platform/export.go | 8 +- cmd/platform/export_test.go | 717 ++++++++---------- cmd/platform/platform_test.go | 66 +- cmd/plugin/add.go | 3 +- cmd/plugin/add_test.go | 152 ++-- cmd/plugin/list.go | 3 +- cmd/plugin/list_test.go | 78 +- cmd/plugin/plugin_test.go | 66 +- cmd/plugin/remove.go | 3 +- cmd/plugin/remove_test.go | 103 ++- cmd/request/request.go | 3 +- cmd/request/request_test.go | 264 +++---- cmd/root_test.go | 253 +++--- internal/commands/platform/export_internal.go | 2 +- internal/errs/pingcli_error.go | 18 +- internal/errs/pingcli_error_test.go | 108 +++ internal/input/input.go | 22 +- internal/input/input_test.go | 102 +-- internal/plugins/plugins.go | 178 +++-- internal/plugins/plugins_test.go | 176 +++++ internal/profiles/koanf.go | 2 +- internal/profiles/validate.go | 136 ++-- internal/profiles/validate_test.go | 202 +++-- internal/testing/testutils/stdio.go | 30 + .../testing/testutils_koanf/koanf_utils.go | 12 +- shared/grpc/pingcli_command_grpc_server.go | 4 + 51 files changed, 2590 insertions(+), 1809 deletions(-) create mode 100644 internal/errs/pingcli_error_test.go create mode 100644 internal/plugins/plugins_test.go create mode 100644 internal/testing/testutils/stdio.go diff --git a/cmd/common/cobra_utils.go b/cmd/common/cobra_utils.go index c06b0724..c1e3233a 100644 --- a/cmd/common/cobra_utils.go +++ b/cmd/common/cobra_utils.go @@ -3,15 +3,23 @@ package common import ( + "errors" "fmt" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/cobra" ) +var ( + argsErrorPrefix = "failed to execute command" + ErrExactArgs = errors.New("incorrect number of arguments") + ErrRangeArgs = errors.New("incorrect number of arguments") +) + func ExactArgs(numArgs int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != numArgs { - return fmt.Errorf("failed to execute '%s': command accepts %d arg(s), received %d", cmd.CommandPath(), numArgs, len(args)) + return &errs.PingCLIError{Prefix: argsErrorPrefix, Err: fmt.Errorf("%w: command accepts %d arg(s), received %d", ErrExactArgs, numArgs, len(args))} } return nil @@ -21,7 +29,7 @@ func ExactArgs(numArgs int) cobra.PositionalArgs { func RangeArgs(minArgs, maxArgs int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < minArgs || len(args) > maxArgs { - return fmt.Errorf("failed to execute '%s': command accepts %d to %d arg(s), received %d", cmd.CommandPath(), minArgs, maxArgs, len(args)) + return &errs.PingCLIError{Prefix: argsErrorPrefix, Err: fmt.Errorf("%w: command accepts %d to %d arg(s), received %d", ErrRangeArgs, minArgs, maxArgs, len(args))} } return nil diff --git a/cmd/completion/cmd.go b/cmd/completion/cmd.go index 0be3d49b..4ca0cc33 100644 --- a/cmd/completion/cmd.go +++ b/cmd/completion/cmd.go @@ -5,6 +5,7 @@ package completion import ( "fmt" + "github.com/pingidentity/pingcli/internal/errs" "github.com/spf13/cobra" ) @@ -53,13 +54,25 @@ PowerShell: func completionCmdRunE(cmd *cobra.Command, args []string) error { switch args[0] { case "bash": - _ = cmd.Root().GenBashCompletionV2(cmd.OutOrStdout(), true) + err := cmd.Root().GenBashCompletionV2(cmd.OutOrStdout(), true) + if err != nil { + return &errs.PingCLIError{Prefix: "", Err: err} + } case "zsh": - _ = cmd.Root().GenZshCompletion(cmd.OutOrStdout()) + err := cmd.Root().GenZshCompletion(cmd.OutOrStdout()) + if err != nil { + return &errs.PingCLIError{Prefix: "", Err: err} + } case "fish": - _ = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) + err := cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) + if err != nil { + return &errs.PingCLIError{Prefix: "", Err: err} + } case "powershell": - _ = cmd.Root().GenPowerShellCompletion(cmd.OutOrStdout()) + err := cmd.Root().GenPowerShellCompletion(cmd.OutOrStdout()) + if err != nil { + return &errs.PingCLIError{Prefix: "", Err: err} + } } return nil diff --git a/cmd/completion/cmd_test.go b/cmd/completion/cmd_test.go index 0732a306..cdb8e6f9 100644 --- a/cmd/completion/cmd_test.go +++ b/cmd/completion/cmd_test.go @@ -5,12 +5,70 @@ package completion_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Completion Command Executes without issue -func TestCompletionCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t) - testutils.CheckExpectedError(t, err, nil) +func Test_CompletionCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Too few arguments", + args: []string{}, + expectErr: true, + expectedErrContains: "accepts 1 arg(s), received 0", + }, + { + name: "Happy Path - bash", + args: []string{"bash"}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"bash", "extra-arg"}, + expectErr: true, + expectedErrContains: "accepts 1 arg(s), received 2", + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"completion"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/add_profile.go b/cmd/config/add_profile.go index 063198b3..2cac3e58 100644 --- a/cmd/config/add_profile.go +++ b/cmd/config/add_profile.go @@ -8,6 +8,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/profiles" "github.com/spf13/cobra" @@ -50,11 +51,11 @@ func configAddProfileRunE(cmd *cobra.Command, args []string) error { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } if err := config_internal.RunInternalConfigAddProfile(os.Stdin, koanfConfig); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/add_profile_test.go b/cmd/config/add_profile_test.go index 043d2b95..3112f5e1 100644 --- a/cmd/config/add_profile_test.go +++ b/cmd/config/add_profile_test.go @@ -5,91 +5,128 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" + config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test config add profile command executes without issue -func TestConfigAddProfileCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "test-profile", - "--description", "test description", - "--set-active=false") - testutils.CheckExpectedError(t, err, nil) -} - -// Test config add profile with multiple case-insensitive profile names -func TestConfigAddProfileCmd_CaseInsensitiveProfileNamesExecute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "same-profile", - "--description", "test description", - "--set-active=false") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "SAME-PROFILE", - "--description", "test description", - "--set-active=false") - testutils.CheckExpectedError(t, err, nil) -} - -// Test config add profile command fails when provided too many arguments -func TestConfigAddProfileCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config add-profile': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func Test_ConfigAddProfileCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test config add profile command fails when provided an invalid flag -func TestConfigAddProfileCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid-flag$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", "--invalid-flag") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test config add profile command fails when provided an invalid value for a flag -func TestConfigAddProfileCmd_InvalidFlagValue(t *testing.T) { - expectedErrorPattern := `^invalid argument ".*" for ".*" flag: strconv\.ParseBool: parsing ".*": invalid syntax$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", "--set-active=invalid-value") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, "test-profile", + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: false, + }, + { + name: "Happy Path - Profile names are case insensitive", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, "DEfAuLt", + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{ + "extra-arg", + }, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{ + "--invalid-flag", + }, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Invalid value for valid flag", + args: []string{ + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=invalid-value", + }, + expectErr: true, + expectedErrIs: customtypes.ErrParseBool, + }, + { + name: "Duplicate profile name", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, "default", + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameAlreadyExists, + }, + { + name: "Invalid profile name", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, "pname&*^*&^$&@!", + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameFormat, + }, + { + name: "Profile name is activeProfile", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, options.RootActiveProfileOption.KoanfKey, + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameSameAsActiveProfileKey, + }, + { + name: "Profile name is empty", + args: []string{ + "--" + options.ConfigAddProfileNameOption.CobraParamName, "", + "--" + options.ConfigAddProfileDescriptionOption.CobraParamName, "test description", + "--" + options.ConfigAddProfileSetActiveOption.CobraParamName + "=false", + }, + expectErr: true, + expectedErrIs: config_internal.ErrNoProfileProvided, + }, + } -// Test config add profile command fails when provided a duplicate profile name -func TestConfigAddProfileCmd_DuplicateProfileName(t *testing.T) { - expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'\. profile already exists$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "default", - "--description", "test description", - "--set-active=false") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test config add profile command fails when provided an invalid profile name -func TestConfigAddProfileCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "pname&*^*&^$&@!", - "--description", "test description", - "--set-active=false") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "add-profile"}, tc.args...)...) -// Test config add profile command fails when provided an invalid set-active value -func TestConfigAddProfileCmd_InvalidSetActiveValue(t *testing.T) { - expectedErrorPattern := `^invalid argument ".*" for "-s, --set-active" flag: strconv\.ParseBool: parsing ".*": invalid syntax$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "test-profile", - "--description", "test description", - "--set-active=invalid-value") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + if !tc.expectErr { + assert.NoError(t, err) + return + } -// Test config add profile command fails when using activeprofile as the profile name -func TestConfigSetCmd_InvalidAddActiveProfile(t *testing.T) { - expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'. name cannot be the same as the active profile key$` - err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", - "--name", "activeprofile", - "--description", "test description", - "--set-active=true") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index 73bb5214..69922940 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -5,62 +5,57 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test Config Command Executes without issue -func TestConfigCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config") - - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Command fails when provided invalid flag -func TestConfigCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Command --help, -h flag -func TestConfigCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "config", "-h") - testutils.CheckExpectedError(t, err, nil) +func Test_ConfigCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"config"}, tc.args...)...) + + if !tc.expectErr { + assert.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } - -// // Test Config Command fails when provided a profile name that does not exist -// func TestConfigCmd_ProfileDoesNotExist(t *testing.T) { -// expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*' profile does not exist$` -// err := testutils_cobra.ExecutePingcli(t, "config", -// "--profile", "nonexistent", -// "--name", "myprofile", -// "--description", "hello") - -// testutils.CheckExpectedError(t, err, &expectedErrorPattern) -// } - -// Test Config Command fails when attempting to update the active profile -// func TestConfigCmd_UpdateActiveProfile(t *testing.T) { -// expectedErrorPattern := `^failed to update profile '.*' name to: .*\. '.*' is the active profile and cannot be deleted$` -// err := testutils_cobra.ExecutePingcli(t, "config", -// "--profile", "default", -// "--name", "myprofile", -// "--description", "hello") - -// testutils.CheckExpectedError(t, err, &expectedErrorPattern) -// } - -// // Test Config Command fails when provided an invalid profile name -// func TestConfigCmd_InvalidProfileName(t *testing.T) { -// expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` -// err := testutils_cobra.ExecutePingcli(t, "config", -// "--profile", "production", -// "--name", "pname&*^*&^$&@!", -// "--description", "hello") - -// testutils.CheckExpectedError(t, err, &expectedErrorPattern) -// } diff --git a/cmd/config/delete_profile.go b/cmd/config/delete_profile.go index cd701eea..564adce9 100644 --- a/cmd/config/delete_profile.go +++ b/cmd/config/delete_profile.go @@ -9,6 +9,7 @@ import ( "github.com/pingidentity/pingcli/internal/autocompletion" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -48,7 +49,7 @@ func configDeleteProfileRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config delete-profile Subcommand Called.") if err := config_internal.RunInternalConfigDeleteProfile(args, os.Stdin); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/delete_profile_test.go b/cmd/config/delete_profile_test.go index 555c67c7..b6cd35e6 100644 --- a/cmd/config/delete_profile_test.go +++ b/cmd/config/delete_profile_test.go @@ -5,47 +5,78 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" ) -// Test Config delete-profile Command Executes without issue -func TestConfigDeleteProfileCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "--yes", "production") - testutils.CheckExpectedError(t, err, nil) -} +func Test_ConfigDeleteProfileCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config delete-profile Command fails when provided too many arguments -func TestConfigDeleteProfileCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute '.*': command accepts 0 to 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "extra-arg", "extra-arg2") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{"--yes", "production"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg", "extra-arg2"}, + expectErr: true, + expectedErrIs: common.ErrRangeArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Non-existent profile", + args: []string{"--yes", "non-existent"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + { + name: "Active profile", + args: []string{"--yes", "default"}, + expectErr: true, + expectedErrIs: profiles.ErrDeleteActiveProfile, + }, + { + name: "Invalid profile name", + args: []string{"--yes", "non-existent"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + } -// Test Config delete-profile Command fails when provided an invalid flag -func TestConfigDeleteProfileCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config delete-profile Command fails when provided an non-existent profile name -func TestConfigDeleteProfileCmd_NonExistentProfileName(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "--yes", "nonexistent") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "delete-profile"}, tc.args...)...) -// Test Config delete-profile Command fails when provided the active profile -func TestConfigDeleteProfileCmd_ActiveProfile(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: '.*' is the active profile and cannot be deleted$` - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "--yes", "default") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + if !tc.expectErr { + assert.NoError(t, err) + return + } -// Test Config delete-profile Command fails when provided an invalid profile name -func TestConfigDeleteProfileCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "delete-profile", "--yes", "pname&*^*&^$&@!") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/get.go b/cmd/config/get.go index 766b6cdc..55c93da4 100644 --- a/cmd/config/get.go +++ b/cmd/config/get.go @@ -6,6 +6,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -46,7 +47,7 @@ func configGetRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config Get Subcommand Called.") if err := config_internal.RunInternalConfigGet(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/get_test.go b/cmd/config/get_test.go index ca433eb4..7912ddb5 100644 --- a/cmd/config/get_test.go +++ b/cmd/config/get_test.go @@ -5,104 +5,84 @@ package config_test import ( "testing" + "github.com/pingidentity/pingcli/cmd/common" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config Get Command Executes without issue -func TestConfigGetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "get", "export") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Get Command fails when provided too many arguments -func TestConfigGetCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config get': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "config", "get", options.RootColorOption.KoanfKey, options.RootOutputFormatOption.KoanfKey) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Get Command Executes when provided a full key -func TestConfigGetCmd_FullKey(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "get", options.PingOneAuthenticationWorkerClientIDOption.KoanfKey) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Get Command Executes when provided a partial key -func TestConfigGetCmd_PartialKey(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "get", "service.pingOne") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Get Command fails when provided an invalid key -func TestConfigGetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to get configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` - err := testutils_cobra.ExecutePingcli(t, "config", "get", "pingcli.invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Get Command fails when provided an invalid flag -func TestConfigGetCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "get", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Get Command --help, -h flag -func TestConfigGetCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "get", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "config", "get", "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Get Command fails when provided no key -func TestConfigGetCmd_NoKey(t *testing.T) { - expectedErrorPattern := `^failed to execute '.*': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingcli(t, "config", "get") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// https://pkg.go.dev/testing#hdr-Examples -func Example_getEmptyMaskedValue() { - t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "get", options.RequestAccessTokenOption.KoanfKey) - - // Output: - // Configuration values for profile 'default' and key 'request.accessToken': - // request.accessToken= - // request.accessTokenExpiry=0 -} - -// https://pkg.go.dev/testing#hdr-Examples -func Example_getMaskedValue() { - t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "get", options.PingFederateBasicAuthPasswordOption.KoanfKey) - - // Output: - // Configuration values for profile 'default' and key 'service.pingFederate.authentication.basicAuth.password': - // service.pingFederate.authentication.basicAuth.password=******** -} - -// https://pkg.go.dev/testing#hdr-Examples -func Example_getUnmaskedValue() { - t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "get", options.RootColorOption.KoanfKey) - - // Output: - // Configuration values for profile 'default' and key 'noColor': - // noColor=true -} - -// https://pkg.go.dev/testing#hdr-Examples -func Example_get_UnmaskValuesFlag() { - t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "get", "--unmask-values", options.RequestAccessTokenOption.KoanfKey) - - // Output: - // Configuration values for profile 'default' and key 'request.accessToken': - // request.accessToken= - // request.accessTokenExpiry=0 +func Test_ConfigGetCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{"export"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{options.RootColorOption.KoanfKey, options.RootOutputFormatOption.KoanfKey}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Full key", + args: []string{options.PingOneAuthenticationWorkerClientIDOption.KoanfKey}, + expectErr: false, + }, + { + name: "Partial key", + args: []string{"service.pingOne"}, + expectErr: false, + }, + { + name: "Invalid key", + args: []string{"pingcli.invalid"}, + expectErr: true, + expectedErrIs: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "No key", + args: []string{}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "get"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/list_keys.go b/cmd/config/list_keys.go index 1ab580e3..c162dc62 100644 --- a/cmd/config/list_keys.go +++ b/cmd/config/list_keys.go @@ -6,6 +6,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -39,7 +40,7 @@ func configListKeysRunE(cmd *cobra.Command, args []string) error { err := config_internal.RunInternalConfigListKeys() if err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go index 450be55d..ebadf9ac 100644 --- a/cmd/config/list_keys_test.go +++ b/cmd/config/list_keys_test.go @@ -3,85 +3,92 @@ package config_test import ( + "slices" + "strings" "testing" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config List Keys Command Executes without issue -func TestConfigListKeysCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "list-keys") - testutils.CheckExpectedError(t, err, nil) -} +func Test_ConfigListKeysCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config List Keys YAML Command --yaml, -y flag -func TestConfigListKeysCmd_YAMLFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", "--yaml") - testutils.CheckExpectedError(t, err, nil) + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Happy Path - yaml flag", + args: []string{"--yaml"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } - err = testutils_cobra.ExecutePingcli(t, "config", "list-keys", "-y") - testutils.CheckExpectedError(t, err, nil) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config List Keys Command --help, -h flag -func TestConfigListKeysCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", "--help") - testutils.CheckExpectedError(t, err, nil) + output := testutils.CaptureStdout(func() { + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "list-keys"}, tc.args...)...) - err = testutils_cobra.ExecutePingcli(t, "config", "list-keys", "-h") - testutils.CheckExpectedError(t, err, nil) -} + if !tc.expectErr { + require.NoError(t, err) + return + } -// Test Config List Keys Command fails when provided too many arguments -func TestConfigListKeysCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config list-keys': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", options.RootColorOption.KoanfKey) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) -// https://pkg.go.dev/testing#hdr-Examples -func Example_listKeysValue() { - t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "list-keys") + if !tc.expectErr && !slices.Contains(tc.args, "--help") { + for _, option := range options.Options() { + if option == options.RootActiveProfileOption { + continue + } - // Output: - // Valid Keys: - // - activeProfile - // - description - // - detailedExitCode - // - export.format - // - export.outputDirectory - // - export.overwrite - // - export.pingOne.environmentID - // - export.serviceGroup - // - export.services - // - license.devopsKey - // - license.devopsUser - // - noColor - // - outputFormat - // - plugins - // - request.accessToken - // - request.accessTokenExpiry - // - request.fail - // - request.service - // - service.pingFederate.adminAPIPath - // - service.pingFederate.authentication.accessTokenAuth.accessToken - // - service.pingFederate.authentication.basicAuth.password - // - service.pingFederate.authentication.basicAuth.username - // - service.pingFederate.authentication.clientCredentialsAuth.clientID - // - service.pingFederate.authentication.clientCredentialsAuth.clientSecret - // - service.pingFederate.authentication.clientCredentialsAuth.scopes - // - service.pingFederate.authentication.clientCredentialsAuth.tokenURL - // - service.pingFederate.authentication.type - // - service.pingFederate.caCertificatePEMFiles - // - service.pingFederate.httpsHost - // - service.pingFederate.insecureTrustAllTLS - // - service.pingFederate.xBypassExternalValidationHeader - // - service.pingOne.authentication.type - // - service.pingOne.authentication.worker.clientID - // - service.pingOne.authentication.worker.clientSecret - // - service.pingOne.authentication.worker.environmentID - // - service.pingOne.regionCode + if slices.Contains(tc.args, "--yaml") { + assert.Contains(t, output, option.KoanfKey[strings.LastIndex(option.KoanfKey, ".")+1:]) + } else { + assert.Contains(t, output, option.KoanfKey) + } + } + } + }) + } } diff --git a/cmd/config/list_profiles.go b/cmd/config/list_profiles.go index 632c7950..559bd589 100644 --- a/cmd/config/list_profiles.go +++ b/cmd/config/list_profiles.go @@ -5,6 +5,7 @@ package config import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -34,7 +35,7 @@ func configListProfilesRunE(cmd *cobra.Command, args []string) error { err := config_internal.RunInternalConfigListProfiles() if err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/list_profiles_test.go b/cmd/config/list_profiles_test.go index bbb0d95d..7acfd4fd 100644 --- a/cmd/config/list_profiles_test.go +++ b/cmd/config/list_profiles_test.go @@ -5,35 +5,65 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config list-profiles Command Executes without issue -func TestConfigListProfilesCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "list-profiles") - testutils.CheckExpectedError(t, err, nil) -} +func Test_ConfigListProfilesCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config list-profiles Command fails when provided too many arguments -func TestConfigListProfilesCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config list-profiles': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "config", "list-profiles", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Config list-profiles Command fails when provided an invalid flag -func TestConfigListProfilesCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "list-profiles", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "list-profiles"}, tc.args...)...) -// Test Config list-profiles Command --help, -h flag -func TestConfigListProfilesCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "list-profiles", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "config", "list-profiles", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/set.go b/cmd/config/set.go index 890a97cf..afdd9512 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -6,6 +6,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -42,7 +43,7 @@ func configSetRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config set Subcommand Called.") if err := config_internal.RunInternalConfigSet(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/set_active_profile.go b/cmd/config/set_active_profile.go index bb0f0a64..1d091834 100644 --- a/cmd/config/set_active_profile.go +++ b/cmd/config/set_active_profile.go @@ -8,6 +8,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/autocompletion" config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -41,7 +42,7 @@ func configSetActiveProfileRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config set-active-profile Subcommand Called.") if err := config_internal.RunInternalConfigSetActiveProfile(args, os.Stdin); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/set_active_profile_test.go b/cmd/config/set_active_profile_test.go index 0684f5a6..ba536046 100644 --- a/cmd/config/set_active_profile_test.go +++ b/cmd/config/set_active_profile_test.go @@ -5,55 +5,83 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config set-active-profile Command Executes without issue -func TestConfigSetActiveProfileCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config set-active-profile Command fails when provided too many arguments -func TestConfigSetActiveProfileCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute '.*': command accepts 0 to 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "extra-arg", "extra-arg2") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config set-active-profile Command fails when provided an invalid flag -func TestConfigSetActiveProfileCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func Test_ConfigSetActiveProfileCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config set-active-profile Command fails when provided an non-existent profile name -func TestConfigSetActiveProfileCmd_NonExistentProfileName(t *testing.T) { - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "nonexistent") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{"production"}, + expectErr: false, + }, + { + name: "Happy Path - set active to current active", + args: []string{"default"}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg", "extra-arg2"}, + expectErr: true, + expectedErrIs: common.ErrRangeArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Non-existent profile", + args: []string{"nonexistent"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + { + name: "Invalid profile name format", + args: []string{"pname&*^*&^$&@!"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + } -// Test Config set-active-profile Command succeeds when provided the active profile -func TestConfigSetActiveProfileCmd_ActiveProfile(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "default") - testutils.CheckExpectedError(t, err, nil) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config set-active-profile Command fails when provided an invalid profile name -func TestConfigSetActiveProfileCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "pname&*^*&^$&@!") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "set-active-profile"}, tc.args...)...) -// Test Config set-active-profile Command --help, -h flag -func TestConfigSetActiveProfileCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "config", "set-active-profile", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index b5518200..1326b46a 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -6,95 +6,123 @@ import ( "fmt" "testing" + "github.com/pingidentity/pingcli/cmd/common" + config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/profiles" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config Set Command Executes without issue -func TestConfigSetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=false", options.RootColorOption.KoanfKey)) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Set Command Fails when provided too few arguments -func TestConfigSetCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config set': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingcli(t, "config", "set") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Set Command Fails when provided too many arguments -func TestConfigSetCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config set': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=false", options.RootColorOption.KoanfKey), fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey)) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Set Command Fails when an invalid key is provided -func TestConfigSetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: key 'pingcli\.invalid' is not recognized as a valid configuration key\.\s*Use 'pingcli config list-keys' to view all available keys` - err := testutils_cobra.ExecutePingcli(t, "config", "set", "pingcli.invalid=true") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Set Command Fails when an invalid value type is provided -func TestConfigSetCmd_InvalidValueType(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key '.*' must be a boolean\. Allowed .*: strconv\.ParseBool: parsing ".*": invalid syntax$` - err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=invalid", options.RootColorOption.KoanfKey)) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func Test_ConfigSetCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{fmt.Sprintf("%s=false", options.RootColorOption.KoanfKey)}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too few arguments", + args: []string{}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Too many arguments", + args: []string{fmt.Sprintf("%s=false", options.RootColorOption.KoanfKey), fmt.Sprintf("%s=true", options.RootColorOption.KoanfKey)}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid key", + args: []string{"pingcli.invalid=true"}, + expectErr: true, + expectedErrIs: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Invalid value type for key", + args: []string{fmt.Sprintf("%s=invalid", options.RootColorOption.KoanfKey)}, + expectErr: true, + expectedErrIs: customtypes.ErrParseBool, + }, + { + name: "No value provided", + args: []string{fmt.Sprintf("%s=", options.RootColorOption.KoanfKey)}, + expectErr: true, + expectedErrIs: config_internal.ErrEmptyValue, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Config Set Command Fails when no value is provided -func TestConfigSetCmd_NoValueProvided(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key '.*' is empty\. Use 'pingcli config unset .*' to unset the key$` - err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=", options.RootColorOption.KoanfKey)) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "set"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } -// Test Config Set Command for key 'pingone.worker.clientId' updates koanf configuration +// TestConfigSetCmd_CheckKoanfConfig verifies that the 'config set' command correctly updates the underlying koanf configuration. func TestConfigSetCmd_CheckKoanfConfig(t *testing.T) { + testutils_koanf.InitKoanfs(t) + koanfKey := options.PingOneAuthenticationWorkerClientIDOption.KoanfKey koanfNewUUID := "12345678-1234-1234-1234-123456789012" err := testutils_cobra.ExecutePingcli(t, "config", "set", fmt.Sprintf("%s=%s", koanfKey, koanfNewUUID)) - testutils.CheckExpectedError(t, err, nil) + require.NoError(t, err) koanfConfig, err := profiles.GetKoanfConfig() - if err != nil { - t.Errorf("Error getting koanf configuration: %v", err) - } + require.NoError(t, err, "Error getting koanf configuration") koanfInstance := koanfConfig.KoanfInstance() profileKoanfKey := "default." + koanfKey koanfNewValue, ok := koanfInstance.Get(profileKoanfKey).(*customtypes.UUID) - if ok && koanfNewValue.String() != koanfNewUUID { - t.Errorf("Expected koanf configuration value to be updated") - } -} - -// Test Config Set Command --help, -h flag -func TestConfigSetCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "set", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "config", "set", "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Set Command Fails when provided an invalid flag -func TestConfigSetCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "set", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + require.True(t, ok, "Koanf value is not of the expected type *customtypes.UUID") + assert.Equal(t, koanfNewUUID, koanfNewValue.String(), "Expected koanf configuration value to be updated") } // https://pkg.go.dev/testing#hdr-Examples func Example_setMaskedValue() { t := testing.T{} + testutils_koanf.InitKoanfs(&t) _ = testutils_cobra.ExecutePingcli(&t, "config", "set", fmt.Sprintf("%s=%s", options.PingFederateBasicAuthPasswordOption.KoanfKey, "1234")) // Output: @@ -105,7 +133,8 @@ func Example_setMaskedValue() { // https://pkg.go.dev/testing#hdr-Examples func Example_set_UnmaskedValuesFlag() { t := testing.T{} - _ = testutils_cobra.ExecutePingcli(&t, "config", "set", "--unmask-values", fmt.Sprintf("%s=%s", options.PingFederateBasicAuthPasswordOption.KoanfKey, "1234")) + testutils_koanf.InitKoanfs(&t) + _ = testutils_cobra.ExecutePingcli(&t, "config", "set", "--"+options.ConfigUnmaskSecretValueOption.CobraParamName, fmt.Sprintf("%s=%s", options.PingFederateBasicAuthPasswordOption.KoanfKey, "1234")) // Output: // SUCCESS: Configuration set successfully: @@ -115,6 +144,7 @@ func Example_set_UnmaskedValuesFlag() { // https://pkg.go.dev/testing#hdr-Examples func Example_setUnmaskedValue() { t := testing.T{} + testutils_koanf.InitKoanfs(&t) _ = testutils_cobra.ExecutePingcli(&t, "config", "set", fmt.Sprintf("%s=%s", options.RootColorOption.KoanfKey, "true")) // Output: diff --git a/cmd/config/unset.go b/cmd/config/unset.go index 40e0f880..793c646f 100644 --- a/cmd/config/unset.go +++ b/cmd/config/unset.go @@ -6,6 +6,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" config_internal "github.com/pingidentity/pingcli/internal/commands/config" "github.com/pingidentity/pingcli/internal/configuration" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -39,7 +40,7 @@ func configUnsetRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config unset Subcommand Called.") if err := config_internal.RunInternalConfigUnset(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index 85748d05..962a0f87 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -5,89 +5,114 @@ package config_test import ( "testing" + "github.com/pingidentity/pingcli/cmd/common" + "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/profiles" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config Unset Command Executes without issue -func TestConfigUnsetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "unset", options.RootColorOption.KoanfKey) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Set Command Fails when provided too few arguments -func TestConfigUnsetCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config unset': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingcli(t, "config", "unset") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func Test_ConfigUnsetCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config Set Command Fails when provided too many arguments -func TestConfigUnsetCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli config unset': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "config", "unset", options.RootColorOption.KoanfKey, options.RootOutputFormatOption.KoanfKey) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{options.RootColorOption.KoanfKey}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too few arguments", + args: []string{}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Too many arguments", + args: []string{options.RootColorOption.KoanfKey, options.RootOutputFormatOption.KoanfKey}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid key", + args: []string{"pingcli.invalid"}, + expectErr: true, + expectedErrIs: configuration.ErrInvalidConfigurationKey, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Config Unset Command Fails when an invalid key is provided -func TestConfigUnsetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key\.\s*Use 'pingcli config list-keys' to view all available keys` - err := testutils_cobra.ExecutePingcli(t, "config", "unset", "pingcli.invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "unset"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } -// Test Config Unset Command for key 'pingone.worker.clientId' updates koanf configuration +// TestConfigUnsetCmd_CheckKoanfConfig verifies that the 'config unset' command correctly updates the underlying koanf configuration. func TestConfigUnsetCmd_CheckKoanfConfig(t *testing.T) { testutils_koanf.InitKoanfs(t) koanfConfig, err := profiles.GetKoanfConfig() - if err != nil { - t.Errorf("Error getting koanf configuration: %v", err) - } + require.NoError(t, err, "Error getting koanf configuration") koanfInstance := koanfConfig.KoanfInstance() koanfKey := options.PingOneAuthenticationWorkerClientIDOption.KoanfKey profileKoanfKey := "default." + koanfKey - koanfOldValue := koanfInstance.String(profileKoanfKey) + // Ensure there is a value to unset + require.NotEmpty(t, koanfInstance.String(profileKoanfKey), "Precondition failed: koanf value is already empty") + // Execute the unset command err = testutils_cobra.ExecutePingcli(t, "config", "unset", koanfKey) - testutils.CheckExpectedError(t, err, nil) + require.NoError(t, err) + // Re-fetch the koanf instance to see the change + koanfConfig, err = profiles.GetKoanfConfig() + require.NoError(t, err, "Error getting koanf configuration") koanfInstance = koanfConfig.KoanfInstance() - koanfNewValue := koanfInstance.String(profileKoanfKey) - if koanfOldValue == koanfNewValue { - t.Errorf("Expected koanf configuration value to be updated. Old: %s, New: %s", koanfOldValue, koanfNewValue) - } - - if koanfNewValue != "" { - t.Errorf("Expected koanf configuration value to be empty. Got: %s", koanfNewValue) - } -} - -// Test Config Unset Command Fails when provided an invalid flag -func TestConfigUnsetCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "unset", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Unset Command --help, -h flag -func TestConfigUnsetCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "unset", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "config", "unset", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Empty(t, koanfNewValue, "Expected koanf configuration value to be empty after unset") } // https://pkg.go.dev/testing#hdr-Examples func Example_unsetMaskedValue() { t := testing.T{} + testutils_koanf.InitKoanfs(&t) _ = testutils_cobra.ExecutePingcli(&t, "config", "unset", options.PingFederateBasicAuthUsernameOption.KoanfKey) // Output: @@ -98,6 +123,7 @@ func Example_unsetMaskedValue() { // https://pkg.go.dev/testing#hdr-Examples func Example_unsetUnmaskedValue() { t := testing.T{} + testutils_koanf.InitKoanfs(&t) _ = testutils_cobra.ExecutePingcli(&t, "config", "unset", options.RootOutputFormatOption.KoanfKey) // Output: diff --git a/cmd/config/view_profile.go b/cmd/config/view_profile.go index 8047d732..51a9fe6d 100644 --- a/cmd/config/view_profile.go +++ b/cmd/config/view_profile.go @@ -6,6 +6,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/autocompletion" config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -42,7 +43,7 @@ func configViewProfileRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Config view-profile Subcommand Called.") if err := config_internal.RunInternalConfigViewProfile(args); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/config/view_profile_test.go b/cmd/config/view_profile_test.go index a70ef76e..6aa4d26e 100644 --- a/cmd/config/view_profile_test.go +++ b/cmd/config/view_profile_test.go @@ -5,44 +5,83 @@ package config_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" + "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Config Set Command Executes without issue -func TestConfigViewProfileCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile") - testutils.CheckExpectedError(t, err, nil) -} +func Test_ConfigViewProfileCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config Set Command Executes with a defined profile -func TestConfigViewProfileCmd_Execute_WithProfileFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile", "production") - testutils.CheckExpectedError(t, err, nil) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path - view active profile", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - view specified profile", + args: []string{"production"}, + expectErr: false, + }, + { + name: "Happy Path - with unmask-values flag", + args: []string{"--unmask-values"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"profile1", "profile2"}, + expectErr: true, + expectedErrIs: common.ErrRangeArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Non-existent profile", + args: []string{"non-existent"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + { + name: "Invalid profile name format", + args: []string{"(*&*(#))"}, + expectErr: true, + expectedErrIs: profiles.ErrProfileNameNotExist, + }, + } -func TestConfigViewProfileCmd_Execute_UnmaskValuesFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile", "--unmask-values") - testutils.CheckExpectedError(t, err, nil) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Config Set Command fails with invalid flag -func TestConfigViewProfileCmd_Execute_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"config", "view-profile"}, tc.args...)...) -// Test Config Set Command fails with non-existent profile -func TestConfigViewProfileCmd_Execute_NonExistentProfile(t *testing.T) { - expectedErrorPattern := `^failed to view profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile", "non-existent") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + if !tc.expectErr { + require.NoError(t, err) + return + } -// Test Config Set Command fails with invalid profile -func TestConfigViewProfileCmd_Execute_InvalidProfile(t *testing.T) { - expectedErrorPattern := `^failed to view profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingcli(t, "config", "view-profile", "(*&*(#))") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/feedback/feedback_test.go b/cmd/feedback/feedback_test.go index 45b97d07..93b27153 100644 --- a/cmd/feedback/feedback_test.go +++ b/cmd/feedback/feedback_test.go @@ -3,44 +3,77 @@ package feedback_test import ( + "fmt" "testing" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/configuration/options" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Feedback Command Executes without issue -func TestFeedbackCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "feedback") - testutils.CheckExpectedError(t, err, nil) -} +func Test_FeedbackCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Feedback Command fails when provided too many arguments -func TestFeedbackCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli feedback': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "feedback", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Happy Path - with profile flag", + args: []string{ + fmt.Sprintf("--%s", options.RootProfileOption.CobraParamName), + "default", + }, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Feedback Command help flag -func TestFeedbackCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "feedback", "--help") - testutils.CheckExpectedError(t, err, nil) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - err = testutils_cobra.ExecutePingcli(t, "feedback", "-h") - testutils.CheckExpectedError(t, err, nil) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"feedback"}, tc.args...)...) -// Test Feedback Command fails with invalid flag -func TestFeedbackCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "feedback", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + if !tc.expectErr { + require.NoError(t, err) + return + } -// Test Feedback Command with valid profile -func TestFeedbackCmd_Profile(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "feedback", "--"+options.RootProfileOption.CobraParamName, "default") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/license/license.go b/cmd/license/license.go index 7e09bedc..45e30b6b 100644 --- a/cmd/license/license.go +++ b/cmd/license/license.go @@ -8,6 +8,7 @@ import ( "github.com/pingidentity/pingcli/cmd/common" license_internal "github.com/pingidentity/pingcli/internal/commands/license" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" "github.com/spf13/cobra" @@ -56,7 +57,7 @@ func licenseRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("License Subcommand Called.") if err := license_internal.RunInternalLicense(); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/license/license_test.go b/cmd/license/license_test.go index 53acb633..1871b418 100644 --- a/cmd/license/license_test.go +++ b/cmd/license/license_test.go @@ -3,85 +3,106 @@ package license_test import ( + "fmt" "testing" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test License Command Executes without issue (with all required flags) -func TestLicenseCmd_Execute(t *testing.T) { +func Test_LicenseCommand(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := testutils_cobra.ExecutePingcli(t, "license", - "--"+options.LicenseProductOption.CobraParamName, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, - "--"+options.LicenseVersionOption.CobraParamName, "12.0") - testutils.CheckExpectedError(t, err, nil) -} - -// Test License Command fails when provided too many arguments -func TestLicenseCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli license': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "license", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test License Command help flag -func TestLicenseCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "license", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "license", "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test License Command fails with invalid flag -func TestLicenseCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "license", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test License Command fails when required product flag is missing -func TestLicenseCmd_MissingProductFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^required flag\(s\) "product" not set$` - err := testutils_cobra.ExecutePingcli(t, "license", - "--"+options.LicenseVersionOption.CobraParamName, "12.0") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test License Command fails when required version flag is missing -func TestLicenseCmd_MissingVersionFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^required flag\(s\) "version" not set$` - err := testutils_cobra.ExecutePingcli(t, "license", - "--"+options.LicenseProductOption.CobraParamName, "pingfederate") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test License Command with shorthand flags -func TestLicenseCmd_ShorthandFlags(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := testutils_cobra.ExecutePingcli(t, "license", - "-"+options.LicenseProductOption.Flag.Shorthand, "pingfederate", - "-"+options.LicenseVersionOption.Flag.Shorthand, "12.0") - testutils.CheckExpectedError(t, err, nil) -} - -// Test License Command with a profile -func TestLicenseCmd_Profile(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - err := testutils_cobra.ExecutePingcli(t, "license", - "--"+options.LicenseProductOption.CobraParamName, "pingfederate", - "--"+options.LicenseVersionOption.CobraParamName, "12.0", - "--"+options.RootProfileOption.CobraParamName, "default") - testutils.CheckExpectedError(t, err, nil) + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{ + "--" + options.LicenseProductOption.CobraParamName, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "--" + options.LicenseVersionOption.CobraParamName, "12.0", + }, + expectErr: false, + }, + { + name: "Happy Path - shorthand flags", + args: []string{ + "-" + options.LicenseProductOption.Flag.Shorthand, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "-" + options.LicenseVersionOption.Flag.Shorthand, "12.0", + }, + expectErr: false, + }, + { + name: "Happy Path - with profile flag", + args: []string{ + "--" + options.LicenseProductOption.CobraParamName, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "--" + options.LicenseVersionOption.CobraParamName, "12.0", + "--" + options.RootProfileOption.CobraParamName, "default", + }, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Missing required product flag", + args: []string{ + "--" + options.LicenseVersionOption.CobraParamName, "12.0", + }, + expectErr: true, + expectedErrContains: fmt.Sprintf(`required flag(s) "%s" not set`, options.LicenseProductOption.CobraParamName), + }, + { + name: "Missing required version flag", + args: []string{ + "--" + options.LicenseProductOption.CobraParamName, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + }, + expectErr: true, + expectedErrContains: fmt.Sprintf(`required flag(s) "%s" not set`, options.LicenseVersionOption.CobraParamName), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"license"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/platform/export.go b/cmd/platform/export.go index ae696486..40867e1c 100644 --- a/cmd/platform/export.go +++ b/cmd/platform/export.go @@ -9,6 +9,7 @@ import ( "github.com/pingidentity/pingcli/internal/autocompletion" platform_internal "github.com/pingidentity/pingcli/internal/commands/platform" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" "github.com/spf13/cobra" @@ -89,7 +90,12 @@ func exportRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Platform Export Subcommand Called.") - return platform_internal.RunInternalExport(cmd.Context(), cmd.Root().Version) + err := platform_internal.RunInternalExport(cmd.Context(), cmd.Root().Version) + if err != nil { + return &errs.PingCLIError{Prefix: "", Err: err} + } + + return nil } func initGeneralExportFlags(cmd *cobra.Command) { diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go index 080404c6..e2379cc6 100644 --- a/cmd/platform/export_test.go +++ b/cmd/platform/export_test.go @@ -4,408 +4,343 @@ package platform_test import ( "os" + "strings" "testing" + "github.com/pingidentity/pingcli/cmd/common" + platform_internal "github.com/pingidentity/pingcli/internal/commands/platform" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Platform Export Command Executes without issue -func TestPlatformExportCmd_Execute(t *testing.T) { +func Test_PlatformExportCommand(t *testing.T) { testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command fails when provided too many arguments -func TestPlatformExportCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli platform export': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when provided invalid flag -func TestPlatformExportCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --help, -h flag -func TestPlatformExportCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "platform", "export", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "platform", "export", "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --service-group, -g flag -func TestPlatformExportCmd_ServiceGroupFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceGroupOption.CobraParamName, "pingone") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --service-group with non-supported service group -func TestPlatformExportCmd_ServiceGroupFlagInvalidServiceGroup(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid argument ".*" for "-g, --service-group" flag: unrecognized service group '.*'\. Must be one of: .*$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportServiceGroupOption.CobraParamName, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --services flag -func TestPlatformExportCmd_ServicesFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --services flag with invalid service -func TestPlatformExportCmd_ServicesFlagInvalidService(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid argument ".*" for "-s, --services" flag: failed to set ExportServices: Invalid service: .*\. Allowed services: .*$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportServiceOption.CobraParamName, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --format flag -func TestPlatformExportCmd_ExportFormatFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportExportFormatOption.CobraParamName, customtypes.ENUM_EXPORT_FORMAT_HCL, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --format flag with invalid format -func TestPlatformExportCmd_ExportFormatFlagInvalidFormat(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid argument ".*" for "-f, --format" flag: unrecognized export format '.*'\. Must be one of: .*$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportExportFormatOption.CobraParamName, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --output-directory flag -func TestPlatformExportCmd_OutputDirectoryFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --output-directory flag with invalid directory -func TestPlatformExportCmd_OutputDirectoryFlagInvalidDirectory(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to create output directory '\/invalid': mkdir \/invalid: .+$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, "/invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --overwrite flag -func TestPlatformExportCmd_OverwriteFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command --overwrite flag false with existing directory -// where the directory already contains a file -func TestPlatformExportCmd_OverwriteFlagFalseWithExistingDirectory(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - _, err := os.Create(outputDir + "/file") //#nosec G304 -- this is a test - if err != nil { - t.Errorf("Error creating file in output directory: %v", err) + testCases := []struct { + name string + args []string + setup func(t *testing.T, tempDir string) + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path - minimal flags", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + }, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Happy path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Happy Path - with service group", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceGroupOption.CobraParamName, "pingone", + }, + expectErr: false, + }, + { + name: "Invalid service group", + args: []string{ + "--" + options.PlatformExportServiceGroupOption.CobraParamName, "invalid", + }, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognisedServiceGroup, + }, + { + name: "Happy Path - with specific service", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + }, + expectErr: false, + }, + { + name: "Happy Path - with specific service and format", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + "--" + options.PlatformExportExportFormatOption.CobraParamName, customtypes.ENUM_EXPORT_FORMAT_HCL, + }, + expectErr: false, + }, + { + name: "Invalid service", + args: []string{ + "--" + options.PlatformExportServiceOption.CobraParamName, "invalid", + }, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognisedExportService, + }, + { + name: "Invalid format", + args: []string{ + "--" + options.PlatformExportExportFormatOption.CobraParamName, "invalid", + }, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognisedFormat, + }, + { + name: "Invalid output directory", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "/invalid-dir", + }, + expectErr: true, + expectedErrIs: platform_internal.ErrCreateOutputDirectory, + }, + { + name: "Overwrite false on non-empty directory", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName + "=false", + }, + setup: func(t *testing.T, tempDir string) { + _, err := os.Create(tempDir + "/file") + require.NoError(t, err) + }, + expectErr: true, + expectedErrIs: platform_internal.ErrOutputDirectoryNotEmpty, + }, + { + name: "Happy Path - overwrite non-empty directory", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + }, + setup: func(t *testing.T, tempDir string) { + _, err := os.Create(tempDir + "/file") + require.NoError(t, err) + }, + expectErr: false, + }, + { + name: "Happy Path - with pingone service and all required flags", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + "--" + options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID"), + "--" + options.PingOneAuthenticationWorkerClientIDOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_ID"), + "--" + options.PingOneAuthenticationWorkerClientSecretOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_SECRET"), + "--" + options.PingOneRegionCodeOption.CobraParamName, os.Getenv("TEST_PINGONE_REGION_CODE"), + }, + expectErr: false, + }, + { + name: "PingOne flags not together", + args: []string{ + "--" + options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID"), + }, + expectErr: true, + expectedErrContains: "if any flags in the group [pingone-worker-environment-id pingone-worker-client-id pingone-worker-client-secret pingone-region-code] are set they must all be set", + }, + { + name: "Happy Path - with pingfederate service and all required flags for Basic Auth", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: false, + }, + { + name: "PingFederate Basic Auth flags not together", + args: []string{ + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + }, + expectErr: true, + expectedErrContains: "if any flags in the group [pingfederate-username pingfederate-password] are set they must all be set", + }, + { + name: "Pingone export fails with invalid credentials", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + "--" + options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID"), + "--" + options.PingOneAuthenticationWorkerClientIDOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_ID"), + "--" + options.PingOneAuthenticationWorkerClientSecretOption.CobraParamName, "invalid", + "--" + options.PingOneRegionCodeOption.CobraParamName, os.Getenv("TEST_PINGONE_REGION_CODE"), + }, + expectErr: true, + expectedErrIs: platform_internal.ErrPingOneInit, + }, + { + name: "Pingfederate export fails with invalid credentials", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "invalid", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: true, + expectedErrIs: platform_internal.ErrPingFederateInit, + }, + { + name: "Pingfederate Client Credentials Auth flags not together", + args: []string{ + "--" + options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test", + }, + expectErr: true, + expectedErrContains: "if any flags in the group [pingfederate-client-id pingfederate-client-secret pingfederate-token-url] are set they must all be set", + }, + { + name: "Pignfederate export fails with invalid Client Credentials Auth credentials", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test", + "--" + options.PingFederateClientCredentialsAuthClientSecretOption.CobraParamName, "invalid", + "--" + options.PingFederateClientCredentialsAuthTokenURLOption.CobraParamName, "https://localhost:9031/as/token.oauth2", + "--" + options.PingFederateClientCredentialsAuthScopesOption.CobraParamName, "email", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS, + }, + expectErr: true, + expectedErrIs: platform_internal.ErrPingFederateInit, + }, + { + name: "Pingfederate export fails with invalid client credentials auth token URL", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test", + "--" + options.PingFederateClientCredentialsAuthClientSecretOption.CobraParamName, "2FederateM0re!", + "--" + options.PingFederateClientCredentialsAuthTokenURLOption.CobraParamName, "https://localhost:9031/as/invalid", + "--" + options.PingFederateClientCredentialsAuthScopesOption.CobraParamName, "email", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS, + }, + expectErr: true, + expectedErrIs: platform_internal.ErrPingFederateInit, + }, + { + name: "Happy path - pingfederate with X-Bypass Header flag set to true", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateXBypassExternalValidationHeaderOption.CobraParamName, + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: false, + }, + { + name: "Happy path - pingfederate with Trust All TLS flag set to true", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateInsecureTrustAllTLSOption.CobraParamName, + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: false, + }, + { + name: "Pingfederate export fails with Trust All TLS flag set to false", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateInsecureTrustAllTLSOption.CobraParamName + "=false", + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: true, + expectedErrIs: platform_internal.ErrPingFederateInit, + }, + { + name: "Happy path - pingfederate with CA certificate PEM files flag set", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateCACertificatePemFilesOption.CobraParamName, "testdata/ssl-server-crt.pem", + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: false, + }, + { + name: "Pingfederate export fails with CA certificate PEM files flag set to invalid file", + args: []string{ + "--" + options.PlatformExportOutputDirectoryOption.CobraParamName, "{{tempdir}}", + "--" + options.PlatformExportOverwriteOption.CobraParamName, + "--" + options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, + "--" + options.PingFederateCACertificatePemFilesOption.CobraParamName, "invalid/crt.pem", + "--" + options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", + "--" + options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", + "--" + options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + }, + expectErr: true, + expectedErrIs: platform_internal.ErrReadCaCertPemFile, + }, } - expectedErrorPattern := `^output directory '[A-Za-z0-9_\-\/]+' is not empty\. Use --overwrite to overwrite existing export data$` - err = testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, - "--"+options.PlatformExportOverwriteOption.CobraParamName+"=false") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command --overwrite flag true with existing directory -// where the directory already contains a file -func TestPlatformExportCmd_OverwriteFlagTrueWithExistingDirectory(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - _, err := os.Create(outputDir + "/file") //#nosec G304 -- this is a test - if err != nil { - t.Errorf("Error creating file in output directory: %v", err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + tempDir := t.TempDir() + finalArgs := make([]string, len(tc.args)) + for i, arg := range tc.args { + finalArgs[i] = strings.ReplaceAll(arg, "{{tempdir}}", tempDir) + } + + if tc.setup != nil { + tc.setup(t, tempDir) + } + + err := testutils_cobra.ExecutePingcli(t, append([]string{"platform", "export"}, finalArgs...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) } - - err = testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, - "--"+options.PlatformExportOverwriteOption.CobraParamName) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command with -// --pingone-worker-environment-id flag -// --pingone-worker-client-id flag -// --pingone-worker-client-secret flag -// --pingone-region flag -func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, - "--"+options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID"), - "--"+options.PingOneAuthenticationWorkerClientIDOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_ID"), - "--"+options.PingOneAuthenticationWorkerClientSecretOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_SECRET"), - "--"+options.PingOneRegionCodeOption.CobraParamName, os.Getenv("TEST_PINGONE_REGION_CODE")) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command fails when not provided required pingone flags together -func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlagRequiredTogether(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^if any flags in the group \[pingone-worker-environment-id pingone-worker-client-id pingone-worker-client-secret pingone-region-code] are set they must all be set; missing \[pingone-region-code pingone-worker-client-id pingone-worker-client-secret]$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command with PingFederate Basic Auth flags -func TestPlatformExportCmd_PingFederateBasicAuthFlags(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export Command fails when not provided required PingFederate Basic Auth flags together -func TestPlatformExportCmd_PingFederateBasicAuthFlagsRequiredTogether(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^if any flags in the group \[pingfederate-username pingfederate-password] are set they must all be set; missing \[pingfederate-password]$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when provided invalid PingOne Client Credential flags -func TestPlatformExportCmd_PingOneClientCredentialFlagsInvalid(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to initialize pingone API client\. Check worker client ID, worker client secret, worker environment ID, and pingone region code configuration values\. oauth2: \"invalid_client\" \"Request denied: Unsupported authentication method \(Correlation ID: .*\)\"$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, - "--"+options.PingOneAuthenticationWorkerEnvironmentIDOption.CobraParamName, os.Getenv("TEST_PINGONE_ENVIRONMENT_ID"), - "--"+options.PingOneAuthenticationWorkerClientIDOption.CobraParamName, os.Getenv("TEST_PINGONE_WORKER_CLIENT_ID"), - "--"+options.PingOneAuthenticationWorkerClientSecretOption.CobraParamName, "invalid", - "--"+options.PingOneRegionCodeOption.CobraParamName, os.Getenv("TEST_PINGONE_REGION_CODE"), - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when provided invalid PingFederate Basic Auth flags -func TestPlatformExportCmd_PingFederateBasicAuthFlagsInvalid(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to initialize PingFederate Go Client. Check authentication type and credentials$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "invalid", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when not provided required PingFederate Client Credentials Auth flags together -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsRequiredTogether(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^if any flags in the group \[pingfederate-client-id pingfederate-client-secret pingfederate-token-url] are set they must all be set; missing \[pingfederate-client-secret pingfederate-token-url]$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when provided invalid PingFederate Client Credentials Auth flags -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalid(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to initialize PingFederate Go Client. Check authentication type and credentials$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test", - "--"+options.PingFederateClientCredentialsAuthClientSecretOption.CobraParamName, "invalid", - "--"+options.PingFederateClientCredentialsAuthTokenURLOption.CobraParamName, "https://localhost:9031/as/token.oauth2", - "--"+options.PingFederateClientCredentialsAuthScopesOption.CobraParamName, "email", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS, - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export Command fails when provided invalid PingFederate OAuth2 Token URL -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalidTokenURL(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to initialize PingFederate Go Client. Check authentication type and credentials$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateClientCredentialsAuthClientIDOption.CobraParamName, "test", - "--"+options.PingFederateClientCredentialsAuthClientSecretOption.CobraParamName, "2FederateM0re!", - "--"+options.PingFederateClientCredentialsAuthTokenURLOption.CobraParamName, "https://localhost:9031/as/invalid", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS, - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command with PingFederate X-Bypass Header set to true -func TestPlatformExportCmd_PingFederateXBypassHeaderFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateXBypassExternalValidationHeaderOption.CobraParamName, - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export command with PingFederate --pingfederate-insecure-trust-all-tls flag set to true -func TestPlatformExportCmd_PingFederateTrustAllTLSFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateInsecureTrustAllTLSOption.CobraParamName, - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export command fails with PingFederate --pingfederate-insecure-trust-all-tls flag set to false -func TestPlatformExportCmd_PingFederateTrustAllTLSFlagFalse(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to initialize PingFederate Go Client. Check authentication type and credentials$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateInsecureTrustAllTLSOption.CobraParamName+"=false", - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command passes with PingFederate -// --pingfederate-insecure-trust-all-tls=false -// and --pingfederate-ca-certificate-pem-files set -func TestPlatformExportCmd_PingFederateCaCertificatePemFiles(t *testing.T) { - testutils_koanf.InitKoanfs(t) - outputDir := t.TempDir() - - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir, - "--"+options.PlatformExportOverwriteOption.CobraParamName, - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateInsecureTrustAllTLSOption.CobraParamName+"=true", - "--"+options.PingFederateCACertificatePemFilesOption.CobraParamName, "testdata/ssl-server-crt.pem", - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Platform Export command fails with --pingfederate-ca-certificate-pem-files set to non-existent file. -func TestPlatformExportCmd_PingFederateCaCertificatePemFilesInvalid(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^failed to read CA certificate PEM file '.*': open .*: no such file or directory$` - err := testutils_cobra.ExecutePingcli(t, "platform", "export", - "--"+options.PlatformExportServiceOption.CobraParamName, customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE, - "--"+options.PingFederateCACertificatePemFilesOption.CobraParamName, "invalid/crt.pem", - "--"+options.PingFederateBasicAuthUsernameOption.CobraParamName, "Administrator", - "--"+options.PingFederateBasicAuthPasswordOption.CobraParamName, "2FederateM0re", - "--"+options.PingFederateAuthenticationTypeOption.CobraParamName, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/cmd/platform/platform_test.go b/cmd/platform/platform_test.go index 6e03ee8c..1de416c8 100644 --- a/cmd/platform/platform_test.go +++ b/cmd/platform/platform_test.go @@ -5,28 +5,58 @@ package platform_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Platform Command Executes without issue -func TestPlatformCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "platform") - testutils.CheckExpectedError(t, err, nil) -} +func Test_PlatformCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Platform Command fails when provided invalid flag -func TestPlatformCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "platform", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"platform"}, tc.args...)...) -// Test Platform Command --help, -h flag -func TestPlatformCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "platform", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "platform", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/plugin/add.go b/cmd/plugin/add.go index c4c74bda..32f90e4b 100644 --- a/cmd/plugin/add.go +++ b/cmd/plugin/add.go @@ -5,6 +5,7 @@ package plugin import ( "github.com/pingidentity/pingcli/cmd/common" plugin_internal "github.com/pingidentity/pingcli/internal/commands/plugin" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func pluginAddRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Plugin Add Subcommand Called.") if err := plugin_internal.RunInternalPluginAdd(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/plugin/add_test.go b/cmd/plugin/add_test.go index ff083a68..295c821b 100644 --- a/cmd/plugin/add_test.go +++ b/cmd/plugin/add_test.go @@ -4,82 +4,110 @@ package plugin_test import ( "os" + "path/filepath" "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" + plugin_internal "github.com/pingidentity/pingcli/internal/commands/plugin" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Plugin add Command Executes without issue -func TestPluginAddCmd_Execute(t *testing.T) { - // Create a temporary PATH for a test plugin +func Test_PluginAddCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + goldenPlugin := createGoldenPluginExecutable(t) + pluginFilename := filepath.Base(goldenPlugin) + require.FileExists(t, goldenPlugin, "Test plugin executable does not exist") + + defer os.Remove(goldenPlugin) + + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{pluginFilename}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Non-existent plugin", + args: []string{ + "non-existent-plugin", + }, + expectErr: true, + expectedErrIs: plugin_internal.ErrPluginNotFound, + }, + { + name: "Too many arguments", + args: []string{"arg", "extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"test-plugin-name", "--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Too few arguments", + args: []string{}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"plugin", "add"}, tc.args...)...) + + if !tc.expectErr { + require.NoError(t, err) + return + } + + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.Contains(t, err.Error(), tc.expectedErrContains) + } + }) + } +} + +func createGoldenPluginExecutable(t *testing.T) string { + t.Helper() + pathDir := t.TempDir() t.Setenv("PATH", pathDir) testPlugin, err := os.CreateTemp(pathDir, "test-plugin-*.sh") - if err != nil { - t.Fatalf("Failed to create temporary plugin file: %v", err) - } - - defer func() { - err = os.Remove(testPlugin.Name()) - if err != nil { - t.Fatalf("Failed to remove temporary plugin file: %v", err) - } - }() + require.NoError(t, err, "Failed to create temporary plugin file") _, err = testPlugin.WriteString("#!/usr/bin/env sh\necho \"Hello, world!\"\nexit 0\n") - if err != nil { - t.Fatalf("Failed to write to temporary plugin file: %v", err) - } + require.NoError(t, err, "Failed to write to temporary plugin file") err = testPlugin.Chmod(0755) - if err != nil { - t.Fatalf("Failed to set permissions on temporary plugin file: %v", err) - } + require.NoError(t, err, "Failed to set permissions on temporary plugin file") err = testPlugin.Close() - if err != nil { - t.Fatalf("Failed to close temporary plugin file: %v", err) - } - - err = testutils_cobra.ExecutePingcli(t, "plugin", "add", testPlugin.Name()) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Plugin add Command fails when provided a non-existent plugin -func TestPluginAddCmd_NonExistentPlugin(t *testing.T) { - expectedErrorPattern := `^failed to add plugin: exec: .*: executable file not found in \$PATH$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "add", "non-existent-plugin") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Plugin add Command fails when provided too many arguments -func TestPluginAddCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli plugin add': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "add", "test-plugin-name", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Plugin add Command fails when provided too few arguments -func TestPluginAddCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli plugin add': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "add") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Plugin add Command fails when provided an invalid flag -func TestPluginAddCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "add", "test-plugin-name", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Plugin add Command --help, -h flag -func TestPluginAddCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "add", "--help") - testutils.CheckExpectedError(t, err, nil) + require.NoError(t, err, "Failed to close temporary plugin file") - err = testutils_cobra.ExecutePingcli(t, "plugin", "add", "-h") - testutils.CheckExpectedError(t, err, nil) + return testPlugin.Name() } diff --git a/cmd/plugin/list.go b/cmd/plugin/list.go index 7618f2a6..63ca4211 100644 --- a/cmd/plugin/list.go +++ b/cmd/plugin/list.go @@ -5,6 +5,7 @@ package plugin import ( "github.com/pingidentity/pingcli/cmd/common" plugin_internal "github.com/pingidentity/pingcli/internal/commands/plugin" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func pluginListRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Plugin List Subcommand Called.") if err := plugin_internal.RunInternalPluginList(); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/plugin/list_test.go b/cmd/plugin/list_test.go index 204f9f32..d2a0c267 100644 --- a/cmd/plugin/list_test.go +++ b/cmd/plugin/list_test.go @@ -5,35 +5,65 @@ package plugin_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Plugin list Command Executes without issue -func TestPluginListCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "list") - testutils.CheckExpectedError(t, err, nil) -} +func Test_PluginListCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Plugin list Command fails when provided too many arguments -func TestPluginListCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli plugin list': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "list", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Plugin list Command fails when provided an invalid flag -func TestPluginListCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "list", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"plugin", "list"}, tc.args...)...) -// Test Plugin list Command --help, -h flag -func TestPluginListCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "list", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "plugin", "list", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/plugin/plugin_test.go b/cmd/plugin/plugin_test.go index 9739c203..e626bef0 100644 --- a/cmd/plugin/plugin_test.go +++ b/cmd/plugin/plugin_test.go @@ -5,28 +5,58 @@ package plugin_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Plugin Command Executes without issue -func TestPluginCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin") - testutils.CheckExpectedError(t, err, nil) -} +func Test_PluginCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Plugin Command fails when provided invalid flag -func TestPluginCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, append([]string{"plugin"}, tc.args...)...) -// Test Plugin Command --help, -h flag -func TestPluginCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "plugin", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/plugin/remove.go b/cmd/plugin/remove.go index 3e00ca10..9a3d68f2 100644 --- a/cmd/plugin/remove.go +++ b/cmd/plugin/remove.go @@ -5,6 +5,7 @@ package plugin import ( "github.com/pingidentity/pingcli/cmd/common" plugin_internal "github.com/pingidentity/pingcli/internal/commands/plugin" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func pluginRemoveRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Plugin Remove Subcommand Called.") if err := plugin_internal.RunInternalPluginRemove(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/plugin/remove_test.go b/cmd/plugin/remove_test.go index b9294375..05ed94a2 100644 --- a/cmd/plugin/remove_test.go +++ b/cmd/plugin/remove_test.go @@ -5,51 +5,76 @@ package plugin_test import ( "testing" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/cmd/common" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Plugin remove Command Executes without issue -func TestPluginRemoveCmd_Execute(t *testing.T) { - t.SkipNow() +func Test_PluginRemoveCommand(t *testing.T) { + testutils_koanf.InitKoanfs(t) - // TODO: A test plugin that responds with a valid RPC configuration is needed - // for pingcli to execute when the plugin is listed as used in pingcli. - // We can probably use a future plugin for testing once made. -} - -// Test Plugin remove Command succeeds when provided a non-existent plugin -func TestPluginRemoveCmd_NonExistentPlugin(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "remove", "non-existent-plugin") - testutils.CheckExpectedError(t, err, nil) -} + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + // { TODO: A test plugin that can be installed is needed to properly test removal. + // name: "Happy Path - remove existing plugin", + // args: []string{"existing-plugin"}, + // expectErr: false, + // }, + { + name: "Happy Path - remove non-existent plugin", + args: []string{"non-existent-plugin"}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"plugin-name", "extra-arg"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Too few arguments", + args: []string{}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"plugin-name", "--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + } -// Test Plugin remove Command fails when provided too many arguments -func TestPluginRemoveCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli plugin remove': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "remove", "test-plugin-name", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Plugin remove Command fails when provided too few arguments -func TestPluginRemoveCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingcli plugin remove': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "remove") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Plugin remove Command fails when provided an invalid flag -func TestPluginRemoveCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "plugin", "remove", "test-plugin-name", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + err := testutils_cobra.ExecutePingcli(t, append([]string{"plugin", "remove"}, tc.args...)...) -// Test Plugin remove Command --help, -h flag -func TestPluginRemoveCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "plugin", "remove", "--help") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - err = testutils_cobra.ExecutePingcli(t, "plugin", "remove", "-h") - testutils.CheckExpectedError(t, err, nil) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) + } } diff --git a/cmd/request/request.go b/cmd/request/request.go index 5a614c2a..b64084a8 100644 --- a/cmd/request/request.go +++ b/cmd/request/request.go @@ -9,6 +9,7 @@ import ( "github.com/pingidentity/pingcli/internal/autocompletion" request_internal "github.com/pingidentity/pingcli/internal/commands/request" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" "github.com/spf13/cobra" @@ -84,7 +85,7 @@ func requestRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Request Subcommand Called.") if err := request_internal.RunInternalRequest(args[0]); err != nil { - return err + return &errs.PingCLIError{Prefix: "", Err: err} } return nil diff --git a/cmd/request/request_test.go b/cmd/request/request_test.go index 55074d1c..5172c35d 100644 --- a/cmd/request/request_test.go +++ b/cmd/request/request_test.go @@ -10,174 +10,152 @@ import ( "regexp" "testing" + "github.com/pingidentity/pingcli/cmd/common" + request_internal "github.com/pingidentity/pingcli/internal/commands/request" "github.com/pingidentity/pingcli/internal/configuration/options" - "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Request Command Executes without issue -func TestRequestCmd_Execute(t *testing.T) { +func Test_RequestCommand_Validation(t *testing.T) { testutils_koanf.InitKoanfs(t) - originalStdout := os.Stdout - pipeReader, pipeWriter, err := os.Pipe() - if err != nil { - t.Fatalf("Failed to create pipe: %v", err) + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + { + name: "Happy Path - with header", + args: []string{ + "--" + options.RequestServiceOption.CobraParamName, "pingone", + "--" + options.RequestHTTPMethodOption.CobraParamName, "GET", + "--" + options.RequestHeaderOption.CobraParamName, "Content-Type: application/json", + fmt.Sprintf("environments/%s/users", os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")), + }, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Too many arguments", + args: []string{"arg1", "arg2"}, + expectErr: true, + expectedErrIs: common.ErrExactArgs, + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag", + }, + { + name: "Invalid service", + args: []string{ + "--" + options.RequestServiceOption.CobraParamName, "invalid-service", + "some/path", + }, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognizedService, + }, + { + name: "Invalid HTTP Method", + args: []string{ + "--" + options.RequestServiceOption.CobraParamName, "pingone", + "--" + options.RequestHTTPMethodOption.CobraParamName, "INVALID", + "some/path", + }, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognizedMethod, + }, + { + name: "Missing required service flag", + args: []string{"some/path"}, + expectErr: true, + expectedErrIs: request_internal.ErrServiceEmpty, + }, + { + name: "Invalid header format", + args: []string{ + "--" + options.RequestServiceOption.CobraParamName, "pingone", + "--" + options.RequestHeaderOption.CobraParamName, "invalid=header", + "some/path", + }, + expectErr: true, + expectedErrIs: nil, + }, + { + name: "Disallowed Authorization header", + args: []string{ + "--" + options.RequestServiceOption.CobraParamName, "pingone", + "--" + options.RequestHeaderOption.CobraParamName, "Authorization: Bearer token", + "some/path", + }, + expectErr: true, + expectedErrIs: customtypes.ErrDisallowedAuthHeader, + }, } - defer func() { - err := pipeReader.Close() - if err != nil { - t.Fatalf("Failed to close pipe: %v", err) - } - }() - os.Stdout = pipeWriter - err = testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "pingone", - "--"+options.RequestHTTPMethodOption.CobraParamName, "GET", - fmt.Sprintf("environments/%s/populations", os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")), - ) - testutils.CheckExpectedError(t, err, nil) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) - os.Stdout = originalStdout - err = pipeWriter.Close() - if err != nil { - t.Fatalf("Failed to close pipe: %v", err) - } - - pipeReaderOut, err := io.ReadAll(pipeReader) - if err != nil { - t.Fatalf("Failed to read from pipe: %v", err) - } + err := testutils_cobra.ExecutePingcli(t, append([]string{"request"}, tc.args...)...) - // Capture response json body - captureGroupName := "BodyJSON" - re := regexp.MustCompile(fmt.Sprintf(`(?s)^.*response:\s+(?P<%s>\{.*\}).*$`, captureGroupName)) - matchData := re.FindSubmatch(pipeReaderOut) - - for index, name := range re.SubexpNames() { - if name == captureGroupName { - if len(matchData) <= index { - t.Fatalf("Failed to capture JSON body: %v", matchData) + if !tc.expectErr { + require.NoError(t, err) + return } - bodyJSON := matchData[index] - // Check for valid JSON - if !json.Valid(bodyJSON) { - t.Errorf("Invalid JSON: %s", bodyJSON) + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) } - } + }) } } -// Test Request Command fails when provided too many arguments -func TestRequestCmd_Execute_TooManyArguments(t *testing.T) { - expectedErrorPattern := `accepts 1 arg\(s\), received 2` - err := testutils_cobra.ExecutePingcli(t, "request", "arg1", "arg2") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Request Command fails when provided invalid flag -func TestRequestCmd_Execute_InvalidFlag(t *testing.T) { - expectedErrorPattern := `unknown flag: --invalid` - err := testutils_cobra.ExecutePingcli(t, "request", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Request Command --help, -h flag -func TestRequestCmd_Execute_Help(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "request", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "request", "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Request Command with Invalid Service -func TestRequestCmd_Execute_InvalidService(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid argument ".*" for "-s, --service" flag: unrecognized Request Service: '.*'. Must be one of: .*$` - err := testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "invalid-service", - "--"+options.RequestHTTPMethodOption.CobraParamName, "GET", - fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar)), - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Request Command with Invalid HTTP Method -func TestRequestCmd_Execute_InvalidHTTPMethod(t *testing.T) { - testutils_koanf.InitKoanfs(t) - - expectedErrorPattern := `^invalid argument ".*" for "-m, --http-method" flag: unrecognized HTTP Method: '.*'. Must be one of: .*$` - err := testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "pingone", - "--"+options.RequestHTTPMethodOption.CobraParamName, "INVALID", - fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar)), - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Request Command with Missing Required Service Flag -func TestRequestCmd_Execute_MissingRequiredServiceFlag(t *testing.T) { - expectedErrorPattern := `failed to send custom request: service is required` - err := testutils_cobra.ExecutePingcli(t, "request", fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar))) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Request Command with Header Flag -func TestRequestCmd_Execute_HeaderFlag(t *testing.T) { +// Test_RequestCommand_E2E performs an end-to-end test of the request command, +// making a real API call and validating the JSON output. +func Test_RequestCommand_E2E(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "pingone", - "--"+options.RequestHTTPMethodOption.CobraParamName, "GET", - "--"+options.RequestHeaderOption.CobraParamName, "Content-Type: application/vnd.pingidentity.user.import+json", - "--"+options.RequestFailOption.CobraParamName, - fmt.Sprintf("environments/%s/users", os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")), - ) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Request Command with Header Flag with and without spacing -func TestRequestCmd_Execute_HeaderFlagSpacing(t *testing.T) { - testutils_koanf.InitKoanfs(t) + originalStdout := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w - err := testutils_cobra.ExecutePingcli(t, "request", + err = testutils_cobra.ExecutePingcli(t, "request", "--"+options.RequestServiceOption.CobraParamName, "pingone", "--"+options.RequestHTTPMethodOption.CobraParamName, "GET", - "--"+options.RequestHeaderOption.CobraParamName, "Test-Header:TestValue", - "--"+options.RequestHeaderOption.CobraParamName, "Test-Header-Two:\tTestValue", - "--"+options.RequestFailOption.CobraParamName, - fmt.Sprintf("environments/%s/users", os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")), + fmt.Sprintf("environments/%s/populations", os.Getenv("TEST_PINGONE_ENVIRONMENT_ID")), ) - testutils.CheckExpectedError(t, err, nil) -} + require.NoError(t, err) -// Test Request Command with invalid Header Flag -func TestRequestCmd_Execute_InvalidHeaderFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) + os.Stdout = originalStdout + require.NoError(t, w.Close()) - expectedErrorPattern := `^invalid argument ".*" for "-r, --header" flag: failed to set Headers: Invalid header: invalid=header. Headers must be in the proper format. Expected regex pattern: .*$` - err := testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "pingone", - "--"+options.RequestHeaderOption.CobraParamName, "invalid=header", - fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar)), - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + outputBytes, err := io.ReadAll(r) + require.NoError(t, err) + require.NoError(t, r.Close()) -// Test Request Command with disallowed Authorization Header Flag -func TestRequestCmd_Execute_DisallowedAuthorizationFlag(t *testing.T) { - testutils_koanf.InitKoanfs(t) + // Capture response json body + re := regexp.MustCompile(`(?s)response:\s+(\{.*\})`) + matches := re.FindSubmatch(outputBytes) + require.Len(t, matches, 2, "Failed to capture JSON body from command output") - expectedErrorPattern := `^invalid argument ".*" for "-r, --header" flag: failed to set Headers: Invalid header: Authorization. Authorization header is not allowed$` - err := testutils_cobra.ExecutePingcli(t, "request", - "--"+options.RequestServiceOption.CobraParamName, "pingone", - "--"+options.RequestHeaderOption.CobraParamName, "Authorization: Bearer token", - fmt.Sprintf("environments/%s/populations", os.Getenv(options.PingOneAuthenticationWorkerEnvironmentIDOption.EnvVar)), - ) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + bodyJSON := matches[1] + assert.NotEmpty(t, bodyJSON, "Response JSON body is empty") + assert.True(t, json.Valid(bodyJSON), "Output JSON is not valid") } diff --git a/cmd/root_test.go b/cmd/root_test.go index 903211e9..5192750a 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -4,163 +4,154 @@ package cmd_test import ( "fmt" - "os" "testing" "github.com/pingidentity/pingcli/cmd" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" "github.com/pingidentity/pingcli/internal/output" - "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Root Command Executes without issue -func TestRootCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Root Command Executes fails when provided an invalid command -func TestRootCmd_InvalidCommand(t *testing.T) { - expectedErrorPattern := `^unknown command "invalid" for "pingcli"$` - err := testutils_cobra.ExecutePingcli(t, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command --help, -h flag -func TestRootCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "-h") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Root Command fails with invalid flag -func TestRootCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingcli(t, "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command Executes when provided the --version, -v flag -func TestRootCmd_VersionFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--version") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "-v") - testutils.CheckExpectedError(t, err, nil) -} +func Test_RootCommand_Validation(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Root Command Executes when provided the --output-format flag -func TestRootCmd_OutputFormatFlag(t *testing.T) { - for _, outputFormat := range customtypes.OutputFormatValidValues() { - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName, outputFormat) - testutils.CheckExpectedError(t, err, nil) + testCases := []struct { + name string + args []string + expectErr bool + expectedErrIs error + expectedErrContains string + }{ + // Basic Command Structure + { + name: "Happy Path - no args", + args: []string{}, + expectErr: false, + }, + { + name: "Happy Path - help", + args: []string{"--help"}, + expectErr: false, + }, + { + name: "Happy Path - version", + args: []string{"--version"}, + expectErr: false, + }, + { + name: "Invalid command", + args: []string{"invalid-command"}, + expectErr: true, + expectedErrContains: "unknown command \"invalid-command\" for \"pingcli\"", + }, + { + name: "Invalid flag", + args: []string{"--invalid-flag"}, + expectErr: true, + expectedErrContains: "unknown flag: --invalid-flag", + }, + { + name: "Happy Path - output-format flag", + args: []string{"--" + options.RootOutputFormatOption.CobraParamName, "json"}, + expectErr: false, + }, + { + name: "Invalid output-format", + args: []string{"--" + options.RootOutputFormatOption.CobraParamName, "invalid"}, + expectErr: true, + expectedErrIs: customtypes.ErrUnrecognizedOutputFormat, + }, + { + name: "No value for output-format", + args: []string{"--" + options.RootOutputFormatOption.CobraParamName}, + expectErr: true, + expectedErrContains: "flag needs an argument: --" + options.RootOutputFormatOption.CobraParamName, + }, + { + name: "Happy Path - no-color flag", + args: []string{"--" + options.RootColorOption.CobraParamName}, + expectErr: false, + }, + { + name: "Invalid no-color value", + args: []string{"--" + options.RootColorOption.CobraParamName + "=invalid"}, + expectErr: true, + expectedErrIs: customtypes.ErrParseBool, + }, + { + name: "Happy Path - config flag", + args: []string{"--" + options.RootConfigOption.CobraParamName, "config.yaml"}, + expectErr: false, + }, + { + name: "No value for config", + args: []string{"--" + options.RootConfigOption.CobraParamName}, + expectErr: true, + expectedErrContains: "flag needs an argument: --" + options.RootConfigOption.CobraParamName, + }, + { + name: "Happy Path - profile flag", + args: []string{"--" + options.RootProfileOption.CobraParamName, "default"}, + expectErr: false, + }, + { + name: "No value for profile", + args: []string{"--" + options.RootProfileOption.CobraParamName}, + expectErr: true, + expectedErrContains: "flag needs an argument: --" + options.RootProfileOption.CobraParamName, + }, + { + name: "Happy Path - detailed-exit-code flag", + args: []string{"--" + options.RootDetailedExitCodeOption.CobraParamName}, + expectErr: false, + }, } -} -// Test Root Command fails when provided an invalid value for the --output-format flag -func TestRootCmd_InvalidOutputFlag(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for "-O, --output-format" flag: unrecognized Output Format: 'invalid'\. Must be one of: [a-z\s,]+$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command fails when provided no value for the --output-format flag -func TestRootCmd_NoValueOutputFlag(t *testing.T) { - expectedErrorPattern := `^flag needs an argument: --output-format$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfs(t) -// Test Root Command Executes output does not change with output-format=text vs output-format=json -func TestRootCmd_OutputFlagTextVsJSON(t *testing.T) { - textOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--"+options.RootOutputFormatOption.CobraParamName, "text") - testutils.CheckExpectedError(t, err, nil) + err := testutils_cobra.ExecutePingcli(t, tc.args...) - jsonOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--"+options.RootOutputFormatOption.CobraParamName, "json") - testutils.CheckExpectedError(t, err, nil) + if !tc.expectErr { + require.NoError(t, err) + return + } - if textOutput != jsonOutput { - t.Errorf("Expected text and json output to be the same") + assert.Error(t, err) + if tc.expectedErrIs != nil { + assert.ErrorIs(t, err, tc.expectedErrIs) + } + if tc.expectedErrContains != "" { + assert.ErrorContains(t, err, tc.expectedErrContains) + } + }) } } -// Test Root Command Executes when provided the --no-color flag -func TestRootCmd_ColorFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName) - testutils.CheckExpectedError(t, err, nil) +func Test_RootCommand_OutputComparison(t *testing.T) { + textOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--output-format", "text") + require.NoError(t, err) - err = testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName+"=false") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Root Command fails when provided an invalid value for the --no-color flag -func TestRootCmd_InvalidColorFlag(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for ".*" flag: strconv\.ParseBool: parsing "invalid": invalid syntax$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName+"=invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command Executes when provided the --config flag -func TestRootCmd_ConfigFlag(t *testing.T) { - // Add the --config args to os.Args - os.Args = append(os.Args, "--"+options.RootConfigOption.CobraParamName, "config.yaml") - - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName, "config.yaml") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Root Command fails when provided no value for the --config flag -func TestRootCmd_NoValueConfigFlag(t *testing.T) { - expectedErrorPattern := `^flag needs an argument: --config$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command fails on non-existent configuration file -func TestRootCmd_NonExistentConfigFile(t *testing.T) { - expectedErrorPattern := `^Configuration file '.*' does not exist. Use the default configuration file location or specify a valid configuration file location with the --config flag\.$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName, "non_existent.yaml") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Root Command Executes when provided the --profile flag -func TestRootCmd_ProfileFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootProfileOption.CobraParamName, "default") - testutils.CheckExpectedError(t, err, nil) -} + jsonOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--output-format", "json") + require.NoError(t, err) -// Test Root Command fails when provided no value for the --profile flag -func TestRootCmd_NoValueProfileFlag(t *testing.T) { - expectedErrorPattern := `^flag needs an argument: --profile$` - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootProfileOption.CobraParamName) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + assert.Equal(t, textOutput, jsonOutput, "Expected text and json output to be the same for the root command") } -// Test Root Command Detailed Exit Code Flag -func TestRootCmd_DetailedExitCodeFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--"+options.RootDetailedExitCodeOption.CobraParamName) - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingcli(t, "-"+options.RootDetailedExitCodeOption.Flag.Shorthand) - testutils.CheckExpectedError(t, err, nil) -} - -// Test Root Command Detailed Exit Code Flag with output Warn -func TestRootCmd_DetailedExitCodeWarnLoggedFunc(t *testing.T) { +func Test_DetailedExitCodeWarnLogged(t *testing.T) { testutils_koanf.InitKoanfs(t) t.Setenv(options.RootDetailedExitCodeOption.EnvVar, "true") + output.Warn("test warning", nil) warnLogged, err := output.DetailedExitCodeWarnLogged() - testutils.CheckExpectedError(t, err, nil) - if !warnLogged { - t.Errorf("Expected DetailedExitCodeWarnLogged to return true") - } + require.NoError(t, err) + assert.True(t, warnLogged, "Expected DetailedExitCodeWarnLogged to return true") } func TestParseArgsForConfigFile(t *testing.T) { @@ -229,9 +220,7 @@ func TestParseArgsForConfigFile(t *testing.T) { } result := cmd.ParseArgsForConfigFile(tc.args) - if result != tc.expected { - t.Errorf("expected %s, got %s", tc.expected, result) - } + assert.Equal(t, tc.expected, result) }) } } diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index b5f4ccae..d59053aa 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -115,7 +115,7 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } - if err = es.Merge(*es2); err != nil { + if err = es.Merge(es2); err != nil { return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: err} } diff --git a/internal/errs/pingcli_error.go b/internal/errs/pingcli_error.go index 650b1edf..63254885 100644 --- a/internal/errs/pingcli_error.go +++ b/internal/errs/pingcli_error.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package errs import ( @@ -12,11 +14,15 @@ type PingCLIError struct { } func (e *PingCLIError) Error() string { + if e == nil || e.Err == nil { + return "" + } + // Check if the wrapped error is also a PingCLIError to avoid redundant prefixes - var err *PingCLIError - if errors.As(e.Err, &err) { - if strings.EqualFold(err.Prefix, e.Prefix) { - return err.Error() + var pingErr *PingCLIError + if errors.As(e.Err, &pingErr) { + if strings.EqualFold(pingErr.Prefix, e.Prefix) { + return pingErr.Error() } } @@ -24,5 +30,9 @@ func (e *PingCLIError) Error() string { } func (e *PingCLIError) Unwrap() error { + if e == nil { + return nil + } + return e.Err } diff --git a/internal/errs/pingcli_error_test.go b/internal/errs/pingcli_error_test.go new file mode 100644 index 00000000..7dc298fd --- /dev/null +++ b/internal/errs/pingcli_error_test.go @@ -0,0 +1,108 @@ +// Copyright © 2025 Ping Identity Corporation + +package errs_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/pingidentity/pingcli/internal/errs" + "github.com/stretchr/testify/require" +) + +func Test_PingCLIError_Error(t *testing.T) { + testErr := errors.New("test error") + prefix1 := "prefix 1" + prefix2 := "prefix 2" + + testCases := []struct { + name string + err *errs.PingCLIError + expectedStr string + expectedAs error + expectedIs error + assertUnwrap require.ErrorAssertionFunc + }{ + { + name: "Happy path", + err: &errs.PingCLIError{ + Prefix: prefix1, + Err: testErr, + }, + expectedStr: fmt.Sprintf("%s: %s", prefix1, testErr.Error()), + expectedAs: &errs.PingCLIError{}, + expectedIs: testErr, + assertUnwrap: require.Error, + }, + { + name: "Nested PingCLIError with same prefix", + err: &errs.PingCLIError{ + Prefix: prefix1, + Err: &errs.PingCLIError{ + Prefix: prefix1, + Err: testErr, + }, + }, + expectedStr: fmt.Sprintf("%s: %s", prefix1, testErr.Error()), + expectedAs: &errs.PingCLIError{}, + expectedIs: testErr, + assertUnwrap: require.Error, + }, + { + name: "Nested PingCLIError with different prefix", + err: &errs.PingCLIError{ + Prefix: prefix2, + Err: &errs.PingCLIError{ + Prefix: prefix1, + Err: testErr, + }, + }, + expectedStr: fmt.Sprintf("%s: %s: %s", prefix2, prefix1, testErr.Error()), + expectedAs: &errs.PingCLIError{}, + expectedIs: testErr, + assertUnwrap: require.Error, + }, + { + name: "Nil inner error", + err: &errs.PingCLIError{ + Prefix: prefix1, + Err: nil, + }, + expectedStr: "", + expectedAs: nil, + expectedIs: nil, + assertUnwrap: require.NoError, + }, + { + name: "Nil PingCLIError", + err: nil, + expectedStr: "", + expectedAs: nil, + expectedIs: nil, + assertUnwrap: require.NoError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.err != nil { + require.Equal(t, tc.expectedStr, tc.err.Error()) + } else { + require.Equal(t, tc.expectedStr, "") + } + + if tc.expectedAs != nil { + var target *errs.PingCLIError + require.ErrorAs(t, tc.err, &target) + } + + if tc.expectedIs != nil { + require.ErrorIs(t, tc.err, tc.expectedIs) + } + + unwrappedErr := errors.Unwrap(tc.err) + tc.assertUnwrap(t, unwrappedErr) + }) + } +} diff --git a/internal/input/input.go b/internal/input/input.go index 862523b6..1d9a5213 100644 --- a/internal/input/input.go +++ b/internal/input/input.go @@ -4,23 +4,15 @@ package input import ( "errors" - "fmt" "io" "github.com/manifoldco/promptui" + "github.com/pingidentity/pingcli/internal/errs" ) -type InputPromptError struct { - Err error -} - -func (e *InputPromptError) Error() string { - return fmt.Sprintf("input prompt failed: %s", e.Err.Error()) -} - -func (e *InputPromptError) Unwrap() error { - return e.Err -} +var ( + inputPromptErrorPrefix = "input prompt error" +) func RunPrompt(message string, validateFunc func(string) error, rc io.ReadCloser) (string, error) { p := promptui.Prompt{ @@ -31,7 +23,7 @@ func RunPrompt(message string, validateFunc func(string) error, rc io.ReadCloser userInput, err := p.Run() if err != nil { - return "", &InputPromptError{Err: err} + return "", &errs.PingCLIError{Prefix: inputPromptErrorPrefix, Err: err} } return userInput, nil @@ -52,7 +44,7 @@ func RunPromptConfirm(message string, rc io.ReadCloser) (bool, error) { return false, nil } - return false, &InputPromptError{Err: err} + return false, &errs.PingCLIError{Prefix: inputPromptErrorPrefix, Err: err} } return true, nil @@ -68,7 +60,7 @@ func RunPromptSelect(message string, items []string, rc io.ReadCloser) (selectio _, selection, err = p.Run() if err != nil { - return "", &InputPromptError{Err: err} + return "", &errs.PingCLIError{Prefix: inputPromptErrorPrefix, Err: err} } return selection, nil diff --git a/internal/input/input_test.go b/internal/input/input_test.go index e1b90821..cb239305 100644 --- a/internal/input/input_test.go +++ b/internal/input/input_test.go @@ -3,121 +3,103 @@ package input_test import ( + "errors" "fmt" "testing" + "github.com/manifoldco/promptui" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/input" "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/stretchr/testify/require" ) func mockValidateFunc(input string) error { if input == "invalid" { - return fmt.Errorf("invalid input") + return errors.New("invalid input") } return nil } -// Test RunPrompt function func TestRunPrompt(t *testing.T) { testInput := "test-input" reader := testutils.WriteStringToPipe(t, fmt.Sprintf("%s\n", testInput)) - parsedInput, err := input.RunPrompt("test", nil, reader) - if err != nil { - t.Errorf("Error running RunPrompt: %v", err) - } - if parsedInput != testInput { - t.Errorf("Expected '%s', but got '%s'", testInput, parsedInput) - } + parsedInput, err := input.RunPrompt("test", nil, reader) + require.NoError(t, err) + require.Equal(t, testInput, parsedInput) } -// Test RunPrompt function with validation func TestRunPromptWithValidation(t *testing.T) { testInput := "test-input" reader := testutils.WriteStringToPipe(t, fmt.Sprintf("%s\n", testInput)) - parsedInput, err := input.RunPrompt("test", mockValidateFunc, reader) - if err != nil { - t.Errorf("Error running RunPrompt: %v", err) - } - if parsedInput != testInput { - t.Errorf("Expected '%s', but got '%s'", testInput, parsedInput) - } + parsedInput, err := input.RunPrompt("test", mockValidateFunc, reader) + require.NoError(t, err) + require.Equal(t, testInput, parsedInput) } -// Test RunPrompt function with validation error func TestRunPromptWithValidationError(t *testing.T) { testInput := "invalid" reader := testutils.WriteStringToPipe(t, fmt.Sprintf("%s\n", testInput)) + _, err := input.RunPrompt("test", mockValidateFunc, reader) - if err == nil { - t.Errorf("Expected error, but got nil") - } + require.Error(t, err) + + var pingErr *errs.PingCLIError + require.ErrorAs(t, err, &pingErr) + require.ErrorIs(t, err, promptui.ErrEOF) } -// Test RunPromptConfirm function func TestRunPromptConfirm(t *testing.T) { reader := testutils.WriteStringToPipe(t, "y\n") - parsedInput, err := input.RunPromptConfirm("test", reader) - if err != nil { - t.Errorf("Error running RunPromptConfirm: %v", err) - } - if !parsedInput { - t.Errorf("Expected true, but got false") - } + parsedInput, err := input.RunPromptConfirm("test", reader) + require.NoError(t, err) + require.True(t, parsedInput) } -// Test RunPromptConfirm function with no input func TestRunPromptConfirmNoInput(t *testing.T) { reader := testutils.WriteStringToPipe(t, "\n") - parsedInput, err := input.RunPromptConfirm("test", reader) - if err != nil { - t.Errorf("Error running RunPromptConfirm: %v", err) - } - if parsedInput { - t.Errorf("Expected false, but got true") - } + parsedInput, err := input.RunPromptConfirm("test", reader) + require.NoError(t, err) + require.False(t, parsedInput) } -// Test RunPromptConfirm function with "n" input func TestRunPromptConfirmNoInputN(t *testing.T) { reader := testutils.WriteStringToPipe(t, "n\n") - parsedInput, err := input.RunPromptConfirm("test", reader) - if err != nil { - t.Errorf("Error running RunPromptConfirm: %v", err) - } - if parsedInput { - t.Errorf("Expected false, but got true") - } + parsedInput, err := input.RunPromptConfirm("test", reader) + require.NoError(t, err) + require.False(t, parsedInput) } -// Test RunPromptConfirm function with junk input func TestRunPromptConfirmJunkInput(t *testing.T) { reader := testutils.WriteStringToPipe(t, "junk\n") - parsedInput, err := input.RunPromptConfirm("test", reader) - if err != nil { - t.Errorf("Error running RunPromptConfirm: %v", err) - } - if parsedInput { - t.Errorf("Expected false, but got true") - } + parsedInput, err := input.RunPromptConfirm("test", reader) + require.NoError(t, err) + require.False(t, parsedInput) } -// Test RunPromptSelect function func TestRunPromptSelect(t *testing.T) { testInput := "test-input" reader := testutils.WriteStringToPipe(t, fmt.Sprintf("%s\n", testInput)) + parsedInput, err := input.RunPromptSelect("test", []string{testInput}, reader) - if err != nil { - t.Errorf("Error running RunPromptSelect: %v", err) - } + require.NoError(t, err) + require.Equal(t, testInput, parsedInput) +} - if parsedInput != testInput { - t.Errorf("Expected '%s', but got '%s'", testInput, parsedInput) - } +func TestRunPromptSelectError(t *testing.T) { + reader := testutils.WriteStringToPipe(t, "\x03") // Simulate Ctrl+C + + _, err := input.RunPromptSelect("test", []string{"test-input"}, reader) + require.Error(t, err) + + var pingErr *errs.PingCLIError + require.ErrorAs(t, err, &pingErr) + require.ErrorIs(t, err, promptui.ErrInterrupt) } diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index 4b150fd2..6be3cfa9 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -4,6 +4,7 @@ package plugins import ( "context" + "errors" "fmt" "io" "os/exec" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/go-hclog" hplugin "github.com/hashicorp/go-plugin" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/shared/grpc" @@ -20,6 +22,16 @@ import ( "github.com/spf13/pflag" ) +var ( + pluginsErrorPrefix = "plugins error" + ErrGetPluginExecutables = errors.New("failed to get configured plugin executables") + ErrCreateRPCClient = errors.New("failed to create plugin rpc client") + ErrDispensePlugin = errors.New("the rpc client failed to dispense plugin executable") + ErrCastPluginInterface = errors.New("failed to cast plugin executable to grpc.PingCliCommand interface") + ErrPluginConfiguration = errors.New("failed to get plugin configuration") + ErrExecutePlugin = errors.New("failed to execute plugin command") +) + func AddAllPluginToCmd(cmd *cobra.Command) error { l := logger.Get() @@ -27,7 +39,7 @@ func AddAllPluginToCmd(cmd *cobra.Command) error { // via the command 'pingcli plugin add ' pluginExecutables, err := profiles.GetOptionValue(options.PluginExecutablesOption) if err != nil { - return fmt.Errorf("failed to get configured plugin executables: %w", err) + return &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetPluginExecutables, err)} } if pluginExecutables == "" { @@ -40,19 +52,24 @@ func AddAllPluginToCmd(cmd *cobra.Command) error { continue } - conf, err := pluginConfiguration(pluginExecutable) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + + conf, err := pluginConfiguration(ctx, pluginExecutable) if err != nil { - return err + return &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: err} } pluginCmd := &cobra.Command{ - Use: conf.Use, - Short: conf.Short, - Long: conf.Long, - Example: conf.Example, DisableFlagsInUseLine: true, // We write our own flags in @Use attribute DisableFlagParsing: true, // Let all flags pass through to plugin + Example: conf.Example, + Long: conf.Long, RunE: createCmdRunE(pluginExecutable), + Short: conf.Short, + Use: conf.Use, } cmd.AddCommand(pluginCmd) @@ -65,7 +82,7 @@ func AddAllPluginToCmd(cmd *cobra.Command) error { // createHPluginClient creates a new hplugin.Client for the given plugin executable. // The caller is responsible for closing the client connection after use. -func createHPluginClient(pluginExecutable string) *hplugin.Client { +func createHPluginClient(ctx context.Context, pluginExecutable string) *hplugin.Client { // We use our own logger for the plugins to communicate to the user. // Discard any other plugin logging details to avoid user communication clutter. logger := hclog.New(&hclog.LoggerOptions{ @@ -78,7 +95,7 @@ func createHPluginClient(pluginExecutable string) *hplugin.Client { client := hplugin.NewClient(&hplugin.ClientConfig{ HandshakeConfig: grpc.HandshakeConfig, Plugins: grpc.PluginMap, - Cmd: exec.CommandContext(context.Background(), pluginExecutable), + Cmd: exec.CommandContext(ctx, pluginExecutable), AllowedProtocols: []hplugin.Protocol{ hplugin.ProtocolGRPC, }, @@ -94,7 +111,7 @@ func dispensePlugin(client *hplugin.Client, pluginExecutable string) (grpc.PingC // Connect via RPC clientProtocol, err := client.Client() if err != nil { - return nil, fmt.Errorf("failed to create Plugin RPC client: %w", err) + return nil, &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrCreateRPCClient, err)} } // All Ping CLI plugins are expected to serve the ENUM_PINGCLI_COMMAND_GRPC plugin via @@ -103,26 +120,26 @@ func dispensePlugin(client *hplugin.Client, pluginExecutable string) (grpc.PingC // raw value of ENUM_PINGCLI_COMMAND_GRPC "pingcli_command_grpc" for the PluginMap key. raw, err := clientProtocol.Dispense(grpc.ENUM_PINGCLI_COMMAND_GRPC) if err != nil { - return nil, fmt.Errorf("the rpc client failed to dispense plugin executable '%s': %w", pluginExecutable, err) + return nil, &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrDispensePlugin, pluginExecutable, err)} } // Cast the dispensed plugin to the interface we expect to work with: grpc.PingCliCommand. // However, this is not a normal interface, but rather implemeted over the RPC connection. plugin, ok := raw.(grpc.PingCliCommand) if !ok { - return nil, fmt.Errorf("failed to cast plugin executable '%s' to grpc.PingCliCommand interface", pluginExecutable) + return nil, &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w '%s'", ErrCastPluginInterface, pluginExecutable)} } return plugin, nil } -func pluginConfiguration(pluginExecutable string) (conf *grpc.PingCliCommandConfiguration, err error) { - client := createHPluginClient(pluginExecutable) +func pluginConfiguration(ctx context.Context, pluginExecutable string) (conf *grpc.PingCliCommandConfiguration, err error) { + client := createHPluginClient(ctx, pluginExecutable) defer client.Kill() plugin, err := dispensePlugin(client, pluginExecutable) if err != nil { - return nil, err + return nil, &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: err} } // The configuration method is defined by the protobuf definition. @@ -132,7 +149,7 @@ func pluginConfiguration(pluginExecutable string) (conf *grpc.PingCliCommandConf // in the help output. resp, err := plugin.Configuration() if err != nil { - return nil, fmt.Errorf("failed to run command from Plugin: %w", err) + return nil, &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrPluginConfiguration, err)} } return resp, nil @@ -140,24 +157,21 @@ func pluginConfiguration(pluginExecutable string) (conf *grpc.PingCliCommandConf func createCmdRunE(pluginExecutable string) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) error { - // Extract global flags before passing args to the plugin - // This allows the host process to handle global flags and only pass plugin-specific args - pluginArgs, err := filterRootFlags(args, cmd.Root().PersistentFlags()) - if err != nil { - return fmt.Errorf("failed to execute plugin command: %w", err) - } + // Because DisableFlagParsing is true, `args` contains all arguments after the command name. + // We need to filter out the persistent flags that belong to the root command. + pluginArgs := filterRootFlags(cmd, args) - client := createHPluginClient(pluginExecutable) + client := createHPluginClient(cmd.Context(), pluginExecutable) defer client.Kill() plugin, err := dispensePlugin(client, pluginExecutable) if err != nil { - return err + return &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: err} } err = plugin.Run(pluginArgs, &shared_logger.SharedLogger{}) if err != nil { - return fmt.Errorf("failed to execute plugin command: %w", err) + return &errs.PingCLIError{Prefix: pluginsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrExecutePlugin, err)} } return nil @@ -166,75 +180,53 @@ func createCmdRunE(pluginExecutable string) func(cmd *cobra.Command, args []stri // filterRootFlags filters out any flags that were parsed by the root command's persistent flags // and processes them for the host application, returning only plugin-specific args. -func filterRootFlags(args []string, persistentFlags *pflag.FlagSet) ([]string, error) { - pluginArgs := []string{} - - var ( - previousArgFlagName = "" - handlePreviousArgAsFlag = false - ) - - for _, arg := range args { - switch { - case handlePreviousArgAsFlag && previousArgFlagName != "": - err := persistentFlags.Set(previousArgFlagName, arg) - if err != nil { - return nil, fmt.Errorf("failed to set persistent flag '%s' with value '%s': %w", previousArgFlagName, arg, err) - } - handlePreviousArgAsFlag = false - case len(arg) > 0 && arg[0] == '-': - // The argument is a flag, remove leading dashes - flagArg := strings.TrimLeft(arg, "-") - - // Handle flags in the format --flag=value - if strings.Contains(flagArg, "=") { - parts := strings.SplitN(flagArg, "=", 2) - flagName := parts[0] - - if flag := persistentFlags.Lookup(flagName); flag != nil { - err := persistentFlags.Set(flagName, parts[1]) - if err != nil { - return nil, fmt.Errorf("failed to set persistent flag '%s' with value '%s': %w", flagName, parts[1], err) - } - } else if len(flagName) == 1 && persistentFlags.ShorthandLookup(flagName) != nil { - flag := persistentFlags.ShorthandLookup(flagName) - err := persistentFlags.Set(flag.Name, parts[1]) - if err != nil { - return nil, fmt.Errorf("failed to set persistent flag '%s' with value '%s': %w", flag.Name, parts[1], err) - } - } else { - pluginArgs = append(pluginArgs, arg) - } - } else { - if flag := persistentFlags.Lookup(flagArg); flag != nil { - if flag.Value.Type() == "bool" { - err := persistentFlags.Set(flagArg, "true") - if err != nil { - return nil, fmt.Errorf("failed to set persistent flag '%s' with value 'true': %w", flagArg, err) - } - } else { - previousArgFlagName = flagArg - handlePreviousArgAsFlag = true - } - } else if len(flagArg) == 1 && persistentFlags.ShorthandLookup(flagArg) != nil { - flag := persistentFlags.ShorthandLookup(flagArg) - if flag.Value.Type() == "bool" { - err := persistentFlags.Set(flag.Name, "true") - if err != nil { - return nil, fmt.Errorf("failed to set persistent flag '%s' with value 'true': %w", flag.Name, err) - } - } else { - previousArgFlagName = flag.Name - handlePreviousArgAsFlag = true - } - } else { - pluginArgs = append(pluginArgs, arg) - } - } - default: - pluginArgs = append(pluginArgs, arg) +func filterRootFlags(cmd *cobra.Command, args []string) []string { + pluginArgs := make([]string, 0) // Initialize as an empty slice + rootFlags := cmd.Root().PersistentFlags() + + // isRootFlag checks if a given argument (like "--profile") is a known persistent flag on the root command. + isRootFlag := func(arg string) *pflag.Flag { + // Positional arguments don't start with a hyphen, so they can't be flags. + if !strings.HasPrefix(arg, "-") { + return nil } + + name := strings.SplitN(strings.TrimLeft(arg, "-"), "=", 2)[0] + + if strings.HasPrefix(arg, "--") { + return rootFlags.Lookup(name) + } + + // It's a shorthand flag. pflag panics if the lookup key is > 1 character, + // so we must ensure the name is a single character. + // NOTE: This does not handle stacked short flags (e.g., `-vp`). The entire + // stacked flag group will be passed to the plugin as a single argument. + if len(name) == 1 { + return rootFlags.ShorthandLookup(name) + } + + return nil } - return pluginArgs, nil + for i := 0; i < len(args); i++ { + arg := args[i] + flag := isRootFlag(arg) + + if flag == nil { + // If it's not a recognized root flag, it must be for the plugin. + pluginArgs = append(pluginArgs, arg) + continue + } + + // It is a root flag. We need to skip it and, if necessary, its value. + // If the flag is a boolean, it has no separate value, so we just skip the flag itself. + // If it's a non-boolean flag and the value is attached with '=', we also just skip this one argument. + if flag.Value.Type() == "bool" || strings.Contains(arg, "=") { + continue + } + + // It's a non-boolean flag in the form "--flag value". We need to skip both. + i++ + } + return pluginArgs } diff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go new file mode 100644 index 00000000..537d281e --- /dev/null +++ b/internal/plugins/plugins_test.go @@ -0,0 +1,176 @@ +// Copyright © 2025 Ping Identity Corporation + +package plugins + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + + hplugin "github.com/hashicorp/go-plugin" + "github.com/pingidentity/pingcli/shared/grpc" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +var ( + testPluginConfig = &grpc.PingCliCommandConfiguration{ + Use: "test-plugin", + Short: "A test plugin", + Long: "A longer description for a test plugin", + Example: "pingcli test-plugin --flag value", + } + testRunError = errors.New("plugin run error") +) + +// mockPingCliCommand is a mock implementation of the grpc.PingCliCommand interface for testing. +type mockPingCliCommand struct{} + +var mockPlugin = &mockPingCliCommand{} + +func (m *mockPingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, error) { + if configErr := os.Getenv("PINGCLI_TEST_PLUGIN_CONFIG_ERROR"); configErr != "" { + return nil, errors.New(configErr) + } + return testPluginConfig, nil +} + +func (m *mockPingCliCommand) Run(args []string, l grpc.Logger) error { + if runErr := os.Getenv("PINGCLI_TEST_PLUGIN_RUN_ERROR"); runErr != "" { + return errors.New(runErr) + } + return fmt.Errorf("args: %s", strings.Join(args, ",")) +} + +func TestMain(m *testing.M) { + if os.Getenv("PINGCLI_TEST_PLUGIN") == "1" { + hplugin.Serve(&hplugin.ServeConfig{ + HandshakeConfig: grpc.HandshakeConfig, + Plugins: map[string]hplugin.Plugin{ + grpc.ENUM_PINGCLI_COMMAND_GRPC: &grpc.PingCliCommandGrpcPlugin{Impl: mockPlugin}, + }, + GRPCServer: hplugin.DefaultGRPCServer, + }) + return + } + os.Exit(m.Run()) +} + +func setupPluginTest(t *testing.T) string { + t.Setenv("PINGCLI_TEST_PLUGIN", "1") + return os.Args[0] +} + +func Test_pluginConfiguration(t *testing.T) { + pluginExec := setupPluginTest(t) + ctx := context.Background() + + t.Run("Happy path", func(t *testing.T) { + conf, err := pluginConfiguration(ctx, pluginExec) + require.NoError(t, err) + require.NotNil(t, conf) + require.Equal(t, testPluginConfig.Use, conf.Use) + }) + + t.Run("Plugin returns error", func(t *testing.T) { + t.Setenv("PINGCLI_TEST_PLUGIN_CONFIG_ERROR", "config error") + _, err := pluginConfiguration(ctx, pluginExec) + require.Error(t, err) + require.ErrorContains(t, err, "config error") + }) + + t.Run("Invalid executable", func(t *testing.T) { + _, err := pluginConfiguration(ctx, "invalid-executable-path") + require.Error(t, err) + }) +} + +func Test_createCmdRunE(t *testing.T) { + pluginExec := setupPluginTest(t) + rootCmd := &cobra.Command{Use: "pingcli"} + rootCmd.PersistentFlags().String("profile", "", "test profile flag") + + pluginCmd := &cobra.Command{ + Use: "plugin", + RunE: createCmdRunE(pluginExec), + DisableFlagParsing: true, // Match the real implementation + } + rootCmd.AddCommand(pluginCmd) + + testCases := []struct { + name string + runError string + args []string + expectedArgs []string + expectError bool + }{ + { + name: "Happy path", + args: []string{"plugin", "--profile", "my-profile", "plugin-arg", "--plugin-flag"}, + expectedArgs: []string{"plugin-arg", "--plugin-flag"}, + }, + { + name: "Plugin returns error", + runError: "plugin run error", + args: []string{"plugin", "arg1"}, + expectedArgs: []string{"arg1"}, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Unset env var from previous runs + t.Setenv("PINGCLI_TEST_PLUGIN_RUN_ERROR", "") + if tc.runError != "" { + t.Setenv("PINGCLI_TEST_PLUGIN_RUN_ERROR", tc.runError) + } + + ctx := context.Background() + rootCmd.SetArgs(tc.args) + err := rootCmd.ExecuteContext(ctx) + + if tc.expectError { + require.Error(t, err) + require.ErrorContains(t, err, tc.runError) + } else { + require.Error(t, err) + expectedErrStr := fmt.Sprintf("args: %s", strings.Join(tc.expectedArgs, ",")) + require.Contains(t, err.Error(), expectedErrStr) + } + }) + } +} + +func Test_filterRootFlags(t *testing.T) { + rootCmd := &cobra.Command{Use: "root"} + rootCmd.PersistentFlags().StringP("profile", "p", "", "profile flag") + rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose flag") + subCmd := &cobra.Command{Use: "sub"} + rootCmd.AddCommand(subCmd) + + testCases := []struct { + name string + args []string + expectedArgs []string + }{ + {"No root flags", []string{"plugin-arg", "--plugin-flag"}, []string{"plugin-arg", "--plugin-flag"}}, + {"Long name root flag", []string{"--profile", "my-profile", "plugin-arg"}, []string{"plugin-arg"}}, + {"Short name root flag", []string{"-p", "my-profile", "plugin-arg"}, []string{"plugin-arg"}}, + {"Root flag with equals", []string{"--profile=my-profile", "plugin-arg"}, []string{"plugin-arg"}}, + {"Boolean root flag", []string{"--verbose", "plugin-arg"}, []string{"plugin-arg"}}, + {"Short boolean root flag", []string{"-v", "plugin-arg"}, []string{"plugin-arg"}}, + {"Mixed flags", []string{"-v", "plugin-arg1", "--profile", "prof", "--plugin-flag", "val"}, []string{"plugin-arg1", "--plugin-flag", "val"}}, + {"No args", []string{}, []string{}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + filteredArgs := filterRootFlags(subCmd, tc.args) + require.Equal(t, tc.expectedArgs, filteredArgs) + }) + } +} diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 2fd7a51d..89d34cae 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -20,6 +20,7 @@ import ( var ( k *KoanfConfig + koanfErrorPrefix = "profile configuration error" ErrNoOptionValue = errors.New("no option value found") ErrKoanfNotInitialized = errors.New("koanf instance is not initialized") ErrProfileNameEmpty = errors.New("invalid profile name: profile name cannot be empty") @@ -35,7 +36,6 @@ var ( ErrKoanfMerge = errors.New("failed to merge koanf configuration") ErrDeleteActiveProfile = errors.New("the active profile cannot be deleted") ErrSetKoanfKeyDefaultValue = errors.New("failed to set koanf key default value") - koanfErrorPrefix = "profile configuration error" ) type KoanfConfig struct { diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index 1e37a23f..261396ec 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -3,6 +3,7 @@ package profiles import ( + "errors" "fmt" "slices" "strings" @@ -11,12 +12,36 @@ import ( "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" +) + +var ( + validateErrorPrefix = "profile validation error" + ErrValidatePingCLIConfiguration = errors.New("failed to validate Ping CLI configuration") + ErrInvalidConfigurationKey = errors.New("invalid configuration key(s) found in profile") + ErrUnrecognizedVariableType = errors.New("unrecognized variable type for key") + ErrValidateBoolean = errors.New("invalid boolean value") + ErrValidateUUID = errors.New("invalid uuid value") + ErrValidateOutputFormat = errors.New("invalid output format value") + ErrValidatePingOneRegionCode = errors.New("invalid pingone region code value") + ErrValidateString = errors.New("invalid string value") + ErrValidateStringSlice = errors.New("invalid string slice value") + ErrValidateExportServiceGroup = errors.New("invalid export service group value") + ErrValidateExportServices = errors.New("invalid export services value") + ErrValidateExportFormat = errors.New("invalid export format value") + ErrValidateHTTPMethod = errors.New("invalid http method value") + ErrValidateRequestService = errors.New("invalid request service value") + ErrValidateInt = errors.New("invalid int value") + ErrValidatePingFederateAuthType = errors.New("invalid pingfederate auth type value") + ErrValidatePingOneAuthType = errors.New("invalid pingone auth type value") + ErrValidateLicenseProduct = errors.New("invalid license product value") + ErrValidateLicenseVersion = errors.New("invalid license version value") ) func Validate() (err error) { koanfConfig, err := GetKoanfConfig() if err != nil { - return fmt.Errorf("failed to get koanf config: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } // Get a slice of all profile names configured in the config.yaml file @@ -24,46 +49,51 @@ func Validate() (err error) { // Validate profile names if err = validateProfileNames(profileNames); err != nil { - return err + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } profileName, err := GetOptionValue(options.RootProfileOption) if err != nil { - return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } if profileName != "" { // Make sure selected profile is in the configuration file if !slices.Contains(profileNames, profileName) { - return fmt.Errorf("failed to validate Ping CLI configuration: '%s' profile not found in configuration "+ - "file %s", profileName, koanfConfig.GetKoanfConfigFile()) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("%w: '%s' profile not found in configuration "+ + "file %s", ErrValidatePingCLIConfiguration, profileName, koanfConfig.GetKoanfConfigFile())} } } - activeProfileName, err := GetOptionValue(options.RootActiveProfileOption) + // active profile has no env var or cobra flag, so always get from config file + activeProfileName, ok, err := KoanfValueFromOption(options.RootActiveProfileOption, "") if err != nil { - return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} + } + if !ok { + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("%w: active profile not set in configuration file %s", + ErrValidatePingCLIConfiguration, koanfConfig.GetKoanfConfigFile())} } // Make sure selected active profile is in the configuration file if !slices.Contains(profileNames, activeProfileName) { - return fmt.Errorf("failed to validate Ping CLI configuration: active profile '%s' not found in configuration "+ - "file %s", activeProfileName, koanfConfig.GetKoanfConfigFile()) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("%w: active profile '%s' not found in configuration "+ + "file %s", ErrValidatePingCLIConfiguration, activeProfileName, koanfConfig.GetKoanfConfigFile())} } // for each profile key, validate the profile koanf for _, pName := range profileNames { subKoanf, err := koanfConfig.GetProfileKoanf(pName) if err != nil { - return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } if err := validateProfileKeys(pName, subKoanf); err != nil { - return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } if err := validateProfileValues(pName, subKoanf); err != nil { - return fmt.Errorf("failed to validate Ping CLI configuration: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } } @@ -73,12 +103,12 @@ func Validate() (err error) { func validateProfileNames(profileNames []string) error { koanfConfig, err := GetKoanfConfig() if err != nil { - return fmt.Errorf("failed to get koanf config: %w", err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } for _, profileName := range profileNames { if err := koanfConfig.ValidateProfileNameFormat(profileName); err != nil { - return err + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } } @@ -104,7 +134,7 @@ func validateProfileKeys(profileName string, profileKoanf *koanf.Koanf) error { invalidKeysStr := strings.Join(invalidKeys, ", ") validKeysStr := strings.Join(validProfileKeys, ", ") - return fmt.Errorf("invalid configuration key(s) found in profile %s: %s\nMust use one of: %s", profileName, invalidKeysStr, validKeysStr) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("%w %s: %s\nMust use one of: %s", ErrInvalidConfigurationKey, profileName, invalidKeysStr, validKeysStr)} } return nil @@ -114,7 +144,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) for key := range profileKoanf.All() { opt, err := configuration.OptionFromKoanfKey(key) if err != nil { - return err + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: err} } vValue := profileKoanf.Get(key) @@ -127,12 +157,12 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: b := new(customtypes.Bool) if err = b.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a boolean value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateBoolean, typedValue, err)} } case bool: continue default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a boolean value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateBoolean, typedValue, typedValue)} } case options.UUID: switch typedValue := vValue.(type) { @@ -141,10 +171,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: u := new(customtypes.UUID) if err = u.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a UUID value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateUUID, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a UUID value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateUUID, typedValue, typedValue)} } case options.OUTPUT_FORMAT: switch typedValue := vValue.(type) { @@ -153,10 +183,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: o := new(customtypes.OutputFormat) if err = o.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an output format value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateOutputFormat, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an output format value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateOutputFormat, typedValue, typedValue)} } case options.PINGONE_REGION_CODE: switch typedValue := vValue.(type) { @@ -165,10 +195,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: prc := new(customtypes.PingOneRegionCode) if err = prc.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingOne Region Code value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidatePingOneRegionCode, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne Region Code value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidatePingOneRegionCode, typedValue, typedValue)} } case options.STRING: switch typedValue := vValue.(type) { @@ -177,10 +207,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: s := new(customtypes.String) if err = s.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateString, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateString, typedValue, typedValue)} } case options.STRING_SLICE: switch typedValue := vValue.(type) { @@ -189,7 +219,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: ss := new(customtypes.StringSlice) if err = ss.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string slice value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateStringSlice, typedValue, err)} } case []any: ss := new(customtypes.StringSlice) @@ -197,14 +227,14 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) switch innerTypedValue := v.(type) { case string: if err = ss.Set(innerTypedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string slice value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateStringSlice, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateStringSlice, typedValue, typedValue)} } } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateStringSlice, typedValue, typedValue)} } case options.EXPORT_SERVICE_GROUP: switch typedValue := vValue.(type) { @@ -213,10 +243,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: esg := new(customtypes.ExportServiceGroup) if err = esg.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service group value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateExportServiceGroup, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service group value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateExportServiceGroup, typedValue, typedValue)} } case options.EXPORT_SERVICES: switch typedValue := vValue.(type) { @@ -225,7 +255,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: es := new(customtypes.ExportServices) if err = es.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateExportServices, typedValue, err)} } case []any: es := new(customtypes.ExportServices) @@ -233,14 +263,14 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) switch innerTypedValue := v.(type) { case string: if err = es.Set(innerTypedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateExportServices, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateExportServices, typedValue, typedValue)} } } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateExportServices, typedValue, typedValue)} } case options.EXPORT_FORMAT: switch typedValue := vValue.(type) { @@ -249,10 +279,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: ef := new(customtypes.ExportFormat) if err = ef.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an export format value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateExportFormat, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an export format value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateExportFormat, typedValue, typedValue)} } case options.REQUEST_HTTP_METHOD: switch typedValue := vValue.(type) { @@ -261,10 +291,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: hm := new(customtypes.HTTPMethod) if err = hm.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an HTTP method value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateHTTPMethod, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an HTTP method value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateHTTPMethod, typedValue, typedValue)} } case options.REQUEST_SERVICE: switch typedValue := vValue.(type) { @@ -273,10 +303,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: rs := new(customtypes.RequestService) if err = rs.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a request service value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateRequestService, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a request service value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateRequestService, typedValue, typedValue)} } case options.INT: switch typedValue := vValue.(type) { @@ -289,10 +319,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: i := new(customtypes.Int) if err = i.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an int value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateInt, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an int value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateInt, typedValue, typedValue)} } case options.PINGFEDERATE_AUTH_TYPE: switch typedValue := vValue.(type) { @@ -301,10 +331,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: pfa := new(customtypes.PingFederateAuthenticationType) if err = pfa.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingFederate Authentication Type value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidatePingFederateAuthType, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingFederate Authentication Type value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidatePingFederateAuthType, typedValue, typedValue)} } case options.PINGONE_AUTH_TYPE: switch typedValue := vValue.(type) { @@ -313,10 +343,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: pat := new(customtypes.PingOneAuthenticationType) if err = pat.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingOne Authentication Type value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidatePingOneAuthType, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne Authentication Type value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidatePingOneAuthType, typedValue, typedValue)} } case options.LICENSE_PRODUCT: switch typedValue := vValue.(type) { @@ -325,10 +355,10 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: lp := new(customtypes.LicenseProduct) if err = lp.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a License Product value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateLicenseProduct, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a License Product value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateLicenseProduct, typedValue, typedValue)} } case options.LICENSE_VERSION: switch typedValue := vValue.(type) { @@ -337,13 +367,13 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) case string: lv := new(customtypes.LicenseVersion) if err = lv.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a License Version value: %w", pName, typedValue, key, err) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s': %w", pName, ErrValidateLicenseVersion, typedValue, err)} } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a License Version value", pName, typedValue, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("profile '%s': %w '%s' of type '%T'", pName, ErrValidateLicenseVersion, typedValue, typedValue)} } default: - return fmt.Errorf("profile '%s': variable type '%d' for key '%s' is not recognized", pName, opt.Type, key) + return &errs.PingCLIError{Prefix: validateErrorPrefix, Err: fmt.Errorf("%w: %d", ErrUnrecognizedVariableType, opt.Type)} } } diff --git a/internal/profiles/validate_test.go b/internal/profiles/validate_test.go index 503116aa..6847eb4c 100644 --- a/internal/profiles/validate_test.go +++ b/internal/profiles/validate_test.go @@ -3,109 +3,163 @@ package profiles_test import ( + "fmt" + "strings" "testing" + "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/profiles" "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Test Validate function -func TestValidate(t *testing.T) { +func Test_Validate(t *testing.T) { testutils_koanf.InitKoanfs(t) - err := profiles.Validate() - if err != nil { - t.Errorf("Validate returned error: %v", err) + testCases := []struct { + name string + fileContents string + expectedError error + }{ + { + name: "Happy path - Default", + fileContents: testutils_koanf.GetDefaultConfigFileContents(), + expectedError: nil, + }, + { + name: "Happy path - Legacy", + fileContents: testutils_koanf.GetDefaultLegacyConfigFileContents(), + }, + { + name: "Invalid uuid", + fileContents: getInvalidUUIDFileContents(t), + expectedError: profiles.ErrValidateUUID, + }, + { + name: "Invalid region", + fileContents: getInvalidRegionFileContents(t), + expectedError: profiles.ErrValidatePingOneRegionCode, + }, + { + name: "Invalid bool", + fileContents: getInvalidBoolFileContents(t), + expectedError: profiles.ErrValidateBoolean, + }, + { + name: "Invalid output format", + fileContents: getInvalidOutputFormatFileContents(t), + expectedError: profiles.ErrValidateOutputFormat, + }, + { + name: "Invalid profile name", + fileContents: getInvalidProfileNameFileContents(t), + expectedError: profiles.ErrProfileNameFormat, + }, } -} - -// Test Validate function with legacy profile -func TestValidateLegacyProfile(t *testing.T) { - testutils_koanf.InitKoanfsCustomFile(t, testutils_koanf.ReturnDefaultLegacyConfigFileContents()) - - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } -} -// Test Validate function with invalid uuid -func TestValidateInvalidProfile(t *testing.T) { - fileContents := `activeProfile: default -default: - description: "default description" - pingOne: - export: - environmentID: "invalid"` + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutils_koanf.InitKoanfsCustomFile(t, tc.fileContents) - testutils_koanf.InitKoanfsCustomFile(t, fileContents) + err := profiles.Validate() - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") + if tc.expectedError != nil { + assert.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + } + }) } } -// Test Validate function with invalid region -func TestValidateInvalidRegion(t *testing.T) { - fileContents := `activeProfile: default -default: - description: "default description" - pingOne: - region: "invalid"` +func getInvalidUUIDFileContents(t *testing.T) string { + t.Helper() - testutils_koanf.InitKoanfsCustomFile(t, fileContents) + pingoneEnvIdKeyParts := strings.Split(options.PlatformExportPingOneEnvironmentIDOption.KoanfKey, ".") + require.Equal(t, 3, len(pingoneEnvIdKeyParts)) - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } + invalidUUIDFileContents := fmt.Sprintf(`%s: default +default: + %s: "default description" + %s: + %s: + %s: "invalid"`, + options.RootActiveProfileOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + pingoneEnvIdKeyParts[0], + pingoneEnvIdKeyParts[1], + pingoneEnvIdKeyParts[2], + ) + + return invalidUUIDFileContents } -// Test Validate function with invalid bool -func TestValidateInvalidBool(t *testing.T) { - fileContents := `activeProfile: default -default: - description: "default description" - pingcli: - noColor: invalid` +func getInvalidRegionFileContents(t *testing.T) string { + t.Helper() - testutils_koanf.InitKoanfsCustomFile(t, fileContents) + pingoneRegionCodeKeyParts := strings.Split(options.PingOneRegionCodeOption.KoanfKey, ".") + require.Equal(t, 3, len(pingoneRegionCodeKeyParts)) - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } + invalidRegionFileContents := fmt.Sprintf(`%s: default +default: + %s: "default description" + %s: + %s: + %s: "invalid"`, + options.RootActiveProfileOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + pingoneRegionCodeKeyParts[0], + pingoneRegionCodeKeyParts[1], + pingoneRegionCodeKeyParts[2], + ) + + return invalidRegionFileContents } -// Test Validate function with invalid output format -func TestValidateInvalidOutputFormat(t *testing.T) { - fileContents := `activeProfile: default +func getInvalidBoolFileContents(t *testing.T) string { + t.Helper() + + invalidBoolFileContents := fmt.Sprintf(`%s: default default: - description: "default description" - pingcli: - outputFormat: invalid` + %s: "default description" + %s: invalid`, + options.RootActiveProfileOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + options.RootColorOption.KoanfKey, + ) + + return invalidBoolFileContents +} - testutils_koanf.InitKoanfsCustomFile(t, fileContents) +func getInvalidOutputFormatFileContents(t *testing.T) string { + t.Helper() - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } + invalidOutputFormatFileContents := fmt.Sprintf(`%s: default +default: + %s: "default description" + %s: invalid`, + options.RootActiveProfileOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + options.RootOutputFormatOption.KoanfKey, + ) + + return invalidOutputFormatFileContents } -// Test Validate function with invalid profile name -func TestValidateInvalidProfileName(t *testing.T) { - fileContents := `activeProfile: default +func getInvalidProfileNameFileContents(t *testing.T) string { + t.Helper() + + invalidProfileNameFileContents := fmt.Sprintf(`%s: default default: - description: "default description" + %s: "default description" invalid(&*^&*^&*^**$): - description: "default description"` + %s: "default description"`, + options.RootActiveProfileOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + options.ProfileDescriptionOption.KoanfKey, + ) - testutils_koanf.InitKoanfsCustomFile(t, fileContents) - - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } + return invalidProfileNameFileContents } diff --git a/internal/testing/testutils/stdio.go b/internal/testing/testutils/stdio.go new file mode 100644 index 00000000..641011c3 --- /dev/null +++ b/internal/testing/testutils/stdio.go @@ -0,0 +1,30 @@ +// Copyright © 2025 Ping Identity Corporation + +package testutils + +import ( + "io" + "os" +) + +// CaptureStdout executes a function and returns its standard output as a string. +func CaptureStdout(f func()) string { + originalStdout := os.Stdout + r, w, _ := os.Pipe() + + defer func() { os.Stdout = originalStdout }() + + os.Stdout = w + + outC := make(chan string) + go func() { + b, _ := io.ReadAll(r) + outC <- string(b) + }() + + f() + + w.Close() + + return <-outC +} diff --git a/internal/testing/testutils_koanf/koanf_utils.go b/internal/testing/testutils_koanf/koanf_utils.go index bf007ade..f93d7d39 100644 --- a/internal/testing/testutils_koanf/koanf_utils.go +++ b/internal/testing/testutils_koanf/koanf_utils.go @@ -103,7 +103,7 @@ func CreateConfigFile(t *testing.T) string { t.Helper() if configFileContents == "" { - configFileContents = strings.Replace(getDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir(), 1) + configFileContents = strings.Replace(GetDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir(), 1) } configFilePath := t.TempDir() + "/config.yaml" @@ -132,7 +132,7 @@ func InitKoanfs(t *testing.T) *profiles.KoanfConfig { configuration.InitAllOptions() - configFileContents = strings.Replace(getDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) + configFileContents = strings.Replace(GetDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) return configureMainKoanf(t) } @@ -140,11 +140,13 @@ func InitKoanfs(t *testing.T) *profiles.KoanfConfig { func InitKoanfsCustomFile(t *testing.T, fileContents string) { t.Helper() - configFileContents = fileContents + configuration.InitAllOptions() + + configFileContents = strings.Replace(fileContents, outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) configureMainKoanf(t) } -func getDefaultConfigFileContents() string { +func GetDefaultConfigFileContents() string { return fmt.Sprintf(defaultConfigFileContentsPattern, outputDirectoryReplacement, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, @@ -157,7 +159,7 @@ func getDefaultConfigFileContents() string { ) } -func ReturnDefaultLegacyConfigFileContents() string { +func GetDefaultLegacyConfigFileContents() string { return fmt.Sprintf(defaultLegacyConfigFileContentsPattern, outputDirectoryReplacement, customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE, diff --git a/shared/grpc/pingcli_command_grpc_server.go b/shared/grpc/pingcli_command_grpc_server.go index 361682ad..1f80c00b 100644 --- a/shared/grpc/pingcli_command_grpc_server.go +++ b/shared/grpc/pingcli_command_grpc_server.go @@ -24,6 +24,10 @@ func (s *PingCliCommandGRPCServer) Configuration(ctx context.Context, req *proto return nil, err } + if cmd == nil { + return nil, errors.New("plugin returned a nil configuration") + } + return &proto.PingCliCommandConfigurationResponse{ Example: &cmd.Example, Long: &cmd.Long, From d70c16401bff402dbe7b46b6db09d6297f3d9980 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 30 Sep 2025 15:37:54 -0600 Subject: [PATCH 08/14] Update Pingfederate go client version --- contributing/development-environment.md | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- internal/commands/platform/export_internal.go | 2 +- internal/connector/exportable_resource.go | 2 +- .../connector/pingfederate/pingfederate_connector.go | 2 +- internal/testing/testutils/utils.go | 2 +- .../authentication_api_application.go | 2 +- .../authentication_policies_fragment.go | 4 ++-- .../authentication_policy_contract.go | 2 +- .../authentication_selector.go | 2 +- .../pingfederate_testable_resources/captcha_provider.go | 2 +- .../pingfederate_testable_resources/certificate_ca.go | 2 +- .../certificates_revocation_ocsp_certificate.go | 2 +- .../identity_store_provisioner.go | 2 +- .../pingfederate_testable_resources/idp_adapter.go | 2 +- .../pingfederate_testable_resources/idp_sp_connection.go | 2 +- .../idp_sts_request_parameters_contract.go | 2 +- .../idp_to_sp_adapter_mapping.go | 2 +- .../idp_token_processor.go | 2 +- .../pingfederate_testable_resources/kerberos_realm.go | 2 +- .../keypairs_oauth_openid_connect_additional_key_set.go | 2 +- .../keypairs_signing_key.go | 2 +- .../keypairs_signing_key_rotation_settings.go | 2 +- .../local_identity_profile.go | 2 +- .../pingfederate_testable_resources/metadata_url.go | 2 +- .../notification_publisher.go | 2 +- .../oauth_access_token_manager.go | 2 +- .../oauth_access_token_mapping.go | 2 +- .../oauth_authentication_policy_contract_mapping.go | 9 +++++---- .../oauth_ciba_server_policy_request_policy.go | 6 +++--- .../pingfederate_testable_resources/oauth_client.go | 2 +- .../oauth_client_registration_policy.go | 2 +- .../oauth_idp_adapter_mapping.go | 2 +- .../pingfederate_testable_resources/oauth_issuer.go | 2 +- .../oauth_token_exchange_processor_policy.go | 2 +- .../oauth_token_exchange_token_generator_mapping.go | 2 +- .../openid_connect_policy.go | 2 +- .../out_of_band_auth_plugins.go | 2 +- .../password_credential_validator.go | 2 +- .../pingone_connection.go | 2 +- .../pingfederate_testable_resources/secret_manager.go | 2 +- ..._settings_ws_trust_sts_settings_issuer_certificate.go | 2 +- .../session_authentication_policy.go | 2 +- .../pingfederate_testable_resources/sp_adapter.go | 2 +- .../sp_authentication_policy_contract_mapping.go | 2 +- .../pingfederate_testable_resources/sp_idp_connection.go | 2 +- .../sp_token_generator.go | 2 +- .../token_processor_to_token_generator_mapping.go | 2 +- 49 files changed, 58 insertions(+), 57 deletions(-) diff --git a/contributing/development-environment.md b/contributing/development-environment.md index 074789da..eead6ff3 100644 --- a/contributing/development-environment.md +++ b/contributing/development-environment.md @@ -143,12 +143,12 @@ go 1.25.1 replace github.com/patrickcping/pingone-go-sdk-v2/management => ../pingone-go-sdk-v2/management replace github.com/patrickcping/pingone-go-sdk-v2/mfa => ../pingone-go-sdk-v2/mfa -replace github.com/pingidentity/pingfederate-go-client/v1220 => ../pingfederate-go-client/v1220 +replace github.com/pingidentity/pingfederate-go-client/v1230 => ../pingfederate-go-client/v1230 require ( github.com/patrickcping/pingone-go-sdk-v2/management v0.60.0 github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.1 - github.com/pingidentity/pingfederate-go-client/v1220 v1220.0.0 + github.com/pingidentity/pingfederate-go-client/v1230 v1230.0.3 ... ) diff --git a/go.mod b/go.mod index 359e0b65..6255b4d5 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/patrickcping/pingone-go-sdk-v2/management v0.61.0 github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.2 github.com/patrickcping/pingone-go-sdk-v2/risk v0.20.0 - github.com/pingidentity/pingfederate-go-client/v1220 v1220.0.0 + github.com/pingidentity/pingfederate-go-client/v1230 v1230.0.3 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index 6e769b32..a3d5bc20 100644 --- a/go.sum +++ b/go.sum @@ -508,8 +508,8 @@ github.com/pavius/impi v0.0.3 h1:DND6MzU+BLABhOZXbELR3FU8b+zDgcq4dOCNLhiTYuI= github.com/pavius/impi v0.0.3/go.mod h1:x/hU0bfdWIhuOT1SKwiJg++yvkk6EuOtJk8WtDZqgr8= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pingidentity/pingfederate-go-client/v1220 v1220.0.0 h1:rCYn/ELV74uLO9dDWNgax03pjn9sw8YVNeIK94qkDmA= -github.com/pingidentity/pingfederate-go-client/v1220 v1220.0.0/go.mod h1:oA+IsXxQB8Glkq90fWakWb6tJ742An5Ca5vwn9GobM4= +github.com/pingidentity/pingfederate-go-client/v1230 v1230.0.3 h1:rXExdhfD3KyicPBsC8uDqYXIQ6yTE85k21H2xpiDP1o= +github.com/pingidentity/pingfederate-go-client/v1230 v1230.0.3/go.mod h1:MGHFs12NFixF0Dvylo2TjgRo51j2Cke3ed/W7ND5koE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index d59053aa..cab40d09 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -30,7 +30,7 @@ import ( "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" - pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) var ( diff --git a/internal/connector/exportable_resource.go b/internal/connector/exportable_resource.go index be0cb164..b3ff659e 100644 --- a/internal/connector/exportable_resource.go +++ b/internal/connector/exportable_resource.go @@ -8,7 +8,7 @@ import ( "regexp" pingoneGoClient "github.com/patrickcping/pingone-go-sdk-v2/pingone" - pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) type ImportBlock struct { diff --git a/internal/connector/pingfederate/pingfederate_connector.go b/internal/connector/pingfederate/pingfederate_connector.go index 69c236e8..130b5eb5 100644 --- a/internal/connector/pingfederate/pingfederate_connector.go +++ b/internal/connector/pingfederate/pingfederate_connector.go @@ -9,7 +9,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/logger" - pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) const ( diff --git a/internal/testing/testutils/utils.go b/internal/testing/testutils/utils.go index 3e3ab110..d38c8f18 100644 --- a/internal/testing/testutils/utils.go +++ b/internal/testing/testutils/utils.go @@ -25,7 +25,7 @@ import ( "github.com/patrickcping/pingone-go-sdk-v2/pingone" "github.com/pingidentity/pingcli/internal/configuration" "github.com/pingidentity/pingcli/internal/connector" - pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) var ( diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_api_application.go b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_api_application.go index 015cf1ac..4ce0dcc5 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_api_application.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_api_application.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func AuthenticationApiApplication(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policies_fragment.go b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policies_fragment.go index f452e98e..561ea16b 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policies_fragment.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policies_fragment.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func AuthenticationPoliciesFragment(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { @@ -42,7 +42,7 @@ func createAuthenticationPoliciesFragment(t *testing.T, clientInfo *connector.Cl clientStruct := client.AuthenticationPolicyFragment{ Id: utils.Pointer("TestFragmentId"), Name: utils.Pointer("TestFragmentName"), - RootNode: &client.AuthenticationPolicyTreeNode{ + RootNode: client.AuthenticationPolicyTreeNode{ Action: client.PolicyActionAggregation{ AuthnSourcePolicyAction: &client.AuthnSourcePolicyAction{ PolicyAction: client.PolicyAction{ diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policy_contract.go b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policy_contract.go index 05562055..53c4aba6 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policy_contract.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_policy_contract.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func AuthenticationPolicyContract(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_selector.go b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_selector.go index 771810a4..d3179d42 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_selector.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/authentication_selector.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func AuthenticationSelector(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/captcha_provider.go b/internal/testing/testutils_resource/pingfederate_testable_resources/captcha_provider.go index a762f0a4..3828f39a 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/captcha_provider.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/captcha_provider.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func CaptchaProvider(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/certificate_ca.go b/internal/testing/testutils_resource/pingfederate_testable_resources/certificate_ca.go index 4f72a860..66a3494b 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/certificate_ca.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/certificate_ca.go @@ -12,7 +12,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func CertificateCa(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/certificates_revocation_ocsp_certificate.go b/internal/testing/testutils_resource/pingfederate_testable_resources/certificates_revocation_ocsp_certificate.go index 1baea5e5..90ec9c28 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/certificates_revocation_ocsp_certificate.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/certificates_revocation_ocsp_certificate.go @@ -12,7 +12,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func CertificatesRevocationOcspCertificate(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/identity_store_provisioner.go b/internal/testing/testutils_resource/pingfederate_testable_resources/identity_store_provisioner.go index 291fef71..7dcf2b14 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/identity_store_provisioner.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/identity_store_provisioner.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdentityStoreProvisioner(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_adapter.go b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_adapter.go index 34881c97..6cd685bd 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_adapter.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_adapter.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdpAdapter(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sp_connection.go b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sp_connection.go index ab375296..da41984c 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sp_connection.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sp_connection.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdpSpConnection(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sts_request_parameters_contract.go b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sts_request_parameters_contract.go index 0a212d68..3b4e826a 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sts_request_parameters_contract.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_sts_request_parameters_contract.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdpStsRequestParametersContract(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_to_sp_adapter_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_to_sp_adapter_mapping.go index c23dd8a7..a705d521 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_to_sp_adapter_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_to_sp_adapter_mapping.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdpToSpAdapterMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_token_processor.go b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_token_processor.go index c94ce771..78feee45 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/idp_token_processor.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/idp_token_processor.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func IdpTokenProcessor(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/kerberos_realm.go b/internal/testing/testutils_resource/pingfederate_testable_resources/kerberos_realm.go index 86a49c4b..1d4ed4d4 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/kerberos_realm.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/kerberos_realm.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func KerberosRealm(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_oauth_openid_connect_additional_key_set.go b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_oauth_openid_connect_additional_key_set.go index d15f7519..b5b9a825 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_oauth_openid_connect_additional_key_set.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_oauth_openid_connect_additional_key_set.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func KeypairsOauthOpenidConnectAdditionalKeySet(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key.go b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key.go index 748d8cc7..f1a82840 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func KeypairsSigningKey(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key_rotation_settings.go b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key_rotation_settings.go index 7b3c3a8d..04e0c065 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key_rotation_settings.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/keypairs_signing_key_rotation_settings.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func KeypairsSigningKeyRotationSettings(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/local_identity_profile.go b/internal/testing/testutils_resource/pingfederate_testable_resources/local_identity_profile.go index 32c7649f..210f1e32 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/local_identity_profile.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/local_identity_profile.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func LocalIdentityProfile(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/metadata_url.go b/internal/testing/testutils_resource/pingfederate_testable_resources/metadata_url.go index 39f34ab3..1c8d5bbc 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/metadata_url.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/metadata_url.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func MetadataUrl(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/notification_publisher.go b/internal/testing/testutils_resource/pingfederate_testable_resources/notification_publisher.go index 8478712d..2fef147d 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/notification_publisher.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/notification_publisher.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func NotificationPublisher(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_manager.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_manager.go index 8286e0e0..c3c4af2d 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_manager.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_manager.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthAccessTokenManager(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_mapping.go index 438dc896..be7c84e9 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_access_token_mapping.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthAccessTokenMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_authentication_policy_contract_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_authentication_policy_contract_mapping.go index 136a6267..eed9d02d 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_authentication_policy_contract_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_authentication_policy_contract_mapping.go @@ -10,7 +10,8 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + "github.com/pingidentity/pingcli/internal/utils" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthAuthenticationPolicyContractMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { @@ -54,7 +55,7 @@ func createOauthAuthenticationPolicyContractMapping(t *testing.T, clientInfo *co AuthenticationPolicyContractRef: client.ResourceLink{ Id: testApcId, }, - Id: "testApcToPersistentGrantMappingId", + Id: utils.Pointer("testApcToPersistentGrantMappingId"), } request = request.Body(clientStruct) @@ -74,10 +75,10 @@ func createOauthAuthenticationPolicyContractMapping(t *testing.T, clientInfo *co return testutils_resource.ResourceInfo{ DeletionIds: []string{ - resource.Id, + *resource.Id, }, CreationInfo: map[testutils_resource.ResourceCreationInfoType]string{ - testutils_resource.ENUM_ID: resource.Id, + testutils_resource.ENUM_ID: *resource.Id, }, } } diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_ciba_server_policy_request_policy.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_ciba_server_policy_request_policy.go index d46dfba7..dbf4e898 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_ciba_server_policy_request_policy.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_ciba_server_policy_request_policy.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthCibaServerPolicyRequestPolicy(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { @@ -62,7 +62,7 @@ func createOauthCibaServerPolicyRequestPolicy(t *testing.T, clientInfo *connecto }, }, }, - IdentityHintMapping: &client.AttributeMapping{ + IdentityHintMapping: client.AttributeMapping{ AttributeContractFulfillment: map[string]client.AttributeFulfillmentValue{ "subject": { Source: client.SourceTypeIdKey{ @@ -78,7 +78,7 @@ func createOauthCibaServerPolicyRequestPolicy(t *testing.T, clientInfo *connecto }, Name: "TestRequestPolicyName", RequireTokenForIdentityHint: utils.Pointer(false), - TransactionLifetime: utils.Pointer(int64(120)), + TransactionLifetime: int64(120), } request = request.Body(clientStruct) diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client.go index 27caaa86..9a0d3c9e 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthClient(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client_registration_policy.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client_registration_policy.go index d2406346..4b37018c 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client_registration_policy.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_client_registration_policy.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthClientRegistrationPolicy(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_idp_adapter_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_idp_adapter_mapping.go index a1648359..5cb1522f 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_idp_adapter_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_idp_adapter_mapping.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthIdpAdapterMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_issuer.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_issuer.go index 1e8cdb7c..ed865b83 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_issuer.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_issuer.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthIssuer(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_processor_policy.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_processor_policy.go index 4fa524c1..bf1c7a0a 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_processor_policy.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_processor_policy.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthTokenExchangeProcessorPolicy(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_token_generator_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_token_generator_mapping.go index ac91142f..52802db2 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_token_generator_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/oauth_token_exchange_token_generator_mapping.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OauthTokenExchangeTokenGeneratorMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/openid_connect_policy.go b/internal/testing/testutils_resource/pingfederate_testable_resources/openid_connect_policy.go index 7ec31ea6..18364b14 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/openid_connect_policy.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/openid_connect_policy.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OpenidConnectPolicy(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/out_of_band_auth_plugins.go b/internal/testing/testutils_resource/pingfederate_testable_resources/out_of_band_auth_plugins.go index 6c5e54a7..2cd4208d 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/out_of_band_auth_plugins.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/out_of_band_auth_plugins.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/testing/testutils_resource/pingone_sso_testable_resources" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func OutOfBandAuthPlugins(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/password_credential_validator.go b/internal/testing/testutils_resource/pingfederate_testable_resources/password_credential_validator.go index 67f20dbc..eabc7211 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/password_credential_validator.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/password_credential_validator.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func PasswordCredentialValidator(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/pingone_connection.go b/internal/testing/testutils_resource/pingfederate_testable_resources/pingone_connection.go index d8459609..c44efde3 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/pingone_connection.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/pingone_connection.go @@ -12,7 +12,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/testing/testutils_resource/pingone_platform_testable_resources" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func PingoneConnection(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/secret_manager.go b/internal/testing/testutils_resource/pingfederate_testable_resources/secret_manager.go index dbcdc801..ff117a99 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/secret_manager.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/secret_manager.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SecretManager(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/server_settings_ws_trust_sts_settings_issuer_certificate.go b/internal/testing/testutils_resource/pingfederate_testable_resources/server_settings_ws_trust_sts_settings_issuer_certificate.go index e10a5429..87cf437a 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/server_settings_ws_trust_sts_settings_issuer_certificate.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/server_settings_ws_trust_sts_settings_issuer_certificate.go @@ -12,7 +12,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func ServerSettingsWsTrustStsSettingsIssuerCertificate(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/session_authentication_policy.go b/internal/testing/testutils_resource/pingfederate_testable_resources/session_authentication_policy.go index ca5531dc..9ae70e4b 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/session_authentication_policy.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/session_authentication_policy.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SessionAuthenticationPolicy(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_adapter.go b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_adapter.go index a3f23fd3..56c69ac7 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_adapter.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_adapter.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SpAdapter(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_authentication_policy_contract_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_authentication_policy_contract_mapping.go index b80c971f..6a46e5e5 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_authentication_policy_contract_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_authentication_policy_contract_mapping.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SpAuthenticationPolicyContractMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_idp_connection.go b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_idp_connection.go index b9ebff8c..d651ec99 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_idp_connection.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_idp_connection.go @@ -12,7 +12,7 @@ import ( "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SpIdpConnection(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_token_generator.go b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_token_generator.go index 41283938..c3d38472 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/sp_token_generator.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/sp_token_generator.go @@ -10,7 +10,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func SpTokenGenerator(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { diff --git a/internal/testing/testutils_resource/pingfederate_testable_resources/token_processor_to_token_generator_mapping.go b/internal/testing/testutils_resource/pingfederate_testable_resources/token_processor_to_token_generator_mapping.go index 006664c0..3fb9fc2f 100644 --- a/internal/testing/testutils_resource/pingfederate_testable_resources/token_processor_to_token_generator_mapping.go +++ b/internal/testing/testutils_resource/pingfederate_testable_resources/token_processor_to_token_generator_mapping.go @@ -11,7 +11,7 @@ import ( "github.com/pingidentity/pingcli/internal/connector/pingfederate/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" "github.com/pingidentity/pingcli/internal/utils" - client "github.com/pingidentity/pingfederate-go-client/v1220/configurationapi" + client "github.com/pingidentity/pingfederate-go-client/v1230/configurationapi" ) func TokenProcessorToTokenGeneratorMapping(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { From 5c571375cad4430debdeebe12a7a0b695f20ece3 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 30 Sep 2025 16:00:10 -0600 Subject: [PATCH 09/14] Linter fixes --- .golangci.yml | 1 - cmd/completion/cmd_test.go | 1 + cmd/config/add_profile_test.go | 1 + cmd/config/config_test.go | 1 + cmd/config/delete_profile_test.go | 1 + cmd/config/get_test.go | 1 + cmd/config/list_keys_test.go | 1 + cmd/config/list_profiles_test.go | 1 + cmd/config/set_active_profile_test.go | 1 + cmd/config/set_test.go | 1 + cmd/config/unset_test.go | 1 + cmd/config/view_profile_test.go | 1 + cmd/feedback/feedback_test.go | 1 + cmd/license/license_test.go | 1 + cmd/platform/export_test.go | 10 ++++++++-- cmd/platform/platform_test.go | 1 + cmd/plugin/add_test.go | 6 +++++- cmd/plugin/list_test.go | 1 + cmd/plugin/plugin_test.go | 1 + cmd/plugin/remove_test.go | 1 + cmd/request/request_test.go | 1 + cmd/root_test.go | 1 + internal/commands/platform/export_internal_test.go | 2 +- internal/customtypes/export_service_group.go | 1 + internal/customtypes/export_service_group_test.go | 4 ++-- internal/customtypes/export_services.go | 4 +--- internal/customtypes/headers.go | 1 + internal/customtypes/headers_test.go | 2 +- internal/customtypes/http_method.go | 1 + internal/customtypes/int.go | 2 ++ internal/customtypes/license_product.go | 1 + internal/customtypes/output_format.go | 1 + internal/customtypes/pingone_auth_type.go | 1 + internal/customtypes/pingone_region_code.go | 1 + internal/customtypes/request_services.go | 1 + internal/plugins/plugins.go | 2 ++ internal/plugins/plugins_test.go | 7 ++++++- internal/testing/testutils/stdio.go | 5 ++++- internal/testing/testutils_koanf/koanf_utils.go | 7 ++++--- 39 files changed, 63 insertions(+), 16 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 59e5a390..792b82b0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,7 +53,6 @@ linters: - staticcheck - tagalign - testableexamples - - testpackage - thelper - tparallel - unconvert diff --git a/cmd/completion/cmd_test.go b/cmd/completion/cmd_test.go index cdb8e6f9..cc770eab 100644 --- a/cmd/completion/cmd_test.go +++ b/cmd/completion/cmd_test.go @@ -59,6 +59,7 @@ func Test_CompletionCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/add_profile_test.go b/cmd/config/add_profile_test.go index 3112f5e1..ff3015e6 100644 --- a/cmd/config/add_profile_test.go +++ b/cmd/config/add_profile_test.go @@ -117,6 +117,7 @@ func Test_ConfigAddProfileCommand(t *testing.T) { if !tc.expectErr { assert.NoError(t, err) + return } diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index 69922940..c7115bcf 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -46,6 +46,7 @@ func Test_ConfigCommand(t *testing.T) { if !tc.expectErr { assert.NoError(t, err) + return } diff --git a/cmd/config/delete_profile_test.go b/cmd/config/delete_profile_test.go index b6cd35e6..f9f187a9 100644 --- a/cmd/config/delete_profile_test.go +++ b/cmd/config/delete_profile_test.go @@ -67,6 +67,7 @@ func Test_ConfigDeleteProfileCommand(t *testing.T) { if !tc.expectErr { assert.NoError(t, err) + return } diff --git a/cmd/config/get_test.go b/cmd/config/get_test.go index 7912ddb5..dcc74c98 100644 --- a/cmd/config/get_test.go +++ b/cmd/config/get_test.go @@ -73,6 +73,7 @@ func Test_ConfigGetCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go index ebadf9ac..30156881 100644 --- a/cmd/config/list_keys_test.go +++ b/cmd/config/list_keys_test.go @@ -64,6 +64,7 @@ func Test_ConfigListKeysCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/list_profiles_test.go b/cmd/config/list_profiles_test.go index 7acfd4fd..50d928c9 100644 --- a/cmd/config/list_profiles_test.go +++ b/cmd/config/list_profiles_test.go @@ -54,6 +54,7 @@ func Test_ConfigListProfilesCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/set_active_profile_test.go b/cmd/config/set_active_profile_test.go index ba536046..89117e49 100644 --- a/cmd/config/set_active_profile_test.go +++ b/cmd/config/set_active_profile_test.go @@ -72,6 +72,7 @@ func Test_ConfigSetActiveProfileCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index 1326b46a..8c2ce1ef 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -84,6 +84,7 @@ func Test_ConfigSetCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index 962a0f87..64426318 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -69,6 +69,7 @@ func Test_ConfigUnsetCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/config/view_profile_test.go b/cmd/config/view_profile_test.go index 6aa4d26e..be4366c0 100644 --- a/cmd/config/view_profile_test.go +++ b/cmd/config/view_profile_test.go @@ -72,6 +72,7 @@ func Test_ConfigViewProfileCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/feedback/feedback_test.go b/cmd/feedback/feedback_test.go index 93b27153..438c38bf 100644 --- a/cmd/feedback/feedback_test.go +++ b/cmd/feedback/feedback_test.go @@ -64,6 +64,7 @@ func Test_FeedbackCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/license/license_test.go b/cmd/license/license_test.go index 1871b418..d5fa7e6e 100644 --- a/cmd/license/license_test.go +++ b/cmd/license/license_test.go @@ -93,6 +93,7 @@ func Test_LicenseCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go index e2379cc6..73b8eddd 100644 --- a/cmd/platform/export_test.go +++ b/cmd/platform/export_test.go @@ -4,6 +4,7 @@ package platform_test import ( "os" + "path/filepath" "strings" "testing" @@ -120,7 +121,9 @@ func Test_PlatformExportCommand(t *testing.T) { "--" + options.PlatformExportOverwriteOption.CobraParamName + "=false", }, setup: func(t *testing.T, tempDir string) { - _, err := os.Create(tempDir + "/file") + t.Helper() + + _, err := os.Create(filepath.Join(tempDir, "file")) // #nosec G304 require.NoError(t, err) }, expectErr: true, @@ -133,7 +136,9 @@ func Test_PlatformExportCommand(t *testing.T) { "--" + options.PlatformExportOverwriteOption.CobraParamName, }, setup: func(t *testing.T, tempDir string) { - _, err := os.Create(tempDir + "/file") + t.Helper() + + _, err := os.Create(filepath.Join(tempDir, "file")) // #nosec G304 require.NoError(t, err) }, expectErr: false, @@ -331,6 +336,7 @@ func Test_PlatformExportCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/platform/platform_test.go b/cmd/platform/platform_test.go index 1de416c8..d986758d 100644 --- a/cmd/platform/platform_test.go +++ b/cmd/platform/platform_test.go @@ -47,6 +47,7 @@ func Test_PlatformCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/plugin/add_test.go b/cmd/plugin/add_test.go index 295c821b..e31fbcf0 100644 --- a/cmd/plugin/add_test.go +++ b/cmd/plugin/add_test.go @@ -22,7 +22,10 @@ func Test_PluginAddCommand(t *testing.T) { pluginFilename := filepath.Base(goldenPlugin) require.FileExists(t, goldenPlugin, "Test plugin executable does not exist") - defer os.Remove(goldenPlugin) + t.Cleanup(func() { + err := os.Remove(goldenPlugin) + require.NoError(t, err, "Failed to remove test plugin executable") + }) testCases := []struct { name string @@ -77,6 +80,7 @@ func Test_PluginAddCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/plugin/list_test.go b/cmd/plugin/list_test.go index d2a0c267..a423174a 100644 --- a/cmd/plugin/list_test.go +++ b/cmd/plugin/list_test.go @@ -54,6 +54,7 @@ func Test_PluginListCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/plugin/plugin_test.go b/cmd/plugin/plugin_test.go index e626bef0..4447b17c 100644 --- a/cmd/plugin/plugin_test.go +++ b/cmd/plugin/plugin_test.go @@ -47,6 +47,7 @@ func Test_PluginCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/plugin/remove_test.go b/cmd/plugin/remove_test.go index 05ed94a2..aeecdf86 100644 --- a/cmd/plugin/remove_test.go +++ b/cmd/plugin/remove_test.go @@ -65,6 +65,7 @@ func Test_PluginRemoveCommand(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/request/request_test.go b/cmd/request/request_test.go index 5172c35d..8a1b530b 100644 --- a/cmd/request/request_test.go +++ b/cmd/request/request_test.go @@ -112,6 +112,7 @@ func Test_RequestCommand_Validation(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/cmd/root_test.go b/cmd/root_test.go index 5192750a..ef0887bf 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -119,6 +119,7 @@ func Test_RootCommand_Validation(t *testing.T) { if !tc.expectErr { require.NoError(t, err) + return } diff --git a/internal/commands/platform/export_internal_test.go b/internal/commands/platform/export_internal_test.go index af6efd6b..3e2542de 100644 --- a/internal/commands/platform/export_internal_test.go +++ b/internal/commands/platform/export_internal_test.go @@ -342,7 +342,7 @@ func createNonEmptyDir(t *testing.T) string { t.Helper() dir := t.TempDir() - file, err := os.CreateTemp(dir, "file-*.tf") + file, err := os.CreateTemp(dir, "file-*.tf") // #nosec G304 require.NoError(t, err) err = file.Close() diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go index 741d7a66..6ec2f25a 100644 --- a/internal/customtypes/export_service_group.go +++ b/internal/customtypes/export_service_group.go @@ -59,6 +59,7 @@ func (esg *ExportServiceGroup) String() string { if esg == nil { return "" } + return string(*esg) } diff --git a/internal/customtypes/export_service_group_test.go b/internal/customtypes/export_service_group_test.go index 79607b2b..4d1be9ac 100644 --- a/internal/customtypes/export_service_group_test.go +++ b/internal/customtypes/export_service_group_test.go @@ -161,8 +161,8 @@ func Test_ExportServiceGroup_GetServicesInGroup(t *testing.T) { }, }, { - name: "non existant group", - cType: utils.Pointer(customtypes.ExportServiceGroup("non-existant")), + name: "non existent group", + cType: utils.Pointer(customtypes.ExportServiceGroup("non-existent")), expectedStrs: []string{}, }, { diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go index 68d1808a..16bc6b92 100644 --- a/internal/customtypes/export_services.go +++ b/internal/customtypes/export_services.go @@ -94,9 +94,7 @@ func (es *ExportServices) SetServicesByServiceGroup(serviceGroup *ExportServiceG return nil } - es.Set(strings.Join(serviceGroup.GetServicesInGroup(), ",")) - - return nil + return es.Set(strings.Join(serviceGroup.GetServicesInGroup(), ",")) } func (es *ExportServices) ContainsPingOneService() bool { diff --git a/internal/customtypes/headers.go b/internal/customtypes/headers.go index 3f839e21..1469d4de 100644 --- a/internal/customtypes/headers.go +++ b/internal/customtypes/headers.go @@ -86,6 +86,7 @@ func (h *HeaderSlice) String() string { if h == nil { return "" } + return strings.Join(h.StringSlice(), ",") } diff --git a/internal/customtypes/headers_test.go b/internal/customtypes/headers_test.go index a89246ab..c1088224 100644 --- a/internal/customtypes/headers_test.go +++ b/internal/customtypes/headers_test.go @@ -224,7 +224,7 @@ func Test_HeaderSlice_SetHttpRequestHeaders(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testutils_koanf.InitKoanfs(t) - req, err := http.NewRequest(http.MethodGet, "http://localhost", nil) + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://localhost", nil) require.NoError(t, err) tc.cType.SetHttpRequestHeaders(req) diff --git a/internal/customtypes/http_method.go b/internal/customtypes/http_method.go index a5b6e6c9..136e33e1 100644 --- a/internal/customtypes/http_method.go +++ b/internal/customtypes/http_method.go @@ -65,6 +65,7 @@ func (hm *HTTPMethod) String() string { if hm == nil { return "" } + return string(*hm) } diff --git a/internal/customtypes/int.go b/internal/customtypes/int.go index fa320c7a..2cc2647d 100644 --- a/internal/customtypes/int.go +++ b/internal/customtypes/int.go @@ -43,6 +43,7 @@ func (i *Int) String() string { if i == nil { return "0" } + return strconv.FormatInt(int64(*i), 10) } @@ -50,5 +51,6 @@ func (i *Int) Int64() int64 { if i == nil { return 0 } + return int64(*i) } diff --git a/internal/customtypes/license_product.go b/internal/customtypes/license_product.go index dfb51a74..f70fa7f1 100644 --- a/internal/customtypes/license_product.go +++ b/internal/customtypes/license_product.go @@ -70,6 +70,7 @@ func (lp *LicenseProduct) String() string { if lp == nil { return "" } + return string(*lp) } diff --git a/internal/customtypes/output_format.go b/internal/customtypes/output_format.go index b1d18abb..98d0db7a 100644 --- a/internal/customtypes/output_format.go +++ b/internal/customtypes/output_format.go @@ -56,6 +56,7 @@ func (o *OutputFormat) String() string { if o == nil { return "" } + return string(*o) } diff --git a/internal/customtypes/pingone_auth_type.go b/internal/customtypes/pingone_auth_type.go index 4765b74d..caa19a5a 100644 --- a/internal/customtypes/pingone_auth_type.go +++ b/internal/customtypes/pingone_auth_type.go @@ -52,6 +52,7 @@ func (pat *PingOneAuthenticationType) String() string { if pat == nil { return "" } + return string(*pat) } diff --git a/internal/customtypes/pingone_region_code.go b/internal/customtypes/pingone_region_code.go index 12236794..5b83b4fd 100644 --- a/internal/customtypes/pingone_region_code.go +++ b/internal/customtypes/pingone_region_code.go @@ -70,6 +70,7 @@ func (prc *PingOneRegionCode) String() string { if prc == nil { return "" } + return string(*prc) } diff --git a/internal/customtypes/request_services.go b/internal/customtypes/request_services.go index c68e490b..e8a3b054 100644 --- a/internal/customtypes/request_services.go +++ b/internal/customtypes/request_services.go @@ -52,6 +52,7 @@ func (rs *RequestService) String() string { if rs == nil { return "" } + return string(*rs) } diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index 6be3cfa9..4e2f0d5b 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -215,6 +215,7 @@ func filterRootFlags(cmd *cobra.Command, args []string) []string { if flag == nil { // If it's not a recognized root flag, it must be for the plugin. pluginArgs = append(pluginArgs, arg) + continue } @@ -228,5 +229,6 @@ func filterRootFlags(cmd *cobra.Command, args []string) []string { // It's a non-boolean flag in the form "--flag value". We need to skip both. i++ } + return pluginArgs } diff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go index 537d281e..4f4ef361 100644 --- a/internal/plugins/plugins_test.go +++ b/internal/plugins/plugins_test.go @@ -23,7 +23,6 @@ var ( Long: "A longer description for a test plugin", Example: "pingcli test-plugin --flag value", } - testRunError = errors.New("plugin run error") ) // mockPingCliCommand is a mock implementation of the grpc.PingCliCommand interface for testing. @@ -35,6 +34,7 @@ func (m *mockPingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, if configErr := os.Getenv("PINGCLI_TEST_PLUGIN_CONFIG_ERROR"); configErr != "" { return nil, errors.New(configErr) } + return testPluginConfig, nil } @@ -42,6 +42,7 @@ func (m *mockPingCliCommand) Run(args []string, l grpc.Logger) error { if runErr := os.Getenv("PINGCLI_TEST_PLUGIN_RUN_ERROR"); runErr != "" { return errors.New(runErr) } + return fmt.Errorf("args: %s", strings.Join(args, ",")) } @@ -54,13 +55,17 @@ func TestMain(m *testing.M) { }, GRPCServer: hplugin.DefaultGRPCServer, }) + return } os.Exit(m.Run()) } func setupPluginTest(t *testing.T) string { + t.Helper() + t.Setenv("PINGCLI_TEST_PLUGIN", "1") + return os.Args[0] } diff --git a/internal/testing/testutils/stdio.go b/internal/testing/testutils/stdio.go index 641011c3..cb52661c 100644 --- a/internal/testing/testutils/stdio.go +++ b/internal/testing/testutils/stdio.go @@ -24,7 +24,10 @@ func CaptureStdout(f func()) string { f() - w.Close() + err := w.Close() + if err != nil { + return "" + } return <-outC } diff --git a/internal/testing/testutils_koanf/koanf_utils.go b/internal/testing/testutils_koanf/koanf_utils.go index f93d7d39..75647c6c 100644 --- a/internal/testing/testutils_koanf/koanf_utils.go +++ b/internal/testing/testutils_koanf/koanf_utils.go @@ -5,6 +5,7 @@ package testutils_koanf import ( "fmt" "os" + "path/filepath" "strings" "testing" @@ -106,7 +107,7 @@ func CreateConfigFile(t *testing.T) string { configFileContents = strings.Replace(GetDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir(), 1) } - configFilePath := t.TempDir() + "/config.yaml" + configFilePath := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(configFilePath, []byte(configFileContents), 0600); err != nil { t.Fatalf("Failed to create config file: %s", err) } @@ -132,7 +133,7 @@ func InitKoanfs(t *testing.T) *profiles.KoanfConfig { configuration.InitAllOptions() - configFileContents = strings.Replace(GetDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) + configFileContents = strings.Replace(GetDefaultConfigFileContents(), outputDirectoryReplacement, filepath.Join(t.TempDir(), "config.yaml"), 1) return configureMainKoanf(t) } @@ -142,7 +143,7 @@ func InitKoanfsCustomFile(t *testing.T, fileContents string) { configuration.InitAllOptions() - configFileContents = strings.Replace(fileContents, outputDirectoryReplacement, t.TempDir()+"/config.yaml", 1) + configFileContents = strings.Replace(fileContents, outputDirectoryReplacement, filepath.Join(t.TempDir(), "config.yaml"), 1) configureMainKoanf(t) } From 028b2df3517441c5f577d0d3f069dc9584b6cd1b Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 30 Sep 2025 16:17:32 -0600 Subject: [PATCH 10/14] Testing fixes --- cmd/common/cobra_utils_test.go | 154 ++++++++++++++---- .../pingone/sso/pingone_sso_connector_test.go | 4 +- internal/profiles/validate_test.go | 8 +- 3 files changed, 126 insertions(+), 40 deletions(-) diff --git a/cmd/common/cobra_utils_test.go b/cmd/common/cobra_utils_test.go index 09506a8f..c1e06d48 100644 --- a/cmd/common/cobra_utils_test.go +++ b/cmd/common/cobra_utils_test.go @@ -6,44 +6,132 @@ import ( "testing" "github.com/pingidentity/pingcli/cmd/common" - "github.com/pingidentity/pingcli/internal/testing/testutils" - "github.com/spf13/cobra" + "github.com/stretchr/testify/require" ) -// Test ExactArgs returns no error when the number of arguments matches the expected number -func TestExactArgs_Matches(t *testing.T) { - posArgsFunc := common.ExactArgs(2) - err := posArgsFunc(nil, []string{"arg1", "arg2"}) - testutils.CheckExpectedError(t, err, nil) -} +func Test_ExactArgs(t *testing.T) { + testCases := []struct { + name string + numArgs int + args []string + expectedErr error + }{ + { + name: "No arguments, expecting 0", + numArgs: 0, + args: []string{}, + expectedErr: nil, + }, + { + name: "One argument, expecting 1", + numArgs: 1, + args: []string{"arg1"}, + expectedErr: nil, + }, + { + name: "Two arguments, expecting 2", + numArgs: 2, + args: []string{"arg1", "arg2"}, + expectedErr: nil, + }, + { + name: "Three arguments, expecting 2", + numArgs: 2, + args: []string{"arg1", "arg2", "arg3"}, + expectedErr: common.ErrExactArgs, + }, + { + name: "No arguments, expecting 1", + numArgs: 1, + args: []string{}, + expectedErr: common.ErrExactArgs, + }, + { + name: "One argument, expecting 0", + numArgs: 0, + args: []string{"arg1"}, + expectedErr: common.ErrExactArgs, + }, + } -// Test ExactArgs returns an error when the number of arguments does not match the expected number -func TestExactArgs_DoesNotMatch(t *testing.T) { - expectedErrorPattern := `^failed to execute 'test': command accepts 2 arg\(s\), received 3$` - posArgsFunc := common.ExactArgs(2) - err := posArgsFunc(&cobra.Command{Use: "test"}, []string{"arg1", "arg2", "arg3"}) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + posArgsFunc := common.ExactArgs(tc.numArgs) + err := posArgsFunc(nil, tc.args) -// Test RangeArgs returns no error when the number of arguments is within the expected range -func TestRangeArgs_Matches(t *testing.T) { - posArgsFunc := common.RangeArgs(2, 4) - err := posArgsFunc(nil, []string{"arg1", "arg2", "arg3"}) - testutils.CheckExpectedError(t, err, nil) + if tc.expectedErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedErr) + } else { + require.NoError(t, err) + } + }) + } } -// Test RangeArgs returns an error when the number of arguments is below the expected range -func TestRangeArgs_BelowRange(t *testing.T) { - expectedErrorPattern := `^failed to execute 'test': command accepts 2 to 4 arg\(s\), received 1$` - posArgsFunc := common.RangeArgs(2, 4) - err := posArgsFunc(&cobra.Command{Use: "test"}, []string{"arg1"}) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} +func Test_RangeArgs(t *testing.T) { + testCases := []struct { + name string + minArgs int + maxArgs int + args []string + expectedErr error + }{ + { + name: "No arguments, expecting 0 to 2", + minArgs: 0, + maxArgs: 2, + args: []string{}, + expectedErr: nil, + }, + { + name: "One argument, expecting 1 to 2", + minArgs: 1, + maxArgs: 2, + args: []string{"arg1"}, + expectedErr: nil, + }, + { + name: "Two arguments, expecting 1 to 2", + minArgs: 1, + maxArgs: 2, + args: []string{"arg1", "arg2"}, + expectedErr: nil, + }, + { + name: "Three arguments, expecting 1 to 2", + minArgs: 1, + maxArgs: 2, + args: []string{"arg1", "arg2", "arg3"}, + expectedErr: common.ErrRangeArgs, + }, + { + name: "No arguments, expecting 1 to 2", + minArgs: 1, + maxArgs: 2, + args: []string{}, + expectedErr: common.ErrRangeArgs, + }, + { + name: "One argument, expecting 0 to 0", + minArgs: 0, + maxArgs: 0, + args: []string{"arg1"}, + expectedErr: common.ErrRangeArgs, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + posArgsFunc := common.RangeArgs(tc.minArgs, tc.maxArgs) + err := posArgsFunc(nil, tc.args) -// Test RangeArgs returns an error when the number of arguments is above the expected range -func TestRangeArgs_AboveRange(t *testing.T) { - expectedErrorPattern := `^failed to execute 'test': command accepts 2 to 4 arg\(s\), received 5$` - posArgsFunc := common.RangeArgs(2, 4) - err := posArgsFunc(&cobra.Command{Use: "test"}, []string{"arg1", "arg2", "arg3", "arg4", "arg5"}) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if tc.expectedErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedErr) + } else { + require.NoError(t, err) + } + }) + } } diff --git a/internal/connector/pingone/sso/pingone_sso_connector_test.go b/internal/connector/pingone/sso/pingone_sso_connector_test.go index f95b440c..38f25252 100644 --- a/internal/connector/pingone/sso/pingone_sso_connector_test.go +++ b/internal/connector/pingone/sso/pingone_sso_connector_test.go @@ -126,9 +126,7 @@ func TestSSOTerraformPlan(t *testing.T) { { name: "ResourceScopePingOneApi", testableResource: pingone_sso_testable_resources.ResourceScopePingOneApi(t, clientInfo), - ignoredErrors: []string{ - "Error: Invalid Attribute Value Match", - }, + ignoredErrors: nil, }, { name: "ResourceSecret", diff --git a/internal/profiles/validate_test.go b/internal/profiles/validate_test.go index 6847eb4c..ba69d664 100644 --- a/internal/profiles/validate_test.go +++ b/internal/profiles/validate_test.go @@ -27,10 +27,10 @@ func Test_Validate(t *testing.T) { fileContents: testutils_koanf.GetDefaultConfigFileContents(), expectedError: nil, }, - { - name: "Happy path - Legacy", - fileContents: testutils_koanf.GetDefaultLegacyConfigFileContents(), - }, + // { // validate() does not support case insensitive profile names or koanf keys + // name: "Happy path - Legacy", + // fileContents: testutils_koanf.GetDefaultLegacyConfigFileContents(), + // }, { name: "Invalid uuid", fileContents: getInvalidUUIDFileContents(t), From b1c8bee1f3411d56fd7286a77bf8605081a34d4b Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 14 Oct 2025 12:48:45 -0500 Subject: [PATCH 11/14] PR review changes --- .../workflows/code-analysis-lint-test.yaml | 2 - .golangci.yml | 1 + cmd/common/cobra_utils.go | 3 - cmd/common/errors.go | 10 +++ internal/autocompletion/config_args.go | 10 ++- internal/autocompletion/errors.go | 15 ++++ internal/autocompletion/root_flags.go | 4 +- .../commands/config/add_profile_internal.go | 6 +- internal/commands/config/common_errors.go | 7 -- internal/commands/config/errors.go | 49 +++++++++++ .../commands/config/list_keys_internal.go | 9 +- internal/commands/config/set_internal.go | 88 +++++++------------ internal/commands/license/errors.go | 15 ++++ internal/commands/license/license_internal.go | 47 +++++----- internal/commands/platform/errors.go | 34 +++++++ internal/commands/platform/export_internal.go | 33 +------ internal/commands/plugin/add_internal.go | 5 +- .../plugin/{common_errors.go => errors.go} | 4 + internal/commands/request/errors.go | 17 ++++ internal/commands/request/request_internal.go | 11 +-- internal/configuration/configuration.go | 6 +- internal/configuration/errors.go | 11 +++ internal/connector/common/common_utils.go | 23 +++-- internal/connector/common/errors.go | 16 ++++ internal/connector/common/resources_common.go | 36 ++++++-- internal/connector/pingone/common.go | 30 ++++--- internal/connector/pingone/errors.go | 9 ++ .../pingone/platform/resources/errors.go | 7 ++ .../resources/gateway_role_assignment.go | 6 +- .../application_flow_policy_assignment.go | 6 +- .../resources/application_resource_grant.go | 6 +- .../resources/application_role_assignment.go | 6 +- .../sso/resources/application_secret.go | 6 +- .../application_sign_on_policy_assignment.go | 6 +- .../connector/pingone/sso/resources/errors.go | 13 +++ .../sso/resources/group_role_assignment.go | 6 +- internal/customtypes/bool.go | 2 - internal/customtypes/common_errors.go | 7 -- internal/customtypes/errors.go | 25 ++++++ internal/customtypes/export_format.go | 2 - internal/customtypes/export_service_group.go | 11 +-- internal/customtypes/export_services.go | 23 +++-- internal/customtypes/headers.go | 7 +- internal/customtypes/http_method.go | 2 - internal/customtypes/int.go | 2 - internal/customtypes/license_product.go | 2 - internal/customtypes/license_version.go | 2 - internal/customtypes/output_format.go | 4 +- .../customtypes/pingfederate_auth_type.go | 2 - internal/customtypes/pingone_auth_type.go | 2 - internal/customtypes/pingone_region_code.go | 4 +- internal/customtypes/request_services.go | 2 - internal/customtypes/uuid.go | 2 - internal/errs/pingcli_error.go | 4 + internal/errs/pingcli_error_test.go | 23 ++--- internal/input/input_test.go | 6 +- internal/plugins/errors.go | 14 +++ internal/plugins/plugins.go | 13 +-- internal/plugins/plugins_test.go | 10 ++- internal/profiles/errors.go | 45 ++++++++++ internal/profiles/koanf.go | 18 +--- internal/profiles/validate.go | 22 +---- internal/testing/testutils/stdio.go | 25 ++++-- shared/grpc/errors.go | 9 ++ shared/grpc/pingcli_command_grpc_server.go | 2 +- tools/generate-command-docs/main.go | 7 +- 66 files changed, 542 insertions(+), 320 deletions(-) create mode 100644 cmd/common/errors.go create mode 100644 internal/autocompletion/errors.go delete mode 100644 internal/commands/config/common_errors.go create mode 100644 internal/commands/config/errors.go create mode 100644 internal/commands/license/errors.go create mode 100644 internal/commands/platform/errors.go rename internal/commands/plugin/{common_errors.go => errors.go} (57%) create mode 100644 internal/commands/request/errors.go create mode 100644 internal/configuration/errors.go create mode 100644 internal/connector/common/errors.go create mode 100644 internal/connector/pingone/errors.go create mode 100644 internal/connector/pingone/platform/resources/errors.go create mode 100644 internal/connector/pingone/sso/resources/errors.go delete mode 100644 internal/customtypes/common_errors.go create mode 100644 internal/customtypes/errors.go create mode 100644 internal/plugins/errors.go create mode 100644 internal/profiles/errors.go create mode 100644 shared/grpc/errors.go diff --git a/.github/workflows/code-analysis-lint-test.yaml b/.github/workflows/code-analysis-lint-test.yaml index 9d919823..cc0a11b6 100644 --- a/.github/workflows/code-analysis-lint-test.yaml +++ b/.github/workflows/code-analysis-lint-test.yaml @@ -143,8 +143,6 @@ jobs: TEST_PINGONE_WORKER_CLIENT_ID: ${{ secrets.TEST_PINGONE_WORKER_CLIENT_ID }} TEST_PINGONE_WORKER_CLIENT_SECRET: ${{ secrets.TEST_PINGONE_WORKER_CLIENT_SECRET }} TEST_PINGONE_REGION_CODE: ${{ secrets.TEST_PINGONE_REGION_CODE }} - TEST_PINGCLI_DEVOPS_USER: ${{ secrets.TEST_PINGCLI_DEVOPS_USER }} - TEST_PINGCLI_DEVOPS_KEY: ${{ secrets.TEST_PINGCLI_DEVOPS_KEY }} onfailure: if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }} diff --git a/.golangci.yml b/.golangci.yml index 792b82b0..02252715 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,6 +9,7 @@ linters: - decorder - dupword - durationcheck + - err113 - errcheck - errchkjson - errname diff --git a/cmd/common/cobra_utils.go b/cmd/common/cobra_utils.go index c1e3233a..fb3f155f 100644 --- a/cmd/common/cobra_utils.go +++ b/cmd/common/cobra_utils.go @@ -3,7 +3,6 @@ package common import ( - "errors" "fmt" "github.com/pingidentity/pingcli/internal/errs" @@ -12,8 +11,6 @@ import ( var ( argsErrorPrefix = "failed to execute command" - ErrExactArgs = errors.New("incorrect number of arguments") - ErrRangeArgs = errors.New("incorrect number of arguments") ) func ExactArgs(numArgs int) cobra.PositionalArgs { diff --git a/cmd/common/errors.go b/cmd/common/errors.go new file mode 100644 index 00000000..bf263212 --- /dev/null +++ b/cmd/common/errors.go @@ -0,0 +1,10 @@ +// Copyright © 2025 Ping Identity Corporation + +package common + +import "errors" + +var ( + ErrExactArgs = errors.New("incorrect number of arguments") + ErrRangeArgs = errors.New("incorrect number of arguments") +) diff --git a/internal/autocompletion/config_args.go b/internal/autocompletion/config_args.go index 43fbb73e..258e3d71 100644 --- a/internal/autocompletion/config_args.go +++ b/internal/autocompletion/config_args.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" "github.com/spf13/cobra" @@ -14,7 +15,8 @@ import ( func ConfigViewProfileFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + wrappedErr := fmt.Errorf("%w: %w", ErrGetConfiguration, err) + output.SystemError((&errs.PingCLIError{Prefix: autocompletionErrorPrefix, Err: wrappedErr}).Error(), nil) } return koanfConfig.ProfileNames(), cobra.ShellCompDirectiveNoFileComp @@ -23,7 +25,8 @@ func ConfigViewProfileFunc(cmd *cobra.Command, args []string, toComplete string) func ConfigReturnNonActiveProfilesFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + wrappedErr := fmt.Errorf("%w: %w", ErrGetConfiguration, err) + output.SystemError((&errs.PingCLIError{Prefix: autocompletionErrorPrefix, Err: wrappedErr}).Error(), nil) } profileNames := koanfConfig.ProfileNames() @@ -33,7 +36,8 @@ func ConfigReturnNonActiveProfilesFunc(cmd *cobra.Command, args []string, toComp activeProfileName, err := profiles.GetOptionValue(options.RootActiveProfileOption) if err != nil { - output.SystemError(fmt.Sprintf("Unable to get active profile: %v", err), nil) + wrappedErr := fmt.Errorf("%w: %w", ErrGetActiveProfile, err) + output.SystemError((&errs.PingCLIError{Prefix: autocompletionErrorPrefix, Err: wrappedErr}).Error(), nil) } nonActiveProfiles := []string{} diff --git a/internal/autocompletion/errors.go b/internal/autocompletion/errors.go new file mode 100644 index 00000000..f7e6d4e1 --- /dev/null +++ b/internal/autocompletion/errors.go @@ -0,0 +1,15 @@ +// Copyright © 2025 Ping Identity Corporation + +package autocompletion + +import "errors" + +var ( + // Common autocompletion errors + ErrGetConfiguration = errors.New("unable to get configuration") + ErrGetActiveProfile = errors.New("unable to get active profile") +) + +const ( + autocompletionErrorPrefix = "autocompletion failed" +) diff --git a/internal/autocompletion/root_flags.go b/internal/autocompletion/root_flags.go index dc9ac1ee..5f21836a 100644 --- a/internal/autocompletion/root_flags.go +++ b/internal/autocompletion/root_flags.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/profiles" "github.com/spf13/cobra" @@ -14,7 +15,8 @@ import ( func RootProfileFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { koanfConfig, err := profiles.GetKoanfConfig() if err != nil { - output.SystemError(fmt.Sprintf("Unable to get configuration: %v", err), nil) + wrappedErr := fmt.Errorf("%w: %w", ErrGetConfiguration, err) + output.SystemError((&errs.PingCLIError{Prefix: autocompletionErrorPrefix, Err: wrappedErr}).Error(), nil) } return koanfConfig.ProfileNames(), cobra.ShellCompDirectiveNoFileComp diff --git a/internal/commands/config/add_profile_internal.go b/internal/commands/config/add_profile_internal.go index f69b6948..c9f73278 100644 --- a/internal/commands/config/add_profile_internal.go +++ b/internal/commands/config/add_profile_internal.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "fmt" "io" "strconv" @@ -17,10 +16,7 @@ import ( ) var ( - addProfileErrorPrefix = "failed to add profile" - ErrNoProfileProvided = errors.New("unable to determine profile name") - ErrSetActiveFlagInvalid = errors.New("invalid value for set-active flag. must be 'true' or 'false'") - ErrKoanfNotInitialized = errors.New("koanf configuration not initialized") + addProfileErrorPrefix = "failed to add profile" ) func RunInternalConfigAddProfile(rc io.ReadCloser, koanfConfig *profiles.KoanfConfig) (err error) { diff --git a/internal/commands/config/common_errors.go b/internal/commands/config/common_errors.go deleted file mode 100644 index 324d690f..00000000 --- a/internal/commands/config/common_errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package config_internal - -import "errors" - -var ( - ErrUndeterminedProfile = errors.New("unable to determine configuration profile") -) diff --git a/internal/commands/config/errors.go b/internal/commands/config/errors.go new file mode 100644 index 00000000..0a8ebde7 --- /dev/null +++ b/internal/commands/config/errors.go @@ -0,0 +1,49 @@ +// Copyright © 2025 Ping Identity Corporation + +package config_internal + +import ( + "errors" + "fmt" + "strings" + + "github.com/pingidentity/pingcli/internal/customtypes" +) + +var ( + // Common errors + ErrUndeterminedProfile = errors.New("unable to determine configuration profile") + + // Add profile errors + ErrNoProfileProvided = errors.New("unable to determine profile name") + ErrSetActiveFlagInvalid = errors.New("invalid value for set-active flag. must be 'true' or 'false'") + ErrKoanfNotInitialized = errors.New("koanf configuration not initialized") + + // List keys errors + ErrRetrieveKeys = errors.New("failed to retrieve configuration keys") + ErrNestedMap = errors.New("failed to create nested map for key") + ErrMarshalKeys = errors.New("failed to marshal keys to YAML format") + + // Set errors + ErrEmptyValue = errors.New("the set value provided is empty. Use 'pingcli config unset %s' to unset a key's configuration") + ErrKeyAssignmentFormat = errors.New("invalid key-value assignment. Expect 'key=value' format") + ErrActiveProfileAssignment = errors.New("invalid active profile assignment. Please use the 'pingcli config set active-profile ' command to set the active profile") + ErrSetKey = errors.New("unable to set key in configuration profile") + ErrMustBeBoolean = errors.New("the value assignment must be a boolean. Allowed [true, false]") + ErrMustBeExportFormat = fmt.Errorf("the value assignment must be a valid export format. Allowed [%s]", strings.Join(customtypes.ExportFormatValidValues(), ", ")) + ErrMustBeExportServiceGroup = fmt.Errorf("the value assignment must be a valid export service group. Allowed [%s]", strings.Join(customtypes.ExportServiceGroupValidValues(), ", ")) + ErrMustBeExportService = fmt.Errorf("the value assignment must be valid export service(s). Allowed [%s]", strings.Join(customtypes.ExportServicesValidValues(), ", ")) + ErrMustBeOutputFormat = fmt.Errorf("the value assignment must be a valid output format. Allowed [%s]", strings.Join(customtypes.OutputFormatValidValues(), ", ")) + ErrMustBePingoneRegionCode = fmt.Errorf("the value assignment must be a valid PingOne region code. Allowed [%s]", strings.Join(customtypes.PingOneRegionCodeValidValues(), ", ")) + ErrMustBeString = errors.New("the value assignment must be a string") + ErrMustBeStringSlice = errors.New("the value assignment must be a string slice") + ErrMustBeUUID = errors.New("the value assignment must be a valid UUID") + ErrMustBePingoneAuthType = fmt.Errorf("the value assignment must be a valid PingOne Authentication Type. Allowed [%s]", strings.Join(customtypes.PingOneAuthenticationTypeValidValues(), ", ")) + ErrMustBePingfederateAuthType = fmt.Errorf("the value assignment must be a valid PingFederate Authentication Type. Allowed [%s]", strings.Join(customtypes.PingFederateAuthenticationTypeValidValues(), ", ")) + ErrMustBeInteger = errors.New("the value assignment must be an integer") + ErrMustBeHttpMethod = fmt.Errorf("the value assignment must be a valid HTTP method. Allowed [%s]", strings.Join(customtypes.HTTPMethodValidValues(), ", ")) + ErrMustBeRequestService = fmt.Errorf("the value assignment must be a valid request service. Allowed [%s]", strings.Join(customtypes.RequestServiceValidValues(), ", ")) + ErrMustBeLicenseProduct = fmt.Errorf("the value assignment must be a valid license product. Allowed [%s]", strings.Join(customtypes.LicenseProductValidValues(), ", ")) + ErrMustBeLicenseVersion = errors.New("the value assignment must be a valid license version. Must be of the form 'major.minor'") + ErrTypeNotRecognized = errors.New("the variable type for the configuration key is not recognized or supported") +) diff --git a/internal/commands/config/list_keys_internal.go b/internal/commands/config/list_keys_internal.go index 6e47cadf..377eea55 100644 --- a/internal/commands/config/list_keys_internal.go +++ b/internal/commands/config/list_keys_internal.go @@ -3,7 +3,7 @@ package config_internal import ( - "errors" + "fmt" "slices" "strings" @@ -16,9 +16,6 @@ import ( ) var ( - ErrRetrieveKeys = errors.New("failed to retrieve configuration keys") - ErrNestedMap = errors.New("failed to create nested map for key") - ErrMarshalKeys = errors.New("failed to marshal keys to YAML format") listKeysErrorPrefix = "failed to get configuration keys list" ) @@ -56,7 +53,9 @@ func returnKeysYamlString() (keysYamlStr string, err error) { } currentMap, currentMapOk = currentMap[k].(map[string]interface{}) if !currentMapOk { - return keysYamlStr, &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: ErrNestedMap} + wrappedErr := fmt.Errorf("key '%s': %w", koanfKey, ErrNestedMap) + + return keysYamlStr, &errs.PingCLIError{Prefix: listKeysErrorPrefix, Err: wrappedErr} } } } diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index 0e6b47fc..adfc0d75 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -3,7 +3,6 @@ package config_internal import ( - "errors" "fmt" "strings" @@ -17,28 +16,7 @@ import ( ) var ( - ErrEmptyValue = errors.New("the set value provided is empty. Use 'pingcli config unset %s' to unset a key's configuration") - ErrKeyAssignmentFormat = errors.New("invalid key-value assignment. Expect 'key=value' format") - ErrActiveProfileAssignment = errors.New("invalid active profile assignment. Please use the 'pingcli config set active-profile ' command to set the active profile") - ErrSetKey = errors.New("unable to set key in configuration profile") - ErrMustBeBoolean = errors.New("the value assignment must be a boolean. Allowed [true, false]") - ErrMustBeExportFormat = fmt.Errorf("the value assignment must be a valid export format. Allowed [%s]", strings.Join(customtypes.ExportFormatValidValues(), ", ")) - ErrMustBeExportServiceGroup = fmt.Errorf("the value assignment must be a valid export service group. Allowed [%s]", strings.Join(customtypes.ExportServiceGroupValidValues(), ", ")) - ErrMustBeExportService = fmt.Errorf("the value assignment must be valid export service(s). Allowed [%s]", strings.Join(customtypes.ExportServicesValidValues(), ", ")) - ErrMustBeOutputFormat = fmt.Errorf("the value assignment must be a valid output format. Allowed [%s]", strings.Join(customtypes.OutputFormatValidValues(), ", ")) - ErrMustBePingoneRegionCode = fmt.Errorf("the value assignment must be a valid PingOne region code. Allowed [%s]", strings.Join(customtypes.PingOneRegionCodeValidValues(), ", ")) - ErrMustBeString = errors.New("the value assignment must be a string") - ErrMustBeStringSlice = errors.New("the value assignment must be a string slice") - ErrMustBeUUID = errors.New("the value assignment must be a valid UUID") - ErrMustBePingoneAuthType = fmt.Errorf("the value assignment must be a valid PingOne Authentication Type. Allowed [%s]", strings.Join(customtypes.PingOneAuthenticationTypeValidValues(), ", ")) - ErrMustBePingfederateAuthType = fmt.Errorf("the value assignment must be a valid PingFederate Authentication Type. Allowed [%s]", strings.Join(customtypes.PingFederateAuthenticationTypeValidValues(), ", ")) - ErrMustBeInteger = errors.New("the value assignment must be an integer") - ErrMustBeHttpMethod = fmt.Errorf("the value assignment must be a valid HTTP method. Allowed [%s]", strings.Join(customtypes.HTTPMethodValidValues(), ", ")) - ErrMustBeRequestService = fmt.Errorf("the value assignment must be a valid request service. Allowed [%s]", strings.Join(customtypes.RequestServiceValidValues(), ", ")) - ErrMustBeLicenseProduct = fmt.Errorf("the value assignment must be a valid license product. Allowed [%s]", strings.Join(customtypes.LicenseProductValidValues(), ", ")) - ErrMustBeLicenseVersion = errors.New("the value assignment must be a valid license version. Must be of the form 'major.minor'") - ErrTypeNotRecognized = errors.New("the variable type for the configuration key is not recognized or supported") - setErrorPrefix = "failed to set configuration" + setErrorPrefix = "failed to set configuration" ) func RunInternalConfigSet(kvPair string) (err error) { @@ -150,146 +128,146 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. case options.BOOL: b := new(customtypes.Bool) if err = b.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeBoolean, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeBoolean, err) } err = profileKoanf.Set(vKey, b) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.EXPORT_FORMAT: exportFormat := new(customtypes.ExportFormat) if err = exportFormat.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeExportFormat, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeExportFormat, err) } err = profileKoanf.Set(vKey, exportFormat) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.EXPORT_SERVICE_GROUP: exportServiceGroup := new(customtypes.ExportServiceGroup) if err = exportServiceGroup.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeExportServiceGroup, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeExportServiceGroup, err) } err = profileKoanf.Set(vKey, exportServiceGroup) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.EXPORT_SERVICES: exportServices := new(customtypes.ExportServices) if err = exportServices.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeExportService, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeExportService, err) } err = profileKoanf.Set(vKey, exportServices) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.OUTPUT_FORMAT: outputFormat := new(customtypes.OutputFormat) if err = outputFormat.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeOutputFormat, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeOutputFormat, err) } err = profileKoanf.Set(vKey, outputFormat) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.PINGONE_REGION_CODE: region := new(customtypes.PingOneRegionCode) if err = region.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBePingoneRegionCode, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBePingoneRegionCode, err) } err = profileKoanf.Set(vKey, region) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.STRING: str := new(customtypes.String) if err = str.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeString, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeString, err) } err = profileKoanf.Set(vKey, str) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.STRING_SLICE: strSlice := new(customtypes.StringSlice) if err = strSlice.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeStringSlice, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeStringSlice, err) } err = profileKoanf.Set(vKey, strSlice) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.UUID: uuid := new(customtypes.UUID) if err = uuid.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeUUID, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeUUID, err) } err = profileKoanf.Set(vKey, uuid) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.PINGONE_AUTH_TYPE: authType := new(customtypes.PingOneAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBePingoneAuthType, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBePingoneAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.PINGFEDERATE_AUTH_TYPE: authType := new(customtypes.PingFederateAuthenticationType) if err = authType.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBePingfederateAuthType, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBePingfederateAuthType, err) } err = profileKoanf.Set(vKey, authType) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.INT: intValue := new(customtypes.Int) if err = intValue.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeInteger, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeInteger, err) } err = profileKoanf.Set(vKey, intValue) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.REQUEST_HTTP_METHOD: httpMethod := new(customtypes.HTTPMethod) if err = httpMethod.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeHttpMethod, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeHttpMethod, err) } err = profileKoanf.Set(vKey, httpMethod) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.REQUEST_SERVICE: service := new(customtypes.RequestService) if err = service.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeRequestService, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeRequestService, err) } err = profileKoanf.Set(vKey, service) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.LICENSE_PRODUCT: licenseProduct := new(customtypes.LicenseProduct) if err = licenseProduct.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeLicenseProduct, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeLicenseProduct, err) } err = profileKoanf.Set(vKey, licenseProduct) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } case options.LICENSE_VERSION: licenseVersion := new(customtypes.LicenseVersion) if err = licenseVersion.Set(vValue); err != nil { - return fmt.Errorf("%w: %w", ErrMustBeLicenseVersion, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrMustBeLicenseVersion, err) } err = profileKoanf.Set(vKey, licenseVersion) if err != nil { - return fmt.Errorf("%w: %w", ErrSetKey, err) + return fmt.Errorf("value for key '%s': %w: %w", vKey, ErrSetKey, err) } default: return &errs.PingCLIError{Prefix: setErrorPrefix, Err: ErrTypeNotRecognized} diff --git a/internal/commands/license/errors.go b/internal/commands/license/errors.go new file mode 100644 index 00000000..771f16ee --- /dev/null +++ b/internal/commands/license/errors.go @@ -0,0 +1,15 @@ +// Copyright © 2025 Ping Identity Corporation + +package license_internal + +import "errors" + +var ( + ErrLicenseDataEmpty = errors.New("returned license data is empty. please check your request parameters") + ErrGetProduct = errors.New("failed to get product option value") + ErrGetVersion = errors.New("failed to get version option value") + ErrGetDevopsUser = errors.New("failed to get devops user option value") + ErrGetDevopsKey = errors.New("failed to get devops key option value") + ErrRequiredValues = errors.New("product, version, devops user, and devops key must be specified for license request") + ErrLicenseRequest = errors.New("license request failed") +) diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go index 00d969eb..a619cfb0 100644 --- a/internal/commands/license/license_internal.go +++ b/internal/commands/license/license_internal.go @@ -16,24 +16,24 @@ import ( ) var ( - ErrLicenseDataEmpty = errors.New("returned license data is empty. please check your request parameters") - ErrGetProduct = errors.New("failed to get product option value") - ErrGetVersion = errors.New("failed to get version option value") - ErrGetDevopsUser = errors.New("failed to get devops user option value") - ErrGetDevopsKey = errors.New("failed to get devops key option value") - ErrRequiredValues = errors.New("product, version, devops user, and devops key must be specified for license request") - ErrLicenseRequest = errors.New("license request failed") - licenseErrorPrefix = "failed to run license request" + licenseErrorPrefix = "failed to run license request" ) +type licenseOptions struct { + product string + version string + devopsUser string + devopsKey string +} + func RunInternalLicense() (err error) { - product, version, devopsUser, devopsKey, err := readLicenseOptionValues() + opts, err := readLicenseOptionValues() if err != nil { return &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } ctx := context.Background() - licenseData, err := runLicenseRequest(ctx, product, version, devopsUser, devopsKey) + licenseData, err := runLicenseRequest(ctx, opts.product, opts.version, opts.devopsUser, opts.devopsKey) if err != nil { return &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: err} } @@ -47,32 +47,35 @@ func RunInternalLicense() (err error) { return nil } -func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, err error) { - product, err = profiles.GetOptionValue(options.LicenseProductOption) +func readLicenseOptionValues() (*licenseOptions, error) { + opts := &licenseOptions{} + var err error + + opts.product, err = profiles.GetOptionValue(options.LicenseProductOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetProduct, err)} + return nil, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetProduct, err)} } - version, err = profiles.GetOptionValue(options.LicenseVersionOption) + opts.version, err = profiles.GetOptionValue(options.LicenseVersionOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetVersion, err)} + return nil, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetVersion, err)} } - devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) + opts.devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsUser, err)} + return nil, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsUser, err)} } - devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) + opts.devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) if err != nil { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsKey, err)} + return nil, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: fmt.Errorf("%w: %w", ErrGetDevopsKey, err)} } - if product == "" || version == "" || devopsUser == "" || devopsKey == "" { - return product, version, devopsUser, devopsKey, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: ErrRequiredValues} + if opts.product == "" || opts.version == "" || opts.devopsUser == "" || opts.devopsKey == "" { + return nil, &errs.PingCLIError{Prefix: licenseErrorPrefix, Err: ErrRequiredValues} } - return product, version, devopsUser, devopsKey, nil + return opts, nil } func runLicenseRequest(ctx context.Context, product, version, devopsUser, devopsKey string) (licenseData string, err error) { diff --git a/internal/commands/platform/errors.go b/internal/commands/platform/errors.go new file mode 100644 index 00000000..691c442e --- /dev/null +++ b/internal/commands/platform/errors.go @@ -0,0 +1,34 @@ +// Copyright © 2025 Ping Identity Corporation + +package platform_internal + +import "errors" + +var ( + ErrNilContext = errors.New("context is nil") + ErrReadCaCertPemFile = errors.New("failed to read CA certificate PEM file") + ErrAppendToCertPool = errors.New("failed to append to certificate pool from PEM file") + ErrBasicAuthEmpty = errors.New("failed to initialize PingFederate service. Basic authentication username and/or password is not set") + ErrAccessTokenEmpty = errors.New("failed to initialize PingFederate service. Access token is not set") + ErrClientCredentialsEmpty = errors.New("failed to initialize PingFederate service. Client ID, Client Secret, and/or Token URL is not set") + ErrPingFederateAuthType = errors.New("failed to initialize PingFederate service. Unrecognized authentication type") + ErrPingFederateInit = errors.New("failed to initialize PingFederate service. Check authentication type and credentials") + ErrHttpTransportNil = errors.New("failed to initialize PingFederate service. HTTP transport is nil") + ErrHttpsHostEmpty = errors.New("failed to initialize PingFederate service. HTTPS host is not set") + ErrPingOneConfigValuesEmpty = errors.New("failed to initialize pingone API client. one of worker client ID, worker client secret, " + + "pingone region code, and/or worker environment ID is not set. configure these properties via parameter flags, " + + "environment variables, or the tool's configuration file (default: $HOME/.pingcli/config.yaml)") + ErrPingOneInit = errors.New("failed to initialize pingone API client. Check worker client ID, worker client secret," + + " worker environment ID, and pingone region code configuration values") + ErrOutputDirectoryEmpty = errors.New("output directory is not set") + ErrGetPresentWorkingDirectory = errors.New("failed to get present working directory") + ErrCreateOutputDirectory = errors.New("failed to create output directory") + ErrReadOutputDirectory = errors.New("failed to read contents of output directory") + ErrOutputDirectoryNotEmpty = errors.New("output directory is not empty. use '--overwrite' to overwrite existing files and export data") + ErrDeterminePingOneExportEnv = errors.New("failed to determine pingone export environment ID") + ErrPingOneClientNil = errors.New("pingone API client is nil") + ErrValidatePingOneEnvId = errors.New("failed to validate pingone environment ID") + ErrPingOneEnvNotExist = errors.New("pingone environment does not exist") + ErrConnectorListNil = errors.New("exportable connectors list is nil") + ErrExportService = errors.New("failed to export service") +) diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index cab40d09..6a6bb0de 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -6,7 +6,6 @@ import ( "context" "crypto/tls" "crypto/x509" - "errors" "fmt" "net/http" "os" @@ -44,33 +43,7 @@ var ( ) var ( - exportErrorPrefix = "failed to export service(s)" - ErrNilContext = errors.New("context is nil") - ErrReadCaCertPemFile = errors.New("failed to read CA certificate PEM file") - ErrAppendToCertPool = errors.New("failed to append to certificate pool from PEM file") - ErrBasicAuthEmpty = errors.New("failed to initialize PingFederate service. Basic authentication username and/or password is not set") - ErrAccessTokenEmpty = errors.New("failed to initialize PingFederate service. Access token is not set") - ErrClientCredentialsEmpty = errors.New("failed to initialize PingFederate service. Client ID, Client Secret, and/or Token URL is not set") - ErrPingFederateAuthType = errors.New("failed to initialize PingFederate service. Unrecognized authentication type") - ErrPingFederateInit = errors.New("failed to initialize PingFederate service. Check authentication type and credentials") - ErrHttpTransportNil = errors.New("failed to initialize PingFederate service. http transport is nil") - ErrHttpsHostEmpty = errors.New("failed to initialize PingFederate service. HTTPS host is not set") - ErrPingOneConfigValuesEmpty = errors.New("failed to initialize pingone API client. one of worker client ID, worker client secret, " + - "pingone region code, and/or worker environment ID is not set. configure these properties via parameter flags, " + - "environment variables, or the tool's configuration file (default: $HOME/.pingcli/config.yaml)") - ErrPingOneInit = errors.New("failed to initialize pingone API client. Check worker client ID, worker client secret," + - " worker environment ID, and pingone region code configuration values") - ErrOutputDirectoryEmpty = errors.New("output directory is not set") - ErrGetPresentWorkingDirectory = errors.New("failed to get present working directory") - ErrCreateOutputDirectory = errors.New("failed to create output directory") - ErrReadOutputDirectory = errors.New("failed to read contents of output directory") - ErrOutputDirectoryNotEmpty = errors.New("output directory is not empty. use '--overwrite' to overwrite existing files and export data") - ErrDeterminePingOneExportEnv = errors.New("failed to determine pingone export environment ID") - ErrPingOneClientNil = errors.New("pingone API client is nil") - ErrValidatePingOneEnvId = errors.New("failed to validate pingone environment ID") - ErrPingOneEnvNotExist = errors.New("pingone environment does not exist") - ErrConnectorListNil = errors.New("exportable connectors list is nil") - ErrExportService = errors.New("failed to export service") + exportErrorPrefix = "failed to export service(s)" ) func RunInternalExport(ctx context.Context, commandVersion string) (err error) { @@ -480,11 +453,11 @@ func validatePingOneExportEnvID(ctx context.Context) (err error) { l.Debug().Msgf("Validating export environment ID...") if ctx == nil { - return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("failed to validate pingone environment ID '%s'. %w", pingoneExportEnvID, ErrNilContext)} + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrValidatePingOneEnvId, pingoneExportEnvID, ErrNilContext)} } if pingoneApiClient == nil { - return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("failed to validate pingone environment ID '%s'. %w", pingoneExportEnvID, ErrPingOneClientNil)} + return &errs.PingCLIError{Prefix: exportErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrValidatePingOneEnvId, pingoneExportEnvID, ErrPingOneClientNil)} } environment, response, err := pingoneApiClient.ManagementAPIClient.EnvironmentsApi.ReadOneEnvironment(ctx, pingoneExportEnvID).Execute() diff --git a/internal/commands/plugin/add_internal.go b/internal/commands/plugin/add_internal.go index 2c6eb0ff..efa08417 100644 --- a/internal/commands/plugin/add_internal.go +++ b/internal/commands/plugin/add_internal.go @@ -3,7 +3,6 @@ package plugin_internal import ( - "errors" "fmt" "os/exec" "strings" @@ -16,9 +15,7 @@ import ( ) var ( - addErrorPrefix = "failed to add plugin" - ErrPluginAlreadyExists = errors.New("plugin executable already exists in configuration") - ErrPluginNotFound = errors.New("plugin executable not found in system PATH") + addErrorPrefix = "failed to add plugin" ) func RunInternalPluginAdd(pluginExecutable string) error { diff --git a/internal/commands/plugin/common_errors.go b/internal/commands/plugin/errors.go similarity index 57% rename from internal/commands/plugin/common_errors.go rename to internal/commands/plugin/errors.go index a35e08cc..2a497177 100644 --- a/internal/commands/plugin/common_errors.go +++ b/internal/commands/plugin/errors.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package plugin_internal import "errors" @@ -6,4 +8,6 @@ var ( ErrPluginNameEmpty = errors.New("plugin executable name is empty") ErrReadPluginNamesConfig = errors.New("failed to read configured plugin executable names") ErrUndeterminedProfile = errors.New("unable to determine configuration profile") + ErrPluginAlreadyExists = errors.New("plugin executable already exists in configuration") + ErrPluginNotFound = errors.New("plugin executable not found in system PATH") ) diff --git a/internal/commands/request/errors.go b/internal/commands/request/errors.go new file mode 100644 index 00000000..e390a149 --- /dev/null +++ b/internal/commands/request/errors.go @@ -0,0 +1,17 @@ +// Copyright © 2025 Ping Identity Corporation + +package request_internal + +import "errors" + +var ( + ErrServiceEmpty = errors.New("service is not set") + ErrUnrecognizedService = errors.New("unrecognized service") + ErrHttpMethodEmpty = errors.New("http method is not set") + ErrUnrecognizedHttpMethod = errors.New("unrecognized http method") + ErrPingOneRegionCodeEmpty = errors.New("PingOne region code is not set") + ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized PingOne region code") + ErrPingOneWorkerEnvIDEmpty = errors.New("PingOne worker environment ID is not set") + ErrPingOneClientIDAndSecretEmpty = errors.New("PingOne client ID and/or client secret is not set") + ErrPingOneAuthenticate = errors.New("failed to authenticate with PingOne") +) diff --git a/internal/commands/request/request_internal.go b/internal/commands/request/request_internal.go index 5ddf8020..8c36d46e 100644 --- a/internal/commands/request/request_internal.go +++ b/internal/commands/request/request_internal.go @@ -25,16 +25,7 @@ import ( ) var ( - requestErrorPrefix = "failed to send custom request" - ErrServiceEmpty = errors.New("service is not set") - ErrUnrecognizedService = errors.New("unrecognized service") - ErrHttpMethodEmpty = errors.New("http method is not set") - ErrUnrecognizedHttpMethod = errors.New("unrecognized http method") - ErrPingOneRegionCodeEmpty = errors.New("PingOne region code is not set") - ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized PingOne region code") - ErrPingOneWorkerEnvIDEmpty = errors.New("PingOne worker environment ID is not set") - ErrPingOneClientIDAndSecretEmpty = errors.New("PingOne client ID and/or client secret is not set") - ErrPingOneAuthenticate = errors.New("failed to authenticate with PingOne") + requestErrorPrefix = "failed to send custom request" ) type PingOneAuthResponse struct { diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 1e28bdd8..6c06118b 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -3,7 +3,6 @@ package configuration import ( - "errors" "slices" "strings" @@ -20,10 +19,7 @@ import ( ) var ( - configurationErrorPrefix = "configuration options error" - ErrInvalidConfigurationKey = errors.New("provided key is not recognized as a valid configuration key.\nuse 'pingcli config list-keys' to view all available keys") - ErrNoOptionForKey = errors.New("no option found for the provided configuration key") - ErrEmptyKeyForOptionSearch = errors.New("empty key provided for option search, too many matches with options not configured with a koanf key") + configurationErrorPrefix = "configuration options error" ) func KoanfKeys() (keys []string) { diff --git a/internal/configuration/errors.go b/internal/configuration/errors.go new file mode 100644 index 00000000..2a47bf46 --- /dev/null +++ b/internal/configuration/errors.go @@ -0,0 +1,11 @@ +// Copyright © 2025 Ping Identity Corporation + +package configuration + +import "errors" + +var ( + ErrInvalidConfigurationKey = errors.New("provided key is not recognized as a valid configuration key.\nuse 'pingcli config list-keys' to view all available keys") + ErrNoOptionForKey = errors.New("no option found for the provided configuration key") + ErrEmptyKeyForOptionSearch = errors.New("empty key provided for option search, too many matches with options not configured with a koanf key") +) diff --git a/internal/connector/common/common_utils.go b/internal/connector/common/common_utils.go index feb076f8..2a6d70d8 100644 --- a/internal/connector/common/common_utils.go +++ b/internal/connector/common/common_utils.go @@ -13,22 +13,27 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) +var ( + utilsErrorPrefix = "connector common utils error" +) + func WriteFiles(exportableResources []connector.ExportableResource, format, outputDir string, overwriteExport bool) (err error) { l := logger.Get() // Parse the HCL import block template hclImportBlockTemplate, err := template.New("HCLImportBlock").Parse(connector.HCLImportBlockTemplate) if err != nil { - return fmt.Errorf("failed to parse HCL import block template. err: %s", err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrParseHCLTemplate, err)} } for _, exportableResource := range exportableResources { importBlocks, err := exportableResource.ExportAll() if err != nil { - return fmt.Errorf("failed to export resource %s. err: %s", exportableResource.ResourceType(), err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w '%s': %w", ErrExportResource, exportableResource.ResourceType(), err)} } if len(*importBlocks) == 0 { @@ -54,12 +59,12 @@ func WriteFiles(exportableResources []connector.ExportableResource, format, outp // This can be changed with the --overwrite export parameter _, err = os.Stat(outputFilePath) if err == nil && !overwriteExport { - return fmt.Errorf("generated import file for %q already exists. Use --overwrite to overwrite existing export data", outputFileName) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w for %q", ErrFileAlreadyExists, outputFileName)} } outputFile, err := os.Create(outputFilePath) if err != nil { - return fmt.Errorf("failed to create export file %q. err: %s", outputFilePath, err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w for %q: %w", ErrCreateExportFile, outputFileName, err)} } defer func() { cErr := outputFile.Close() @@ -81,10 +86,10 @@ func WriteFiles(exportableResources []connector.ExportableResource, format, outp case customtypes.ENUM_EXPORT_FORMAT_HCL: err := hclImportBlockTemplate.Execute(outputFile, importBlock) if err != nil { - return fmt.Errorf("failed to write import template to file %q. err: %s", outputFilePath, err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w for %q: %w", ErrWriteTemplateToFile, outputFileName, err)} } default: - return fmt.Errorf("unrecognized export format %q. Must be one of: %s", format, customtypes.ExportFormatValidValues()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of '%s'", ErrUnrecognizedExportFormat, format, customtypes.ExportFormatValidValues())} } } } @@ -96,17 +101,17 @@ func writeHeader(format, outputFilePath string, outputFile *os.File) error { // Parse the HCL header hclImportHeaderTemplate, err := template.New("HCLImportHeader").Parse(connector.HCLImportHeaderTemplate) if err != nil { - return fmt.Errorf("failed to parse HCL import header template. err: %s", err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w: %w", ErrParseHCLTemplate, err)} } switch format { case customtypes.ENUM_EXPORT_FORMAT_HCL: err := hclImportHeaderTemplate.Execute(outputFile, nil) if err != nil { - return fmt.Errorf("failed to write import template to file %q. err: %s", outputFilePath, err.Error()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w for %q: %w", ErrWriteTemplateToFile, outputFilePath, err)} } default: - return fmt.Errorf("unrecognized export format %q. Must be one of: %s", format, customtypes.ExportFormatValidValues()) + return &errs.PingCLIError{Prefix: utilsErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of '%s'", ErrUnrecognizedExportFormat, format, customtypes.ExportFormatValidValues())} } return nil diff --git a/internal/connector/common/errors.go b/internal/connector/common/errors.go new file mode 100644 index 00000000..741ff4b4 --- /dev/null +++ b/internal/connector/common/errors.go @@ -0,0 +1,16 @@ +// Copyright © 2025 Ping Identity Corporation + +package common + +import "errors" + +var ( + ErrParseHCLTemplate = errors.New("failed to parse HCL import block template") + ErrExportResource = errors.New("failed to export resource") + ErrFileAlreadyExists = errors.New("generated import file already exists. use --overwrite to overwrite existing export data") + ErrCreateExportFile = errors.New("failed to create export file") + ErrWriteTemplateToFile = errors.New("failed to write import block template to file") + ErrUnrecognizedExportFormat = errors.New("unrecognized export format") + ErrResourceRequestFailed = errors.New("resource request was not successful") + ErrExportResources = errors.New("failed to export resource") +) diff --git a/internal/connector/common/resources_common.go b/internal/connector/common/resources_common.go index f32ddc21..f42ff627 100644 --- a/internal/connector/common/resources_common.go +++ b/internal/connector/common/resources_common.go @@ -9,17 +9,19 @@ import ( "net/http" "slices" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" ) const ( SINGLETON_ID_COMMENT_DATA = "This resource is a singleton, so the value of 'ID' in the import block does not matter - it is just a placeholder and required by terraform." + resourceUtilsErrorPrefix = "connector resource utils error" ) func CheckSingletonResource(response *http.Response, err error, apiFuncName, resourceType string) (bool, error) { ok, err := HandleClientResponse(response, err, apiFuncName, resourceType) if err != nil { - return false, err + return false, &errs.PingCLIError{Prefix: resourceUtilsErrorPrefix, Err: err} } if !ok { return false, nil @@ -52,7 +54,7 @@ func HandleClientResponse(response *http.Response, err error, apiFunctionName st } if response == nil { - return false, fmt.Errorf("%s Request for resource '%s' was not successful. Response is nil", apiFunctionName, resourceType) + return false, &errs.PingCLIError{Prefix: resourceUtilsErrorPrefix, Err: fmt.Errorf("%w: %q - %q. Response is nil", ErrResourceRequestFailed, apiFunctionName, resourceType)} } defer func() { @@ -60,6 +62,7 @@ func HandleClientResponse(response *http.Response, err error, apiFunctionName st if cErr != nil { rErr = errors.Join(rErr, cErr) } + rErr = &errs.PingCLIError{Prefix: resourceUtilsErrorPrefix, Err: rErr} }() // When the client returns forbidden, warn user and skip export of resource @@ -76,18 +79,35 @@ func HandleClientResponse(response *http.Response, err error, apiFunctionName st // Error on any other non-200 response if response.StatusCode >= 300 || response.StatusCode < 200 { - return false, fmt.Errorf("%s Request for resource '%s' was not successful. \nResponse Code: %s\nResponse Body: %s", apiFunctionName, resourceType, response.Status, response.Body) + return false, &errs.PingCLIError{ + Prefix: resourceUtilsErrorPrefix, + Err: fmt.Errorf( + "%w: %q - %q. Response Code: %s, Response Body: %s", + ErrResourceRequestFailed, + apiFunctionName, + resourceType, + response.Status, + response.Body), + } } return true, nil } func DataNilError(resourceType string, response *http.Response) error { - return fmt.Errorf("failed to export resource '%s'.\n"+ - "API Client request for resource '%s' was not successful. response data is nil.\n"+ - "response code: %s\n"+ - "response body: %s", - resourceType, resourceType, response.Status, response.Body) + return &errs.PingCLIError{ + Prefix: resourceUtilsErrorPrefix, + Err: fmt.Errorf( + "%w: %q. API Client request for resource '%s' was not successful. response data is nil.\n"+ + "response code: %s\n"+ + "response body: %s", + ErrExportResources, + resourceType, + resourceType, + response.Status, + response.Body, + ), + } } func GenerateCommentInformation(data map[string]string) string { diff --git a/internal/connector/pingone/common.go b/internal/connector/pingone/common.go index 817f2c95..445f71d8 100644 --- a/internal/connector/pingone/common.go +++ b/internal/connector/pingone/common.go @@ -11,23 +11,28 @@ import ( "github.com/patrickcping/pingone-go-sdk-v2/mfa" "github.com/patrickcping/pingone-go-sdk-v2/risk" "github.com/pingidentity/pingcli/internal/connector/common" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/output" ) +var ( + pingoneConnectorCommonErrorPrefix = "pingone connector common utils error" +) + func GetAuthorizeAPIObjectsFromIterator[T any](iter authorize.EntityArrayPagedIterator, clientFuncName, extractionFuncName, resourceType string) ([]T, error) { apiObjects := []T{} for cursor, err := range iter { ok, err := common.HandleClientResponse(cursor.HTTPResponse, err, clientFuncName, resourceType) if err != nil { - return nil, err + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: err} } // A warning was given when handling the client response. Return nil apiObjects to skip export of resource if !ok { return nil, nil } - nilErr := common.DataNilError(resourceType, cursor.HTTPResponse) + nilErr := &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: common.DataNilError(resourceType, cursor.HTTPResponse)} if cursor.EntityArray == nil { return nil, nilErr @@ -55,14 +60,14 @@ func GetManagementAPIObjectsFromIterator[T any](iter management.EntityArrayPaged for cursor, err := range iter { ok, err := common.HandleClientResponse(cursor.HTTPResponse, err, clientFuncName, resourceType) if err != nil { - return nil, err + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: err} } // A warning was given when handling the client response. Return nil apiObjects to skip export of resource if !ok { return nil, nil } - nilErr := common.DataNilError(resourceType, cursor.HTTPResponse) + nilErr := &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: common.DataNilError(resourceType, cursor.HTTPResponse)} if cursor.EntityArray == nil { return nil, nilErr @@ -90,14 +95,14 @@ func GetMfaAPIObjectsFromIterator[T any](iter mfa.EntityArrayPagedIterator, clie for cursor, err := range iter { ok, err := common.HandleClientResponse(cursor.HTTPResponse, err, clientFuncName, resourceType) if err != nil { - return nil, err + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: err} } // A warning was given when handling the client response. Return nil apiObjects to skip export of resource if !ok { return nil, nil } - nilErr := common.DataNilError(resourceType, cursor.HTTPResponse) + nilErr := &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: common.DataNilError(resourceType, cursor.HTTPResponse)} if cursor.EntityArray == nil { return nil, nilErr @@ -125,14 +130,14 @@ func GetRiskAPIObjectsFromIterator[T any](iter risk.EntityArrayPagedIterator, cl for cursor, err := range iter { ok, err := common.HandleClientResponse(cursor.HTTPResponse, err, clientFuncName, resourceType) if err != nil { - return nil, err + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: err} } // A warning was given when handling the client response. Return nil apiObjects to skip export of resource if !ok { return nil, nil } - nilErr := common.DataNilError(resourceType, cursor.HTTPResponse) + nilErr := &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: common.DataNilError(resourceType, cursor.HTTPResponse)} if cursor.EntityArray == nil { return nil, nilErr @@ -157,12 +162,15 @@ func GetRiskAPIObjectsFromIterator[T any](iter risk.EntityArrayPagedIterator, cl func getAPIObjectFromEmbedded[T any](embedded reflect.Value, extractionFuncName, resourceType string) ([]T, error) { embeddedExtractionFunc := embedded.MethodByName(extractionFuncName) if !embeddedExtractionFunc.IsValid() { - return nil, fmt.Errorf("failed to find extraction function '%s' for resource '%s'", extractionFuncName, resourceType) + return nil, &errs.PingCLIError{ + Prefix: pingoneConnectorCommonErrorPrefix, + Err: fmt.Errorf("%w. Function %q. Resource %q", ErrUnknownExtractionFunction, extractionFuncName, resourceType), + } } reflectValues := embeddedExtractionFunc.Call(nil) if len(reflectValues) == 0 { - return nil, fmt.Errorf("failed to get reflect value from embedded. embedded is empty") + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: ErrEmbeddedEmpty} } rInterface := reflectValues[0].Interface() @@ -172,7 +180,7 @@ func getAPIObjectFromEmbedded[T any](embedded reflect.Value, extractionFuncName, apiObject, apiObjectOk := rInterface.([]T) if !apiObjectOk { - return nil, fmt.Errorf("failed to cast reflect value to %s", resourceType) + return nil, &errs.PingCLIError{Prefix: pingoneConnectorCommonErrorPrefix, Err: fmt.Errorf("%w. Resource Type %q", ErrCastReflectValue, resourceType)} } return apiObject, nil diff --git a/internal/connector/pingone/errors.go b/internal/connector/pingone/errors.go new file mode 100644 index 00000000..27d8defb --- /dev/null +++ b/internal/connector/pingone/errors.go @@ -0,0 +1,9 @@ +package pingone + +import "errors" + +var ( + ErrUnknownExtractionFunction = errors.New("failed to find extraction function") + ErrEmbeddedEmpty = errors.New("failed to get reflect value from embedded. embedded is empty") + ErrCastReflectValue = errors.New("failed to cast reflect value") +) diff --git a/internal/connector/pingone/platform/resources/errors.go b/internal/connector/pingone/platform/resources/errors.go new file mode 100644 index 00000000..c206cef2 --- /dev/null +++ b/internal/connector/pingone/platform/resources/errors.go @@ -0,0 +1,7 @@ +package resources + +import "errors" + +var ( + ErrRoleNameNotFound = errors.New("role name not found for role ID") +) diff --git a/internal/connector/pingone/platform/resources/gateway_role_assignment.go b/internal/connector/pingone/platform/resources/gateway_role_assignment.go index 6c0307b6..675effd9 100644 --- a/internal/connector/pingone/platform/resources/gateway_role_assignment.go +++ b/internal/connector/pingone/platform/resources/gateway_role_assignment.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneGatewayRoleAssignmentResource{} + _ connector.ExportableResource = &PingOneGatewayRoleAssignmentResource{} + gatewayRoleAssignmentExportErrorPrefix = "pingone_gateway_role_assignment resource export error" ) type PingOneGatewayRoleAssignmentResource struct { @@ -155,5 +157,5 @@ func (r *PingOneGatewayRoleAssignmentResource) getRoleAssignmentRoleName(roleId } } - return nil, false, fmt.Errorf("failed to export resource '%s'. No role name found for Role ID '%s'", r.ResourceType(), roleId) + return nil, false, &errs.PingCLIError{Prefix: gatewayRoleAssignmentExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrRoleNameNotFound, roleId)} } diff --git a/internal/connector/pingone/sso/resources/application_flow_policy_assignment.go b/internal/connector/pingone/sso/resources/application_flow_policy_assignment.go index bb257cd3..9101352a 100644 --- a/internal/connector/pingone/sso/resources/application_flow_policy_assignment.go +++ b/internal/connector/pingone/sso/resources/application_flow_policy_assignment.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneApplicationFlowPolicyAssignmentResource{} + _ connector.ExportableResource = &PingOneApplicationFlowPolicyAssignmentResource{} + applicationFlowPolicyAssignmentExportErrorPrefix = "pingone_application_flow_policy_assignment resource export error" ) type PingOneApplicationFlowPolicyAssignmentResource struct { @@ -166,5 +168,5 @@ func (r *PingOneApplicationFlowPolicyAssignmentResource) getFlowPolicyName(flowP } } - return "", false, fmt.Errorf("unable to get Flow Policy Name for Flow Policy ID: %s", flowPolicyId) + return "", false, &errs.PingCLIError{Prefix: applicationFlowPolicyAssignmentExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrFlowPolicyNameNotFound, flowPolicyId)} } diff --git a/internal/connector/pingone/sso/resources/application_resource_grant.go b/internal/connector/pingone/sso/resources/application_resource_grant.go index 7a6d605a..01c2e419 100644 --- a/internal/connector/pingone/sso/resources/application_resource_grant.go +++ b/internal/connector/pingone/sso/resources/application_resource_grant.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneApplicationResourceGrantResource{} + _ connector.ExportableResource = &PingOneApplicationResourceGrantResource{} + applicationResourceGrantExportErrorPrefix = "pingone_application_resource_grant resource export error" ) type PingOneApplicationResourceGrantResource struct { @@ -170,5 +172,5 @@ func (r *PingOneApplicationResourceGrantResource) getGrantResourceName(grantReso } } - return "", false, fmt.Errorf("unable to get resource name for grant resource ID: %s", grantResourceId) + return "", false, &errs.PingCLIError{Prefix: applicationResourceGrantExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrResourceNameNotFound, grantResourceId)} } diff --git a/internal/connector/pingone/sso/resources/application_role_assignment.go b/internal/connector/pingone/sso/resources/application_role_assignment.go index fe22ee9b..991b4774 100644 --- a/internal/connector/pingone/sso/resources/application_role_assignment.go +++ b/internal/connector/pingone/sso/resources/application_role_assignment.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneApplicationRoleAssignmentResource{} + _ connector.ExportableResource = &PingOneApplicationRoleAssignmentResource{} + applicationRoleAssignmentExportErrorPrefix = "pingone_application_role_assignment resource export error" ) type PingOneApplicationRoleAssignmentResource struct { @@ -179,5 +181,5 @@ func (r *PingOneApplicationRoleAssignmentResource) getRoleName(roleId string) (m } } - return "", false, fmt.Errorf("unable to get role name for role ID: %s", roleId) + return "", false, &errs.PingCLIError{Prefix: applicationRoleAssignmentExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrRoleNameNotFound, roleId)} } diff --git a/internal/connector/pingone/sso/resources/application_secret.go b/internal/connector/pingone/sso/resources/application_secret.go index 51454977..9490cbc5 100644 --- a/internal/connector/pingone/sso/resources/application_secret.go +++ b/internal/connector/pingone/sso/resources/application_secret.go @@ -12,13 +12,15 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" "github.com/pingidentity/pingcli/internal/output" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneApplicationSecretResource{} + _ connector.ExportableResource = &PingOneApplicationSecretResource{} + applicationSecretExportErrorPrefix = "pingone_application_secret resource export error" // #nosec G101 -- This is not a credential ) type PingOneApplicationSecretResource struct { @@ -132,7 +134,7 @@ func (r *PingOneApplicationSecretResource) checkApplicationSecretData(applicatio if response.StatusCode == http.StatusForbidden { return false, nil } else { - return false, fmt.Errorf("error: Expected 403 Forbidden response - worker apps cannot read their own secret\n%s Response Code: %s\nResponse Body: %s", "ReadApplicationSecret", response.Status, response.Body) + return false, &errs.PingCLIError{Prefix: applicationSecretExportErrorPrefix, Err: fmt.Errorf("%w: ReadApplicationSecret Response Code: %s Response Body: %v", ErrUnexpectedResponse, response.Status, response.Body)} } } diff --git a/internal/connector/pingone/sso/resources/application_sign_on_policy_assignment.go b/internal/connector/pingone/sso/resources/application_sign_on_policy_assignment.go index 0c8085ea..4092c073 100644 --- a/internal/connector/pingone/sso/resources/application_sign_on_policy_assignment.go +++ b/internal/connector/pingone/sso/resources/application_sign_on_policy_assignment.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneApplicationSignOnPolicyAssignmentResource{} + _ connector.ExportableResource = &PingOneApplicationSignOnPolicyAssignmentResource{} + applicationSignOnPolicyAssignmentExportErrorPrefix = "pingone_application_sign_on_policy_assignment resource export error" ) type PingOneApplicationSignOnPolicyAssignmentResource struct { @@ -165,5 +167,5 @@ func (r *PingOneApplicationSignOnPolicyAssignmentResource) getSignOnPolicyName(s } } - return "", false, fmt.Errorf("unable to get sign-on policy name for sign-on policy ID: %s", signOnPolicyId) + return "", false, &errs.PingCLIError{Prefix: applicationSignOnPolicyAssignmentExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrSignOnPolicyNameNotFound, signOnPolicyId)} } diff --git a/internal/connector/pingone/sso/resources/errors.go b/internal/connector/pingone/sso/resources/errors.go new file mode 100644 index 00000000..d9a91ce8 --- /dev/null +++ b/internal/connector/pingone/sso/resources/errors.go @@ -0,0 +1,13 @@ +// Copyright © 2025 Ping Identity Corporation + +package resources + +import "errors" + +var ( + ErrFlowPolicyNameNotFound = errors.New("flow policy name not found for flow policy ID") + ErrResourceNameNotFound = errors.New("resource name not found for grant resource ID") + ErrRoleNameNotFound = errors.New("role name not found for role ID") + ErrUnexpectedResponse = errors.New("unexpected response - worker apps cannot read their own secret") + ErrSignOnPolicyNameNotFound = errors.New("sign-on policy name not found for sign-on policy ID") +) diff --git a/internal/connector/pingone/sso/resources/group_role_assignment.go b/internal/connector/pingone/sso/resources/group_role_assignment.go index a2f519ea..89fae470 100644 --- a/internal/connector/pingone/sso/resources/group_role_assignment.go +++ b/internal/connector/pingone/sso/resources/group_role_assignment.go @@ -10,12 +10,14 @@ import ( "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/errs" "github.com/pingidentity/pingcli/internal/logger" ) // Verify that the resource satisfies the exportable resource interface var ( - _ connector.ExportableResource = &PingOneGroupRoleAssignmentResource{} + _ connector.ExportableResource = &PingOneGroupRoleAssignmentResource{} + groupRoleAssignmentExportErrorPrefix = "pingone_group_role_assignment resource export error" ) type PingOneGroupRoleAssignmentResource struct { @@ -146,5 +148,5 @@ func (r *PingOneGroupRoleAssignmentResource) getRoleName(roleId string) (*manage } } - return nil, false, fmt.Errorf("unable to get role name for role ID: %s", roleId) + return nil, false, &errs.PingCLIError{Prefix: groupRoleAssignmentExportErrorPrefix, Err: fmt.Errorf("%w: %q", ErrRoleNameNotFound, roleId)} } diff --git a/internal/customtypes/bool.go b/internal/customtypes/bool.go index 1a3077df..9315e74a 100644 --- a/internal/customtypes/bool.go +++ b/internal/customtypes/bool.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "strconv" @@ -13,7 +12,6 @@ import ( var ( boolErrorPrefix = "custom type bool error" - ErrParseBool = errors.New("failed to parse value as bool") ) type Bool bool diff --git a/internal/customtypes/common_errors.go b/internal/customtypes/common_errors.go deleted file mode 100644 index 217cbb1e..00000000 --- a/internal/customtypes/common_errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package customtypes - -import "errors" - -var ( - ErrCustomTypeNil = errors.New("failed to set value. custom type is nil") -) diff --git a/internal/customtypes/errors.go b/internal/customtypes/errors.go new file mode 100644 index 00000000..64a027da --- /dev/null +++ b/internal/customtypes/errors.go @@ -0,0 +1,25 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes + +import "errors" + +var ( + ErrCustomTypeNil = errors.New("failed to set value. An internal error occurred") + ErrParseBool = errors.New("failed to parse value as bool") + ErrParseInt = errors.New("failed to parse value as int") + ErrInvalidUUID = errors.New("invalid uuid") + ErrInvalidHeaderFormat = errors.New("invalid header format. must be in `key:value` format") + ErrDisallowedAuthHeader = errors.New("authorization header is not allowed") + ErrUnrecognizedMethod = errors.New("unrecognized http method") + ErrUnrecognizedService = errors.New("unrecognized request service") + ErrUnrecognizedOutputFormat = errors.New("unrecognized output format") + ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized pingone region code") + ErrUnrecognizedPingOneAuth = errors.New("unrecognized pingone authentication type") + ErrUnrecognizedPingFederateAuth = errors.New("unrecognized pingfederate authentication type") + ErrUnrecognizedProduct = errors.New("unrecognized license product") + ErrInvalidVersionFormat = errors.New("invalid version format, must be 'major.minor'") + ErrUnrecognisedFormat = errors.New("unrecognized export format") + ErrUnrecognisedServiceGroup = errors.New("unrecognized service group") + ErrUnrecognisedExportService = errors.New("unrecognized service") +) diff --git a/internal/customtypes/export_format.go b/internal/customtypes/export_format.go index 537e777f..b0cf5495 100644 --- a/internal/customtypes/export_format.go +++ b/internal/customtypes/export_format.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -18,7 +17,6 @@ const ( var ( exportFormatErrorPrefix = "custom type export format error" - ErrUnrecognisedFormat = errors.New("unrecognized export format") ) type ExportFormat string diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go index 6ec2f25a..1f2916f4 100644 --- a/internal/customtypes/export_service_group.go +++ b/internal/customtypes/export_service_group.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -18,7 +17,6 @@ const ( var ( exportServiceGroupErrorPrefix = "custom type export service group error" - ErrUnrecognisedServiceGroup = errors.New("unrecognized service group") ) type ExportServiceGroup string @@ -35,14 +33,9 @@ func (esg *ExportServiceGroup) Set(serviceGroup string) error { return nil } - // Create a map of valid service groups to check the user provided group against + // Check if the user provided group is valid validServiceGroups := ExportServiceGroupValidValues() - validServiceGroupMap := make(map[string]struct{}, len(validServiceGroups)) - for _, s := range validServiceGroups { - validServiceGroupMap[s] = struct{}{} - } - - if _, ok := validServiceGroupMap[serviceGroup]; !ok { + if !slices.Contains(validServiceGroups, serviceGroup) { return &errs.PingCLIError{Prefix: exportServiceGroupErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedServiceGroup, serviceGroup, strings.Join(validServiceGroups, ", "))} } diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go index 16bc6b92..71a32e3b 100644 --- a/internal/customtypes/export_services.go +++ b/internal/customtypes/export_services.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -22,8 +21,7 @@ const ( ) var ( - exportServicesErrorPrefix = "custom type export services error" - ErrUnrecognisedExportService = errors.New("unrecognized service") + exportServicesErrorPrefix = "custom type export services error" ) type ExportServices []string @@ -50,11 +48,7 @@ func (es *ExportServices) Set(servicesStr string) error { } // Create a map of valid service values to check against user-provided services - validServices := ExportServicesValidValues() - validServiceMap := make(map[string]string, len(validServices)) - for _, s := range validServices { - validServiceMap[strings.ToLower(s)] = s - } + validServiceMap := ExportServicesValidValuesMap() // Create a map of existing services set in the ExportServices object existingServices := make(map[string]struct{}, len(*es)) @@ -70,7 +64,7 @@ func (es *ExportServices) Set(servicesStr string) error { enumService, ok := validServiceMap[service] if !ok { - return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedExportService, service, strings.Join(validServices, ", "))} + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedExportService, service, strings.Join(ExportServicesValidValues(), ", "))} } if _, ok := existingServices[enumService]; ok { @@ -176,3 +170,14 @@ func ExportServicesValidValues() []string { return allServices } + +// ExportServicesValidValuesMap returns a map of valid export service values with lowercase keys +func ExportServicesValidValuesMap() map[string]string { + validServices := ExportServicesValidValues() + validServiceMap := make(map[string]string, len(validServices)) + for _, s := range validServices { + validServiceMap[strings.ToLower(s)] = s + } + + return validServiceMap +} diff --git a/internal/customtypes/headers.go b/internal/customtypes/headers.go index 1469d4de..2f40d60a 100644 --- a/internal/customtypes/headers.go +++ b/internal/customtypes/headers.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "net/http" "regexp" @@ -15,10 +14,8 @@ import ( ) var ( - headerErrorPrefix = "custom type header error" - ErrInvalidHeaderFormat = errors.New("invalid header format. must be in `key:value` format") - ErrDisallowedAuthHeader = errors.New("authorization header is not allowed") - headerRegex = regexp.MustCompile(`(^[^\s:]+):(.*)`) + headerErrorPrefix = "custom type header error" + headerRegex = regexp.MustCompile(`(^[^\s:]+):(.*)`) ) type Header struct { diff --git a/internal/customtypes/http_method.go b/internal/customtypes/http_method.go index 136e33e1..e6efe563 100644 --- a/internal/customtypes/http_method.go +++ b/internal/customtypes/http_method.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -22,7 +21,6 @@ const ( var ( httpMethodErrorPrefix = "custom type http method error" - ErrUnrecognizedMethod = errors.New("unrecognized http method") ) type HTTPMethod string diff --git a/internal/customtypes/int.go b/internal/customtypes/int.go index 2cc2647d..3c324af7 100644 --- a/internal/customtypes/int.go +++ b/internal/customtypes/int.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "strconv" @@ -13,7 +12,6 @@ import ( var ( intErrorPrefix = "custom type int error" - ErrParseInt = errors.New("failed to parse value as int") ) type Int int64 diff --git a/internal/customtypes/license_product.go b/internal/customtypes/license_product.go index f70fa7f1..d09dfa4f 100644 --- a/internal/customtypes/license_product.go +++ b/internal/customtypes/license_product.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -24,7 +23,6 @@ const ( var ( licenseProductErrorPrefix = "custom type license product error" - ErrUnrecognizedProduct = errors.New("unrecognized license product") ) type LicenseProduct string diff --git a/internal/customtypes/license_version.go b/internal/customtypes/license_version.go index 3666dfb2..792ccded 100644 --- a/internal/customtypes/license_version.go +++ b/internal/customtypes/license_version.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "regexp" @@ -13,7 +12,6 @@ import ( var ( licenseVersionErrorPrefix = "custom type license version error" - ErrInvalidVersionFormat = errors.New("invalid version format, must be 'major.minor'") ) type LicenseVersion string diff --git a/internal/customtypes/output_format.go b/internal/customtypes/output_format.go index 98d0db7a..f8a5a596 100644 --- a/internal/customtypes/output_format.go +++ b/internal/customtypes/output_format.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -18,8 +17,7 @@ const ( ) var ( - outputFormatErrorPrefix = "custom type output format error" - ErrUnrecognizedOutputFormat = errors.New("unrecognized output format") + outputFormatErrorPrefix = "custom type output format error" ) type OutputFormat string diff --git a/internal/customtypes/pingfederate_auth_type.go b/internal/customtypes/pingfederate_auth_type.go index 6e9fead5..2f6c6756 100644 --- a/internal/customtypes/pingfederate_auth_type.go +++ b/internal/customtypes/pingfederate_auth_type.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -20,7 +19,6 @@ const ( var ( pingFederateAuthTypeErrorPrefix = "custom type pingfederate authentication type error" - ErrUnrecognizedPingFederateAuth = errors.New("unrecognized pingfederate authentication type") ) type PingFederateAuthenticationType string diff --git a/internal/customtypes/pingone_auth_type.go b/internal/customtypes/pingone_auth_type.go index caa19a5a..f97bbcca 100644 --- a/internal/customtypes/pingone_auth_type.go +++ b/internal/customtypes/pingone_auth_type.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -18,7 +17,6 @@ const ( var ( pingOneAuthTypeErrorPrefix = "custom type pingone auth type error" - ErrUnrecognizedPingOneAuth = errors.New("unrecognized pingone authentication type") ) type PingOneAuthenticationType string diff --git a/internal/customtypes/pingone_region_code.go b/internal/customtypes/pingone_region_code.go index 5b83b4fd..03b9361c 100644 --- a/internal/customtypes/pingone_region_code.go +++ b/internal/customtypes/pingone_region_code.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -27,8 +26,7 @@ const ( ) var ( - pingOneRegionCodeErrorPrefix = "custom type pingone region code error" - ErrUnrecognizedPingOneRegionCode = errors.New("unrecognized pingone region code") + pingOneRegionCodeErrorPrefix = "custom type pingone region code error" ) type PingOneRegionCode string diff --git a/internal/customtypes/request_services.go b/internal/customtypes/request_services.go index e8a3b054..4fa8452c 100644 --- a/internal/customtypes/request_services.go +++ b/internal/customtypes/request_services.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "slices" "strings" @@ -18,7 +17,6 @@ const ( var ( requestServiceErrorPrefix = "custom type request service error" - ErrUnrecognizedService = errors.New("unrecognized request service") ) type RequestService string diff --git a/internal/customtypes/uuid.go b/internal/customtypes/uuid.go index a60e2c8a..69448fc8 100644 --- a/internal/customtypes/uuid.go +++ b/internal/customtypes/uuid.go @@ -3,7 +3,6 @@ package customtypes import ( - "errors" "fmt" "github.com/hashicorp/go-uuid" @@ -13,7 +12,6 @@ import ( var ( uuidErrorPrefix = "custom type uuid error" - ErrInvalidUUID = errors.New("invalid uuid") ) type UUID string diff --git a/internal/errs/pingcli_error.go b/internal/errs/pingcli_error.go index 63254885..199d3c4e 100644 --- a/internal/errs/pingcli_error.go +++ b/internal/errs/pingcli_error.go @@ -26,6 +26,10 @@ func (e *PingCLIError) Error() string { } } + if e.Prefix == "" { + return e.Err.Error() + } + return fmt.Sprintf("%s: %s", e.Prefix, e.Err.Error()) } diff --git a/internal/errs/pingcli_error_test.go b/internal/errs/pingcli_error_test.go index 7dc298fd..422156f4 100644 --- a/internal/errs/pingcli_error_test.go +++ b/internal/errs/pingcli_error_test.go @@ -11,8 +11,11 @@ import ( "github.com/stretchr/testify/require" ) +var ( + errTest = errors.New("test error") +) + func Test_PingCLIError_Error(t *testing.T) { - testErr := errors.New("test error") prefix1 := "prefix 1" prefix2 := "prefix 2" @@ -28,11 +31,11 @@ func Test_PingCLIError_Error(t *testing.T) { name: "Happy path", err: &errs.PingCLIError{ Prefix: prefix1, - Err: testErr, + Err: errTest, }, - expectedStr: fmt.Sprintf("%s: %s", prefix1, testErr.Error()), + expectedStr: fmt.Sprintf("%s: %s", prefix1, errTest.Error()), expectedAs: &errs.PingCLIError{}, - expectedIs: testErr, + expectedIs: errTest, assertUnwrap: require.Error, }, { @@ -41,12 +44,12 @@ func Test_PingCLIError_Error(t *testing.T) { Prefix: prefix1, Err: &errs.PingCLIError{ Prefix: prefix1, - Err: testErr, + Err: errTest, }, }, - expectedStr: fmt.Sprintf("%s: %s", prefix1, testErr.Error()), + expectedStr: fmt.Sprintf("%s: %s", prefix1, errTest.Error()), expectedAs: &errs.PingCLIError{}, - expectedIs: testErr, + expectedIs: errTest, assertUnwrap: require.Error, }, { @@ -55,12 +58,12 @@ func Test_PingCLIError_Error(t *testing.T) { Prefix: prefix2, Err: &errs.PingCLIError{ Prefix: prefix1, - Err: testErr, + Err: errTest, }, }, - expectedStr: fmt.Sprintf("%s: %s: %s", prefix2, prefix1, testErr.Error()), + expectedStr: fmt.Sprintf("%s: %s: %s", prefix2, prefix1, errTest.Error()), expectedAs: &errs.PingCLIError{}, - expectedIs: testErr, + expectedIs: errTest, assertUnwrap: require.Error, }, { diff --git a/internal/input/input_test.go b/internal/input/input_test.go index cb239305..16c19ba3 100644 --- a/internal/input/input_test.go +++ b/internal/input/input_test.go @@ -14,9 +14,13 @@ import ( "github.com/stretchr/testify/require" ) +var ( + errInvalidInput = errors.New("invalid input") +) + func mockValidateFunc(input string) error { if input == "invalid" { - return errors.New("invalid input") + return errInvalidInput } return nil diff --git a/internal/plugins/errors.go b/internal/plugins/errors.go new file mode 100644 index 00000000..256e85fd --- /dev/null +++ b/internal/plugins/errors.go @@ -0,0 +1,14 @@ +// Copyright © 2025 Ping Identity Corporation + +package plugins + +import "errors" + +var ( + ErrGetPluginExecutables = errors.New("failed to get configured plugin executables") + ErrCreateRPCClient = errors.New("failed to create plugin rpc client") + ErrDispensePlugin = errors.New("the rpc client failed to dispense plugin executable") + ErrCastPluginInterface = errors.New("failed to cast plugin executable to grpc.PingCliCommand interface") + ErrPluginConfiguration = errors.New("failed to get plugin configuration") + ErrExecutePlugin = errors.New("failed to execute plugin command") +) diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index 4e2f0d5b..8be5b5d5 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -4,7 +4,6 @@ package plugins import ( "context" - "errors" "fmt" "io" "os/exec" @@ -23,13 +22,7 @@ import ( ) var ( - pluginsErrorPrefix = "plugins error" - ErrGetPluginExecutables = errors.New("failed to get configured plugin executables") - ErrCreateRPCClient = errors.New("failed to create plugin rpc client") - ErrDispensePlugin = errors.New("the rpc client failed to dispense plugin executable") - ErrCastPluginInterface = errors.New("failed to cast plugin executable to grpc.PingCliCommand interface") - ErrPluginConfiguration = errors.New("failed to get plugin configuration") - ErrExecutePlugin = errors.New("failed to execute plugin command") + pluginsErrorPrefix = "plugins error" ) func AddAllPluginToCmd(cmd *cobra.Command) error { @@ -185,7 +178,7 @@ func filterRootFlags(cmd *cobra.Command, args []string) []string { rootFlags := cmd.Root().PersistentFlags() // isRootFlag checks if a given argument (like "--profile") is a known persistent flag on the root command. - isRootFlag := func(arg string) *pflag.Flag { + lookupRootFlag := func(arg string) *pflag.Flag { // Positional arguments don't start with a hyphen, so they can't be flags. if !strings.HasPrefix(arg, "-") { return nil @@ -210,7 +203,7 @@ func filterRootFlags(cmd *cobra.Command, args []string) []string { for i := 0; i < len(args); i++ { arg := args[i] - flag := isRootFlag(arg) + flag := lookupRootFlag(arg) if flag == nil { // If it's not a recognized root flag, it must be for the plugin. diff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go index 4f4ef361..29f431ba 100644 --- a/internal/plugins/plugins_test.go +++ b/internal/plugins/plugins_test.go @@ -23,6 +23,10 @@ var ( Long: "A longer description for a test plugin", Example: "pingcli test-plugin --flag value", } + + errPluginConfigError = errors.New("plugin configuration error") + errPluginRunError = errors.New("plugin run error") + errPluginArgs = errors.New("plugin received args") ) // mockPingCliCommand is a mock implementation of the grpc.PingCliCommand interface for testing. @@ -32,7 +36,7 @@ var mockPlugin = &mockPingCliCommand{} func (m *mockPingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, error) { if configErr := os.Getenv("PINGCLI_TEST_PLUGIN_CONFIG_ERROR"); configErr != "" { - return nil, errors.New(configErr) + return nil, fmt.Errorf("%w: %s", errPluginConfigError, configErr) } return testPluginConfig, nil @@ -40,10 +44,10 @@ func (m *mockPingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, func (m *mockPingCliCommand) Run(args []string, l grpc.Logger) error { if runErr := os.Getenv("PINGCLI_TEST_PLUGIN_RUN_ERROR"); runErr != "" { - return errors.New(runErr) + return fmt.Errorf("%w: %s", errPluginRunError, runErr) } - return fmt.Errorf("args: %s", strings.Join(args, ",")) + return fmt.Errorf("%w: %s", errPluginArgs, strings.Join(args, ",")) } func TestMain(m *testing.M) { diff --git a/internal/profiles/errors.go b/internal/profiles/errors.go new file mode 100644 index 00000000..629f5e41 --- /dev/null +++ b/internal/profiles/errors.go @@ -0,0 +1,45 @@ +// Copyright © 2025 Ping Identity Corporation + +package profiles + +import "errors" + +var ( + // Validation errors + ErrValidatePingCLIConfiguration = errors.New("failed to validate Ping CLI configuration") + ErrInvalidConfigurationKey = errors.New("invalid configuration key(s) found in profile") + ErrUnrecognizedVariableType = errors.New("unrecognized variable type for key") + ErrValidateBoolean = errors.New("invalid boolean value") + ErrValidateUUID = errors.New("invalid uuid value") + ErrValidateOutputFormat = errors.New("invalid output format value") + ErrValidatePingOneRegionCode = errors.New("invalid pingone region code value") + ErrValidateString = errors.New("invalid string value") + ErrValidateStringSlice = errors.New("invalid string slice value") + ErrValidateExportServiceGroup = errors.New("invalid export service group value") + ErrValidateExportServices = errors.New("invalid export services value") + ErrValidateExportFormat = errors.New("invalid export format value") + ErrValidateHTTPMethod = errors.New("invalid http method value") + ErrValidateRequestService = errors.New("invalid request service value") + ErrValidateInt = errors.New("invalid int value") + ErrValidatePingFederateAuthType = errors.New("invalid pingfederate auth type value") + ErrValidatePingOneAuthType = errors.New("invalid pingone auth type value") + ErrValidateLicenseProduct = errors.New("invalid license product value") + ErrValidateLicenseVersion = errors.New("invalid license version value") + + // Koanf errors + ErrNoOptionValue = errors.New("no option value found") + ErrKoanfNotInitialized = errors.New("koanf instance is not initialized") + ErrProfileNameEmpty = errors.New("invalid profile name: profile name cannot be empty") + ErrProfileNameFormat = errors.New("invalid profile name: profile name must contain only alphanumeric characters, underscores, and dashes") + ErrProfileNameSameAsActiveProfileKey = errors.New("invalid profile name: profile name cannot be the same as the active profile key") + ErrSetActiveProfile = errors.New("error setting active profile") + ErrWriteKoanfFile = errors.New("failed to write configuration file to disk") + ErrProfileNameNotExist = errors.New("invalid profile name: profile name does not exist") + ErrProfileNameAlreadyExists = errors.New("invalid profile name: profile name already exists") + ErrKoanfProfileExtractAndLoad = errors.New("failed to extract and load profile configuration") + ErrSetKoanfKeyValue = errors.New("failed to set koanf key value") + ErrMarshalKoanf = errors.New("failed to marshal koanf configuration") + ErrKoanfMerge = errors.New("failed to merge koanf configuration") + ErrDeleteActiveProfile = errors.New("the active profile cannot be deleted") + ErrSetKoanfKeyDefaultValue = errors.New("failed to set koanf key default value") +) diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 89d34cae..4edd61fd 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -3,7 +3,6 @@ package profiles import ( - "errors" "fmt" "os" "regexp" @@ -20,22 +19,7 @@ import ( var ( k *KoanfConfig - koanfErrorPrefix = "profile configuration error" - ErrNoOptionValue = errors.New("no option value found") - ErrKoanfNotInitialized = errors.New("koanf instance is not initialized") - ErrProfileNameEmpty = errors.New("invalid profile name: profile name cannot be empty") - ErrProfileNameFormat = errors.New("invalid profile name: profile name must contain only alphanumeric characters, underscores, and dashes") - ErrProfileNameSameAsActiveProfileKey = errors.New("invalid profile name: profile name cannot be the same as the active profile key") - ErrSetActiveProfile = errors.New("error setting active profile") - ErrWriteKoanfFile = errors.New("failed to write configuration file to disk") - ErrProfileNameNotExist = errors.New("invalid profile name: profile name does not exist") - ErrProfileNameAlreadyExists = errors.New("invalid profile name: profile name already exists") - ErrKoanfProfileExtractAndLoad = errors.New("failed to extract and load profile configuration") - ErrSetKoanfKeyValue = errors.New("failed to set koanf key value") - ErrMarshalKoanf = errors.New("failed to marshal koanf configuration") - ErrKoanfMerge = errors.New("failed to merge koanf configuration") - ErrDeleteActiveProfile = errors.New("the active profile cannot be deleted") - ErrSetKoanfKeyDefaultValue = errors.New("failed to set koanf key default value") + koanfErrorPrefix = "profile configuration error" ) type KoanfConfig struct { diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index 261396ec..945758e7 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -3,7 +3,6 @@ package profiles import ( - "errors" "fmt" "slices" "strings" @@ -16,26 +15,7 @@ import ( ) var ( - validateErrorPrefix = "profile validation error" - ErrValidatePingCLIConfiguration = errors.New("failed to validate Ping CLI configuration") - ErrInvalidConfigurationKey = errors.New("invalid configuration key(s) found in profile") - ErrUnrecognizedVariableType = errors.New("unrecognized variable type for key") - ErrValidateBoolean = errors.New("invalid boolean value") - ErrValidateUUID = errors.New("invalid uuid value") - ErrValidateOutputFormat = errors.New("invalid output format value") - ErrValidatePingOneRegionCode = errors.New("invalid pingone region code value") - ErrValidateString = errors.New("invalid string value") - ErrValidateStringSlice = errors.New("invalid string slice value") - ErrValidateExportServiceGroup = errors.New("invalid export service group value") - ErrValidateExportServices = errors.New("invalid export services value") - ErrValidateExportFormat = errors.New("invalid export format value") - ErrValidateHTTPMethod = errors.New("invalid http method value") - ErrValidateRequestService = errors.New("invalid request service value") - ErrValidateInt = errors.New("invalid int value") - ErrValidatePingFederateAuthType = errors.New("invalid pingfederate auth type value") - ErrValidatePingOneAuthType = errors.New("invalid pingone auth type value") - ErrValidateLicenseProduct = errors.New("invalid license product value") - ErrValidateLicenseVersion = errors.New("invalid license version value") + validateErrorPrefix = "profile validation error" ) func Validate() (err error) { diff --git a/internal/testing/testutils/stdio.go b/internal/testing/testutils/stdio.go index cb52661c..c330251b 100644 --- a/internal/testing/testutils/stdio.go +++ b/internal/testing/testutils/stdio.go @@ -3,11 +3,14 @@ package testutils import ( + "context" "io" "os" + "time" ) // CaptureStdout executes a function and returns its standard output as a string. +// If the function takes longer than 30 seconds to complete, it returns an empty string. func CaptureStdout(f func()) string { originalStdout := os.Stdout r, w, _ := os.Pipe() @@ -16,18 +19,28 @@ func CaptureStdout(f func()) string { os.Stdout = w - outC := make(chan string) + outC := make(chan string, 1) go func() { b, _ := io.ReadAll(r) outC <- string(b) }() - f() + done := make(chan struct{}) + go func() { + f() + _ = w.Close() + close(done) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + select { + case <-ctx.Done(): + _ = w.Close() - err := w.Close() - if err != nil { return "" + case <-done: + return <-outC } - - return <-outC } diff --git a/shared/grpc/errors.go b/shared/grpc/errors.go new file mode 100644 index 00000000..9933a51d --- /dev/null +++ b/shared/grpc/errors.go @@ -0,0 +1,9 @@ +// Copyright © 2025 Ping Identity Corporation + +package grpc + +import "errors" + +var ( + ErrNilConfiguration = errors.New("plugin returned a nil configuration") +) diff --git a/shared/grpc/pingcli_command_grpc_server.go b/shared/grpc/pingcli_command_grpc_server.go index 1f80c00b..24d5e5e8 100644 --- a/shared/grpc/pingcli_command_grpc_server.go +++ b/shared/grpc/pingcli_command_grpc_server.go @@ -25,7 +25,7 @@ func (s *PingCliCommandGRPCServer) Configuration(ctx context.Context, req *proto } if cmd == nil { - return nil, errors.New("plugin returned a nil configuration") + return nil, ErrNilConfiguration } return &proto.PingCliCommandConfigurationResponse{ diff --git a/tools/generate-command-docs/main.go b/tools/generate-command-docs/main.go index e3bbedfa..c1a531da 100644 --- a/tools/generate-command-docs/main.go +++ b/tools/generate-command-docs/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "flag" "fmt" "os" @@ -17,6 +18,10 @@ import ( "github.com/spf13/pflag" ) +var ( + errPathOutsideBase = errors.New("refusing to read path outside base directory") +) + func main() { outDir := flag.String("o", "./docs", "Output directory for AsciiDoc command pages") date := flag.String("date", time.Now().Format("January 2, 2006"), "Created/revision date used in headers (e.g., March 23, 2025)") @@ -343,7 +348,7 @@ func readFileIfWithin(path, base string) ([]byte, error) { cleanBase := filepath.Clean(base) cleanPath := filepath.Clean(path) if !strings.HasPrefix(cleanPath+string(os.PathSeparator), cleanBase+string(os.PathSeparator)) { - return nil, fmt.Errorf("refusing to read path outside base directory: %s", path) + return nil, fmt.Errorf("%w: %s", errPathOutsideBase, path) } data, err := os.ReadFile(cleanPath) // #nosec G304 path validated above if err != nil { From 66c89b925186734a35e07523558b4337aa673a68 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 14 Oct 2025 13:29:16 -0500 Subject: [PATCH 12/14] fix test cases --- internal/connector/common/resources_common.go | 2 +- internal/errs/pingcli_error.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/connector/common/resources_common.go b/internal/connector/common/resources_common.go index f42ff627..6cf042eb 100644 --- a/internal/connector/common/resources_common.go +++ b/internal/connector/common/resources_common.go @@ -61,8 +61,8 @@ func HandleClientResponse(response *http.Response, err error, apiFunctionName st cErr := response.Body.Close() if cErr != nil { rErr = errors.Join(rErr, cErr) + rErr = &errs.PingCLIError{Prefix: resourceUtilsErrorPrefix, Err: rErr} } - rErr = &errs.PingCLIError{Prefix: resourceUtilsErrorPrefix, Err: rErr} }() // When the client returns forbidden, warn user and skip export of resource diff --git a/internal/errs/pingcli_error.go b/internal/errs/pingcli_error.go index 199d3c4e..1fc4de83 100644 --- a/internal/errs/pingcli_error.go +++ b/internal/errs/pingcli_error.go @@ -26,11 +26,16 @@ func (e *PingCLIError) Error() string { } } + // if both are empty this just returns an empty string anyways + errMsg := e.Err.Error() if e.Prefix == "" { - return e.Err.Error() + return errMsg + } + if errMsg == "" { + return e.Prefix } - return fmt.Sprintf("%s: %s", e.Prefix, e.Err.Error()) + return fmt.Sprintf("%s: %s", e.Prefix, errMsg) } func (e *PingCLIError) Unwrap() error { From ba02982c816a379ba176381a3081dd1544006883 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 14 Oct 2025 13:39:32 -0500 Subject: [PATCH 13/14] copilot review suggestions --- cmd/common/errors.go | 4 ++-- cmd/platform/export_test.go | 6 +++--- internal/customtypes/errors.go | 6 +++--- internal/customtypes/export_format.go | 2 +- internal/customtypes/export_format_test.go | 2 +- internal/customtypes/export_service_group.go | 2 +- internal/customtypes/export_service_group_test.go | 2 +- internal/customtypes/export_services.go | 2 +- internal/customtypes/export_services_test.go | 2 +- internal/testing/testutils/stdio.go | 7 +++++-- 10 files changed, 19 insertions(+), 16 deletions(-) diff --git a/cmd/common/errors.go b/cmd/common/errors.go index bf263212..0b0e39bc 100644 --- a/cmd/common/errors.go +++ b/cmd/common/errors.go @@ -5,6 +5,6 @@ package common import "errors" var ( - ErrExactArgs = errors.New("incorrect number of arguments") - ErrRangeArgs = errors.New("incorrect number of arguments") + ErrExactArgs = errors.New("exact number of arguments not provided") + ErrRangeArgs = errors.New("argument count not in valid range") ) diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go index 73b8eddd..14c99590 100644 --- a/cmd/platform/export_test.go +++ b/cmd/platform/export_test.go @@ -69,7 +69,7 @@ func Test_PlatformExportCommand(t *testing.T) { "--" + options.PlatformExportServiceGroupOption.CobraParamName, "invalid", }, expectErr: true, - expectedErrIs: customtypes.ErrUnrecognisedServiceGroup, + expectedErrIs: customtypes.ErrUnrecognizedServiceGroup, }, { name: "Happy Path - with specific service", @@ -96,7 +96,7 @@ func Test_PlatformExportCommand(t *testing.T) { "--" + options.PlatformExportServiceOption.CobraParamName, "invalid", }, expectErr: true, - expectedErrIs: customtypes.ErrUnrecognisedExportService, + expectedErrIs: customtypes.ErrUnrecognizedExportService, }, { name: "Invalid format", @@ -104,7 +104,7 @@ func Test_PlatformExportCommand(t *testing.T) { "--" + options.PlatformExportExportFormatOption.CobraParamName, "invalid", }, expectErr: true, - expectedErrIs: customtypes.ErrUnrecognisedFormat, + expectedErrIs: customtypes.ErrUnrecognizedFormat, }, { name: "Invalid output directory", diff --git a/internal/customtypes/errors.go b/internal/customtypes/errors.go index 64a027da..729c29f1 100644 --- a/internal/customtypes/errors.go +++ b/internal/customtypes/errors.go @@ -19,7 +19,7 @@ var ( ErrUnrecognizedPingFederateAuth = errors.New("unrecognized pingfederate authentication type") ErrUnrecognizedProduct = errors.New("unrecognized license product") ErrInvalidVersionFormat = errors.New("invalid version format, must be 'major.minor'") - ErrUnrecognisedFormat = errors.New("unrecognized export format") - ErrUnrecognisedServiceGroup = errors.New("unrecognized service group") - ErrUnrecognisedExportService = errors.New("unrecognized service") + ErrUnrecognizedFormat = errors.New("unrecognized export format") + ErrUnrecognizedServiceGroup = errors.New("unrecognized service group") + ErrUnrecognizedExportService = errors.New("unrecognized service") ) diff --git a/internal/customtypes/export_format.go b/internal/customtypes/export_format.go index b0cf5495..166a5fef 100644 --- a/internal/customtypes/export_format.go +++ b/internal/customtypes/export_format.go @@ -37,7 +37,7 @@ func (ef *ExportFormat) Set(format string) error { case strings.EqualFold(format, ""): *ef = ExportFormat("") default: - return &errs.PingCLIError{Prefix: exportFormatErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedFormat, format, strings.Join(ExportFormatValidValues(), ", "))} + return &errs.PingCLIError{Prefix: exportFormatErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognizedFormat, format, strings.Join(ExportFormatValidValues(), ", "))} } return nil diff --git a/internal/customtypes/export_format_test.go b/internal/customtypes/export_format_test.go index 031a9651..7317a9ff 100644 --- a/internal/customtypes/export_format_test.go +++ b/internal/customtypes/export_format_test.go @@ -34,7 +34,7 @@ func Test_ExportFormat_Set(t *testing.T) { name: "Invalid value", cType: new(customtypes.ExportFormat), formatStr: "invalid", - expectedError: customtypes.ErrUnrecognisedFormat, + expectedError: customtypes.ErrUnrecognizedFormat, }, { name: "Nil custom type", diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go index 1f2916f4..a6ac365a 100644 --- a/internal/customtypes/export_service_group.go +++ b/internal/customtypes/export_service_group.go @@ -36,7 +36,7 @@ func (esg *ExportServiceGroup) Set(serviceGroup string) error { // Check if the user provided group is valid validServiceGroups := ExportServiceGroupValidValues() if !slices.Contains(validServiceGroups, serviceGroup) { - return &errs.PingCLIError{Prefix: exportServiceGroupErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedServiceGroup, serviceGroup, strings.Join(validServiceGroups, ", "))} + return &errs.PingCLIError{Prefix: exportServiceGroupErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognizedServiceGroup, serviceGroup, strings.Join(validServiceGroups, ", "))} } *esg = ExportServiceGroup(serviceGroup) diff --git a/internal/customtypes/export_service_group_test.go b/internal/customtypes/export_service_group_test.go index 4d1be9ac..24185e2e 100644 --- a/internal/customtypes/export_service_group_test.go +++ b/internal/customtypes/export_service_group_test.go @@ -29,7 +29,7 @@ func Test_ExportServiceGroup_Set(t *testing.T) { name: "Invalid value", cType: new(customtypes.ExportServiceGroup), value: "invalid", - expectedError: customtypes.ErrUnrecognisedServiceGroup, + expectedError: customtypes.ErrUnrecognizedServiceGroup, }, { name: "Happy path - empty", diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go index 71a32e3b..e4c56e56 100644 --- a/internal/customtypes/export_services.go +++ b/internal/customtypes/export_services.go @@ -64,7 +64,7 @@ func (es *ExportServices) Set(servicesStr string) error { enumService, ok := validServiceMap[service] if !ok { - return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognisedExportService, service, strings.Join(ExportServicesValidValues(), ", "))} + return &errs.PingCLIError{Prefix: exportServicesErrorPrefix, Err: fmt.Errorf("%w '%s': must be one of %s", ErrUnrecognizedExportService, service, strings.Join(ExportServicesValidValues(), ", "))} } if _, ok := existingServices[enumService]; ok { diff --git a/internal/customtypes/export_services_test.go b/internal/customtypes/export_services_test.go index 83226548..90176e59 100644 --- a/internal/customtypes/export_services_test.go +++ b/internal/customtypes/export_services_test.go @@ -90,7 +90,7 @@ func Test_ExportServices_Set(t *testing.T) { cType: new(customtypes.ExportServices), servicesStrs: []string{"invalid"}, expectedNumServices: 0, - expectedError: customtypes.ErrUnrecognisedExportService, + expectedError: customtypes.ErrUnrecognizedExportService, }, { name: "Nil custom type", diff --git a/internal/testing/testutils/stdio.go b/internal/testing/testutils/stdio.go index c330251b..d13708d2 100644 --- a/internal/testing/testutils/stdio.go +++ b/internal/testing/testutils/stdio.go @@ -13,10 +13,13 @@ import ( // If the function takes longer than 30 seconds to complete, it returns an empty string. func CaptureStdout(f func()) string { originalStdout := os.Stdout - r, w, _ := os.Pipe() + r, w, err := os.Pipe() + if err != nil { + return "" + } + defer r.Close() defer func() { os.Stdout = originalStdout }() - os.Stdout = w outC := make(chan string, 1) From 48d61f0835b93ee9aaec1dc7b7399ee8d7a5e303 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 14 Oct 2025 13:45:33 -0500 Subject: [PATCH 14/14] linter fixes --- internal/testing/testutils/stdio.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testing/testutils/stdio.go b/internal/testing/testutils/stdio.go index d13708d2..807e9b36 100644 --- a/internal/testing/testutils/stdio.go +++ b/internal/testing/testutils/stdio.go @@ -17,7 +17,7 @@ func CaptureStdout(f func()) string { if err != nil { return "" } - defer r.Close() + defer func() { _ = r.Close() }() defer func() { os.Stdout = originalStdout }() os.Stdout = w