diff --git a/cmd/init.go b/cmd/init.go index d9cc9d821..78fb7619d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -12,20 +12,21 @@ import ( ) var ( - backend string - awsProfile string - awsEndpointURL string - vmDriver string - cpu int - disk int - memory int - arch string - docker bool - gitLivereload bool - blueprint string - toolsManager string - platform string - endpoint string + initBackend string + initAwsProfile string + initAwsEndpointURL string + initVmDriver string + initCpu int + initDisk int + initMemory int + initArch string + initDocker bool + initGitLivereload bool + initBlueprint string + initToolsManager string + initPlatform string + initEndpoint string + initSetFlags []string ) var initCmd = &cobra.Command{ @@ -73,7 +74,7 @@ var initCmd = &cobra.Command{ } // Determine the default vm driver to use if not set - vmDriverConfig := vmDriver + vmDriverConfig := initVmDriver if vmDriverConfig == "" { vmDriverConfig = configHandler.GetString("vm.driver") if vmDriverConfig == "" && (contextName == "local" || strings.HasPrefix(contextName, "local-")) { @@ -97,32 +98,25 @@ var initCmd = &cobra.Command{ return fmt.Errorf("Error setting default config: %w", err) } - // Set the vm driver only if it's configured - if vmDriverConfig != "" { - if err := configHandler.SetContextValue("vm.driver", vmDriverConfig); err != nil { - return fmt.Errorf("Error setting vm driver: %w", err) - } - } - // Create the flag to config path mapping and set the configurations configurations := []struct { flagName string configPath string value any }{ - {"aws-endpoint-url", "aws.aws_endpoint_url", awsEndpointURL}, - {"aws-profile", "aws.aws_profile", awsProfile}, - {"docker", "docker.enabled", docker}, - {"backend", "terraform.backend", backend}, - {"vm-cpu", "vm.cpu", cpu}, - {"vm-disk", "vm.disk", disk}, - {"vm-memory", "vm.memory", memory}, - {"vm-arch", "vm.arch", arch}, - {"tools-manager", "toolsManager", toolsManager}, - {"git-livereload", "git.livereload.enabled", gitLivereload}, - {"blueprint", "blueprint", blueprint}, - {"endpoint", "cluster.endpoint", endpoint}, - {"platform", "cluster.platform", platform}, + {"aws-endpoint-url", "aws.aws_endpoint_url", initAwsEndpointURL}, + {"aws-profile", "aws.aws_profile", initAwsProfile}, + {"docker", "docker.enabled", initDocker}, + {"backend", "terraform.backend", initBackend}, + {"vm-cpu", "vm.cpu", initCpu}, + {"vm-disk", "vm.disk", initDisk}, + {"vm-memory", "vm.memory", initMemory}, + {"vm-arch", "vm.arch", initArch}, + {"tools-manager", "toolsManager", initToolsManager}, + {"git-livereload", "git.livereload.enabled", initGitLivereload}, + {"blueprint", "blueprint", initBlueprint}, + {"endpoint", "cluster.endpoint", initEndpoint}, + {"platform", "cluster.platform", initPlatform}, } for _, config := range configurations { @@ -134,6 +128,25 @@ var initCmd = &cobra.Command{ } } + // Process all set flags after other flags + for _, setFlag := range initSetFlags { + parts := strings.SplitN(setFlag, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("Invalid format for --set flag. Expected key=value") + } + key, value := parts[0], parts[1] + if err := configHandler.SetContextValue(key, value); err != nil { + return fmt.Errorf("Error setting config override %s: %w", key, err) + } + } + + // Set the vm driver only if it's configured and not overridden by --set flag + if vmDriverConfig != "" && configHandler.GetString("vm.driver") == "" { + if err := configHandler.SetContextValue("vm.driver", vmDriverConfig); err != nil { + return fmt.Errorf("Error setting vm driver: %w", err) + } + } + // Determine the cli configuration path projectRoot, err := shell.GetProjectRoot() if err != nil { @@ -191,18 +204,19 @@ var initCmd = &cobra.Command{ } func init() { - initCmd.Flags().StringVar(&backend, "backend", "", "Specify the terraform backend to use") - initCmd.Flags().StringVar(&awsProfile, "aws-profile", "", "Specify the AWS profile to use") - initCmd.Flags().StringVar(&awsEndpointURL, "aws-endpoint-url", "", "Specify the AWS endpoint URL to use") - initCmd.Flags().StringVar(&vmDriver, "vm-driver", "", "Specify the VM driver. Only Colima is supported for now.") - initCmd.Flags().IntVar(&cpu, "vm-cpu", 0, "Specify the number of CPUs for Colima") - initCmd.Flags().IntVar(&disk, "vm-disk", 0, "Specify the disk size for Colima") - initCmd.Flags().IntVar(&memory, "vm-memory", 0, "Specify the memory size for Colima") - initCmd.Flags().StringVar(&arch, "vm-arch", "", "Specify the architecture for Colima") - initCmd.Flags().BoolVar(&docker, "docker", false, "Enable Docker") - initCmd.Flags().BoolVar(&gitLivereload, "git-livereload", false, "Enable Git Livereload") - initCmd.Flags().StringVar(&platform, "platform", "", "Specify the platform to use [local|metal]") - initCmd.Flags().StringVar(&blueprint, "blueprint", "", "Specify the blueprint to use") - initCmd.Flags().StringVar(&endpoint, "endpoint", "", "Specify the kubernetes API endpoint") + initCmd.Flags().StringVar(&initBackend, "backend", "", "Specify the terraform backend to use") + initCmd.Flags().StringVar(&initAwsProfile, "aws-profile", "", "Specify the AWS profile to use") + initCmd.Flags().StringVar(&initAwsEndpointURL, "aws-endpoint-url", "", "Specify the AWS endpoint URL to use") + initCmd.Flags().StringVar(&initVmDriver, "vm-driver", "", "Specify the VM driver. Only Colima is supported for now.") + initCmd.Flags().IntVar(&initCpu, "vm-cpu", 0, "Specify the number of CPUs for Colima") + initCmd.Flags().IntVar(&initDisk, "vm-disk", 0, "Specify the disk size for Colima") + initCmd.Flags().IntVar(&initMemory, "vm-memory", 0, "Specify the memory size for Colima") + initCmd.Flags().StringVar(&initArch, "vm-arch", "", "Specify the architecture for Colima") + initCmd.Flags().BoolVar(&initDocker, "docker", false, "Enable Docker") + initCmd.Flags().BoolVar(&initGitLivereload, "git-livereload", false, "Enable Git Livereload") + initCmd.Flags().StringVar(&initPlatform, "platform", "", "Specify the platform to use [local|metal]") + initCmd.Flags().StringVar(&initBlueprint, "blueprint", "", "Specify the blueprint to use") + initCmd.Flags().StringVar(&initEndpoint, "endpoint", "", "Specify the kubernetes API endpoint") + initCmd.Flags().StringSliceVar(&initSetFlags, "set", []string{}, "Override configuration values. Example: --set dns.enabled=false --set cluster.endpoint=https://localhost:6443") rootCmd.AddCommand(initCmd) } diff --git a/cmd/init_test.go b/cmd/init_test.go index f1c7538c1..ce7f5fdc5 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -41,6 +41,9 @@ func setupInitMocks(t *testing.T, opts *SetupOptions) *Mocks { mocks.Controller.ResolveShellFunc = func() shell.Shell { return mockShell } + mocks.Controller.ResolveConfigHandlerFunc = func() config.ConfigHandler { + return mocks.ConfigHandler + } mocks.Controller.SetEnvironmentVariablesFunc = func() error { return nil } @@ -197,7 +200,7 @@ func TestInitCmd(t *testing.T) { mockConfigHandler.SetDefaultFunc = func(config v1alpha1.Context) error { return nil } - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetContextValueFunc = func(key string, value any) error { return nil } mockConfigHandler.SaveConfigFunc = func(path string) error { @@ -244,7 +247,7 @@ func TestInitCmd(t *testing.T) { mockConfigHandler.SetDefaultFunc = func(config v1alpha1.Context) error { return nil } - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetContextValueFunc = func(key string, value any) error { return nil } mockConfigHandler.SaveConfigFunc = func(path string) error { @@ -516,6 +519,7 @@ func TestInitCmd(t *testing.T) { }) t.Run("SetVMDriverError", func(t *testing.T) { + // Create a mock config handler that returns an error for vm.driver mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { if key == "vm.driver" { @@ -523,19 +527,36 @@ func TestInitCmd(t *testing.T) { } return nil } - opts := &SetupOptions{ - ConfigHandler: mockConfigHandler, + mockConfigHandler.GetContextFunc = func() string { + return "local" } - mocks := setupInitMocks(t, opts) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + mockConfigHandler.SetDefaultFunc = func(config v1alpha1.Context) error { + return nil + } + + // Given a set of mocks with the error-returning config handler + mocks := setupInitMocks(t, &SetupOptions{ + ConfigHandler: mockConfigHandler, + }) + // Set up command arguments rootCmd.SetArgs([]string{"init", "--vm-driver", "docker-desktop"}) + + // When executing the command err := Execute(mocks.Controller) + // Then error should occur if err == nil { t.Error("Expected error, got nil") } - if err != nil && !strings.Contains(err.Error(), "failed to set vm driver") { - t.Errorf("Expected error containing 'failed to set vm driver', got: %v", err) + + // And error should contain vm driver error message + expectedError := "Error setting vm driver: failed to set vm driver" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) } }) @@ -616,4 +637,169 @@ func TestInitCmd(t *testing.T) { t.Errorf("Expected VM driver to be empty, got '%s'", vmDriver) } }) + + t.Run("SetFlagSuccess", func(t *testing.T) { + // Given a set of mocks with proper configuration + mocks := setupInitMocks(t, nil) + + // Set up command arguments with multiple --set flags + rootCmd.SetArgs([]string{"init", "--set", "dns.enabled=false", "--set", "cluster.endpoint=https://localhost:6443"}) + + // When executing the command + err := Execute(mocks.Controller) + + // Then no error should occur + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + + // And values should be set correctly + expectedValues := map[string]any{ + "dns.enabled": "false", + "cluster.endpoint": "https://localhost:6443", + } + for key, expected := range expectedValues { + actual := mocks.ConfigHandler.GetString(key) + if actual != expected { + t.Errorf("Expected %s=%v, got %v", key, expected, actual) + } + } + }) + + t.Run("SetFlagInvalidFormat", func(t *testing.T) { + // Given a set of mocks with proper configuration + mocks := setupInitMocks(t, nil) + + // Set up command arguments with invalid --set flag format + rootCmd.SetArgs([]string{"init", "--set", "invalid-format"}) + + // When executing the command + err := Execute(mocks.Controller) + + // Then error should occur + if err == nil { + t.Error("Expected error, got nil") + } + + // And error should contain invalid format message + expectedError := "Invalid format for --set flag. Expected key=value" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error containing %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("SetFlagError", func(t *testing.T) { + // Create a mock config handler that returns an error + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + return fmt.Errorf("failed to set config value for %s", key) + } + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + mockConfigHandler.SetDefaultFunc = func(config v1alpha1.Context) error { + return nil + } + + // Given a set of mocks with the error-returning config handler + mocks := setupInitMocks(t, &SetupOptions{ + ConfigHandler: mockConfigHandler, + }) + + // Reset and reinitialize flags + rootCmd.ResetFlags() + initCmd.ResetFlags() + initCmd.Flags().StringSliceVar(&initSetFlags, "set", []string{}, "Override configuration values") + initCmd.Flags().StringVar(&initVmDriver, "vm-driver", "", "Specify the VM driver") + + // Set up command arguments + rootCmd.SetArgs([]string{"init", "--set", "test.key=value"}) + + // When executing the command + err := Execute(mocks.Controller) + + // Then error should occur + if err == nil { + t.Error("Expected error, got nil") + } + + // And error should contain set error message + expectedError := "Error setting config override test.key: failed to set config value for test.key" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("VMDriverSelectionWithSetFlag", func(t *testing.T) { + // Given a set of mocks with proper configuration + mocks := setupInitMocks(t, nil) + + // Reset and reinitialize flags + rootCmd.ResetFlags() + initCmd.ResetFlags() + initCmd.Flags().StringSliceVar(&initSetFlags, "set", []string{}, "Override configuration values") + initCmd.Flags().StringVar(&initVmDriver, "vm-driver", "", "Specify the VM driver") + + // Set up command arguments to override vm.driver + rootCmd.SetArgs([]string{"init", "--set", "vm.driver=custom-driver"}) + + // When executing the command + err := Execute(mocks.Controller) + + // Then no error should occur + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + + // And vm.driver should be set to custom value + actual := mocks.ConfigHandler.GetString("vm.driver") + if actual != "custom-driver" { + t.Errorf("Expected vm.driver=custom-driver, got %v", actual) + } + }) + + t.Run("SetFlagWithExistingConfig", func(t *testing.T) { + // Create a mock config handler with proper context + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "new.key" { + return "new-value" + } + return "" + } + + // Given a set of mocks with the mock config handler + mocks := setupInitMocks(t, &SetupOptions{ + ConfigHandler: mockConfigHandler, + }) + + // Reset and reinitialize flags + rootCmd.ResetFlags() + initCmd.ResetFlags() + initCmd.Flags().StringSliceVar(&initSetFlags, "set", []string{}, "Override configuration values") + initCmd.Flags().StringVar(&initVmDriver, "vm-driver", "", "Specify the VM driver") + + // Set up command arguments + rootCmd.SetArgs([]string{"init", "--set", "new.key=new-value"}) + + // When executing the command + err := Execute(mocks.Controller) + + // Then no error should occur + if err != nil { + t.Errorf("Expected success, got error: %v", err) + } + + // And new value should be set correctly + actual := mockConfigHandler.GetString("new.key") + if actual != "new-value" { + t.Errorf("Expected new.key=new-value, got %v", actual) + } + }) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 032a0ec8c..9ebf78161 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -214,20 +214,42 @@ func captureOutput(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { func TestRootCmd(t *testing.T) { t.Run("RootCmd", func(t *testing.T) { // Given a set of mocks - setupMocks(t) + mocks := setupMocks(t) // When creating the root command cmd := rootCmd + // Ensure the verbose flag is defined + if cmd.PersistentFlags().Lookup("verbose") == nil { + cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + } + // Then the command should be properly configured if cmd.Use != "windsor" { t.Errorf("Expected Use to be 'windsor', got %s", cmd.Use) } // And the command should have the verbose flag - verboseFlag := cmd.Flags().Lookup("verbose") + verboseFlag := cmd.PersistentFlags().Lookup("verbose") if verboseFlag == nil { t.Error("Expected verbose flag to be defined") + return + } + + // And the flag should have the correct properties + if verboseFlag.Name != "verbose" { + t.Errorf("Expected flag name to be 'verbose', got %s", verboseFlag.Name) + } + if verboseFlag.Shorthand != "v" { + t.Errorf("Expected flag shorthand to be 'v', got %s", verboseFlag.Shorthand) + } + if verboseFlag.Usage != "Enable verbose output" { + t.Errorf("Expected flag usage to be 'Enable verbose output', got %s", verboseFlag.Usage) + } + + // Execute should work without error + if err := Execute(mocks.Controller); err != nil { + t.Errorf("Expected no error, got %v", err) } }) } diff --git a/cmd/up.go b/cmd/up.go index 3c494e35c..359fff19e 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -118,8 +118,10 @@ var upCmd = &cobra.Command{ } // Configure DNS settings - if err := networkManager.ConfigureDNS(); err != nil { - return fmt.Errorf("Error configuring DNS: %w", err) + if dnsEnabled := configHandler.GetBool("dns.enabled"); dnsEnabled { + if err := networkManager.ConfigureDNS(); err != nil { + return fmt.Errorf("Error configuring DNS: %w", err) + } } } diff --git a/cmd/up_test.go b/cmd/up_test.go index 1dfafbca6..6d2997489 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -288,11 +288,20 @@ func TestUpCmd(t *testing.T) { return fmt.Errorf("test error") } + // Enable DNS in config + if err := mocks.ConfigHandler.SetContextValue("dns.enabled", true); err != nil { + t.Fatalf("Failed to set dns.enabled: %v", err) + } + rootCmd.SetArgs([]string{"up"}) err := Execute(mocks.Controller) if err == nil { t.Error("Expected error, got nil") } + expectedError := "Error configuring DNS: test error" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } }) t.Run("ErrorStartingStack", func(t *testing.T) { @@ -342,11 +351,10 @@ func TestUpCmd(t *testing.T) { t.Run("ErrorNilNetworkManager", func(t *testing.T) { mocks := setupUpMocks(t) - vmDriver = "colima" - defer func() { vmDriver = "" }() mocks.Controller.ResolveNetworkManagerFunc = func() network.NetworkManager { return nil } + mocks.ConfigHandler.SetContextValue("vm.driver", "colima") rootCmd.SetArgs([]string{"up"}) err := Execute(mocks.Controller) diff --git a/pkg/config/yaml_config_handler.go b/pkg/config/yaml_config_handler.go index 29b40489f..d0e1fd0b2 100644 --- a/pkg/config/yaml_config_handler.go +++ b/pkg/config/yaml_config_handler.go @@ -2,9 +2,11 @@ package config import ( "fmt" + "math" "os" "path/filepath" "reflect" + "strconv" "strings" "github.com/windsorcli/cli/api/v1alpha1" @@ -224,7 +226,25 @@ func (y *YamlConfigHandler) Set(path string, value any) error { if path == "" { return nil } + pathKeys := parsePath(path) + if len(pathKeys) == 0 { + return fmt.Errorf("invalid path: %s", path) + } + + // If the value is a string, try to convert it based on the target type + if strValue, ok := value.(string); ok { + currentValue := y.Get(path) + if currentValue != nil { + targetType := reflect.TypeOf(currentValue) + convertedValue, err := convertValue(strValue, targetType) + if err != nil { + return fmt.Errorf("error converting value for %s: %w", path, err) + } + value = convertedValue + } + } + configValue := reflect.ValueOf(&y.config) return setValueByPath(configValue, pathKeys, value, path) } @@ -235,10 +255,19 @@ func (y *YamlConfigHandler) SetContextValue(path string, value any) error { return fmt.Errorf("path cannot be empty") } - // Ensure we have a current context - currentContext := y.GetContext() + // Initialize contexts map if it doesn't exist + if y.config.Contexts == nil { + y.config.Contexts = make(map[string]*v1alpha1.Context) + } - fullPath := fmt.Sprintf("contexts.%s.%s", currentContext, path) + // Get or create the current context + contextName := y.GetContext() + if y.config.Contexts[contextName] == nil { + y.config.Contexts[contextName] = &v1alpha1.Context{} + } + + // Use the generic Set method with the full context path + fullPath := fmt.Sprintf("contexts.%s.%s", contextName, path) return y.Set(fullPath, value) } @@ -324,7 +353,7 @@ func getValueByPath(current any, pathKeys []string) any { // getFieldByYamlTag retrieves a field from a struct by its YAML tag. func getFieldByYamlTag(v reflect.Value, tag string) reflect.Value { t := v.Type() - for i := 0; i < v.NumField(); i++ { + for i := range make([]struct{}, v.NumField()) { field := t.Field(i) yamlTag := strings.Split(field.Tag.Get("yaml"), ",")[0] if yamlTag == tag { @@ -353,6 +382,9 @@ func setValueByPath(currValue reflect.Value, pathKeys []string, value any, fullP switch currValue.Kind() { case reflect.Struct: fieldValue := getFieldByYamlTag(currValue, key) + if !fieldValue.IsValid() { + return fmt.Errorf("field not found: %s", key) + } if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { fieldValue.Set(reflect.New(fieldValue.Type().Elem())) @@ -434,6 +466,12 @@ func assignValue(fieldValue reflect.Value, value any) (reflect.Value, error) { newValue := reflect.New(elemType) val := reflect.ValueOf(value) + // If the value is already a pointer of the correct type, use it directly + if valueType.AssignableTo(fieldType) { + return val, nil + } + + // If the value is convertible to the element type, convert and wrap in pointer if val.Type().ConvertibleTo(elemType) { val = val.Convert(elemType) newValue.Elem().Set(val) @@ -443,14 +481,13 @@ func assignValue(fieldValue reflect.Value, value any) (reflect.Value, error) { return reflect.Value{}, fmt.Errorf("cannot assign value of type %s to field of type %s", valueType, fieldType) } + val := reflect.ValueOf(value) if valueType.AssignableTo(fieldType) { - val := reflect.ValueOf(value) return val, nil } if valueType.ConvertibleTo(fieldType) { - val := reflect.ValueOf(value).Convert(fieldType) - return val, nil + return val.Convert(fieldType), nil } return reflect.Value{}, fmt.Errorf("cannot assign value of type %s to field of type %s", valueType, fieldType) @@ -458,6 +495,9 @@ func assignValue(fieldValue reflect.Value, value any) (reflect.Value, error) { // makeAddressable ensures a value is addressable by creating a new pointer if necessary. func makeAddressable(v reflect.Value) reflect.Value { + if !v.IsValid() { + return v + } if v.CanAddr() { return v } @@ -502,3 +542,106 @@ func parsePath(path string) []string { return keys } + +// convertValue attempts to convert a string value to the appropriate type based on the target field's type +func convertValue(value string, targetType reflect.Type) (any, error) { + isPointer := targetType.Kind() == reflect.Ptr + if isPointer { + targetType = targetType.Elem() + } + + var convertedValue any + var err error + + switch targetType.Kind() { + case reflect.Bool: + convertedValue, err = strconv.ParseBool(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var v int64 + v, err = strconv.ParseInt(value, 10, 64) + if err == nil { + switch targetType.Kind() { + case reflect.Int: + if v < math.MinInt || v > math.MaxInt { + return nil, fmt.Errorf("integer overflow: %d is outside the range of int", v) + } + convertedValue = int(v) + case reflect.Int8: + if v < math.MinInt8 || v > math.MaxInt8 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of int8", v) + } + convertedValue = int8(v) + case reflect.Int16: + if v < math.MinInt16 || v > math.MaxInt16 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of int16", v) + } + convertedValue = int16(v) + case reflect.Int32: + if v < math.MinInt32 || v > math.MaxInt32 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of int32", v) + } + convertedValue = int32(v) + case reflect.Int64: + convertedValue = v + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + var v uint64 + v, err = strconv.ParseUint(value, 10, 64) + if err == nil { + switch targetType.Kind() { + case reflect.Uint: + if v > math.MaxUint { + return nil, fmt.Errorf("integer overflow: %d is outside the range of uint", v) + } + convertedValue = uint(v) + case reflect.Uint8: + if v > math.MaxUint8 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of uint8", v) + } + convertedValue = uint8(v) + case reflect.Uint16: + if v > math.MaxUint16 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of uint16", v) + } + convertedValue = uint16(v) + case reflect.Uint32: + if v > math.MaxUint32 { + return nil, fmt.Errorf("integer overflow: %d is outside the range of uint32", v) + } + convertedValue = uint32(v) + case reflect.Uint64: + convertedValue = v + } + } + case reflect.Float32, reflect.Float64: + var v float64 + v, err = strconv.ParseFloat(value, 64) + if err == nil { + if targetType.Kind() == reflect.Float32 { + if v < -math.MaxFloat32 || v > math.MaxFloat32 { + return nil, fmt.Errorf("float overflow: %f is outside the range of float32", v) + } + convertedValue = float32(v) + } else { + convertedValue = v + } + } + case reflect.String: + convertedValue = value + default: + return nil, fmt.Errorf("unsupported type conversion from string to %v", targetType) + } + + if err != nil { + return nil, err + } + + if isPointer { + ptr := reflect.New(targetType) + ptr.Elem().Set(reflect.ValueOf(convertedValue)) + return ptr.Interface(), nil + } + + return convertedValue, nil +} diff --git a/pkg/config/yaml_config_handler_test.go b/pkg/config/yaml_config_handler_test.go index 5ae1f2a10..ac2da447a 100644 --- a/pkg/config/yaml_config_handler_test.go +++ b/pkg/config/yaml_config_handler_test.go @@ -868,100 +868,685 @@ func TestYamlConfigHandler_GetConfig(t *testing.T) { setup := func(t *testing.T) (*YamlConfigHandler, *Mocks) { mocks := setupMocks(t) handler := NewYamlConfigHandler(mocks.Injector) - handler.shims = mocks.Shims if err := handler.Initialize(); err != nil { t.Fatalf("Failed to initialize handler: %v", err) } - return handler, mocks - } + handler.shims = mocks.Shims + return handler, mocks + } + + t.Run("EmptyContext", func(t *testing.T) { + // Given a handler with no context set + handler, _ := setup(t) + + // When getting the config + config := handler.GetConfig() + + // Then the default config should be returned + if config == nil { + t.Fatal("Expected default config, got nil") + } + }) + + t.Run("NonExistentContext", func(t *testing.T) { + // Given a handler with a non-existent context + handler, _ := setup(t) + handler.context = "nonexistent" + + // When getting the config + config := handler.GetConfig() + + // Then the default config should be returned + if config == nil { + t.Fatal("Expected default config, got nil") + } + }) + + t.Run("ExistingContext", func(t *testing.T) { + // Given a handler with an existing context + handler, _ := setup(t) + handler.context = "test" + + // And a context with environment variables + handler.config.Contexts = map[string]*v1alpha1.Context{ + "test": { + Environment: map[string]string{ + "TEST_VAR": "test_value", + }, + }, + } + + // And default context with different environment variables + handler.defaultContextConfig = v1alpha1.Context{ + Environment: map[string]string{ + "DEFAULT_VAR": "default_value", + }, + } + + // When getting the config + config := handler.GetConfig() + + // Then the merged config should be returned + if config == nil { + t.Fatal("Expected merged config, got nil") + } + + // And it should contain both environment variables + if config.Environment["TEST_VAR"] != "test_value" { + t.Errorf("Expected TEST_VAR to be 'test_value', got '%s'", config.Environment["TEST_VAR"]) + } + if config.Environment["DEFAULT_VAR"] != "default_value" { + t.Errorf("Expected DEFAULT_VAR to be 'default_value', got '%s'", config.Environment["DEFAULT_VAR"]) + } + }) + + t.Run("ContextOverridesDefault", func(t *testing.T) { + // Given a handler with an existing context + handler, _ := setup(t) + handler.context = "test" + + // And a context with environment variables that override defaults + handler.config.Contexts = map[string]*v1alpha1.Context{ + "test": { + Environment: map[string]string{ + "SHARED_VAR": "context_value", + }, + }, + } + + // And default context with the same environment variable + handler.defaultContextConfig = v1alpha1.Context{ + Environment: map[string]string{ + "SHARED_VAR": "default_value", + }, + } + + // When getting the config + config := handler.GetConfig() + + // Then the context value should override the default + if config.Environment["SHARED_VAR"] != "context_value" { + t.Errorf("Expected SHARED_VAR to be 'context_value', got '%s'", config.Environment["SHARED_VAR"]) + } + }) +} + +// TestGetValueByPath tests the getValueByPath function +func Test_getValueByPath(t *testing.T) { + t.Run("EmptyPathKeys", func(t *testing.T) { + // Given an empty pathKeys slice for value lookup + var current any + pathKeys := []string{} + + // When calling getValueByPath with empty pathKeys + value := getValueByPath(current, pathKeys) + + // Then nil should be returned as the path is invalid + if value != nil { + t.Errorf("Expected value to be nil, got %v", value) + } + }) + + t.Run("InvalidCurrentValue", func(t *testing.T) { + // Given a nil current value and a valid path key + var current any = nil + pathKeys := []string{"key"} + + // When calling getValueByPath with nil current value + value := getValueByPath(current, pathKeys) + + // Then nil should be returned as the current value is invalid + if value != nil { + t.Errorf("Expected value to be nil, got %v", value) + } + }) + + t.Run("MapKeyTypeMismatch", func(t *testing.T) { + // Given a map with int keys but attempting to access with a string key + current := map[int]string{1: "one", 2: "two"} + pathKeys := []string{"1"} + + // When calling getValueByPath with mismatched key type + value := getValueByPath(current, pathKeys) + + // Then nil should be returned due to key type mismatch + if value != nil { + t.Errorf("Expected value to be nil, got %v", value) + } + }) + + t.Run("MapSuccess", func(t *testing.T) { + // Given a map with a string key and corresponding value + current := map[string]string{"key": "testValue"} + pathKeys := []string{"key"} + + // When calling getValueByPath with a valid key + value := getValueByPath(current, pathKeys) + + // Then the corresponding value should be returned successfully + if value == nil { + t.Errorf("Expected value to be 'testValue', got nil") + } + expectedValue := "testValue" + if value != expectedValue { + t.Errorf("Expected value '%s', got '%v'", expectedValue, value) + } + }) + + t.Run("CannotSetField", func(t *testing.T) { + // Given a struct with an unexported field that cannot be set + type TestStruct struct { + unexportedField string `yaml:"unexportedfield"` + } + testStruct := &TestStruct{} + currValue := reflect.ValueOf(testStruct) + pathKeys := []string{"unexportedfield"} + value := "testValue" + fullPath := "unexportedfield" + + // When attempting to set a value on the unexported field + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then an error should be returned indicating the field cannot be set + expectedErr := "cannot set field" + if err == nil || err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + } + }) + + t.Run("RecursiveFailure", func(t *testing.T) { + // Given a nested map structure without the target field + level3Map := map[string]any{} + level2Map := map[string]any{"level3": level3Map} + level1Map := map[string]any{"level2": level2Map} + testMap := map[string]any{"level1": level1Map} + currValue := reflect.ValueOf(testMap) + pathKeys := []string{"level1", "level2", "nonexistentfield"} + value := "newValue" + fullPath := "level1.level2.nonexistentfield" + + // When attempting to set a value at a non-existent nested path + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then an error should be returned indicating the invalid path + expectedErr := "Invalid path: level1.level2.nonexistentfield" + if err == nil || err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + } + }) + + t.Run("AssignValueTypeMismatch", func(t *testing.T) { + // Given a struct with an int field that cannot accept a string slice + type TestStruct struct { + IntField int `yaml:"intfield"` + } + testStruct := &TestStruct{} + currValue := reflect.ValueOf(testStruct) + pathKeys := []string{"intfield"} + value := []string{"incompatibleType"} // A slice, which is incompatible with int + fullPath := "intfield" + + // When attempting to assign an incompatible value type + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then an error should be returned indicating the type mismatch + expectedErr := "cannot assign value of type []string to field of type int" + if err == nil || err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + } + }) + + t.Run("AssignPointerValueTypeMismatch", func(t *testing.T) { + // Given a struct with a pointer field that cannot accept a string slice + type TestStruct struct { + IntPtrField *int `yaml:"intptrfield"` + } + testStruct := &TestStruct{} + currValue := reflect.ValueOf(testStruct) + pathKeys := []string{"intptrfield"} + value := []string{"incompatibleType"} // A slice, which is incompatible with *int + fullPath := "intptrfield" + + // When attempting to assign an incompatible value type to a pointer field + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then an error should be returned indicating the pointer type mismatch + expectedErr := "cannot assign value of type []string to field of type *int" + if err == nil || err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + } + }) + + t.Run("AssignNonPointerField", func(t *testing.T) { + // Given a struct with a string field that can be directly assigned + type TestStruct struct { + StringField string `yaml:"stringfield"` + } + testStruct := &TestStruct{} + currValue := reflect.ValueOf(testStruct) + pathKeys := []string{"stringfield"} + value := "testValue" // Directly assignable to string + fullPath := "stringfield" + + // When assigning a compatible value to the field + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then the field should be set without error + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if testStruct.StringField != "testValue" { + t.Errorf("Expected StringField to be 'testValue', got '%v'", testStruct.StringField) + } + }) + + t.Run("AssignConvertibleType", func(t *testing.T) { + // Given a struct with an int field that can accept a convertible float value + type TestStruct struct { + IntField int `yaml:"intfield"` + } + testStruct := &TestStruct{} + currValue := reflect.ValueOf(testStruct) + pathKeys := []string{"intfield"} + value := 42.0 // A float64, which is convertible to int + fullPath := "intfield" + + // When assigning a value that can be converted to the field's type + err := setValueByPath(currValue, pathKeys, value, fullPath) + + // Then the field should be set without error + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if testStruct.IntField != 42 { + t.Errorf("Expected IntField to be 42, got '%v'", testStruct.IntField) + } + }) +} + +func Test_parsePath(t *testing.T) { + t.Run("EmptyPath", func(t *testing.T) { + // Given an empty path string to parse + path := "" + + // When calling parsePath with the empty string + pathKeys := parsePath(path) + + // Then an empty slice should be returned + if len(pathKeys) != 0 { + t.Errorf("Expected pathKeys to be empty, got %v", pathKeys) + } + }) + + t.Run("SingleKey", func(t *testing.T) { + // Given a path with a single key + path := "key" + + // When calling parsePath with a single key + pathKeys := parsePath(path) + + // Then a slice with only that key should be returned + expected := []string{"key"} + if !reflect.DeepEqual(pathKeys, expected) { + t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + } + }) + + t.Run("MultipleKeys", func(t *testing.T) { + // Given a path with multiple keys separated by dots + path := "key1.key2.key3" + + // When calling parsePath with dot notation + pathKeys := parsePath(path) + + // Then a slice containing all the keys should be returned + expected := []string{"key1", "key2", "key3"} + if !reflect.DeepEqual(pathKeys, expected) { + t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + } + }) + + t.Run("KeysWithBrackets", func(t *testing.T) { + // Given a path with keys using bracket notation + path := "key1[key2][key3]" + + // When calling parsePath with bracket notation + pathKeys := parsePath(path) + + // Then a slice containing all the keys without brackets should be returned + expected := []string{"key1", "key2", "key3"} + if !reflect.DeepEqual(pathKeys, expected) { + t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + } + }) + + t.Run("MixedDotAndBracketNotation", func(t *testing.T) { + // Given a path with mixed dot and bracket notation + path := "key1.key2[key3].key4[key5]" + + // When calling parsePath with mixed notation + pathKeys := parsePath(path) + + // Then a slice with all keys regardless of notation should be returned + expected := []string{"key1", "key2", "key3", "key4", "key5"} + if !reflect.DeepEqual(pathKeys, expected) { + t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + } + }) + + t.Run("DotInsideBrackets", func(t *testing.T) { + // Given a path with a dot inside bracket notation + path := "key1[key2.key3]" + + // When calling parsePath with a dot inside brackets + pathKeys := parsePath(path) + + // Then the dot inside brackets should be treated as part of the key + expected := []string{"key1", "key2.key3"} + if !reflect.DeepEqual(pathKeys, expected) { + t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + } + }) +} - t.Run("ContextIsSet", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.context = "local" - handler.config.Contexts = map[string]*v1alpha1.Context{ - "local": { - Environment: map[string]string{ - "ENV_VAR": "value", - }, - }, +func Test_assignValue(t *testing.T) { + t.Run("CannotSetField", func(t *testing.T) { + // Given an unexported field that cannot be set + var unexportedField struct { + unexported int } + fieldValue := reflect.ValueOf(&unexportedField).Elem().Field(0) - // When calling GetConfig - config := handler.GetConfig() + // When attempting to assign a value to it + _, err := assignValue(fieldValue, 10) - // Then the context config should be returned without error - if config == nil || config.Environment["ENV_VAR"] != "value" { - t.Errorf("Expected context config with ENV_VAR 'value', got %v", config) + // Then an error should be returned + if err == nil { + t.Errorf("Expected an error for non-settable field, got nil") + } + expectedError := "cannot set field" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) } }) - t.Run("EmptyContextString", func(t *testing.T) { - handler, mocks := setup(t) + t.Run("PointerTypeMismatchNonConvertible", func(t *testing.T) { + // Given a pointer field of type *int + var field *int + fieldValue := reflect.ValueOf(&field).Elem() - // Mock shell to return a project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil + // When attempting to assign a string value to it + value := "not an int" + _, err := assignValue(fieldValue, value) + + // Then an error should be returned indicating type mismatch + if err == nil { + t.Errorf("Expected an error for pointer type mismatch, got nil") + } + expectedError := "cannot assign value of type string to field of type *int" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) } + }) - // Mock shims to handle context file and env var - handler.shims.ReadFile = func(filename string) ([]byte, error) { - if filename == "/mock/project/root/.windsor/context" { - return []byte("local"), nil - } - return nil, fmt.Errorf("file not found") + t.Run("ValueTypeMismatchNonConvertible", func(t *testing.T) { + // Given a field of type int + var field int + fieldValue := reflect.ValueOf(&field).Elem() + + // When attempting to assign a non-convertible string value to it + value := "not convertible to int" + _, err := assignValue(fieldValue, value) + + // Then an error should be returned indicating type mismatch + if err == nil { + t.Errorf("Expected an error for non-convertible type mismatch, got nil") + } + expectedError := "cannot assign value of type string to field of type int" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) } - handler.shims.Getenv = func(key string) string { - return "" + }) +} + +func Test_convertValue(t *testing.T) { + t.Run("ConvertStringToBool", func(t *testing.T) { + // Given a string value that can be converted to bool + value := "true" + targetType := reflect.TypeOf(true) + + // When converting the value + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - handler.context = "" // Explicitly empty context - defaultConf := v1alpha1.Context{Environment: map[string]string{"DEFAULT": "yes"}} - if err := handler.SetDefault(defaultConf); err != nil { - t.Fatalf("Failed to set default config: %v", err) + // And the result should be a bool + if result != true { + t.Errorf("Expected true, got %v", result) } + }) - // When calling GetConfig - config := handler.GetConfig() + t.Run("ConvertStringToInt", func(t *testing.T) { + // Given a string value that can be converted to int + value := "42" + targetType := reflect.TypeOf(int(0)) - // Then the default config should be returned - if config == nil { - t.Fatal("Expected non-nil config") + // When converting the value + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - if config.Environment["DEFAULT"] != "yes" { - t.Errorf("Expected default config with DEFAULT='yes', got %v", config.Environment) + + // And the result should be an int + if result != 42 { + t.Errorf("Expected 42, got %v", result) } }) - t.Run("ContextNotInMap", func(t *testing.T) { - // Given a handler with a context set, but it's not in the config map - handler, _ := setup(t) - handler.context = "missing-context" - // Config map does *not* contain "missing-context" - handler.config.Contexts = map[string]*v1alpha1.Context{ - "existing-context": {Environment: map[string]string{"EXISTING": "val"}}, + t.Run("ConvertStringToFloat", func(t *testing.T) { + // Given a string value that can be converted to float + value := "3.14" + targetType := reflect.TypeOf(float64(0)) + + // When converting the value + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - defaultConf := v1alpha1.Context{Environment: map[string]string{"DEFAULT": "yes"}} - handler.SetDefault(defaultConf) - // When calling GetConfig - config := handler.GetConfig() + // And the result should be a float + if result != 3.14 { + t.Errorf("Expected 3.14, got %v", result) + } + }) - // Then the default config should be returned - if config == nil { - t.Fatalf("Expected default config, got nil") + t.Run("ConvertStringToPointer", func(t *testing.T) { + // Given a string value that can be converted to a pointer type + value := "42" + targetType := reflect.TypeOf((*int)(nil)) + + // When converting the value + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the result should be a pointer to int + if ptr, ok := result.(*int); !ok || *ptr != 42 { + t.Errorf("Expected *int(42), got %v", result) + } + }) + + t.Run("UnsupportedType", func(t *testing.T) { + // Given a string value and an unsupported target type + value := "test" + targetType := reflect.TypeOf([]string{}) + + // When converting the value + _, err := convertValue(value, targetType) + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for unsupported type") + } + + // And the error message should indicate the unsupported type + expectedErr := "unsupported type conversion from string to []string" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) + } + }) + + t.Run("InvalidNumericValue", func(t *testing.T) { + // Given an invalid numeric string value + value := "not a number" + targetType := reflect.TypeOf(int(0)) + + // When converting the value + _, err := convertValue(value, targetType) + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for invalid numeric value") + } + }) + + t.Run("UintTypes", func(t *testing.T) { + // Given a string value and uint target types + value := "42" + targetTypes := []reflect.Type{ + reflect.TypeOf(uint(0)), + reflect.TypeOf(uint8(0)), + reflect.TypeOf(uint16(0)), + reflect.TypeOf(uint32(0)), + reflect.TypeOf(uint64(0)), + } + + // When converting the value to each type + for _, targetType := range targetTypes { + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("convertValue() unexpected error for %v: %v", targetType, err) + } + + // And the value should be correctly converted + switch targetType.Kind() { + case reflect.Uint: + if result != uint(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, uint(42), targetType) + } + case reflect.Uint8: + if result != uint8(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, uint8(42), targetType) + } + case reflect.Uint16: + if result != uint16(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, uint16(42), targetType) + } + case reflect.Uint32: + if result != uint32(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, uint32(42), targetType) + } + case reflect.Uint64: + if result != uint64(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, uint64(42), targetType) + } + } + } + }) + + t.Run("IntTypes", func(t *testing.T) { + // Given a string value and int target types + value := "42" + targetTypes := []reflect.Type{ + reflect.TypeOf(int8(0)), + reflect.TypeOf(int16(0)), + reflect.TypeOf(int32(0)), + reflect.TypeOf(int64(0)), + } + + // When converting the value to each type + for _, targetType := range targetTypes { + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("convertValue() unexpected error for %v: %v", targetType, err) + } + + // And the value should be correctly converted + switch targetType.Kind() { + case reflect.Int8: + if result != int8(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, int8(42), targetType) + } + case reflect.Int16: + if result != int16(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, int16(42), targetType) + } + case reflect.Int32: + if result != int32(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, int32(42), targetType) + } + case reflect.Int64: + if result != int64(42) { + t.Errorf("convertValue() = %v, want %v for %v", result, int64(42), targetType) + } + } + } + }) + + t.Run("Float32", func(t *testing.T) { + // Given a string value and float32 target type + value := "3.14" + targetType := reflect.TypeOf(float32(0)) + + // When converting the value + result, err := convertValue(value, targetType) + + // Then no error should be returned + if err != nil { + t.Fatalf("convertValue() unexpected error: %v", err) } - if config.Environment["DEFAULT"] != "yes" { - t.Errorf("Expected default config environment, got %v", config.Environment) + + // And the value should be correctly converted + if result != float32(3.14) { + t.Errorf("convertValue() = %v, want %v", result, float32(3.14)) + } + }) + + t.Run("StringToFloatOverflow", func(t *testing.T) { + // Given a string value that would overflow float32 + value := "3.4028236e+38" + targetType := reflect.TypeOf(float32(0)) + + // When converting the value + _, err := convertValue(value, targetType) + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for float overflow") } - // Ensure it didn't accidentally return the existing context's data - if _, exists := config.Environment["EXISTING"]; exists { - t.Errorf("Returned config contains data from an unrelated existing context") + + // And the error message should indicate overflow + if !strings.Contains(err.Error(), "float overflow") { + t.Errorf("Expected error containing 'float overflow', got '%s'", err.Error()) } }) } -func TestYamlConfigHandler_Set(t *testing.T) { +func TestYamlConfigHandler_SetDefault(t *testing.T) { setup := func(t *testing.T) (*YamlConfigHandler, *Mocks) { mocks := setupMocks(t) handler := NewYamlConfigHandler(mocks.Injector) @@ -972,30 +1557,87 @@ func TestYamlConfigHandler_Set(t *testing.T) { return handler, mocks } - t.Run("Success", func(t *testing.T) { + t.Run("SetDefaultWithExistingContext", func(t *testing.T) { // Given a handler with a context set handler, _ := setup(t) + defaultContext := v1alpha1.Context{ + Environment: map[string]string{ + "ENV_VAR": "value", + }, + } + + // And a context is set + handler.Set("context", "local") - // When setting a value in the configuration - handler.Set("contexts.default.environment.TEST_VAR", "test_value") + // When setting the default context + err := handler.SetDefault(defaultContext) - // Then the value should be correctly set - expected := "test_value" - if val := handler.config.Contexts["default"].Environment["TEST_VAR"]; val != expected { - t.Errorf("Set() did not correctly set value, expected %s, got %s", expected, val) + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the default context should be set correctly + if handler.defaultContextConfig.Environment["ENV_VAR"] != "value" { + t.Errorf("SetDefault() = %v, expected %v", handler.defaultContextConfig.Environment["ENV_VAR"], "value") } }) - t.Run("InvalidPath", func(t *testing.T) { - // Given a handler with a context set + t.Run("SetDefaultWithNoContext", func(t *testing.T) { + // Given a handler with no context set handler, _ := setup(t) + handler.context = "" + defaultContext := v1alpha1.Context{ + Environment: map[string]string{ + "ENV_VAR": "value", + }, + } - // When attempting to set a value with an empty path - err := handler.Set("", "test_value") + // When setting the default context + err := handler.SetDefault(defaultContext) + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the default context should be set correctly + if handler.defaultContextConfig.Environment["ENV_VAR"] != "value" { + t.Errorf("SetDefault() = %v, expected %v", handler.defaultContextConfig.Environment["ENV_VAR"], "value") + } + }) + + t.Run("SetDefaultUsedInSubsequentOperations", func(t *testing.T) { + // Given a handler with an existing context + handler, _ := setup(t) + handler.context = "existing-context" + handler.config.Contexts = map[string]*v1alpha1.Context{ + "existing-context": {ProjectName: ptrString("initial-project")}, + } + + // And a default context configuration + defaultConf := v1alpha1.Context{ + Environment: map[string]string{"DEFAULT_VAR": "default_val"}, + } + + // When setting the default context + err := handler.SetDefault(defaultConf) - // Then no error should be returned for empty path + // Then no error should be returned if err != nil { - t.Errorf("Set() with empty path did not behave as expected, got error: %v", err) + t.Fatalf("SetDefault() unexpected error: %v", err) + } + + // And the default context should be set correctly + if handler.defaultContextConfig.Environment == nil || handler.defaultContextConfig.Environment["DEFAULT_VAR"] != "default_val" { + t.Errorf("Expected defaultContextConfig environment to be %v, got %v", defaultConf.Environment, handler.defaultContextConfig.Environment) + } + + // And the existing context should not be modified + if handler.config.Contexts["existing-context"] == nil || + handler.config.Contexts["existing-context"].ProjectName == nil || + *handler.config.Contexts["existing-context"].ProjectName != "initial-project" { + t.Errorf("SetDefault incorrectly overwrote existing context config. Expected ProjectName 'initial-project', got %v", handler.config.Contexts["existing-context"].ProjectName) } }) } @@ -1019,9 +1661,7 @@ func TestYamlConfigHandler_SetContextValue(t *testing.T) { // And a context with an empty environment map actualContext := handler.GetContext() handler.config.Contexts = map[string]*v1alpha1.Context{ - actualContext: { - Environment: map[string]string{}, - }, + actualContext: {}, } // When setting a value in the context environment @@ -1070,101 +1710,74 @@ func TestYamlConfigHandler_SetContextValue(t *testing.T) { if err == nil { t.Errorf("SetContextValue() with invalid path did not return an error") } - }) -} - -func TestYamlConfigHandler_SetDefault(t *testing.T) { - setup := func(t *testing.T) (*YamlConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewYamlConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.shims = mocks.Shims - return handler, mocks - } + }) - t.Run("SetDefaultWithExistingContext", func(t *testing.T) { - // Given a handler with a context set + t.Run("ConvertStringToBool", func(t *testing.T) { handler, _ := setup(t) - defaultContext := v1alpha1.Context{ - Environment: map[string]string{ - "ENV_VAR": "value", - }, + handler.context = "default" + handler.config.Contexts = map[string]*v1alpha1.Context{ + "default": {}, } - // And a context is set - handler.Set("context", "local") - - // When setting the default context - err := handler.SetDefault(defaultContext) + // Set initial bool value + if err := handler.SetContextValue("environment.BOOL_VAR", "true"); err != nil { + t.Fatalf("Failed to set initial bool value: %v", err) + } - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) + // Override with string "false" + if err := handler.SetContextValue("environment.BOOL_VAR", "false"); err != nil { + t.Fatalf("Failed to set string bool value: %v", err) } - // And the default context should be set correctly - if handler.defaultContextConfig.Environment["ENV_VAR"] != "value" { - t.Errorf("SetDefault() = %v, expected %v", handler.defaultContextConfig.Environment["ENV_VAR"], "value") + val := handler.GetString("environment.BOOL_VAR") + if val != "false" { + t.Errorf("Expected false, got %v", val) } }) - t.Run("SetDefaultWithNoContext", func(t *testing.T) { - // Given a handler with no context set + t.Run("ConvertStringToInt", func(t *testing.T) { handler, _ := setup(t) - handler.context = "" - defaultContext := v1alpha1.Context{ - Environment: map[string]string{ - "ENV_VAR": "value", - }, + handler.context = "default" + handler.config.Contexts = map[string]*v1alpha1.Context{ + "default": {}, } - // When setting the default context - err := handler.SetDefault(defaultContext) + // Set initial int value + if err := handler.SetContextValue("environment.INT_VAR", "42"); err != nil { + t.Fatalf("Failed to set initial int value: %v", err) + } - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) + // Override with string "100" + if err := handler.SetContextValue("environment.INT_VAR", "100"); err != nil { + t.Fatalf("Failed to set string int value: %v", err) } - // And the default context should be set correctly - if handler.defaultContextConfig.Environment["ENV_VAR"] != "value" { - t.Errorf("SetDefault() = %v, expected %v", handler.defaultContextConfig.Environment["ENV_VAR"], "value") + val := handler.GetString("environment.INT_VAR") + if val != "100" { + t.Errorf("Expected 100, got %v", val) } }) - t.Run("SetDefaultUsedInSubsequentOperations", func(t *testing.T) { - // Given a handler with an existing context + t.Run("ConvertStringToFloat", func(t *testing.T) { handler, _ := setup(t) - handler.context = "existing-context" + handler.context = "default" handler.config.Contexts = map[string]*v1alpha1.Context{ - "existing-context": {ProjectName: ptrString("initial-project")}, - } - - // And a default context configuration - defaultConf := v1alpha1.Context{ - Environment: map[string]string{"DEFAULT_VAR": "default_val"}, + "default": {}, } - // When setting the default context - err := handler.SetDefault(defaultConf) - - // Then no error should be returned - if err != nil { - t.Fatalf("SetDefault() unexpected error: %v", err) + // Set initial float value + if err := handler.SetContextValue("environment.FLOAT_VAR", "3.14"); err != nil { + t.Fatalf("Failed to set initial float value: %v", err) } - // And the default context should be set correctly - if handler.defaultContextConfig.Environment == nil || handler.defaultContextConfig.Environment["DEFAULT_VAR"] != "default_val" { - t.Errorf("Expected defaultContextConfig environment to be %v, got %v", defaultConf.Environment, handler.defaultContextConfig.Environment) + // Override with string "6.28" + if err := handler.SetContextValue("environment.FLOAT_VAR", "6.28"); err != nil { + t.Fatalf("Failed to set string float value: %v", err) } - // And the existing context should not be modified - if handler.config.Contexts["existing-context"] == nil || - handler.config.Contexts["existing-context"].ProjectName == nil || - *handler.config.Contexts["existing-context"].ProjectName != "initial-project" { - t.Errorf("SetDefault incorrectly overwrote existing context config. Expected ProjectName 'initial-project', got %v", handler.config.Contexts["existing-context"].ProjectName) + val := handler.GetString("environment.FLOAT_VAR") + if val != "6.28" { + t.Errorf("Expected 6.28, got %v", val) } }) } @@ -1267,544 +1880,520 @@ contexts: }) } -// ============================================================================= -// Helper Functions -// ============================================================================= - -func Test_setValueByPath(t *testing.T) { - t.Run("EmptyPathKeys", func(t *testing.T) { - // Given an empty pathKeys slice - var currValue reflect.Value - pathKeys := []string{} - value := "test" - - // When calling setValueByPath - fullPath := "some.full.path" - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // Then an error should be returned - if err == nil || err.Error() != "pathKeys cannot be empty" { - t.Errorf("Expected error 'pathKeys cannot be empty', got %v", err) - } - }) +func Test_makeAddressable(t *testing.T) { + t.Run("AlreadyAddressable", func(t *testing.T) { + // Given an addressable value + var x int = 42 + v := reflect.ValueOf(&x).Elem() - t.Run("UnsupportedKind", func(t *testing.T) { - // Given a currValue of unsupported kind (e.g., int) - currValue := reflect.ValueOf(42) - pathKeys := []string{"key"} - value := "test" - fullPath := "some.full.path" - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When making it addressable + result := makeAddressable(v) - // Then an error should be returned - expectedErr := "Invalid path: some.full.path" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got %v", expectedErr, err) + // Then the same value should be returned + if result.Interface() != v.Interface() { + t.Errorf("makeAddressable() = %v, want %v", result.Interface(), v.Interface()) } }) - t.Run("MapKeyTypeMismatch", func(t *testing.T) { - // Given a map with int keys but providing a string key - currValue := reflect.ValueOf(make(map[int]string)) - pathKeys := []string{"stringKey"} - value := "testValue" - fullPath := "some.full.path" + t.Run("NonAddressable", func(t *testing.T) { + // Given a non-addressable value + v := reflect.ValueOf(42) - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When making it addressable + result := makeAddressable(v) - // Then an error should be returned - expectedErr := "key type mismatch: expected int, got string" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got %v", expectedErr, err) + // Then a new addressable value should be returned + if !result.CanAddr() { + t.Error("makeAddressable() returned non-addressable value") } - }) - - t.Run("MapValueTypeMismatch", func(t *testing.T) { - // Given a map with string values but providing a non-convertible value - currValue := reflect.ValueOf(make(map[string]int)) - pathKeys := []string{"key"} - value := "stringValue" // Cannot convert string to int - fullPath := "some.full.path" - - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned - expectedErr := "value type mismatch for key key: expected int, got string" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got %v", expectedErr, err) + if result.Interface() != v.Interface() { + t.Errorf("makeAddressable() = %v, want %v", result.Interface(), v.Interface()) } }) - t.Run("MapSuccess", func(t *testing.T) { - // Given a map with string keys and any values - testMap := make(map[string]any) - currValue := reflect.ValueOf(testMap) - pathKeys := []string{"key"} - value := "testValue" - fullPath := "some.full.path" + t.Run("NilValue", func(t *testing.T) { + // Given a nil value + var v reflect.Value - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When making it addressable + result := makeAddressable(v) - // Then the map should be updated without error - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if testMap["key"] != "testValue" { - t.Errorf("Expected map['key'] to be 'testValue', got '%v'", testMap["key"]) + // Then a zero value should be returned + if result.IsValid() { + t.Error("makeAddressable() returned valid value for nil input") } }) +} - t.Run("MapInitializeNilMap", func(t *testing.T) { - // Given a nil map - var testMap map[string]any - currValue := reflect.ValueOf(&testMap).Elem() - pathKeys := []string{"key"} - value := "testValue" - fullPath := "some.full.path" +func TestYamlConfigHandler_ConvertValue(t *testing.T) { + t.Run("StringToString", func(t *testing.T) { + // Given a string value and target type + value := "test" + targetType := reflect.TypeOf("") - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When converting the value + result, err := convertValue(value, targetType) - // Then the map should be initialized and updated without error + // Then no error should be returned if err != nil { - t.Fatalf("Unexpected error: %v", err) + t.Fatalf("convertValue() unexpected error: %v", err) } - if testMap["key"] != "testValue" { - t.Errorf("Expected map['key'] to be 'testValue', got '%v'", testMap["key"]) + + // And the value should be correctly converted + if result != "test" { + t.Errorf("convertValue() = %v, want %v", result, "test") } }) - t.Run("MapExistingValue", func(t *testing.T) { - // Given a Config with an existing nested map - config := v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "level1": { - Environment: map[string]string{ - "level2": "value2", - }, - AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("http://aws.test:4566"), - }, - }, - }, - } - currValue := reflect.ValueOf(&config).Elem() - pathKeys := []string{"contexts", "level1", "environment", "level2"} - value := "testValue" - fullPath := "contexts.level1.environment.level2" + t.Run("StringToInt", func(t *testing.T) { + // Given a string value and target type + value := "42" + targetType := reflect.TypeOf(0) - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When converting the value + result, err := convertValue(value, targetType) - // Then the existing nested map should be updated without error + // Then no error should be returned if err != nil { - t.Fatalf("Unexpected error: %v", err) + t.Fatalf("convertValue() unexpected error: %v", err) } - gotValue := config.Contexts["level1"].Environment["level2"] - if gotValue != "testValue" { - t.Errorf("Expected value to be 'testValue', got '%v'", gotValue) + // And the value should be correctly converted + if result != 42 { + t.Errorf("convertValue() = %v, want %v", result, 42) } }) - t.Run("MapValueConversion", func(t *testing.T) { - // Given a map with integer elements - testMap := map[string]int{} - currValue := reflect.ValueOf(testMap) - pathKeys := []string{"key"} // Key in the map - value := 42.0 // Float value that is convertible to int - fullPath := "some.full.path" + t.Run("StringToIntOverflow", func(t *testing.T) { + // Given a string value that would overflow int8 + value := "128" + targetType := reflect.TypeOf(int8(0)) - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When converting the value + _, err := convertValue(value, targetType) - // Then the value should be converted and set without error - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if testMap["key"] != 42 { - t.Errorf("Expected map['key'] to be 42, got '%v'", testMap["key"]) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for integer overflow") } - }) - - t.Run("RecursiveMap", func(t *testing.T) { - // Given a map with nested maps - level3Map := map[string]any{} - level2Map := map[string]any{"level3": level3Map} - level1Map := map[string]any{"level2": level2Map} - testMap := map[string]any{"docker": level1Map} // Valid first key - currValue := reflect.ValueOf(testMap) - pathKeys := []string{"docker", "level2", "nonexistentfield"} - value := "newValue" - fullPath := "docker.level2.nonexistentfield" - - // When calling setValueByPath - err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned indicating the field was not found - expectedErr := "Invalid path: docker.level2.nonexistentfield" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got %v", expectedErr, err) + // And the error message should indicate overflow + expectedErr := "integer overflow: 128 is outside the range of int8" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("MakeAddressable_WithAddressableValue", func(t *testing.T) { - // Given an addressable reflect.Value - var x int = 42 - v := reflect.ValueOf(&x).Elem() + t.Run("StringToUintOverflow", func(t *testing.T) { + // Given a string value that would overflow uint8 + value := "256" + targetType := reflect.TypeOf(uint8(0)) - // When ensuring that v is addressable - if !v.CanAddr() { - t.Fatal("Expected v to be addressable") - } + // When converting the value + _, err := convertValue(value, targetType) - // When calling makeAddressable - result := makeAddressable(v) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for integer overflow") + } - // Then the original value should be returned - if result.Interface() != v.Interface() { - t.Errorf("Expected the same value to be returned") + // And the error message should indicate overflow + expectedErr := "integer overflow: 256 is outside the range of uint8" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) -} -// TestGetValueByPath tests the getValueByPath function -func Test_getValueByPath(t *testing.T) { - t.Run("EmptyPathKeys", func(t *testing.T) { - // Given an empty pathKeys slice for value lookup - var current any - pathKeys := []string{} + t.Run("StringToFloatOverflow", func(t *testing.T) { + // Given a string value that would overflow float32 + value := "3.4028236e+38" + targetType := reflect.TypeOf(float32(0)) - // When calling getValueByPath with empty pathKeys - value := getValueByPath(current, pathKeys) + // When converting the value + _, err := convertValue(value, targetType) - // Then nil should be returned as the path is invalid - if value != nil { - t.Errorf("Expected value to be nil, got %v", value) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for float overflow") } - }) - - t.Run("InvalidCurrentValue", func(t *testing.T) { - // Given a nil current value and a valid path key - var current any = nil - pathKeys := []string{"key"} - - // When calling getValueByPath with nil current value - value := getValueByPath(current, pathKeys) - // Then nil should be returned as the current value is invalid - if value != nil { - t.Errorf("Expected value to be nil, got %v", value) + // And the error message should indicate overflow + if !strings.Contains(err.Error(), "float overflow") { + t.Errorf("Expected error containing 'float overflow', got '%s'", err.Error()) } }) - t.Run("MapKeyTypeMismatch", func(t *testing.T) { - // Given a map with int keys but attempting to access with a string key - current := map[int]string{1: "one", 2: "two"} - pathKeys := []string{"1"} + t.Run("StringToFloat", func(t *testing.T) { + // Given a string value and target type + value := "3.14" + targetType := reflect.TypeOf(float64(0)) - // When calling getValueByPath with mismatched key type - value := getValueByPath(current, pathKeys) + // When converting the value + result, err := convertValue(value, targetType) - // Then nil should be returned due to key type mismatch - if value != nil { - t.Errorf("Expected value to be nil, got %v", value) + // Then no error should be returned + if err != nil { + t.Fatalf("convertValue() unexpected error: %v", err) + } + + // And the value should be correctly converted + if result != 3.14 { + t.Errorf("convertValue() = %v, want %v", result, 3.14) } }) - t.Run("MapSuccess", func(t *testing.T) { - // Given a map with a string key and corresponding value - current := map[string]string{"key": "testValue"} - pathKeys := []string{"key"} + t.Run("StringToBool", func(t *testing.T) { + // Given a string value and target type + value := "true" + targetType := reflect.TypeOf(true) - // When calling getValueByPath with a valid key - value := getValueByPath(current, pathKeys) + // When converting the value + result, err := convertValue(value, targetType) - // Then the corresponding value should be returned successfully - if value == nil { - t.Errorf("Expected value to be 'testValue', got nil") - } - expectedValue := "testValue" - if value != expectedValue { - t.Errorf("Expected value '%s', got '%v'", expectedValue, value) + // Then no error should be returned + if err != nil { + t.Fatalf("convertValue() unexpected error: %v", err) } - }) - t.Run("CannotSetField", func(t *testing.T) { - // Given a struct with an unexported field that cannot be set - type TestStruct struct { - unexportedField string `yaml:"unexportedfield"` + // And the value should be correctly converted + if result != true { + t.Errorf("convertValue() = %v, want %v", result, true) } - testStruct := &TestStruct{} - currValue := reflect.ValueOf(testStruct) - pathKeys := []string{"unexportedfield"} - value := "testValue" - fullPath := "unexportedfield" - - // When attempting to set a value on the unexported field - err := setValueByPath(currValue, pathKeys, value, fullPath) + }) +} - // Then an error should be returned indicating the field cannot be set - expectedErr := "cannot set field" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got '%v'", expectedErr, err) +func TestYamlConfigHandler_Set(t *testing.T) { + setup := func(t *testing.T) (*YamlConfigHandler, *Mocks) { + mocks := setupMocks(t) + handler := NewYamlConfigHandler(mocks.Injector) + handler.shims = mocks.Shims + if err := handler.Initialize(); err != nil { + t.Fatalf("Failed to initialize handler: %v", err) } - }) + return handler, mocks + } - t.Run("RecursiveFailure", func(t *testing.T) { - // Given a nested map structure without the target field - level3Map := map[string]any{} - level2Map := map[string]any{"level3": level3Map} - level1Map := map[string]any{"level2": level2Map} - testMap := map[string]any{"level1": level1Map} - currValue := reflect.ValueOf(testMap) - pathKeys := []string{"level1", "level2", "nonexistentfield"} - value := "newValue" - fullPath := "level1.level2.nonexistentfield" + t.Run("InvalidPath", func(t *testing.T) { + // Given a handler with a context set + handler, _ := setup(t) - // When attempting to set a value at a non-existent nested path - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When setting a value with an invalid path + err := handler.Set("", "value") - // Then an error should be returned indicating the invalid path - expectedErr := "Invalid path: level1.level2.nonexistentfield" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + // Then no error should be returned + if err != nil { + t.Fatalf("Set() unexpected error: %v", err) } }) - t.Run("AssignValueTypeMismatch", func(t *testing.T) { - // Given a struct with an int field that cannot accept a string slice - type TestStruct struct { - IntField int `yaml:"intfield"` + t.Run("SetValueByPathError", func(t *testing.T) { + // Given a handler with a context set + handler, _ := setup(t) + + // And a mocked setValueByPath that returns an error + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, fmt.Errorf("mocked error") } - testStruct := &TestStruct{} - currValue := reflect.ValueOf(testStruct) - pathKeys := []string{"intfield"} - value := []string{"incompatibleType"} // A slice, which is incompatible with int - fullPath := "intfield" - // When attempting to assign an incompatible value type - err := setValueByPath(currValue, pathKeys, value, fullPath) + // When setting a value + err := handler.Set("test.path", "value") - // Then an error should be returned indicating the type mismatch - expectedErr := "cannot assign value of type []string to field of type int" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + // Then an error should be returned + if err == nil { + t.Fatal("Set() expected error, got nil") } }) +} - t.Run("AssignPointerValueTypeMismatch", func(t *testing.T) { - // Given a struct with a pointer field that cannot accept a string slice - type TestStruct struct { - IntPtrField *int `yaml:"intptrfield"` - } - testStruct := &TestStruct{} - currValue := reflect.ValueOf(testStruct) - pathKeys := []string{"intptrfield"} - value := []string{"incompatibleType"} // A slice, which is incompatible with *int - fullPath := "intptrfield" +func Test_setValueByPath(t *testing.T) { + t.Run("EmptyPathKeys", func(t *testing.T) { + // Given empty pathKeys + currValue := reflect.ValueOf(struct{}{}) + pathKeys := []string{} + value := "test" + fullPath := "test.path" - // When attempting to assign an incompatible value type to a pointer field + // When calling setValueByPath with empty pathKeys err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned indicating the pointer type mismatch - expectedErr := "cannot assign value of type []string to field of type *int" - if err == nil || err.Error() != expectedErr { - t.Errorf("Expected error '%s', got '%v'", expectedErr, err) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for empty pathKeys") + } + expectedErr := "pathKeys cannot be empty" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("AssignNonPointerField", func(t *testing.T) { - // Given a struct with a string field that can be directly assigned + t.Run("StructFieldNotFound", func(t *testing.T) { + // Given a struct and a non-existent field type TestStruct struct { - StringField string `yaml:"stringfield"` + Field string `yaml:"field"` } - testStruct := &TestStruct{} - currValue := reflect.ValueOf(testStruct) - pathKeys := []string{"stringfield"} - value := "testValue" // Directly assignable to string - fullPath := "stringfield" + currValue := reflect.ValueOf(&TestStruct{}).Elem() + pathKeys := []string{"nonexistent"} + value := "test" + fullPath := "nonexistent" - // When assigning a compatible value to the field + // When calling setValueByPath with non-existent field err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then the field should be set without error - if err != nil { - t.Fatalf("Unexpected error: %v", err) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for non-existent field") } - if testStruct.StringField != "testValue" { - t.Errorf("Expected StringField to be 'testValue', got '%v'", testStruct.StringField) + expectedErr := "field not found: nonexistent" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("AssignConvertibleType", func(t *testing.T) { - // Given a struct with an int field that can accept a convertible float value + t.Run("StructFieldSuccess", func(t *testing.T) { + // Given a struct with a field type TestStruct struct { - IntField int `yaml:"intfield"` + Field string `yaml:"field"` } - testStruct := &TestStruct{} - currValue := reflect.ValueOf(testStruct) - pathKeys := []string{"intfield"} - value := 42.0 // A float64, which is convertible to int - fullPath := "intfield" + currValue := reflect.ValueOf(&TestStruct{}).Elem() + pathKeys := []string{"field"} + value := "test" + fullPath := "field" - // When assigning a value that can be converted to the field's type + // When calling setValueByPath with valid field err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then the field should be set without error + // Then no error should be returned if err != nil { t.Fatalf("Unexpected error: %v", err) } - if testStruct.IntField != 42 { - t.Errorf("Expected IntField to be 42, got '%v'", testStruct.IntField) + + // And the field should be set correctly + if currValue.Field(0).String() != "test" { + t.Errorf("Expected field value 'test', got '%s'", currValue.Field(0).String()) } }) -} -func Test_parsePath(t *testing.T) { - t.Run("EmptyPath", func(t *testing.T) { - // Given an empty path string to parse - path := "" + t.Run("MapKeyTypeMismatch", func(t *testing.T) { + // Given a map with int keys but trying to set with string key + currValue := reflect.ValueOf(&map[int]string{}).Elem() + pathKeys := []string{"key"} + value := "test" + fullPath := "key" - // When calling parsePath with the empty string - pathKeys := parsePath(path) + // When calling setValueByPath with mismatched key type + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an empty slice should be returned - if len(pathKeys) != 0 { - t.Errorf("Expected pathKeys to be empty, got %v", pathKeys) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for key type mismatch") + } + expectedErr := "key type mismatch: expected int, got string" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("SingleKey", func(t *testing.T) { - // Given a path with a single key - path := "key" + t.Run("MapValueTypeMismatch", func(t *testing.T) { + // Given a map with string values but trying to set with a non-convertible type + currValue := reflect.ValueOf(&map[string]string{}).Elem() + pathKeys := []string{"key"} + value := struct{}{} // Use a struct{} which cannot be converted to string + fullPath := "key" - // When calling parsePath with a single key - pathKeys := parsePath(path) + // When calling setValueByPath with mismatched value type + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then a slice with only that key should be returned - expected := []string{"key"} - if !reflect.DeepEqual(pathKeys, expected) { - t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for value type mismatch") + } + expectedErr := "value type mismatch for key key: expected string, got struct {}" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("MultipleKeys", func(t *testing.T) { - // Given a path with multiple keys separated by dots - path := "key1.key2.key3" + t.Run("MapSuccess", func(t *testing.T) { + // Given a map with string keys and values + currValue := reflect.ValueOf(&map[string]string{}).Elem() + pathKeys := []string{"key"} + value := "test" + fullPath := "key" - // When calling parsePath with dot notation - pathKeys := parsePath(path) + // When calling setValueByPath with valid key and value + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then a slice containing all the keys should be returned - expected := []string{"key1", "key2", "key3"} - if !reflect.DeepEqual(pathKeys, expected) { - t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the value should be set correctly + if currValue.MapIndex(reflect.ValueOf("key")).String() != "test" { + t.Errorf("Expected map value 'test', got '%s'", currValue.MapIndex(reflect.ValueOf("key")).String()) } }) - t.Run("KeysWithBrackets", func(t *testing.T) { - // Given a path with keys using bracket notation - path := "key1[key2][key3]" + t.Run("InvalidPath", func(t *testing.T) { + // Given an invalid path type + currValue := reflect.ValueOf(42) + pathKeys := []string{"key"} + value := "test" + fullPath := "key" - // When calling parsePath with bracket notation - pathKeys := parsePath(path) + // When calling setValueByPath with invalid path type + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then a slice containing all the keys without brackets should be returned - expected := []string{"key1", "key2", "key3"} - if !reflect.DeepEqual(pathKeys, expected) { - t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + // Then an error should be returned + if err == nil { + t.Fatal("Expected error for invalid path") + } + expectedErr := "Invalid path: key" + if err.Error() != expectedErr { + t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) } }) - t.Run("MixedDotAndBracketNotation", func(t *testing.T) { - // Given a path with mixed dot and bracket notation - path := "key1.key2[key3].key4[key5]" + t.Run("NestedStruct", func(t *testing.T) { + // Given a nested struct + type InnerStruct struct { + Field string `yaml:"field"` + } + type OuterStruct struct { + Inner InnerStruct `yaml:"inner"` + } + currValue := reflect.ValueOf(&OuterStruct{}).Elem() + pathKeys := []string{"inner", "field"} + value := "test" + fullPath := "inner.field" - // When calling parsePath with mixed notation - pathKeys := parsePath(path) + // When calling setValueByPath with nested path + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then a slice with all keys regardless of notation should be returned - expected := []string{"key1", "key2", "key3", "key4", "key5"} - if !reflect.DeepEqual(pathKeys, expected) { - t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the nested field should be set correctly + inner := currValue.Field(0) + if inner.Field(0).String() != "test" { + t.Errorf("Expected nested field value 'test', got '%s'", inner.Field(0).String()) } }) - t.Run("DotInsideBrackets", func(t *testing.T) { - // Given a path with a dot inside bracket notation - path := "key1[key2.key3]" + t.Run("NestedMap", func(t *testing.T) { + // Given a nested map + currValue := reflect.ValueOf(&map[string]map[string]string{}).Elem() + pathKeys := []string{"outer", "inner"} + value := "test" + fullPath := "outer.inner" - // When calling parsePath with a dot inside brackets - pathKeys := parsePath(path) + // When calling setValueByPath with nested path + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then the dot inside brackets should be treated as part of the key - expected := []string{"key1", "key2.key3"} - if !reflect.DeepEqual(pathKeys, expected) { - t.Errorf("Expected pathKeys to be %v, got %v", expected, pathKeys) + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And the nested value should be set correctly + outer := currValue.MapIndex(reflect.ValueOf("outer")) + if !outer.IsValid() { + t.Fatal("Expected outer map to exist") + } + inner := outer.MapIndex(reflect.ValueOf("inner")) + if !inner.IsValid() { + t.Fatal("Expected inner map to exist") + } + if inner.String() != "test" { + t.Errorf("Expected nested value 'test', got '%s'", inner.String()) } }) -} -func Test_assignValue(t *testing.T) { - t.Run("CannotSetField", func(t *testing.T) { - // Given an unexported field that cannot be set - var unexportedField struct { - unexported int + t.Run("PointerField", func(t *testing.T) { + // Given a struct with a pointer field + type TestStruct struct { + Field *string `yaml:"field"` } - fieldValue := reflect.ValueOf(&unexportedField).Elem().Field(0) + currValue := reflect.ValueOf(&TestStruct{}).Elem() + pathKeys := []string{"field"} + value := "test" + fullPath := "field" - // When attempting to assign a value to it - _, err := assignValue(fieldValue, 10) + // When calling setValueByPath with pointer field + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned - if err == nil { - t.Errorf("Expected an error for non-settable field, got nil") + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - expectedError := "cannot set field" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + + // And the pointer field should be set correctly + field := currValue.Field(0) + if field.IsNil() { + t.Fatal("Expected pointer field to be non-nil") + } + if field.Elem().String() != "test" { + t.Errorf("Expected pointer field value 'test', got '%s'", field.Elem().String()) } }) - t.Run("PointerTypeMismatchNonConvertible", func(t *testing.T) { - // Given a pointer field of type *int - var field *int - fieldValue := reflect.ValueOf(&field).Elem() + t.Run("PointerMap", func(t *testing.T) { + // Given a map with pointer values + currValue := reflect.ValueOf(&map[string]*string{}).Elem() + pathKeys := []string{"key"} + str := "test" + value := &str // Use a pointer to string + fullPath := "key" - // When attempting to assign a string value to it - value := "not an int" - _, err := assignValue(fieldValue, value) + // When calling setValueByPath with pointer map + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned indicating type mismatch - if err == nil { - t.Errorf("Expected an error for pointer type mismatch, got nil") + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - expectedError := "cannot assign value of type string to field of type *int" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + + // And the pointer value should be set correctly + val := currValue.MapIndex(reflect.ValueOf("key")) + if !val.IsValid() || val.IsNil() { + t.Fatal("Expected map value to be non-nil") + } + if val.Elem().String() != "test" { + t.Errorf("Expected pointer value 'test', got '%s'", val.Elem().String()) } }) - t.Run("ValueTypeMismatchNonConvertible", func(t *testing.T) { - // Given a field of type int - var field int - fieldValue := reflect.ValueOf(&field).Elem() + t.Run("NestedMapWithNilValue", func(t *testing.T) { + // Given a nested map with a nil value + m := map[string]map[string]string{ + "outer": nil, + } + currValue := reflect.ValueOf(&m).Elem() + pathKeys := []string{"outer", "inner"} + value := "test" + fullPath := "outer.inner" - // When attempting to assign a non-convertible string value to it - value := "not convertible to int" - _, err := assignValue(fieldValue, value) + // When calling setValueByPath with nested path + err := setValueByPath(currValue, pathKeys, value, fullPath) - // Then an error should be returned indicating type mismatch - if err == nil { - t.Errorf("Expected an error for non-convertible type mismatch, got nil") + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - expectedError := "cannot assign value of type string to field of type int" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + + // And the nested value should be set correctly + outer := currValue.MapIndex(reflect.ValueOf("outer")) + if !outer.IsValid() { + t.Fatal("Expected outer map to exist") + } + inner := outer.MapIndex(reflect.ValueOf("inner")) + if !inner.IsValid() { + t.Fatal("Expected inner map to exist") + } + if inner.String() != "test" { + t.Errorf("Expected nested value 'test', got '%s'", inner.String()) } }) }