diff --git a/api/v1alpha1/cluster/cluster_config.go b/api/v1alpha1/cluster/cluster_config.go index 7070af071..cf6b40ee7 100644 --- a/api/v1alpha1/cluster/cluster_config.go +++ b/api/v1alpha1/cluster/cluster_config.go @@ -25,7 +25,7 @@ type NodeGroupConfig struct { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` } diff --git a/api/v1alpha1/cluster/cluster_config_test.go b/api/v1alpha1/cluster/cluster_config_test.go index 5acd36d02..0a5a8d213 100644 --- a/api/v1alpha1/cluster/cluster_config_test.go +++ b/api/v1alpha1/cluster/cluster_config_test.go @@ -29,7 +29,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -51,7 +51,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -80,7 +80,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -102,7 +102,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -193,7 +193,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -210,7 +210,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -234,7 +234,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -251,7 +251,7 @@ func TestClusterConfig_Merge(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -333,7 +333,7 @@ func TestClusterConfig_Copy(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ @@ -355,7 +355,7 @@ func TestClusterConfig_Copy(t *testing.T) { CPU *int `yaml:"cpu,omitempty"` Memory *int `yaml:"memory,omitempty"` Image *string `yaml:"image,omitempty"` - Nodes map[string]NodeConfig `yaml:"nodes,omitempty"` + Nodes map[string]NodeConfig `yaml:"-"` HostPorts []string `yaml:"hostports,omitempty"` Volumes []string `yaml:"volumes,omitempty"` }{ diff --git a/cmd/init.go b/cmd/init.go index 308116f9f..917c8931a 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -54,6 +54,7 @@ var initCmd = &cobra.Command{ ctx = context.WithValue(ctx, "quiet", true) ctx = context.WithValue(ctx, "decrypt", true) + ctx = context.WithValue(ctx, "initPipeline", true) envPipeline, err := pipelines.WithPipeline(injector, ctx, "envPipeline") if err != nil { return fmt.Errorf("failed to set up env pipeline: %w", err) @@ -76,72 +77,67 @@ var initCmd = &cobra.Command{ } configHandler := injector.Resolve("configHandler").(config.ConfigHandler) - + // Initialize the config handler to ensure schema validator is available if err := configHandler.Initialize(); err != nil { return fmt.Errorf("failed to initialize config handler: %w", err) } - - // Load the schema to enable validation of --set flags - if err := configHandler.LoadContextConfig(); err != nil { - return fmt.Errorf("failed to load context config: %w", err) - } // Set provider in context if it's been set (either via --provider or --platform) if initProvider != "" { - if err := configHandler.SetContextValue("provider", initProvider); err != nil { + if err := configHandler.Set("provider", initProvider); err != nil { return fmt.Errorf("failed to set provider: %w", err) } } // Set other configuration values if initBackend != "" { - if err := configHandler.SetContextValue("terraform.backend.type", initBackend); err != nil { + if err := configHandler.Set("terraform.backend.type", initBackend); err != nil { return fmt.Errorf("failed to set terraform.backend.type: %w", err) } } if initAwsProfile != "" { - if err := configHandler.SetContextValue("aws.profile", initAwsProfile); err != nil { + if err := configHandler.Set("aws.profile", initAwsProfile); err != nil { return fmt.Errorf("failed to set aws.profile: %w", err) } } if initAwsEndpointURL != "" { - if err := configHandler.SetContextValue("aws.endpoint_url", initAwsEndpointURL); err != nil { + if err := configHandler.Set("aws.endpoint_url", initAwsEndpointURL); err != nil { return fmt.Errorf("failed to set aws.endpoint_url: %w", err) } } if initVmDriver != "" { - if err := configHandler.SetContextValue("vm.driver", initVmDriver); err != nil { + if err := configHandler.Set("vm.driver", initVmDriver); err != nil { return fmt.Errorf("failed to set vm.driver: %w", err) } } if initCpu > 0 { - if err := configHandler.SetContextValue("vm.cpu", initCpu); err != nil { + if err := configHandler.Set("vm.cpu", initCpu); err != nil { return fmt.Errorf("failed to set vm.cpu: %w", err) } } if initDisk > 0 { - if err := configHandler.SetContextValue("vm.disk", initDisk); err != nil { + if err := configHandler.Set("vm.disk", initDisk); err != nil { return fmt.Errorf("failed to set vm.disk: %w", err) } } if initMemory > 0 { - if err := configHandler.SetContextValue("vm.memory", initMemory); err != nil { + if err := configHandler.Set("vm.memory", initMemory); err != nil { return fmt.Errorf("failed to set vm.memory: %w", err) } } if initArch != "" { - if err := configHandler.SetContextValue("vm.arch", initArch); err != nil { + if err := configHandler.Set("vm.arch", initArch); err != nil { return fmt.Errorf("failed to set vm.arch: %w", err) } } if initDocker { - if err := configHandler.SetContextValue("docker.enabled", true); err != nil { + if err := configHandler.Set("docker.enabled", true); err != nil { return fmt.Errorf("failed to set docker.enabled: %w", err) } } if initGitLivereload { - if err := configHandler.SetContextValue("git.livereload.enabled", true); err != nil { + if err := configHandler.Set("git.livereload.enabled", true); err != nil { return fmt.Errorf("failed to set git.livereload.enabled: %w", err) } } @@ -150,7 +146,7 @@ var initCmd = &cobra.Command{ for _, setFlag := range initSetFlags { parts := strings.SplitN(setFlag, "=", 2) if len(parts) == 2 { - if err := configHandler.SetContextValue(parts[0], parts[1]); err != nil { + if err := configHandler.Set(parts[0], parts[1]); err != nil { return fmt.Errorf("failed to set %s: %w", parts[0], err) } } diff --git a/cmd/init_test.go b/cmd/init_test.go index 7ac13f9b0..734e8d285 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -226,13 +226,13 @@ func TestInitCmd(t *testing.T) { } }) - t.Run("ConfigHandlerSetContextValueError", func(t *testing.T) { + t.Run("ConfigHandlerSetError", func(t *testing.T) { // Given a temporary directory with mocked dependencies mocks := setupInitTest(t) // And a config handler that fails to set context values mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("failed to set %s", key) } mocks.Injector.Register("configHandler", mockConfigHandler) @@ -1066,7 +1066,7 @@ func TestInitCmd(t *testing.T) { // And a config handler that fails to set values mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("failed to set %s", key) } mocks.Injector.Register("configHandler", mockConfigHandler) diff --git a/pkg/config/config_handler.go b/pkg/config/config_handler.go index 71f211b76..c08b8ab55 100644 --- a/pkg/config/config_handler.go +++ b/pkg/config/config_handler.go @@ -2,8 +2,7 @@ package config import ( "fmt" - "math" - "os" + "maps" "path/filepath" "reflect" "strconv" @@ -11,7 +10,6 @@ import ( "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" ) @@ -24,16 +22,14 @@ import ( type ConfigHandler interface { Initialize() error - LoadConfig(path string) error + LoadConfig() error LoadConfigString(content string) error - LoadContextConfig() error GetString(key string, defaultValue ...string) string GetInt(key string, defaultValue ...int) int GetBool(key string, defaultValue ...bool) bool GetStringSlice(key string, defaultValue ...[]string) []string GetStringMap(key string, defaultValue ...map[string]string) map[string]string Set(key string, value any) error - SetContextValue(key string, value any) error Get(key string) any SaveConfig(overwrite ...bool) error SetDefault(context v1alpha1.Context) error @@ -43,12 +39,9 @@ type ConfigHandler interface { GetConfigRoot() (string, error) Clean() error IsLoaded() bool - IsContextConfigLoaded() bool - SetSecretsProvider(provider secrets.SecretsProvider) GenerateContextID() error LoadSchema(schemaPath string) error LoadSchemaFromBytes(schemaContent []byte) error - GetSchemaDefaults() (map[string]any, error) GetContextValues() (map[string]any, error) } @@ -61,18 +54,13 @@ const ( // configHandler is the concrete implementation of the ConfigHandler interface that provides // YAML-based configuration management with support for contexts, schemas, and values files. type configHandler struct { - injector di.Injector - shell shell.Shell - config v1alpha1.Config - context string - secretsProviders []secrets.SecretsProvider - loaded bool - shims *Shims - schemaValidator *SchemaValidator - contextValues map[string]any - path string - defaultContextConfig v1alpha1.Context - loadedContexts map[string]bool + injector di.Injector + shell shell.Shell + context string + loaded bool + shims *Shims + schemaValidator *SchemaValidator + data map[string]any } // ============================================================================= @@ -82,14 +70,11 @@ type configHandler struct { // NewConfigHandler creates a new ConfigHandler instance with default context configuration. func NewConfigHandler(injector di.Injector) ConfigHandler { handler := &configHandler{ - injector: injector, - shims: NewShims(), - contextValues: make(map[string]any), - loadedContexts: make(map[string]bool), + injector: injector, + shims: NewShims(), + data: make(map[string]any), } - handler.config.Version = "v1alpha1" - return handler } @@ -97,7 +82,10 @@ func NewConfigHandler(injector di.Injector) ConfigHandler { // Public Methods // ============================================================================= -// Initialize sets up the config handler by resolving and storing the shell dependency. +// Initialize configures the configHandler by resolving and storing the shell dependency from the injector. +// It also initializes the schema validator and sets its shims field. If the internal data map is nil, +// Initialize creates a new map to store configuration data. This method must be called before other methods +// to ensure dependencies and state are properly set up. func (c *configHandler) Initialize() error { shell, ok := c.injector.Resolve("shell").(shell.Shell) if !ok { @@ -108,65 +96,76 @@ func (c *configHandler) Initialize() error { c.schemaValidator = NewSchemaValidator(c.shell) c.schemaValidator.Shims = c.shims + if c.data == nil { + c.data = make(map[string]any) + } + return nil } -// LoadConfigString loads configuration from a YAML string into the internal config structure. -// It unmarshals the YAML, records which contexts were present in the input, validates and sets -// the config version, and marks the configuration as loaded. Returns an error if unmarshalling -// fails or if the config version is unsupported. +// LoadConfigString loads YAML configuration directly into the internal data map for testing purposes. +// It unmarshals the provided YAML string and, if a "contexts" key exists, extracts and merges only +// the configuration for the current context. If no "contexts" structure is present, it merges the entire +// map. This method is primarily intended for test helpers and not for production use; load configuration +// in production with LoadConfig instead. Returns an error if YAML unmarshalling fails. func (c *configHandler) LoadConfigString(content string) error { if content == "" { return nil } - var tempConfig v1alpha1.Config - if err := c.shims.YamlUnmarshal([]byte(content), &tempConfig); err != nil { + var dataMap map[string]any + if err := c.shims.YamlUnmarshal([]byte(content), &dataMap); err != nil { return fmt.Errorf("error unmarshalling yaml: %w", err) } - if tempConfig.Contexts != nil { - for contextName := range tempConfig.Contexts { - c.loadedContexts[contextName] = true - } - } - - if err := c.shims.YamlUnmarshal([]byte(content), &c.config); err != nil { - return fmt.Errorf("error unmarshalling yaml: %w", err) - } + contextName := c.GetContext() - if c.config.Version == "" { - c.config.Version = "v1alpha1" - } else if c.config.Version != "v1alpha1" { - return fmt.Errorf("unsupported config version: %s", c.config.Version) - } + if contexts, ok := dataMap["contexts"]; ok { + var contextsMap map[string]any + switch v := contexts.(type) { + case map[string]any: + contextsMap = v + case map[any]any: + contextsMap = make(map[string]any) + for k, val := range v { + if strKey, ok := k.(string); ok { + contextsMap[strKey] = val + } + } + } - return nil -} + if contextsMap != nil { + if contextData, ok := contextsMap[contextName]; ok { + var contextMap map[string]any + switch v := contextData.(type) { + case map[string]any: + contextMap = v + case map[any]any: + contextMap = c.convertInterfaceMap(v) + } -// LoadConfig loads the configuration from the specified path. If the file does not exist, it does nothing. -func (c *configHandler) LoadConfig(path string) error { - c.path = path - if _, err := c.shims.Stat(path); os.IsNotExist(err) { - return nil + if contextMap != nil { + c.data = c.deepMerge(c.data, contextMap) + c.loaded = true + return nil + } + } + } } - data, err := c.shims.ReadFile(path) - if err != nil { - return fmt.Errorf("error reading config file: %w", err) - } + c.data = c.deepMerge(c.data, dataMap) + c.loaded = true - return c.LoadConfigString(string(data)) + return nil } -// LoadContextConfig loads the context-specific windsor.yaml file for the current context. -// It is intended for pipelines that only require static context configuration, not dynamic values. -// The values.yaml file is loaded lazily upon first access via Get() or SetContextValue(). -// Returns nil if already loaded, or if loading succeeds; otherwise returns an error for shell or file-related issues. -func (c *configHandler) LoadContextConfig() error { - if c.loaded { - return nil - } +// LoadConfig loads and merges all configuration sources for the current context into the internal data map. +// It performs the following actions, in order: loads schema defaults (if available); merges root windsor.yaml +// context section (if it exists); merges any context-specific windsor.yaml/yml file; then merges values.yaml for +// dynamic fields, validating values.yaml against the loaded schema (if one is present). All configuration is +// accumulated into one map structure. If any file is loaded, config state is marked as loaded. Returns an error +// for any I/O or validation failure. Must call Initialize() first. +func (c *configHandler) LoadConfig() error { if c.shell == nil { return fmt.Errorf("shell not initialized") } @@ -177,8 +176,44 @@ func (c *configHandler) LoadContextConfig() error { } contextName := c.GetContext() - contextConfigDir := filepath.Join(projectRoot, "contexts", contextName) + hasLoadedFiles := false + + if c.schemaValidator != nil && c.schemaValidator.Schema == nil { + schemaPath := filepath.Join(projectRoot, "contexts", "_template", "schema.yaml") + if _, err := c.shims.Stat(schemaPath); err == nil { + if err := c.schemaValidator.LoadSchema(schemaPath); err != nil { + return fmt.Errorf("error loading schema: %w", err) + } + } + } + + rootConfigPath := filepath.Join(projectRoot, "windsor.yaml") + if _, err := c.shims.Stat(rootConfigPath); err == nil { + fileData, err := c.shims.ReadFile(rootConfigPath) + if err != nil { + return fmt.Errorf("error reading root config file: %w", err) + } + + var rootConfig v1alpha1.Config + if err := c.shims.YamlUnmarshal(fileData, &rootConfig); err != nil { + return fmt.Errorf("error unmarshalling root config: %w", err) + } + + if rootConfig.Contexts != nil && rootConfig.Contexts[contextName] != nil { + contextData, err := c.shims.YamlMarshal(rootConfig.Contexts[contextName]) + if err != nil { + return fmt.Errorf("error marshalling context config: %w", err) + } + var contextMap map[string]any + if err := c.shims.YamlUnmarshal(contextData, &contextMap); err != nil { + return fmt.Errorf("error unmarshalling context config to map: %w", err) + } + c.data = c.deepMerge(c.data, contextMap) + } + hasLoadedFiles = true + } + contextConfigDir := filepath.Join(projectRoot, "contexts", contextName) yamlPath := filepath.Join(contextConfigDir, "windsor.yaml") ymlPath := filepath.Join(contextConfigDir, "windsor.yml") @@ -190,56 +225,58 @@ func (c *configHandler) LoadContextConfig() error { } if contextConfigPath != "" { - data, err := c.shims.ReadFile(contextConfigPath) + fileData, err := c.shims.ReadFile(contextConfigPath) if err != nil { return fmt.Errorf("error reading context config file: %w", err) } - var contextConfig v1alpha1.Context - if err := c.shims.YamlUnmarshal(data, &contextConfig); err != nil { + var contextMap map[string]any + if err := c.shims.YamlUnmarshal(fileData, &contextMap); err != nil { return fmt.Errorf("error unmarshalling context yaml: %w", err) } - if c.config.Contexts == nil { - c.config.Contexts = make(map[string]*v1alpha1.Context) + c.data = c.deepMerge(c.data, contextMap) + hasLoadedFiles = true + } + + valuesPath := filepath.Join(contextConfigDir, "values.yaml") + if _, err := c.shims.Stat(valuesPath); err == nil { + fileData, err := c.shims.ReadFile(valuesPath) + if err != nil { + return fmt.Errorf("error reading values.yaml: %w", err) } - if c.config.Contexts[contextName] == nil { - c.config.Contexts[contextName] = &v1alpha1.Context{} + var values map[string]any + if err := c.shims.YamlUnmarshal(fileData, &values); err != nil { + return fmt.Errorf("error unmarshalling values.yaml: %w", err) } - c.config.Contexts[contextName].Merge(&contextConfig) + if c.schemaValidator != nil && c.schemaValidator.Schema != nil { + if result, err := c.schemaValidator.Validate(values); err != nil { + return fmt.Errorf("error validating values.yaml: %w", err) + } else if !result.Valid { + return fmt.Errorf("values.yaml validation failed: %v", result.Errors) + } + } - c.loadedContexts[contextName] = true + c.data = c.deepMerge(c.data, values) + hasLoadedFiles = true } - if len(c.config.Contexts) > 0 { + if hasLoadedFiles { c.loaded = true } return nil } -// IsContextConfigLoaded returns true if the base configuration is loaded, the current context name is set, -// and a context-specific configuration has been loaded for the current context. Returns false otherwise. -func (c *configHandler) IsContextConfigLoaded() bool { - if !c.loaded { - return false - } - - contextName := c.GetContext() - if contextName == "" { - return false - } - - return c.loadedContexts[contextName] -} - -// SaveConfig writes the current configuration state to disk. It writes the root windsor.yaml file with only the version field, -// and creates or updates the current context's windsor.yaml file and values.yaml containing dynamic schema-based values. -// The root windsor.yaml is created if missing; the context windsor.yaml is created if missing and not tracked in the root config; -// values.yaml is created if context values are present. If the overwrite parameter is true, existing files are updated with the -// current in-memory state. All operations are performed for the current context. +// SaveConfig writes the current configuration state to disk. +// This function separates the configuration fields of the active context into two distinct files +// within the context directory under the project root. Static fields that match the v1alpha1.Context +// schema are written to a windsor.yaml file, and dynamic fields that do not match the static schema are +// written to a values.yaml file. If overwrite is specified, existing windsor.yaml will be overwritten; +// otherwise, it is only created if missing. Returns an error if writing fails, or if required shims +// or shell are not initialized. func (c *configHandler) SaveConfig(overwrite ...bool) error { if c.shell == nil { return fmt.Errorf("shell not initialized") @@ -255,153 +292,141 @@ func (c *configHandler) SaveConfig(overwrite ...bool) error { return fmt.Errorf("error retrieving project root: %w", err) } - rootConfigPath := filepath.Join(projectRoot, "windsor.yaml") contextName := c.GetContext() - contextConfigPath := filepath.Join(projectRoot, "contexts", contextName, "windsor.yaml") - - rootExists := false - if _, err := c.shims.Stat(rootConfigPath); err == nil { - rootExists = true - } + contextDir := filepath.Join(projectRoot, "contexts", contextName) - contextExists := false - if _, err := c.shims.Stat(contextConfigPath); err == nil { - contextExists = true + if err := c.shims.MkdirAll(contextDir, 0755); err != nil { + return fmt.Errorf("error creating context directory: %w", err) } - contextExistsInRoot := c.loadedContexts[contextName] - - shouldCreateRootConfig := !rootExists - shouldCreateContextConfig := !contextExists && !contextExistsInRoot - shouldUpdateRootConfig := shouldOverwrite && rootExists - shouldUpdateContextConfig := shouldOverwrite && contextExists - - if shouldCreateRootConfig || shouldUpdateRootConfig { - rootConfig := struct { - Version string `yaml:"version"` - }{ - Version: c.config.Version, + rootConfigPath := filepath.Join(projectRoot, "windsor.yaml") + if _, err := c.shims.Stat(rootConfigPath); err != nil { + rootConfig := map[string]any{ + "version": "v1alpha1", } - - data, err := c.shims.YamlMarshal(rootConfig) + rootData, err := c.shims.YamlMarshal(rootConfig) if err != nil { return fmt.Errorf("error marshalling root config: %w", err) } - - if err := c.shims.WriteFile(rootConfigPath, data, 0644); err != nil { + if err := c.shims.WriteFile(rootConfigPath, rootData, 0644); err != nil { return fmt.Errorf("error writing root config: %w", err) } } - if shouldCreateContextConfig || shouldUpdateContextConfig { - var contextConfig v1alpha1.Context + staticFields, dynamicFields := c.separateStaticAndDynamicFields(c.data) - if c.config.Contexts != nil && c.config.Contexts[contextName] != nil { - contextConfig = *c.config.Contexts[contextName] - } else { - contextConfig = c.defaultContextConfig + if len(staticFields) > 0 { + contextConfigPath := filepath.Join(contextDir, "windsor.yaml") + contextExists := false + if _, err := c.shims.Stat(contextConfigPath); err == nil { + contextExists = true } - contextDir := filepath.Join(projectRoot, "contexts", contextName) - if err := c.shims.MkdirAll(contextDir, 0755); err != nil { - return fmt.Errorf("error creating context directory: %w", err) - } + if !contextExists || shouldOverwrite { + contextStruct := c.mapToContext(staticFields) + data, err := c.shims.YamlMarshal(contextStruct) + if err != nil { + return fmt.Errorf("error marshalling context config: %w", err) + } - data, err := c.shims.YamlMarshal(contextConfig) - if err != nil { - return fmt.Errorf("error marshalling context config: %w", err) + if err := c.shims.WriteFile(contextConfigPath, data, 0644); err != nil { + return fmt.Errorf("error writing context config: %w", err) + } } + } - if err := c.shims.WriteFile(contextConfigPath, data, 0644); err != nil { - return fmt.Errorf("error writing context config: %w", err) + if len(dynamicFields) > 0 { + valuesPath := filepath.Join(contextDir, "values.yaml") + data, err := c.shims.YamlMarshal(dynamicFields) + if err != nil { + return fmt.Errorf("error marshalling values.yaml: %w", err) } - } - if len(c.contextValues) > 0 { - if err := c.saveContextValues(); err != nil { - return fmt.Errorf("error saving values.yaml: %w", err) + if err := c.shims.WriteFile(valuesPath, data, 0644); err != nil { + return fmt.Errorf("error writing values.yaml: %w", err) } } return nil } -// SetDefault sets the given context configuration as the default and merges it with any -// existing context configuration. If no context exists, the default becomes the context. -// If a context exists, it merges the default with the existing context, with existing -// values taking precedence over defaults. +// SetDefault sets the default context configuration in the config handler's internal data. +// It marshals the given v1alpha1.Context struct to a map and merges it into the handler's data. +// This method is typically used during initialization when context files do not yet exist. func (c *configHandler) SetDefault(context v1alpha1.Context) error { - c.defaultContextConfig = context - currentContext := c.GetContext() - contextKey := fmt.Sprintf("contexts.%s", currentContext) - - if c.Get(contextKey) == nil { - return c.Set(contextKey, &context) + contextData, err := c.shims.YamlMarshal(context) + if err != nil { + return fmt.Errorf("error marshalling context: %w", err) } - if c.config.Contexts == nil { - c.config.Contexts = make(map[string]*v1alpha1.Context) - } - if c.config.Contexts[currentContext] == nil { - c.config.Contexts[currentContext] = &v1alpha1.Context{} + var contextMap map[string]any + if err := c.shims.YamlUnmarshal(contextData, &contextMap); err != nil { + return fmt.Errorf("error unmarshalling context to map: %w", err) } - defaultCopy := context.DeepCopy() - existingCopy := c.config.Contexts[currentContext].DeepCopy() - defaultCopy.Merge(existingCopy) - c.config.Contexts[currentContext] = defaultCopy + c.data = c.deepMerge(c.data, contextMap) return nil } -// Get returns the value at the specified configuration path using the configured lookup precedence. -// Lookup order is: context configuration from windsor.yaml, then values.yaml, then schema defaults. -// Returns nil if the path is empty or if no value is found in any source. +// Get retrieves the value at the specified configuration path from the internal data map. +// If the value is not found in the current data, and the schema validator is available, +// it falls back to returning a default value from the schema for the top-level key or +// deeper nested keys as appropriate. Returns nil if the path is empty or no value is found. func (c *configHandler) Get(path string) any { if path == "" { return nil } pathKeys := parsePath(path) + value := getValueByPathFromMap(c.data, pathKeys) - value := getValueByPath(c.config, pathKeys) - if value != nil { - return value - } - - if len(pathKeys) >= 2 && pathKeys[0] == "contexts" { - if len(pathKeys) >= 3 && c.loaded { - if err := c.ensureValuesYamlLoaded(); err != nil { - } - if c.contextValues != nil { - key := pathKeys[2] - if value, exists := c.contextValues[key]; exists { - return value + if value == nil && len(pathKeys) > 0 && c.schemaValidator != nil && c.schemaValidator.Schema != nil { + defaults, err := c.schemaValidator.GetSchemaDefaults() + if err == nil && defaults != nil { + if topLevelDefault, exists := defaults[pathKeys[0]]; exists { + if len(pathKeys) == 1 { + return topLevelDefault + } + if defaultMap, ok := topLevelDefault.(map[string]any); ok { + return getValueByPathFromMap(defaultMap, pathKeys[1:]) + } + if interfaceMap, ok := topLevelDefault.(map[any]any); ok { + convertedMap := c.convertInterfaceMap(interfaceMap) + return getValueByPathFromMap(convertedMap, pathKeys[1:]) } } } + } - value = getValueByPath(c.defaultContextConfig, pathKeys[2:]) - if value != nil { - return value - } + return value +} + +// getValueByPathFromMap returns the value in a nested map[string]any at the location specified by the pathKeys slice. +// It traverses the map according to the keys, returning the value found at the leaf, or nil if any key is missing or the value is not a map. +func getValueByPathFromMap(data map[string]any, pathKeys []string) any { + if len(pathKeys) == 0 { + return nil } - if len(pathKeys) == 1 && c.schemaValidator != nil && c.schemaValidator.Schema != nil { - defaults, err := c.schemaValidator.GetSchemaDefaults() - if err == nil { - if value, exists := defaults[pathKeys[0]]; exists { - return value + current := any(data) + for _, key := range pathKeys { + if m, ok := current.(map[string]any); ok { + val, exists := m[key] + if !exists { + return nil } + current = val + } else { + return nil } } - return nil + return current } // GetString retrieves a string value for the specified key from the configuration, with an optional default value. // If the key is not found, it returns the provided default value or an empty string if no default is provided. func (c *configHandler) GetString(key string, defaultValue ...string) string { - contextKey := fmt.Sprintf("contexts.%s.%s", c.context, key) - value := c.Get(contextKey) + value := c.Get(key) if value == nil { if len(defaultValue) > 0 { return defaultValue[0] @@ -412,27 +437,65 @@ func (c *configHandler) GetString(key string, defaultValue ...string) string { return strValue } -// GetInt retrieves an integer value for the specified key from the configuration, with an optional default value. +// GetInt retrieves an integer value for the specified key from the configuration. +// It accepts an optional default value. The function safely converts supported types (int, int64, uint64, uint) +// to int with appropriate overflow protection, and parses string values if they represent valid integer literals. +// Types that cannot be converted (such as float64 or invalid strings) are ignored and the default is used. +// If the key is not found or if conversion fails, the provided default value or 0 is returned. func (c *configHandler) GetInt(key string, defaultValue ...int) int { - contextKey := fmt.Sprintf("contexts.%s.%s", c.context, key) - value := c.Get(contextKey) + value := c.Get(key) if value == nil { if len(defaultValue) > 0 { return defaultValue[0] } return 0 } - intValue, ok := value.(int) - if !ok { - return 0 + if intValue, ok := value.(int); ok { + return intValue + } + if int64Value, ok := value.(int64); ok { + maxInt := int64(^uint(0) >> 1) + minInt := -maxInt - 1 + if int64Value > maxInt || int64Value < minInt { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + return int(int64Value) + } + if uint64Value, ok := value.(uint64); ok { + if uint64Value > uint64(^uint(0)>>1) { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + return int(uint64Value) + } + if uintValue, ok := value.(uint); ok { + if uintValue > uint(^uint(0)>>1) { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + return int(uintValue) } - return intValue + if strValue, ok := value.(string); ok { + if intVal, err := strconv.Atoi(strValue); err == nil { + return intVal + } + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 } // GetBool retrieves a boolean value for the specified key from the configuration, with an optional default value. func (c *configHandler) GetBool(key string, defaultValue ...bool) bool { - contextKey := fmt.Sprintf("contexts.%s.%s", c.context, key) - value := c.Get(contextKey) + value := c.Get(key) if value == nil { if len(defaultValue) > 0 { return defaultValue[0] @@ -445,101 +508,96 @@ func (c *configHandler) GetBool(key string, defaultValue ...bool) bool { return false } -// GetStringSlice retrieves a slice of strings for the specified key from the configuration, with an optional default value. -// If the key is not found, it returns the provided default value or an empty slice if no default is provided. +// GetStringSlice retrieves a slice of strings for the specified key from the configuration. +// It supports both []string and []any (such as from YAML unmarshaling). +// If the key is not found, the function returns the provided default value or an empty slice if no default is supplied. func (c *configHandler) GetStringSlice(key string, defaultValue ...[]string) []string { - contextKey := fmt.Sprintf("contexts.%s.%s", c.context, key) - value := c.Get(contextKey) + value := c.Get(key) if value == nil { if len(defaultValue) > 0 { return defaultValue[0] } return []string{} } - strSlice, ok := value.([]string) - if !ok { - return []string{} + if strSlice, ok := value.([]string); ok { + return strSlice + } + if interfaceSlice, ok := value.([]any); ok { + strSlice := make([]string, 0, len(interfaceSlice)) + for _, v := range interfaceSlice { + if str, ok := v.(string); ok { + strSlice = append(strSlice, str) + } else { + strSlice = append(strSlice, fmt.Sprintf("%v", v)) + } + } + return strSlice } - return strSlice + return []string{} } // GetStringMap retrieves a map of string key-value pairs for the specified key from the configuration. // If the key is not found, it returns the provided default value or an empty map if no default is provided. +// The method handles values that are map[string]string, map[string]any, or map[any]any, +// converting all map values to strings as needed to produce a map[string]string result. func (c *configHandler) GetStringMap(key string, defaultValue ...map[string]string) map[string]string { - contextKey := fmt.Sprintf("contexts.%s.%s", c.context, key) - value := c.Get(contextKey) + value := c.Get(key) if value == nil { if len(defaultValue) > 0 { return defaultValue[0] } return map[string]string{} } - - strMap, ok := value.(map[string]string) - if !ok { - return map[string]string{} - } - - return strMap -} - -// Set updates the value at the specified path in the configuration using reflection. -// It parses the path, performs type conversion if necessary, and sets the value in the config structure. -// An error is returned if the path is invalid, conversion fails, or the update cannot be performed. -func (c *configHandler) Set(path string, value any) error { - if path == "" { - return nil + if strMap, ok := value.(map[string]string); ok { + return strMap } - - pathKeys := parsePath(path) - if len(pathKeys) == 0 { - return fmt.Errorf("invalid path: %s", path) - } - - if strValue, ok := value.(string); ok { - currentValue := c.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) + if interfaceMap, ok := value.(map[string]any); ok { + strMap := make(map[string]string, len(interfaceMap)) + for k, v := range interfaceMap { + if str, ok := v.(string); ok { + strMap[k] = str + } else { + strMap[k] = fmt.Sprintf("%v", v) } - value = convertedValue } + return strMap + } + if interfaceMap, ok := value.(map[any]any); ok { + strMap := make(map[string]string) + for k, v := range interfaceMap { + if strKey, ok := k.(string); ok { + if strVal, ok := v.(string); ok { + strMap[strKey] = strVal + } else { + strMap[strKey] = fmt.Sprintf("%v", v) + } + } + } + return strMap } - - configValue := reflect.ValueOf(&c.config) - return setValueByPath(configValue, pathKeys, value, path) + return map[string]string{} } -// SetContextValue sets a value at the given path for the current context, updating config or values.yaml in memory. -// The key must not be empty or have invalid dot notation. Values are schema-validated if available. -// Changes require SaveConfig to persist. Returns error if path or value is invalid, or conversion fails. -func (c *configHandler) SetContextValue(path string, value any) error { +// Set assigns a configuration value at the specified hierarchical path in the configHandler's internal data map. +// The input value is automatically converted to the appropriate type according to the schema, if available. +// If a schema is present, Set validates only the dynamic fields of the configuration map after the new value is set. +// Returns an error if the path is invalid, schema validation fails, or if value assignment encounters an issue. +// Changes made by this method are in-memory and must be persisted separately via SaveConfig. +func (c *configHandler) Set(path string, value any) error { if path == "" { return fmt.Errorf("path cannot be empty") } if strings.Contains(path, "..") || strings.HasPrefix(path, ".") || strings.HasSuffix(path, ".") { return fmt.Errorf("invalid path format: %s", path) } - if c.isKeyInStaticSchema(path) { - if c.config.Contexts == nil { - c.config.Contexts = make(map[string]*v1alpha1.Context) - } - contextName := c.GetContext() - if c.config.Contexts[contextName] == nil { - c.config.Contexts[contextName] = &v1alpha1.Context{} - } - fullPath := fmt.Sprintf("contexts.%s.%s", contextName, path) - return c.Set(fullPath, value) - } - if err := c.ensureValuesYamlLoaded(); err != nil { - return fmt.Errorf("error loading values.yaml: %w", err) - } + convertedValue := c.convertStringValue(value) - c.contextValues[path] = convertedValue + pathKeys := parsePath(path) + setValueInMap(c.data, pathKeys, convertedValue) + if c.schemaValidator != nil && c.schemaValidator.Schema != nil { - if result, err := c.schemaValidator.Validate(c.contextValues); err != nil { + _, dynamicFields := c.separateStaticAndDynamicFields(c.data) + if result, err := c.schemaValidator.Validate(dynamicFields); err != nil { return fmt.Errorf("error validating context value: %w", err) } else if !result.Valid { return fmt.Errorf("context value validation failed: %v", result.Errors) @@ -548,22 +606,22 @@ func (c *configHandler) SetContextValue(path string, value any) error { return nil } -// GetConfig returns the context config object for the current context, or the default if none is set. +// GetConfig returns the context configuration as a v1alpha1.Context struct by marshalling +// the configHandler's internal data map to YAML and then unmarshalling it into the struct. +// This provides backward compatibility for code that relies on the statically typed Context. +// Returns a pointer to an empty Context if marshaling or unmarshaling fails. func (c *configHandler) GetConfig() *v1alpha1.Context { - defaultConfigCopy := c.defaultContextConfig.DeepCopy() - context := c.context - - if context == "" { - return defaultConfigCopy + contextData, err := c.shims.YamlMarshal(c.data) + if err != nil { + return &v1alpha1.Context{} } - if ctx, ok := c.config.Contexts[context]; ok { - mergedConfig := defaultConfigCopy - mergedConfig.Merge(ctx) - return mergedConfig + var context v1alpha1.Context + if err := c.shims.YamlUnmarshal(contextData, &context); err != nil { + return &v1alpha1.Context{} } - return defaultConfigCopy + return &context } // GetContext retrieves the current context from the environment, file, or defaults to "local" @@ -573,7 +631,7 @@ func (c *configHandler) GetContext() string { envContext := c.shims.Getenv("WINDSOR_CONTEXT") if envContext != "" { c.context = envContext - } else { + } else if c.shell != nil { projectRoot, err := c.shell.GetProjectRoot() if err != nil { c.context = contextName @@ -586,6 +644,8 @@ func (c *configHandler) GetContext() string { c.context = string(data) } } + } else { + c.context = contextName } return c.context @@ -656,11 +716,6 @@ func (c *configHandler) IsLoaded() bool { return c.loaded } -// SetSecretsProvider sets the secrets provider for the config handler -func (c *configHandler) SetSecretsProvider(provider secrets.SecretsProvider) { - c.secretsProviders = append(c.secretsProviders, provider) -} - // LoadSchema loads the schema.yaml file from the specified directory // Returns error if schema file doesn't exist or is invalid func (c *configHandler) LoadSchema(schemaPath string) error { @@ -679,43 +734,19 @@ func (c *configHandler) LoadSchemaFromBytes(schemaContent []byte) error { return c.schemaValidator.LoadSchemaFromBytes(schemaContent) } -// GetSchemaDefaults extracts default values from the loaded schema -// Returns defaults as a map suitable for merging with user values -func (c *configHandler) GetSchemaDefaults() (map[string]any, error) { - if c.schemaValidator == nil { - return nil, fmt.Errorf("schema validator not initialized") - } - return c.schemaValidator.GetSchemaDefaults() -} - -// GetContextValues returns merged context values from schema defaults, windsor.yaml (via GetConfig), and values.yaml -// Merge order: schema defaults (base) -> context config -> values.yaml (highest priority) +// GetContextValues returns a merged configuration map composed of schema defaults and current config data. +// The result provides all configuration values, with schema defaults filled in for missing keys, ensuring +// downstream consumers (such as blueprint processing) always receive a complete set of config values. +// If the schema validator or schema is unavailable, only the currently loaded data is returned. func (c *configHandler) GetContextValues() (map[string]any, error) { - if err := c.ensureValuesYamlLoaded(); err != nil { - return nil, err - } - result := make(map[string]any) - - schemaDefaults, err := c.GetSchemaDefaults() - if err == nil && schemaDefaults != nil { - result = c.deepMerge(result, schemaDefaults) - } - - contextConfig := c.GetConfig() - contextData, err := c.shims.YamlMarshal(contextConfig) - if err != nil { - return nil, fmt.Errorf("error marshalling context config: %w", err) - } - - var contextMap map[string]any - if err := c.shims.YamlUnmarshal(contextData, &contextMap); err != nil { - return nil, fmt.Errorf("error unmarshalling context config to map: %w", err) + if c.schemaValidator != nil && c.schemaValidator.Schema != nil { + defaults, err := c.schemaValidator.GetSchemaDefaults() + if err == nil && defaults != nil { + result = c.deepMerge(result, defaults) + } } - - result = c.deepMerge(result, contextMap) - result = c.deepMerge(result, c.contextValues) - + result = c.deepMerge(result, c.data) return result, nil } @@ -736,7 +767,7 @@ func (c *configHandler) GenerateContextID() error { } id := "w" + string(b) - return c.SetContextValue("id", id) + return c.Set("id", id) } // Ensure configHandler implements ConfigHandler @@ -746,89 +777,58 @@ var _ ConfigHandler = (*configHandler)(nil) // Private Methods // ============================================================================= -// ensureValuesYamlLoaded loads and validates the values.yaml file for the current context, and loads the schema if required. -// It initializes c.contextValues by reading values.yaml from the context directory, unless already loaded or not required. -// If values.yaml or the schema is missing, it initializes an empty map and performs schema validation if possible. -// Returns an error if any schema loading, reading, or unmarshaling fails. -func (c *configHandler) ensureValuesYamlLoaded() error { - if c.contextValues != nil { - return nil - } - if c.shell == nil || !c.loaded { - c.contextValues = make(map[string]any) - return nil - } - projectRoot, err := c.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } - contextName := c.GetContext() - contextConfigDir := filepath.Join(projectRoot, "contexts", contextName) - valuesPath := filepath.Join(contextConfigDir, "values.yaml") - if c.schemaValidator != nil && c.schemaValidator.Schema == nil { - schemaPath := filepath.Join(projectRoot, "contexts", "_template", "schema.yaml") - if _, err := c.shims.Stat(schemaPath); err == nil { - if err := c.schemaValidator.LoadSchema(schemaPath); err != nil { - return fmt.Errorf("error loading schema: %w", err) - } - } - } - if _, err := c.shims.Stat(valuesPath); err == nil { - data, err := c.shims.ReadFile(valuesPath) - if err != nil { - return fmt.Errorf("error reading values.yaml: %w", err) - } - var values map[string]any - if err := c.shims.YamlUnmarshal(data, &values); err != nil { - return fmt.Errorf("error unmarshalling values.yaml: %w", err) - } - if c.schemaValidator != nil && c.schemaValidator.Schema != nil { - if result, err := c.schemaValidator.Validate(values); err != nil { - return fmt.Errorf("error validating values.yaml: %w", err) - } else if !result.Valid { - return fmt.Errorf("values.yaml validation failed: %v", result.Errors) - } +// convertInterfaceMap recursively converts map[any]any to map[string]any +func (c *configHandler) convertInterfaceMap(m map[any]any) map[string]any { + result := make(map[string]any) + for k, v := range m { + strKey, ok := k.(string) + if !ok { + continue } - c.contextValues = values - } else { - c.contextValues = make(map[string]any) - } - return nil -} -// saveContextValues writes the current contextValues map to a values.yaml file in the context directory for the current context. -// This function ensures that the target context directory exists. If a schema validator is configured, it validates the contextValues -// against the schema before saving. The function marshals the values to YAML, writes them to values.yaml, and returns an error if -// validation, marshalling, or file writing fails. -func (c *configHandler) saveContextValues() error { - if c.schemaValidator != nil && c.schemaValidator.Schema != nil { - if result, err := c.schemaValidator.Validate(c.contextValues); err != nil { - return fmt.Errorf("error validating values.yaml: %w", err) - } else if !result.Valid { - return fmt.Errorf("values.yaml validation failed: %v", result.Errors) + switch val := v.(type) { + case map[any]any: + result[strKey] = c.convertInterfaceMap(val) + case map[string]any: + result[strKey] = val + default: + result[strKey] = val } } + return result +} - configRoot, err := c.GetConfigRoot() +// mapToContext converts a map to a v1alpha1.Context struct by marshaling and unmarshaling. +// This ensures that yaml tags (like yaml:"-") are respected when saving to files. +func (c *configHandler) mapToContext(data map[string]any) *v1alpha1.Context { + contextData, err := c.shims.YamlMarshal(data) if err != nil { - return fmt.Errorf("error getting config root: %w", err) + return &v1alpha1.Context{} } - if err := c.shims.MkdirAll(configRoot, 0755); err != nil { - return fmt.Errorf("error creating context directory: %w", err) + var context v1alpha1.Context + if err := c.shims.YamlUnmarshal(contextData, &context); err != nil { + return &v1alpha1.Context{} } - valuesPath := filepath.Join(configRoot, "values.yaml") - data, err := c.shims.YamlMarshal(c.contextValues) - if err != nil { - return fmt.Errorf("error marshalling values.yaml: %w", err) - } + return &context +} + +// separateStaticAndDynamicFields splits the data map into static fields (matching v1alpha1.Context schema) +// and dynamic fields (everything else). This is used when saving to separate windsor.yaml from values.yaml. +func (c *configHandler) separateStaticAndDynamicFields(data map[string]any) (static map[string]any, dynamic map[string]any) { + static = make(map[string]any) + dynamic = make(map[string]any) - if err := c.shims.WriteFile(valuesPath, data, 0644); err != nil { - return fmt.Errorf("error writing values.yaml: %w", err) + for key, value := range data { + if c.isKeyInStaticSchema(key) { + static[key] = value + } else { + dynamic[key] = value + } } - return nil + return static, dynamic } // isKeyInStaticSchema determines whether the provided key exists as a top-level field @@ -962,10 +962,7 @@ func (c *configHandler) convertStringByPattern(str string) any { // deepMerge recursively merges two maps with overlay values taking precedence. // Nested maps are merged rather than replaced. Non-map values in overlay replace base values. func (c *configHandler) deepMerge(base, overlay map[string]any) map[string]any { - result := make(map[string]any) - for k, v := range base { - result[k] = v - } + result := maps.Clone(base) for k, overlayValue := range overlay { if baseValue, exists := result[k]; exists { if baseMap, baseIsMap := baseValue.(map[string]any); baseIsMap { @@ -980,224 +977,49 @@ func (c *configHandler) deepMerge(base, overlay map[string]any) map[string]any { return result } -// getValueByPath retrieves a value by navigating through a struct or map using YAML tags. -func getValueByPath(current any, pathKeys []string) any { - if len(pathKeys) == 0 { - return nil - } - - currValue := reflect.ValueOf(current) - if !currValue.IsValid() { - return nil - } - - for _, key := range pathKeys { - for currValue.Kind() == reflect.Ptr && !currValue.IsNil() { - currValue = currValue.Elem() - } - if currValue.Kind() == reflect.Ptr && currValue.IsNil() { - return nil - } - - switch currValue.Kind() { - case reflect.Struct: - fieldValue := getFieldByYamlTag(currValue, key) - currValue = fieldValue - - case reflect.Map: - mapKey := reflect.ValueOf(key) - if !mapKey.Type().AssignableTo(currValue.Type().Key()) { - return nil - } - mapValue := currValue.MapIndex(mapKey) - if !mapValue.IsValid() { - return nil - } - currValue = mapValue - - default: - return nil - } - } - - if currValue.Kind() == reflect.Ptr { - if currValue.IsNil() { - return nil - } - currValue = currValue.Elem() - } - - if currValue.IsValid() && currValue.CanInterface() { - return currValue.Interface() - } - - return nil -} - -// 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 := range make([]struct{}, v.NumField()) { - field := t.Field(i) - yamlTag := strings.Split(field.Tag.Get("yaml"), ",")[0] - if yamlTag == tag { - return v.Field(i) - } - } - return reflect.Value{} -} +// ============================================================================= +// Private Helpers +// ============================================================================= -// setValueByPath sets a value in a struct or map by navigating through it using YAML tags. -func setValueByPath(currValue reflect.Value, pathKeys []string, value any, fullPath string) error { +// setValueInMap sets a value in a nested map structure, creating any intermediate maps as needed. +// setValueInMap sets a value in a nested map structure at the specified path, creating intermediate maps as needed. +// It navigates or creates each intermediate key in the provided pathKeys slice, and assigns the final value at the leaf key. +// This function mutates the provided data map in place. +func setValueInMap(data map[string]any, pathKeys []string, value any) { if len(pathKeys) == 0 { - return fmt.Errorf("pathKeys cannot be empty") + return } - key := pathKeys[0] - isLast := len(pathKeys) == 1 - - if currValue.Kind() == reflect.Ptr { - if currValue.IsNil() { - currValue.Set(reflect.New(currValue.Type().Elem())) - } - currValue = currValue.Elem() + if len(pathKeys) == 1 { + data[pathKeys[0]] = value + return } - 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())) - } - - if fieldValue.Kind() == reflect.Map && fieldValue.IsNil() { - fieldValue.Set(reflect.MakeMap(fieldValue.Type())) - } - - if isLast { - newFieldValue, err := assignValue(fieldValue, value) - if err != nil { - return err - } - fieldValue.Set(newFieldValue) - } else { - err := setValueByPath(fieldValue, pathKeys[1:], value, fullPath) - if err != nil { - return err - } - } - - case reflect.Map: - if currValue.IsNil() { - currValue.Set(reflect.MakeMap(currValue.Type())) - } - - mapKey := reflect.ValueOf(key) - if !mapKey.Type().AssignableTo(currValue.Type().Key()) { - return fmt.Errorf("key type mismatch: expected %s, got %s", currValue.Type().Key(), mapKey.Type()) - } - - var nextValue reflect.Value + current := data + for i := 0; i < len(pathKeys)-1; i++ { + key := pathKeys[i] - if isLast { - val := reflect.ValueOf(value) - if !val.Type().AssignableTo(currValue.Type().Elem()) { - if val.Type().ConvertibleTo(currValue.Type().Elem()) { - val = val.Convert(currValue.Type().Elem()) - } else { - return fmt.Errorf("value type mismatch for key %s: expected %s, got %s", key, currValue.Type().Elem(), val.Type()) - } - } - currValue.SetMapIndex(mapKey, val) - } else { - nextValue = currValue.MapIndex(mapKey) - if !nextValue.IsValid() { - nextValue = reflect.New(currValue.Type().Elem()).Elem() + if existing, ok := current[key]; ok { + if existingMap, ok := existing.(map[string]any); ok { + current = existingMap } else { - nextValue = makeAddressable(nextValue) + newMap := make(map[string]any) + current[key] = newMap + current = newMap } - - err := setValueByPath(nextValue, pathKeys[1:], value, fullPath) - if err != nil { - return err - } - - currValue.SetMapIndex(mapKey, nextValue) - } - - default: - return fmt.Errorf("Invalid path: %s", fullPath) - } - - return nil -} - -// assignValue assigns a value to a struct field, performing type conversion if necessary. -// It supports string-to-type conversion, pointer assignment, and type compatibility checks. -// Returns a reflect.Value suitable for assignment or an error if conversion is not possible. -func assignValue(fieldValue reflect.Value, value any) (reflect.Value, error) { - if !fieldValue.CanSet() { - return reflect.Value{}, fmt.Errorf("cannot set field") - } - - fieldType := fieldValue.Type() - valueType := reflect.TypeOf(value) - - if strValue, ok := value.(string); ok { - convertedValue, err := convertValue(strValue, fieldType) - if err == nil { - return reflect.ValueOf(convertedValue), nil - } - } - - if fieldType.Kind() == reflect.Ptr { - elemType := fieldType.Elem() - newValue := reflect.New(elemType) - val := reflect.ValueOf(value) - - if valueType.AssignableTo(fieldType) { - return val, nil - } - - if val.Type().ConvertibleTo(elemType) { - val = val.Convert(elemType) - newValue.Elem().Set(val) - return newValue, nil + } else { + newMap := make(map[string]any) + current[key] = newMap + current = newMap } - - 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) { - return val, nil - } - - if valueType.ConvertibleTo(fieldType) { - return val.Convert(fieldType), nil - } - - return reflect.Value{}, fmt.Errorf("cannot assign value of type %s to field of type %s", valueType, fieldType) + current[pathKeys[len(pathKeys)-1]] = value } -// 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 - } - addr := reflect.New(v.Type()) - addr.Elem().Set(v) - return addr.Elem() -} - -// parsePath parses a path string into a slice of keys, supporting both dot and bracket notation. +// parsePath splits a hierarchical path string into its individual key segments. +// It supports dotted notation and bracket notation for keys, returning a slice of key strings. +// For example, "foo.bar[baz]" would be parsed into []string{"foo", "bar", "baz"}. func parsePath(path string) []string { var keys []string var currentKey strings.Builder @@ -1233,106 +1055,3 @@ 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/config_handler_private_test.go b/pkg/config/config_handler_private_test.go index bc51b19c4..734eed548 100644 --- a/pkg/config/config_handler_private_test.go +++ b/pkg/config/config_handler_private_test.go @@ -1,3929 +1,866 @@ package config import ( - "fmt" "os" "path/filepath" - "reflect" - "strings" "testing" - "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/api/v1alpha1/cluster" - "github.com/windsorcli/cli/api/v1alpha1/vm" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" ) -// 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) - } - }) +// ============================================================================= +// Test Setup +// ============================================================================= - 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"} +func setupPrivateTestHandler(t *testing.T) (*configHandler, string) { + t.Helper() - // When calling getValueByPath with mismatched key type - value := getValueByPath(current, pathKeys) + tmpDir := t.TempDir() + injector := di.NewInjector() - // Then nil should be returned due to key type mismatch - if value != nil { - t.Errorf("Expected value to be nil, got %v", value) - } - }) + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return tmpDir, nil + } + injector.Register("shell", mockShell) - 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"} + handler := NewConfigHandler(injector).(*configHandler) + handler.Initialize() - // When calling getValueByPath with a valid key - value := getValueByPath(current, pathKeys) + return handler, tmpDir +} - // 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) - } - }) +// ============================================================================= +// Test Private Methods +// ============================================================================= - 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"` +func TestConfigHandler_getValueByPathFromMap(t *testing.T) { + t.Run("ReturnsValueFromSimplePath", func(t *testing.T) { + data := map[string]any{ + "key": "value", } - 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) + result := getValueByPathFromMap(data, []string{"key"}) - // 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) + if result != "value" { + t.Errorf("Expected 'value', got '%v'", result) } }) - 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"` + t.Run("NavigatesNestedMaps", func(t *testing.T) { + data := map[string]any{ + "parent": map[string]any{ + "child": map[string]any{ + "key": "nested_value", + }, + }, } - 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) + result := getValueByPathFromMap(data, []string{"parent", "child", "key"}) - // 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) + if result != "nested_value" { + t.Errorf("Expected 'nested_value', got '%v'", result) } }) - 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"` + t.Run("ReturnsNilForMissingKey", func(t *testing.T) { + data := map[string]any{ + "key": "value", } - 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) + result := getValueByPathFromMap(data, []string{"missing"}) - // 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) + if result != nil { + t.Errorf("Expected nil, got '%v'", result) } }) - 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"` + t.Run("ReturnsNilForEmptyPath", func(t *testing.T) { + data := map[string]any{ + "key": "value", } - 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) + result := getValueByPathFromMap(data, []string{}) - // 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) + if result != nil { + t.Errorf("Expected nil for empty path, got '%v'", result) } }) - 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"` + t.Run("ReturnsNilForMissingIntermediateKey", func(t *testing.T) { + data := map[string]any{ + "parent": "not_a_map", } - 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) + result := getValueByPathFromMap(data, []string{"parent", "child"}) - // 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) + if result != nil { + t.Errorf("Expected nil when intermediate key is not a map, got '%v'", result) } }) } -func Test_parsePath(t *testing.T) { - t.Run("EmptyPath", func(t *testing.T) { - // Given an empty path string to parse - path := "" +func TestConfigHandler_setValueInMap(t *testing.T) { + t.Run("SetsSingleLevelValue", func(t *testing.T) { + data := make(map[string]any) - // When calling parsePath with the empty string - pathKeys := parsePath(path) + setValueInMap(data, []string{"key"}, "value") - // Then an empty slice should be returned - if len(pathKeys) != 0 { - t.Errorf("Expected pathKeys to be empty, got %v", pathKeys) + if data["key"] != "value" { + t.Errorf("Expected value to be set, got %v", data) } }) - t.Run("SingleKey", func(t *testing.T) { - // Given a path with a single key - path := "key" + t.Run("CreatesNestedMaps", func(t *testing.T) { + data := make(map[string]any) - // When calling parsePath with a single key - pathKeys := parsePath(path) + setValueInMap(data, []string{"parent", "child", "key"}, "nested_value") - // 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) + parent, ok := data["parent"].(map[string]any) + if !ok { + t.Fatal("Expected parent to be a map") } - }) - 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) + child, ok := parent["child"].(map[string]any) + if !ok { + t.Fatal("Expected child to be a map") } - }) - 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) + if child["key"] != "nested_value" { + t.Errorf("Expected 'nested_value', got '%v'", child["key"]) } }) - t.Run("MixedDotAndBracketNotation", func(t *testing.T) { - // Given a path with mixed dot and bracket notation - path := "key1.key2[key3].key4[key5]" + t.Run("OverwritesExistingValue", func(t *testing.T) { + data := map[string]any{ + "key": "old_value", + } - // When calling parsePath with mixed notation - pathKeys := parsePath(path) + setValueInMap(data, []string{"key"}, "new_value") - // 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) + if data["key"] != "new_value" { + t.Errorf("Expected value to be overwritten, got %v", data["key"]) } }) - t.Run("DotInsideBrackets", func(t *testing.T) { - // Given a path with a dot inside bracket notation - path := "key1[key2.key3]" + t.Run("HandlesEmptyPathGracefully", func(t *testing.T) { + data := make(map[string]any) - // When calling parsePath with a dot inside brackets - pathKeys := parsePath(path) + setValueInMap(data, []string{}, "value") - // 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) + if len(data) != 0 { + t.Error("Expected empty path to be handled gracefully") } }) } -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 +func TestConfigHandler_convertInterfaceMap(t *testing.T) { + t.Run("ConvertsSimpleInterfaceMap", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + input := map[interface{}]interface{}{ + "key1": "value1", + "key2": "value2", } - fieldValue := reflect.ValueOf(&unexportedField).Elem().Field(0) - // When attempting to assign a value to it - _, err := assignValue(fieldValue, 10) + result := handler.convertInterfaceMap(input) - // Then an error should be returned - if err == nil { - t.Errorf("Expected an error for non-settable field, got nil") + if result["key1"] != "value1" { + t.Errorf("Expected key1='value1', got '%v'", result["key1"]) } - expectedError := "cannot set field" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + if result["key2"] != "value2" { + t.Errorf("Expected key2='value2', got '%v'", result["key2"]) } }) - t.Run("PointerTypeMismatchNonConvertible", func(t *testing.T) { - // Given a pointer field of type *int - var field *int - fieldValue := reflect.ValueOf(&field).Elem() + t.Run("ConvertsNestedInterfaceMaps", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + input := map[interface{}]interface{}{ + "parent": map[interface{}]interface{}{ + "child": "nested_value", + }, + } - // When attempting to assign a string value to it - value := "not an int" - _, err := assignValue(fieldValue, value) + result := handler.convertInterfaceMap(input) - // Then an error should be returned indicating type mismatch - if err == nil { - t.Errorf("Expected an error for pointer type mismatch, got nil") + parent, ok := result["parent"].(map[string]any) + if !ok { + t.Fatal("Expected parent to be converted to map[string]any") } - 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()) + + if parent["child"] != "nested_value" { + t.Errorf("Expected nested value, got '%v'", parent["child"]) } }) - t.Run("ValueTypeMismatchNonConvertible", func(t *testing.T) { - // Given a field of type int - var field int - fieldValue := reflect.ValueOf(&field).Elem() + t.Run("SkipsNonStringKeys", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When attempting to assign a non-convertible string value to it - value := "not convertible to int" - _, err := assignValue(fieldValue, value) + input := map[interface{}]interface{}{ + 123: "numeric_key", + "key": "string_key", + } + + result := handler.convertInterfaceMap(input) - // Then an error should be returned indicating type mismatch - if err == nil { - t.Errorf("Expected an error for non-convertible type mismatch, got nil") + if len(result) != 1 { + t.Errorf("Expected only string keys to be included, got %d keys", len(result)) } - 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()) + if result["key"] != "string_key" { + t.Error("Expected string key to be included") } }) } -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) - } +func TestConfigHandler_separateStaticAndDynamicFields(t *testing.T) { + t.Run("SeparatesBasedOnStaticSchema", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // And the result should be a bool - if result != true { - t.Errorf("Expected true, got %v", result) + data := map[string]any{ + "provider": "generic", + "cluster": map[string]any{"enabled": true}, + "custom_key": "custom_value", + "another_key": "another_value", } - }) - t.Run("ConvertStringToInt", func(t *testing.T) { - // Given a string value that can be converted to int - value := "42" - targetType := reflect.TypeOf(int(0)) + staticFields, dynamicFields := handler.separateStaticAndDynamicFields(data) - // 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 staticFields["provider"] != "generic" { + t.Error("Expected provider in static fields") } - - // And the result should be an int - if result != 42 { - t.Errorf("Expected 42, got %v", result) + if staticFields["cluster"] == nil { + t.Error("Expected cluster in static fields") } - }) - - 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) + if staticFields["custom_key"] != nil { + t.Error("Expected custom_key NOT in static fields") } - // And the result should be a float - if result != 3.14 { - t.Errorf("Expected 3.14, got %v", result) + if dynamicFields["custom_key"] != "custom_value" { + t.Error("Expected custom_key in dynamic fields") + } + if dynamicFields["another_key"] != "another_value" { + t.Error("Expected another_key in dynamic fields") + } + if dynamicFields["provider"] != nil { + t.Error("Expected provider NOT in dynamic fields") } }) +} - 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) +func TestConfigHandler_isKeyInStaticSchema(t *testing.T) { + t.Run("ReturnsTrueForStaticFields", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } + staticKeys := []string{"provider", "cluster", "dns", "docker", "git", "network", "terraform", "vm"} + for _, key := range staticKeys { + result := handler.isKeyInStaticSchema(key) - // 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) + if !result { + t.Errorf("Expected '%s' to be in static schema", key) + } } }) - t.Run("UnsupportedType", func(t *testing.T) { - // Given a string value and an unsupported target type - value := "test" - targetType := reflect.TypeOf([]string{}) + t.Run("ReturnsFalseForDynamicFields", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When converting the value - _, err := convertValue(value, targetType) + dynamicKeys := []string{"custom_key", "user_field", "dev", "my_variable"} + for _, key := range dynamicKeys { + result := handler.isKeyInStaticSchema(key) - // 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()) + if result { + t.Errorf("Expected '%s' NOT to be in static schema", key) + } } }) - t.Run("InvalidNumericValue", func(t *testing.T) { - // Given an invalid numeric string value - value := "not a number" - targetType := reflect.TypeOf(int(0)) + t.Run("HandlesNestedPaths", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When converting the value - _, err := convertValue(value, targetType) + result := handler.isKeyInStaticSchema("cluster.workers.count") - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for invalid numeric value") + if !result { + t.Error("Expected nested cluster path to be recognized as static") } }) +} - 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)), - } +func TestConfigHandler_deepMerge(t *testing.T) { + t.Run("MergesSimpleMaps", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When converting the value to each type - for _, targetType := range targetTypes { - result, err := convertValue(value, targetType) + base := map[string]any{ + "key1": "value1", + } + overlay := map[string]any{ + "key2": "value2", + } - // Then no error should be returned - if err != nil { - t.Fatalf("convertValue() unexpected error for %v: %v", targetType, err) - } + result := handler.deepMerge(base, overlay) - // 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) - } - } + if result["key1"] != "value1" { + t.Error("Expected base key to be preserved") + } + if result["key2"] != "value2" { + t.Error("Expected overlay key to be added") } }) - 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)), - } + t.Run("OverridesNonMapValues", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When converting the value to each type - for _, targetType := range targetTypes { - result, err := convertValue(value, targetType) + base := map[string]any{ + "key": "old_value", + } + overlay := map[string]any{ + "key": "new_value", + } - // Then no error should be returned - if err != nil { - t.Fatalf("convertValue() unexpected error for %v: %v", targetType, err) - } + result := handler.deepMerge(base, overlay) - // 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) - } - } + if result["key"] != "new_value" { + t.Errorf("Expected 'new_value', got '%v'", result["key"]) } }) - 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) + t.Run("MergesNestedMaps", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Then no error should be returned - if err != nil { - t.Fatalf("convertValue() unexpected error: %v", err) + base := map[string]any{ + "parent": map[string]any{ + "key1": "value1", + }, } - - // And the value should be correctly converted - if result != float32(3.14) { - t.Errorf("convertValue() = %v, want %v", result, float32(3.14)) + overlay := map[string]any{ + "parent": map[string]any{ + "key2": "value2", + }, } - }) - - 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) + result := handler.deepMerge(base, overlay) - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for float overflow") + parent, ok := result["parent"].(map[string]any) + if !ok { + t.Fatal("Expected parent to be a map") } - // 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()) + if parent["key1"] != "value1" { + t.Error("Expected nested base key to be preserved") + } + if parent["key2"] != "value2" { + t.Error("Expected nested overlay key to be added") } }) } -func TestConfigHandler_SetDefault(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler, mocks - } +func TestConfigHandler_mapToContext(t *testing.T) { + t.Run("ConvertsMapToContextStruct", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(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", + data := map[string]any{ + "provider": "test_provider", + "dns": map[string]any{ + "domain": "test.local", }, } - // And a context is set - handler.Set("context", "local") - - // When setting the default context - err := handler.SetDefault(defaultContext) + result := handler.mapToContext(data) - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) + if result == nil { + t.Fatal("Expected non-nil result") } - - // And the default context should be set correctly - if handler.(*configHandler).defaultContextConfig.Environment["ENV_VAR"] != "value" { - t.Errorf("SetDefault() = %v, expected %v", handler.(*configHandler).defaultContextConfig.Environment["ENV_VAR"], "value") + if result.Provider == nil || *result.Provider != "test_provider" { + t.Error("Expected provider to be converted") + } + if result.DNS == nil || result.DNS.Domain == nil || *result.DNS.Domain != "test.local" { + t.Error("Expected dns.domain to be converted") } }) - t.Run("SetDefaultWithNoContext", func(t *testing.T) { - // Given a handler with no context set - handler, _ := setup(t) - handler.(*configHandler).context = "" - defaultContext := v1alpha1.Context{ - Environment: map[string]string{ - "ENV_VAR": "value", + t.Run("ExcludesNodesFieldWithYamlDashTag", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + data := map[string]any{ + "cluster": map[string]any{ + "workers": map[string]any{ + "count": float64(2), + "nodes": map[string]any{ + "worker-1": map[string]any{ + "endpoint": "127.0.0.1:50001", + }, + }, + }, }, } - // When setting the default context - err := handler.SetDefault(defaultContext) + result := handler.mapToContext(data) - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) + if result == nil || result.Cluster == nil { + t.Fatal("Expected cluster.workers to exist") } - // And the default context should be set correctly - if handler.(*configHandler).defaultContextConfig.Environment["ENV_VAR"] != "value" { - t.Errorf("SetDefault() = %v, expected %v", handler.(*configHandler).defaultContextConfig.Environment["ENV_VAR"], "value") - } - }) - - t.Run("SetDefaultUsedInSubsequentOperations", func(t *testing.T) { - // Given a handler with an existing context - handler, _ := setup(t) - handler.(*configHandler).context = "existing-context" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "existing-context": {}, + if len(result.Cluster.Workers.Nodes) > 0 { + t.Error("Expected nodes field to be excluded due to yaml:\"-\" tag") } - // And a default context configuration - defaultConf := v1alpha1.Context{ - Environment: map[string]string{"DEFAULT_VAR": "default_val"}, + if result.Cluster.Workers.Count == nil || *result.Cluster.Workers.Count != 2 { + t.Error("Expected count field to be included") } + }) - // When setting the default context - err := handler.SetDefault(defaultConf) + t.Run("HandlesMarshalError", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Then no error should be returned - if err != nil { - t.Fatalf("SetDefault() unexpected error: %v", err) + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, os.ErrInvalid } - // And the default context should be set correctly - if handler.(*configHandler).defaultContextConfig.Environment == nil || handler.(*configHandler).defaultContextConfig.Environment["DEFAULT_VAR"] != "default_val" { - t.Errorf("Expected defaultContextConfig environment to be %v, got %v", defaultConf.Environment, handler.(*configHandler).defaultContextConfig.Environment) - } + result := handler.mapToContext(map[string]any{"test": "value"}) - // And the existing context should not be modified - if handler.(*configHandler).config.Contexts["existing-context"] == nil { - t.Errorf("SetDefault incorrectly overwrote existing context config") + if result == nil { + t.Error("Expected empty context on marshal error, got nil") } }) - t.Run("SetDefaultMergesWithExistingContext", func(t *testing.T) { - // Given a handler with an existing context containing some values - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "test": { - ID: ptrString("existing-id"), - VM: &vm.VMConfig{ - Driver: ptrString("docker-desktop"), - }, - Environment: map[string]string{ - "EXISTING_VAR": "existing_value", - "OVERRIDE_VAR": "context_value", - }, - }, - } + t.Run("HandlesUnmarshalError", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // And a default context with overlapping and additional values - defaultContext := v1alpha1.Context{ - VM: &vm.VMConfig{ - CPU: ptrInt(4), - }, - Environment: map[string]string{ - "DEFAULT_VAR": "default_value", - "OVERRIDE_VAR": "default_value", - }, + handler.shims.YamlUnmarshal = func(data []byte, v any) error { + return os.ErrInvalid } - // When setting the default context - err := handler.SetDefault(defaultContext) - - // Then no error should be returned - if err != nil { - t.Fatalf("SetDefault() unexpected error: %v", err) - } + result := handler.mapToContext(map[string]any{"test": "value"}) - // And the context should merge defaults with existing values - ctx := handler.(*configHandler).config.Contexts["test"] - if ctx == nil { - t.Fatal("Context was removed during SetDefault") + if result == nil { + t.Error("Expected empty context on unmarshal error, got nil") } + }) +} - // Existing values should be preserved - if ctx.ID == nil || *ctx.ID != "existing-id" { - t.Errorf("Expected ID to be preserved as 'existing-id', got %v", ctx.ID) - } - if ctx.VM.Driver == nil || *ctx.VM.Driver != "docker-desktop" { - t.Errorf("Expected VM driver to be preserved as 'docker-desktop', got %v", ctx.VM.Driver) - } - if ctx.Environment["EXISTING_VAR"] != "existing_value" { - t.Errorf("Expected EXISTING_VAR to be preserved as 'existing_value', got '%s'", ctx.Environment["EXISTING_VAR"]) - } - if ctx.Environment["OVERRIDE_VAR"] != "context_value" { - t.Errorf("Expected OVERRIDE_VAR to keep context value 'context_value', got '%s'", ctx.Environment["OVERRIDE_VAR"]) - } +func TestConfigHandler_parsePath(t *testing.T) { + t.Run("ParsesSimpleDotNotation", func(t *testing.T) { + result := parsePath("parent.child.key") - // Default values should be added where not present - if ctx.VM.CPU == nil || *ctx.VM.CPU != 4 { - t.Errorf("Expected VM CPU to be added from default as 4, got %v", ctx.VM.CPU) + expected := []string{"parent", "child", "key"} + if len(result) != len(expected) { + t.Fatalf("Expected %d keys, got %d", len(expected), len(result)) } - if ctx.Environment["DEFAULT_VAR"] != "default_value" { - t.Errorf("Expected DEFAULT_VAR to be added from default as 'default_value', got '%s'", ctx.Environment["DEFAULT_VAR"]) + for i, key := range expected { + if result[i] != key { + t.Errorf("Expected key[%d]='%s', got '%s'", i, key, result[i]) + } } }) - t.Run("SetDefaultMergesComplexNestedStructures", func(t *testing.T) { - // Given a handler with an existing context containing some values - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "test": { - ID: ptrString("existing-id"), - Environment: map[string]string{ - "EXISTING_VAR": "existing_value", - }, - }, - } + t.Run("ParsesBracketNotation", func(t *testing.T) { + result := parsePath("parent[child].key") - // And a default context with additional values - defaultContext := v1alpha1.Context{ - Environment: map[string]string{ - "DEFAULT_VAR": "default_value", - }, + expected := []string{"parent", "child", "key"} + if len(result) != len(expected) { + t.Fatalf("Expected %d keys, got %d", len(expected), len(result)) } - - // When setting the default context - err := handler.SetDefault(defaultContext) - - // Then no error should be returned - if err != nil { - t.Fatalf("SetDefault() unexpected error: %v", err) + for i, key := range expected { + if result[i] != key { + t.Errorf("Expected key[%d]='%s', got '%s'", i, key, result[i]) + } } + }) - // And the context should have both existing and default values - ctx := handler.(*configHandler).config.Contexts["test"] - if ctx == nil { - t.Fatal("Context was removed during SetDefault") - } + t.Run("HandlesMixedNotation", func(t *testing.T) { + result := parsePath("a.b[c].d") - // Existing values should be preserved - if ctx.ID == nil || *ctx.ID != "existing-id" { - t.Errorf("Expected ID to be preserved as 'existing-id', got %v", ctx.ID) - } - if ctx.Environment["EXISTING_VAR"] != "existing_value" { - t.Errorf("Expected EXISTING_VAR to be preserved as 'existing_value', got '%s'", ctx.Environment["EXISTING_VAR"]) + expected := []string{"a", "b", "c", "d"} + if len(result) != len(expected) { + t.Fatalf("Expected %d keys, got %d", len(expected), len(result)) } - - // Default values should be added where not present - if ctx.Environment["DEFAULT_VAR"] != "default_value" { - t.Errorf("Expected DEFAULT_VAR to be added from default as 'default_value', got '%s'", ctx.Environment["DEFAULT_VAR"]) + for i, key := range expected { + if result[i] != key { + t.Errorf("Expected key[%d]='%s', got '%s'", i, key, result[i]) + } } }) - t.Run("SetDefaultWithNilContextsMap", func(t *testing.T) { - // Given a handler with a nil contexts map - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).config.Contexts = nil + t.Run("HandlesSingleKey", func(t *testing.T) { + result := parsePath("key") - // And a default context - defaultContext := v1alpha1.Context{ - Environment: map[string]string{ - "DEFAULT_VAR": "default_value", - }, + if len(result) != 1 || result[0] != "key" { + t.Errorf("Expected ['key'], got %v", result) } + }) +} - // When setting the default context - err := handler.SetDefault(defaultContext) - - // Then no error should be returned - if err != nil { - t.Fatalf("SetDefault() unexpected error: %v", err) - } +func TestConfigHandler_convertStringValue(t *testing.T) { + t.Run("ConvertsTrueString", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // And the contexts map should be created with the default - if handler.(*configHandler).config.Contexts == nil { - t.Fatal("Expected contexts map to be created") - } + result := handler.convertStringValue("true") - ctx := handler.(*configHandler).config.Contexts["test"] - if ctx == nil { - t.Fatal("Expected test context to be created") - } - if ctx.Environment["DEFAULT_VAR"] != "default_value" { - t.Errorf("Expected DEFAULT_VAR to be 'default_value', got '%s'", ctx.Environment["DEFAULT_VAR"]) + if result != true { + t.Errorf("Expected boolean true, got %v (%T)", result, result) } }) -} -func TestConfigHandler_SetContextValue(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).path = filepath.Join(t.TempDir(), "config.yaml") - return handler, mocks - } + t.Run("ConvertsFalseString", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - t.Run("Success", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "test" + result := handler.convertStringValue("false") - // And a context with an empty environment map - actualContext := handler.GetContext() - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - actualContext: {}, + if result != false { + t.Errorf("Expected boolean false, got %v (%T)", result, result) } + }) - // When setting a value in the context environment - err := handler.SetContextValue("environment.TEST_VAR", "test_value") + t.Run("ConvertsIntegerString", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Then no error should be returned - if err != nil { - t.Fatalf("SetContextValue() unexpected error: %v", err) - } + result := handler.convertStringValue("42") - // And the value should be correctly set in the context - expected := "test_value" - if val := handler.(*configHandler).config.Contexts[actualContext].Environment["TEST_VAR"]; val != expected { - t.Errorf("SetContextValue() did not correctly set value, expected %s, got %s", expected, val) + if result != 42 { + t.Errorf("Expected integer 42, got %v (%T)", result, result) } }) - t.Run("EmptyPath", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) + t.Run("ConvertsFloatString", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When attempting to set a value with an empty path - err := handler.SetContextValue("", "test_value") + result := handler.convertStringValue("3.14") - // Then an error should be returned - if err == nil { - t.Errorf("SetContextValue() with empty path did not return an error") + if result != 3.14 { + t.Errorf("Expected float 3.14, got %v (%T)", result, result) } + }) + + t.Run("LeavesOtherStringsAsIs", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // And the error message should be as expected - expectedErr := "path cannot be empty" - if err.Error() != expectedErr { - t.Errorf("Expected error message '%s', got '%s'", expectedErr, err.Error()) + result := handler.convertStringValue("regular_string") + + if result != "regular_string" { + t.Errorf("Expected 'regular_string', got %v (%T)", result, result) } }) - t.Run("SetFails", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "test" + t.Run("PassesThroughNonStringValues", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When attempting to set a value with an invalid path - err := handler.SetContextValue("invalid..path", "test_value") + result := handler.convertStringValue(42) - // Then an error should be returned - if err == nil { - t.Errorf("SetContextValue() with invalid path did not return an error") + if result != 42 { + t.Errorf("Expected 42 to pass through, got %v", result) } }) - t.Run("ConvertStringToBool", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": {}, - } + t.Run("ConvertsWithSchemaTypeInfo", func(t *testing.T) { + // Given a config handler with schema type information + handler, tmpDir := setupPrivateTestHandler(t) - // Set initial bool value - if err := handler.SetContextValue("environment.BOOL_VAR", "true"); err != nil { - t.Fatalf("Failed to set initial bool value: %v", err) - } + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + typed_bool: + type: boolean + typed_int: + type: integer + typed_number: + type: number +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) - // Override with string "false" - if err := handler.SetContextValue("environment.BOOL_VAR", "false"); err != nil { - t.Fatalf("Failed to set string bool value: %v", err) - } + // When converting strings with schema type info + boolResult := handler.convertStringValue("true") + intResult := handler.convertStringValue("42") - val := handler.GetString("environment.BOOL_VAR") - if val != "false" { - t.Errorf("Expected false, got %v", val) + // Then values should be converted according to schema + if boolResult != true { + t.Errorf("Expected true, got %v", boolResult) + } + if intResult != 42 { + t.Errorf("Expected 42, got %v", intResult) } }) - t.Run("ConvertStringToInt", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": {}, - } + t.Run("FallsBackToPatternMatching", func(t *testing.T) { + // Given a config handler without schema + handler, _ := setupPrivateTestHandler(t) - // Set initial int value - if err := handler.SetContextValue("environment.INT_VAR", "42"); err != nil { - t.Fatalf("Failed to set initial int value: %v", err) - } + // When converting strings without schema + boolResult := handler.convertStringValue("false") + intResult := handler.convertStringValue("123") + floatResult := handler.convertStringValue("1.5") - // Override with string "100" - if err := handler.SetContextValue("environment.INT_VAR", "100"); err != nil { - t.Fatalf("Failed to set string int value: %v", err) + // Then values should be converted using pattern matching + if boolResult != false { + t.Errorf("Expected false from pattern, got %v", boolResult) } - - val := handler.GetString("environment.INT_VAR") - if val != "100" { - t.Errorf("Expected 100, got %v", val) + if intResult != 123 { + t.Errorf("Expected 123 from pattern, got %v", intResult) } - }) - - t.Run("ConvertStringToFloat", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": {}, + if floatResult != 1.5 { + t.Errorf("Expected 1.5 from pattern, got %v", floatResult) } + }) +} - // 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) - } +func TestConfigHandler_convertStringToType(t *testing.T) { + t.Run("ConvertsBooleanTrue", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // 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) - } + result := handler.convertStringToType("true", "boolean") - val := handler.GetString("environment.FLOAT_VAR") - if val != "6.28" { - t.Errorf("Expected 6.28, got %v", val) + if result != true { + t.Errorf("Expected true, got %v", result) } }) - t.Run("ConvertStringToBoolPointer", func(t *testing.T) { - // Given a handler with a default context - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": {}, - } + t.Run("ConvertsBooleanFalse", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // When setting a string "false" to a bool pointer field (dns.enabled) - if err := handler.SetContextValue("dns.enabled", "false"); err != nil { - t.Fatalf("Failed to set dns.enabled=false from string: %v", err) - } + result := handler.convertStringToType("false", "boolean") - // Then the value should be correctly set as a boolean - config := handler.GetConfig() - if config.DNS == nil || config.DNS.Enabled == nil || *config.DNS.Enabled != false { - t.Errorf("Expected dns.enabled to be false, got %v", config.DNS.Enabled) + if result != false { + t.Errorf("Expected false, got %v", result) } + }) - // And when setting "true" as well - if err := handler.SetContextValue("dns.enabled", "true"); err != nil { - t.Fatalf("Failed to set dns.enabled=true from string: %v", err) - } + t.Run("ConvertsBooleanCaseInsensitive", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + result := handler.convertStringToType("TRUE", "boolean") - config = handler.GetConfig() - if config.DNS == nil || config.DNS.Enabled == nil || *config.DNS.Enabled != true { - t.Errorf("Expected dns.enabled to be true, got %v", config.DNS.Enabled) + if result != true { + t.Errorf("Expected true for 'TRUE', got %v", result) } }) - t.Run("SchemaRoutingAndInitialization", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "test" + t.Run("ConvertsInteger", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Test invalid path formats - err := handler.SetContextValue("..invalid", "value") - if err == nil { - t.Error("Expected error for invalid path") - } + result := handler.convertStringToType("42", "integer") - // Test static schema routing (goes to context config) - err = handler.SetContextValue("environment.STATIC_VAR", "static_value") - if err != nil { - t.Fatalf("Failed to set static schema value: %v", err) - } - if handler.(*configHandler).config.Contexts["test"].Environment["STATIC_VAR"] != "static_value" { - t.Error("Static value should be in context config") + if result != 42 { + t.Errorf("Expected 42, got %v", result) } + }) - // Test dynamic schema routing (goes to contextValues) - err = handler.SetContextValue("dynamic_key", "dynamic_value") - if err != nil { - t.Fatalf("Failed to set dynamic schema value: %v", err) - } - if handler.(*configHandler).contextValues["dynamic_key"] != "dynamic_value" { - t.Error("Dynamic value should be in contextValues") - } + t.Run("ConvertsNumber", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Test initialization when not loaded - handler.(*configHandler).loaded = false - handler.(*configHandler).contextValues = nil - err = handler.SetContextValue("not_loaded_key", "not_loaded_value") - if err != nil { - t.Fatalf("Failed to set value when not loaded: %v", err) - } - if handler.(*configHandler).contextValues["not_loaded_key"] != "not_loaded_value" { - t.Error("contextValues should be initialized even when not loaded") + result := handler.convertStringToType("3.14", "number") + + if result != 3.14 { + t.Errorf("Expected 3.14, got %v", result) } }) - t.Run("SchemaAwareTypeConversion", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).loaded = true + t.Run("ReturnsStringForStringType", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Set up schema validator with type definitions - handler.(*configHandler).schemaValidator = &SchemaValidator{ - Schema: map[string]any{ - "properties": map[string]any{ - "dev": map[string]any{ - "type": "boolean", - }, - "port": map[string]any{ - "type": "integer", - }, - "ratio": map[string]any{ - "type": "number", - }, - "name": map[string]any{ - "type": "string", - }, - }, - }, - } + result := handler.convertStringToType("test", "string") - // Test boolean conversion - err := handler.SetContextValue("dev", "true") - if err != nil { - t.Fatalf("Failed to set boolean value: %v", err) - } - if handler.(*configHandler).contextValues["dev"] != true { - t.Errorf("Expected boolean true, got %v (%T)", handler.(*configHandler).contextValues["dev"], handler.(*configHandler).contextValues["dev"]) + if result != "test" { + t.Errorf("Expected 'test', got %v", result) } + }) - // Test integer conversion - err = handler.SetContextValue("port", "8080") - if err != nil { - t.Fatalf("Failed to set integer value: %v", err) - } - if handler.(*configHandler).contextValues["port"] != 8080 { - t.Errorf("Expected integer 8080, got %v (%T)", handler.(*configHandler).contextValues["port"], handler.(*configHandler).contextValues["port"]) - } + t.Run("ReturnsNilForInvalidBoolean", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Test number conversion - err = handler.SetContextValue("ratio", "3.14") - if err != nil { - t.Fatalf("Failed to set number value: %v", err) - } - if handler.(*configHandler).contextValues["ratio"] != 3.14 { - t.Errorf("Expected number 3.14, got %v (%T)", handler.(*configHandler).contextValues["ratio"], handler.(*configHandler).contextValues["ratio"]) - } + result := handler.convertStringToType("not_a_bool", "boolean") - // Test string conversion (should remain string) - err = handler.SetContextValue("name", "test") - if err != nil { - t.Fatalf("Failed to set string value: %v", err) - } - if handler.(*configHandler).contextValues["name"] != "test" { - t.Errorf("Expected string 'test', got %v (%T)", handler.(*configHandler).contextValues["name"], handler.(*configHandler).contextValues["name"]) + if result != nil { + t.Errorf("Expected nil for invalid boolean, got %v", result) } }) - t.Run("FallbackPatternConversion", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).loaded = true + t.Run("ReturnsNilForInvalidInteger", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // No schema validator - should use pattern matching + result := handler.convertStringToType("not_an_int", "integer") - // Test boolean pattern matching - err := handler.SetContextValue("enabled", "true") - if err != nil { - t.Fatalf("Failed to set boolean value: %v", err) - } - if handler.(*configHandler).contextValues["enabled"] != true { - t.Errorf("Expected boolean true, got %v (%T)", handler.(*configHandler).contextValues["enabled"], handler.(*configHandler).contextValues["enabled"]) + if result != nil { + t.Errorf("Expected nil for invalid integer, got %v", result) } + }) - // Test integer pattern matching - err = handler.SetContextValue("count", "42") - if err != nil { - t.Fatalf("Failed to set integer value: %v", err) - } - if handler.(*configHandler).contextValues["count"] != 42 { - t.Errorf("Expected integer 42, got %v (%T)", handler.(*configHandler).contextValues["count"], handler.(*configHandler).contextValues["count"]) - } + t.Run("ReturnsNilForInvalidNumber", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Test float pattern matching - err = handler.SetContextValue("rate", "2.5") - if err != nil { - t.Fatalf("Failed to set float value: %v", err) - } - if handler.(*configHandler).contextValues["rate"] != 2.5 { - t.Errorf("Expected float 2.5, got %v (%T)", handler.(*configHandler).contextValues["rate"], handler.(*configHandler).contextValues["rate"]) + result := handler.convertStringToType("not_a_number", "number") + + if result != nil { + t.Errorf("Expected nil for invalid number, got %v", result) } }) - t.Run("SchemaConversionFailure", func(t *testing.T) { - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).loaded = true - - // Set up schema validator with boolean type and validation support - mockShell := handler.(*configHandler).shell - mockValidator := NewSchemaValidator(mockShell) - mockValidator.Schema = map[string]any{ - "properties": map[string]any{ - "dev": map[string]any{ - "type": "boolean", - }, - }, - } - handler.(*configHandler).schemaValidator = mockValidator + t.Run("ReturnsNilForUnknownType", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Test invalid boolean value - should now fail validation - err := handler.SetContextValue("dev", "invalid") - if err == nil { - t.Fatal("Expected validation error for invalid boolean value, got nil") - } - if !strings.Contains(err.Error(), "validation failed") && !strings.Contains(err.Error(), "type mismatch") { - t.Errorf("Expected validation error, got: %v", err) + result := handler.convertStringToType("value", "unknown_type") + + if result != nil { + t.Errorf("Expected nil for unknown type, got %v", result) } }) } -func TestConfigHandler_convertStringValue(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler - } +func TestConfigHandler_convertStringByPattern(t *testing.T) { + t.Run("RecognizesBooleanTrue", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - t.Run("NonStringValue", func(t *testing.T) { - handler := setup(t) - - // Non-string values should be returned as-is - result := handler.(*configHandler).convertStringValue(42) - if result != 42 { - t.Errorf("Expected 42, got %v", result) - } + result := handler.convertStringByPattern("true") - result = handler.(*configHandler).convertStringValue(true) if result != true { t.Errorf("Expected true, got %v", result) } }) - t.Run("SchemaAwareConversion", func(t *testing.T) { - handler := setup(t) + t.Run("RecognizesBooleanFalse", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - // Set up schema validator - handler.(*configHandler).schemaValidator = &SchemaValidator{ - Schema: map[string]any{ - "properties": map[string]any{ - "enabled": map[string]any{ - "type": "boolean", - }, - "count": map[string]any{ - "type": "integer", - }, - "rate": map[string]any{ - "type": "number", - }, - }, - }, - } - - // Test boolean conversion - result := handler.(*configHandler).convertStringValue("true") - if result != true { - t.Errorf("Expected boolean true, got %v (%T)", result, result) - } - - // Test integer conversion - result = handler.(*configHandler).convertStringValue("42") - if result != 42 { - t.Errorf("Expected integer 42, got %v (%T)", result, result) - } - - // Test number conversion - result = handler.(*configHandler).convertStringValue("3.14") - if result != 3.14 { - t.Errorf("Expected number 3.14, got %v (%T)", result, result) - } - }) - - t.Run("PatternMatchingFallback", func(t *testing.T) { - handler := setup(t) - // No schema validator - should use pattern matching - - // Test boolean pattern - result := handler.(*configHandler).convertStringValue("true") - if result != true { - t.Errorf("Expected boolean true, got %v (%T)", result, result) - } - - result = handler.(*configHandler).convertStringValue("false") - if result != false { - t.Errorf("Expected boolean false, got %v (%T)", result, result) - } - - // Test integer pattern - result = handler.(*configHandler).convertStringValue("123") - if result != 123 { - t.Errorf("Expected integer 123, got %v (%T)", result, result) - } - - // Test float pattern - result = handler.(*configHandler).convertStringValue("45.67") - if result != 45.67 { - t.Errorf("Expected float 45.67, got %v (%T)", result, result) - } - - // Test string (no conversion) - result = handler.(*configHandler).convertStringValue("hello") - if result != "hello" { - t.Errorf("Expected string 'hello', got %v (%T)", result, result) - } - }) -} - -func TestConfigHandler_getExpectedTypeFromSchema(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler - } - - t.Run("ValidSchema", func(t *testing.T) { - handler := setup(t) - - handler.(*configHandler).schemaValidator = &SchemaValidator{ - Schema: map[string]any{ - "properties": map[string]any{ - "enabled": map[string]any{ - "type": "boolean", - }, - "count": map[string]any{ - "type": "integer", - }, - }, - }, - } - - // Test existing property - result := handler.(*configHandler).getExpectedTypeFromSchema("enabled") - if result != "boolean" { - t.Errorf("Expected 'boolean', got '%s'", result) - } - - result = handler.(*configHandler).getExpectedTypeFromSchema("count") - if result != "integer" { - t.Errorf("Expected 'integer', got '%s'", result) - } - - // Test non-existing property - result = handler.(*configHandler).getExpectedTypeFromSchema("nonexistent") - if result != "" { - t.Errorf("Expected empty string, got '%s'", result) - } - }) - - t.Run("NoSchemaValidator", func(t *testing.T) { - handler := setup(t) - // No schema validator - - result := handler.(*configHandler).getExpectedTypeFromSchema("anykey") - if result != "" { - t.Errorf("Expected empty string, got '%s'", result) - } - }) - - t.Run("InvalidSchema", func(t *testing.T) { - handler := setup(t) - - handler.(*configHandler).schemaValidator = &SchemaValidator{ - Schema: map[string]any{ - "properties": "invalid", // Should be map[string]any - }, - } - - result := handler.(*configHandler).getExpectedTypeFromSchema("anykey") - if result != "" { - t.Errorf("Expected empty string, got '%s'", result) - } - }) -} - -func TestConfigHandler_convertStringToType(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler - } - - handler := setup(t) - - t.Run("BooleanConversion", func(t *testing.T) { - result := handler.(*configHandler).convertStringToType("true", "boolean") - if result != true { - t.Errorf("Expected true, got %v", result) - } - - result = handler.(*configHandler).convertStringToType("false", "boolean") - if result != false { - t.Errorf("Expected false, got %v", result) - } - - result = handler.(*configHandler).convertStringToType("invalid", "boolean") - if result != nil { - t.Errorf("Expected nil, got %v", result) - } - }) - - t.Run("IntegerConversion", func(t *testing.T) { - result := handler.(*configHandler).convertStringToType("42", "integer") - if result != 42 { - t.Errorf("Expected 42, got %v", result) - } - - result = handler.(*configHandler).convertStringToType("invalid", "integer") - if result != nil { - t.Errorf("Expected nil, got %v", result) - } - }) - - t.Run("NumberConversion", func(t *testing.T) { - result := handler.(*configHandler).convertStringToType("3.14", "number") - if result != 3.14 { - t.Errorf("Expected 3.14, got %v", result) - } - - result = handler.(*configHandler).convertStringToType("invalid", "number") - if result != nil { - t.Errorf("Expected nil, got %v", result) - } - }) - - t.Run("StringConversion", func(t *testing.T) { - result := handler.(*configHandler).convertStringToType("hello", "string") - if result != "hello" { - t.Errorf("Expected 'hello', got %v", result) - } - }) -} - -func TestConfigHandler_LoadConfigString(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.SetContext("test") - - // And a valid YAML configuration string - yamlContent := ` -version: v1alpha1 -contexts: - test: - environment: - TEST_VAR: test_value` - - // When loading the configuration string - err := handler.LoadConfigString(yamlContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("LoadConfigString() unexpected error: %v", err) - } - - // And the value should be correctly loaded - value := handler.GetString("environment.TEST_VAR") - if value != "test_value" { - t.Errorf("Expected TEST_VAR = 'test_value', got '%s'", value) - } - }) - - t.Run("EmptyContent", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - - // When loading an empty configuration string - err := handler.LoadConfigString("") - - // Then no error should be returned - if err != nil { - t.Fatalf("LoadConfigString() unexpected error: %v", err) - } - }) - - t.Run("InvalidYAML", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - - // And an invalid YAML string - yamlContent := `invalid: yaml: content: [}` - - // When loading the invalid YAML - err := handler.LoadConfigString(yamlContent) - - // Then an error should be returned - if err == nil { - t.Fatal("LoadConfigString() expected error for invalid YAML") - } - - // And the error message should indicate YAML unmarshalling failure - if !strings.Contains(err.Error(), "error unmarshalling yaml") { - t.Errorf("Expected error about invalid YAML, got: %v", err) - } - }) - - t.Run("UnsupportedVersion", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - - // And a YAML string with an unsupported version - yamlContent := ` -version: v2alpha1 -contexts: - test: {}` - - // When loading the YAML with unsupported version - err := handler.LoadConfigString(yamlContent) - - // Then an error should be returned - if err == nil { - t.Fatal("LoadConfigString() expected error for unsupported version") - } - - // And the error message should indicate unsupported version - if !strings.Contains(err.Error(), "unsupported config version") { - t.Errorf("Expected error about unsupported version, 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() - - // When making it addressable - result := makeAddressable(v) - - // Then the same value should be returned - if result.Interface() != v.Interface() { - t.Errorf("makeAddressable() = %v, want %v", result.Interface(), v.Interface()) - } - }) - - t.Run("NonAddressable", func(t *testing.T) { - // Given a non-addressable value - v := reflect.ValueOf(42) - - // When making it addressable - result := makeAddressable(v) - - // Then a new addressable value should be returned - if !result.CanAddr() { - t.Error("makeAddressable() returned non-addressable value") - } - if result.Interface() != v.Interface() { - t.Errorf("makeAddressable() = %v, want %v", result.Interface(), v.Interface()) - } - }) - - t.Run("NilValue", func(t *testing.T) { - // Given a nil value - var v reflect.Value - - // When making it addressable - result := makeAddressable(v) - - // Then a zero value should be returned - if result.IsValid() { - t.Error("makeAddressable() returned valid value for nil input") - } - }) -} - -func TestConfigHandler_ConvertValue(t *testing.T) { - t.Run("StringToString", func(t *testing.T) { - // Given a string value and target type - value := "test" - targetType := reflect.TypeOf("") - - // 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) - } - - // And the value should be correctly converted - if result != "test" { - t.Errorf("convertValue() = %v, want %v", result, "test") - } - }) - - t.Run("StringToInt", func(t *testing.T) { - // Given a string value and target type - value := "42" - targetType := reflect.TypeOf(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) - } - - // And the value should be correctly converted - if result != 42 { - t.Errorf("convertValue() = %v, want %v", result, 42) - } - }) - - t.Run("StringToIntOverflow", func(t *testing.T) { - // Given a string value that would overflow int8 - value := "128" - targetType := reflect.TypeOf(int8(0)) - - // When converting the value - _, err := convertValue(value, targetType) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for integer overflow") - } - - // 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("StringToUintOverflow", func(t *testing.T) { - // Given a string value that would overflow uint8 - value := "256" - targetType := reflect.TypeOf(uint8(0)) - - // When converting the value - _, err := convertValue(value, targetType) - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for integer overflow") - } - - // 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()) - } - }) - - 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") - } - - // 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("StringToFloat", func(t *testing.T) { - // Given a string value and target type - 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("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("StringToBool", func(t *testing.T) { - // Given a string value and target type - 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("convertValue() unexpected error: %v", err) - } - - // And the value should be correctly converted - if result != true { - t.Errorf("convertValue() = %v, want %v", result, true) - } - }) -} - -func TestConfigHandler_Set(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("InvalidPath", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - - // When setting a value with an invalid path - err := handler.Set("", "value") - - // Then no error should be returned - if err != nil { - t.Fatalf("Set() unexpected error: %v", err) - } - }) - - 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.(*configHandler).shims.YamlMarshal = func(v any) ([]byte, error) { - return nil, fmt.Errorf("mocked error") - } - - // When setting a value - err := handler.Set("test.path", "value") - - // Then an error should be returned - if err == nil { - t.Fatal("Set() expected error, got nil") - } - }) -} - -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 calling setValueByPath with empty pathKeys - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("StructFieldNotFound", func(t *testing.T) { - // Given a struct and a non-existent field - type TestStruct struct { - Field string `yaml:"field"` - } - currValue := reflect.ValueOf(&TestStruct{}).Elem() - pathKeys := []string{"nonexistent"} - value := "test" - fullPath := "nonexistent" - - // When calling setValueByPath with non-existent field - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for non-existent field") - } - expectedErr := "field not found: nonexistent" - if err.Error() != expectedErr { - t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error()) - } - }) - - t.Run("StructFieldSuccess", func(t *testing.T) { - // Given a struct with a field - type TestStruct struct { - Field string `yaml:"field"` - } - currValue := reflect.ValueOf(&TestStruct{}).Elem() - pathKeys := []string{"field"} - value := "test" - fullPath := "field" - - // When calling setValueByPath with valid field - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - // 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()) - } - }) - - 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 setValueByPath with mismatched key type - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("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 setValueByPath with mismatched value type - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("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 setValueByPath with valid key and value - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("InvalidPath", func(t *testing.T) { - // Given an invalid path type - currValue := reflect.ValueOf(42) - pathKeys := []string{"key"} - value := "test" - fullPath := "key" - - // When calling setValueByPath with invalid path type - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("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 setValueByPath with nested path - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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("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 setValueByPath with nested path - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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()) - } - }) - - t.Run("PointerField", func(t *testing.T) { - // Given a struct with a pointer field - type TestStruct struct { - Field *string `yaml:"field"` - } - currValue := reflect.ValueOf(&TestStruct{}).Elem() - pathKeys := []string{"field"} - value := "test" - fullPath := "field" - - // When calling setValueByPath with pointer field - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - // 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("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 calling setValueByPath with pointer map - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // Then no error should be returned - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - // 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("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 calling setValueByPath with nested path - err := setValueByPath(currValue, pathKeys, value, fullPath) - - // 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 TestConfigHandler_GenerateContextID(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("WhenContextIDExists", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) - - // And an existing context ID - existingID := "w1234567" - handler.SetContextValue("id", existingID) - - // When GenerateContextID is called - err := handler.GenerateContextID() - - // Then no error should be returned - if err != nil { - t.Fatalf("GenerateContextID() unexpected error: %v", err) - } - - // And the existing ID should remain unchanged - if got := handler.GetString("id"); got != existingID { - t.Errorf("Expected ID = %v, got = %v", existingID, got) - } - }) - - t.Run("WhenContextIDDoesNotExist", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) - - // When GenerateContextID is called - err := handler.GenerateContextID() - - // Then no error should be returned - if err != nil { - t.Fatalf("GenerateContextID() unexpected error: %v", err) - } - - // And a new ID should be generated - id := handler.GetString("id") - if id == "" { - t.Fatal("Expected non-empty ID") - } - - // And the ID should start with 'w' and be 8 characters long - if len(id) != 8 || !strings.HasPrefix(id, "w") { - t.Errorf("Expected ID to start with 'w' and be 8 characters long, got: %s", id) - } - }) - - t.Run("WhenRandomGenerationFails", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) - - // And a mocked crypto/rand that fails - handler.(*configHandler).shims.CryptoRandRead = func([]byte) (int, error) { - return 0, fmt.Errorf("mocked crypto/rand error") - } - - // When GenerateContextID is called - err := handler.GenerateContextID() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - - // And the error message should be as expected - expectedError := "failed to generate random context ID: mocked crypto/rand error" - if err.Error() != expectedError { - t.Errorf("Expected error = %v, got = %v", expectedError, err) - } - }) -} - -// Test specifically for the flag override issue we're experiencing -func TestConfigHandler_FlagOverrideIssue(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler, mocks - } - - t.Run("FlagValuePreservedAfterSetDefault", func(t *testing.T) { - // Given a handler that simulates loading existing config (like init pipeline does) - handler, _ := setup(t) - handler.(*configHandler).context = "local" - - // Simulate existing config with different VM driver - existingConfig := `version: v1alpha1 -contexts: - local: - id: existing-id - vm: - driver: existing-driver` - - err := handler.LoadConfigString(existingConfig) - if err != nil { - t.Fatalf("Failed to load config: %v", err) - } - - // Simulate flag being set (like cmd/init.go does) - err = handler.SetContextValue("vm.driver", "colima") - if err != nil { - t.Fatalf("Failed to set flag value: %v", err) - } - - // Verify flag value is set correctly before SetDefault - vmDriver := handler.GetString("vm.driver") - if vmDriver != "colima" { - t.Errorf("Expected vm.driver to be 'colima' before SetDefault, got '%s'", vmDriver) - } - - // Simulate SetDefault being called (like init pipeline does) - // Use a default config that has no VM section (like DefaultConfig_Full) - defaultConfig := v1alpha1.Context{ - Environment: map[string]string{ - "DEFAULT_VAR": "default_value", - }, - Provider: ptrString("local"), - } - - err = handler.SetDefault(defaultConfig) - if err != nil { - t.Fatalf("Failed to set default: %v", err) - } - - // Then the flag value should still be preserved after SetDefault - vmDriverAfter := handler.GetString("vm.driver") - if vmDriverAfter != "colima" { - t.Errorf("Expected vm.driver to remain 'colima' after SetDefault, got '%s'", vmDriverAfter) - } - - // And the default values should be added - provider := handler.GetString("provider") - if provider != "local" { - t.Errorf("Expected provider to be added as 'local', got '%s'", provider) - } - }) -} - -// ============================================================================= -// LoadContextConfig Tests -// ============================================================================= - -func TestConfigHandler_LoadContextConfig(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("SuccessWithContextConfig", func(t *testing.T) { - // Given a configHandler with existing config - handler, mocks := setup(t) - - // Load base configuration - baseConfig := `version: v1alpha1 -contexts: - production: - provider: aws - environment: - BASE_VAR: base_value` - if err := handler.LoadConfigString(baseConfig); err != nil { - t.Fatalf("Failed to load base config: %v", err) - } - - // Set current context to production - if err := handler.SetContext("production"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // Override the shim to return the correct context - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "production" - } - return "" - } - - // Create context-specific config file - projectRoot, _ := mocks.Shell.GetProjectRootFunc() - contextDir := filepath.Join(projectRoot, "contexts", "production") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - contextConfig := `provider: local -environment: - CONTEXT_VAR: context_value - BASE_VAR: overridden_value -aws: - enabled: true` - - if err := os.WriteFile(contextConfigPath, []byte(contextConfig), 0644); err != nil { - t.Fatalf("Failed to write context config: %v", err) - } - - // Override shims to allow reading the actual context file - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return os.Stat(name) - } - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - return os.ReadFile(filename) - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then no error should be returned - if err != nil { - t.Fatalf("LoadContextConfig() unexpected error: %v", err) - } - - // And the context configuration should be merged - if handler.GetString("provider") != "local" { - t.Errorf("Expected provider to be overridden to 'local', got '%s'", handler.GetString("provider")) - } - if handler.GetString("environment.CONTEXT_VAR") != "context_value" { - t.Errorf("Expected CONTEXT_VAR to be 'context_value', got '%s'", handler.GetString("environment.CONTEXT_VAR")) - } - if handler.GetString("environment.BASE_VAR") != "overridden_value" { - t.Errorf("Expected BASE_VAR to be overridden to 'overridden_value', got '%s'", handler.GetString("environment.BASE_VAR")) - } - if !handler.GetBool("aws.enabled") { - t.Error("Expected aws.enabled to be true") - } - }) - - t.Run("SuccessWithYmlExtension", func(t *testing.T) { - // Given a configHandler - handler, mocks := setup(t) - if err := handler.SetContext("local"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // Override the shim to return the correct context - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "local" - } - return "" - } - - // Create context-specific config file with .yml extension - projectRoot, _ := mocks.Shell.GetProjectRootFunc() - contextDir := filepath.Join(projectRoot, "contexts", "local") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - contextConfigPath := filepath.Join(contextDir, "windsor.yml") - contextConfig := `provider: local -environment: - TEST_VAR: test_value` - - if err := os.WriteFile(contextConfigPath, []byte(contextConfig), 0644); err != nil { - t.Fatalf("Failed to write context config: %v", err) - } - - // Override shims to allow reading the actual context file - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return os.Stat(name) - } - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - return os.ReadFile(filename) - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then no error should be returned - if err != nil { - t.Fatalf("LoadContextConfig() unexpected error: %v", err) - } - - // And the context configuration should be loaded - if handler.GetString("provider") != "local" { - t.Errorf("Expected provider to be 'local', got '%s'", handler.GetString("provider")) - } - if handler.GetString("environment.TEST_VAR") != "test_value" { - t.Errorf("Expected TEST_VAR to be 'test_value', got '%s'", handler.GetString("environment.TEST_VAR")) - } - }) - - t.Run("SuccessWithoutContextConfig", func(t *testing.T) { - // Given a configHandler without context-specific config - handler, _ := setup(t) - if err := handler.SetContext("nonexistent"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then no error should be returned - if err != nil { - t.Fatalf("LoadContextConfig() unexpected error: %v", err) - } - }) - - t.Run("ErrorReadingContextConfigFile", func(t *testing.T) { - // Given a configHandler - handler, mocks := setup(t) - if err := handler.SetContext("test"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // And a context config file that exists but cannot be read - projectRoot, _ := mocks.Shell.GetProjectRootFunc() - contextDir := filepath.Join(projectRoot, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - if err := os.WriteFile(contextConfigPath, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to write context config: %v", err) - } - - // Mock ReadFile to return an error - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - // Normalize path separators for cross-platform compatibility - normalizedPath := filepath.ToSlash(filename) - if strings.Contains(normalizedPath, "contexts/test/windsor.yaml") { - return nil, fmt.Errorf("mocked read error") - } - return os.ReadFile(filename) - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then an error should be returned - if err == nil { - t.Fatal("LoadContextConfig() expected error, got nil") - } - - // The error should be from reading the context config file - expectedError := "error reading context config file" - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("LoadContextConfig() error = %v, expected to contain '%s'", err, expectedError) - } - }) - - t.Run("ErrorUnmarshallingContextConfig", func(t *testing.T) { - // Given a configHandler - handler, mocks := setup(t) - if err := handler.SetContext("test"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // Override the shim to return the correct context - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - - // And a context config file with invalid YAML - projectRoot, _ := mocks.Shell.GetProjectRootFunc() - contextDir := filepath.Join(projectRoot, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - invalidYaml := `provider: aws -invalid yaml: [ -` - if err := os.WriteFile(contextConfigPath, []byte(invalidYaml), 0644); err != nil { - t.Fatalf("Failed to write context config: %v", err) - } - - // Override shims to allow reading the actual context file - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return os.Stat(name) - } - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - return os.ReadFile(filename) - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then an error should be returned - if err == nil { - t.Fatal("LoadContextConfig() expected error, got nil") - } - - // And the error message should contain the expected text - if !strings.Contains(err.Error(), "error unmarshalling context yaml") { - t.Errorf("LoadContextConfig() error = %v, expected to contain 'error unmarshalling context yaml'", err) - } - }) - - t.Run("ErrorShellNotInitialized", func(t *testing.T) { - // Given a configHandler without shell - handler, _ := setup(t) - handler.(*configHandler).shell = nil - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then an error should be returned - if err == nil { - t.Fatal("LoadContextConfig() expected error, got nil") - } - - // And the error message should be as expected - expectedError := "shell not initialized" - if err.Error() != expectedError { - t.Errorf("LoadContextConfig() error = %v, expected '%s'", err, expectedError) - } - }) - - t.Run("ErrorGettingProjectRoot", func(t *testing.T) { - // Given a configHandler with shell that returns error - handler, mocks := setup(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mocked project root error") - } - - // When LoadContextConfig is called - err := handler.LoadContextConfig() - - // Then an error should be returned - if err == nil { - t.Fatal("LoadContextConfig() expected error, got nil") - } - - // And the error message should be as expected - expectedError := "error retrieving project root: mocked project root error" - if err.Error() != expectedError { - t.Errorf("LoadContextConfig() error = %v, expected '%s'", err, expectedError) - } - }) - - t.Run("SimulateInitPipelineWorkflow", func(t *testing.T) { - // Given a configHandler simulating the exact init pipeline workflow - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - // Create existing root config with only version (common in real scenarios) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) - - // Step 1: Load existing config like init pipeline does in BasePipeline.Initialize - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) - } - - // Step 2: Set context like init pipeline does - if err := handler.SetContext("local"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // Step 3: Set default configuration like init pipeline does - if err := handler.SetDefault(DefaultConfig); err != nil { - t.Fatalf("Failed to set default config: %v", err) - } - - // Step 4: Generate context ID like init pipeline does - if err := handler.GenerateContextID(); err != nil { - t.Fatalf("Failed to generate context ID: %v", err) - } - - // Step 5: Save config like init pipeline does - err := handler.SaveConfig() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And the context config should be created since context is not defined in root - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was not created at %s, this reproduces the user's issue", contextConfigPath) - } - - // And the root config should not be overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "version: v1alpha1") { - t.Errorf("Root config appears to have been overwritten") - } - }) - - t.Run("ContextNotSetInRootConfigInitially", func(t *testing.T) { - // Given a configHandler that mimics the exact init flow - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - // Create existing root config with only version (user's scenario) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) - - // Load the existing root config - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) - } - - // Set the context but DON'T call Set() to add context data yet - handler.(*configHandler).context = "local" - - // Debug: Check state before adding any context data - t.Logf("Config.Contexts before setting any context data: %+v", handler.(*configHandler).config.Contexts) - - // When SaveConfig is called without any context configuration being set - err := handler.SaveConfig() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Check if context config was created - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was NOT created at %s - this reproduces the user's issue", contextConfigPath) - } else { - t.Logf("Context config file WAS created at %s", contextConfigPath) - } - }) - - t.Run("ReproduceActualIssue", func(t *testing.T) { - // Given a real-world scenario where a root windsor.yaml exists with only version - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - // Create existing root config with only version (exact user scenario) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) - - // Step 1: Load existing config like init pipeline does - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) - } - - // Step 2: Set context - if err := handler.SetContext("local"); err != nil { - t.Fatalf("Failed to set context: %v", err) - } - - // Step 3: Set default configuration (this would add context data) - if err := handler.SetDefault(DefaultConfig); err != nil { - t.Fatalf("Failed to set default config: %v", err) - } - - // Step 4: Generate context ID - if err := handler.GenerateContextID(); err != nil { - t.Fatalf("Failed to generate context ID: %v", err) - } - - // Debug: Check config state before SaveConfig - t.Logf("Config before SaveConfig: %+v", handler.(*configHandler).config) - if handler.(*configHandler).config.Contexts != nil { - if ctx, exists := handler.(*configHandler).config.Contexts["local"]; exists { - t.Logf("local context exists in config: %+v", ctx) - } else { - t.Logf("local context does NOT exist in config") - } - } else { - t.Logf("Config.Contexts is nil") - } - - // Step 5: Save config (the critical call) - err := handler.SaveConfig() - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Check if context config file was created - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was NOT created at %s - this is the bug!", contextConfigPath) - } else { - content, _ := os.ReadFile(contextConfigPath) - t.Logf("Context config file WAS created with content: %s", string(content)) - } - - // Check root config wasn't overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "version: v1alpha1") { - t.Errorf("Root config appears to have been overwritten: %s", string(rootContent)) - } - }) -} -func TestConfigHandler_saveContextValues(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a configHandler with context values - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = map[string]any{ - "database_url": "postgres://localhost:5432/test", - "api_key": "secret123", - } - - // When saveContextValues is called - err := handler.(*configHandler).saveContextValues() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And the values.yaml file should be created - valuesPath := filepath.Join(tempDir, "contexts", "test", "values.yaml") - if _, err := handler.(*configHandler).shims.Stat(valuesPath); os.IsNotExist(err) { - t.Fatalf("values.yaml file was not created at %s", valuesPath) - } - }) - - t.Run("GetConfigRootError", func(t *testing.T) { - // Given a configHandler with a shell that returns an error - handler, mocks := setup(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("failed to get project root") - } - - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = map[string]any{"key": "value"} - - // When saveContextValues is called - err := handler.(*configHandler).saveContextValues() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - - expectedError := "error getting config root: failed to get project root" - if err.Error() != expectedError { - t.Errorf("Expected error: %s, got: %s", expectedError, err.Error()) - } - }) - - t.Run("MkdirAllError", func(t *testing.T) { - // Given a configHandler with a shims that fails on MkdirAll - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - // Mock MkdirAll to return an error - originalMkdirAll := mocks.Shims.MkdirAll - mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { - return fmt.Errorf("mkdir failed") - } - - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = map[string]any{"key": "value"} - - // When saveContextValues is called - err := handler.(*configHandler).saveContextValues() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - - expectedError := "error creating context directory: mkdir failed" - if err.Error() != expectedError { - t.Errorf("Expected error: %s, got: %s", expectedError, err.Error()) - } - - // Restore original function - mocks.Shims.MkdirAll = originalMkdirAll - }) -} - -func TestConfigHandler_ensureValuesYamlLoaded(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - - t.Run("AlreadyLoaded", func(t *testing.T) { - // Given a handler with contextValues already loaded - handler, _ := setup(t) - handler.(*configHandler).contextValues = map[string]any{"existing": "value"} - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should remain unchanged - if handler.(*configHandler).contextValues["existing"] != "value" { - t.Error("contextValues should remain unchanged") - } - }) - - t.Run("ShellNotInitialized", func(t *testing.T) { - // Given a handler with no shell initialized - handler := &configHandler{} - - // When ensureValuesYamlLoaded is called - err := handler.ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should be initialized as empty - if handler.contextValues == nil { - t.Error("contextValues should be initialized") - } - if len(handler.contextValues) != 0 { - t.Errorf("Expected empty contextValues, got: %v", handler.contextValues) - } - }) - - t.Run("ConfigNotLoaded", func(t *testing.T) { - // Given a handler with shell but not loaded - handler, _ := setup(t) - handler.(*configHandler).loaded = false - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should be initialized as empty - if handler.(*configHandler).contextValues == nil { - t.Error("contextValues should be initialized") - } - if len(handler.(*configHandler).contextValues) != 0 { - t.Errorf("Expected empty contextValues, got: %v", handler.(*configHandler).contextValues) - } - }) - - t.Run("ErrorGettingProjectRoot", func(t *testing.T) { - // Given a handler with shell returning error on GetProjectRoot - handler, mocks := setup(t) - handler.(*configHandler).loaded = true - handler.(*configHandler).contextValues = nil // Ensure it's not already loaded - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root error") - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error retrieving project root") { - t.Errorf("Expected 'error retrieving project root', got: %v", err) - } - }) - - t.Run("LoadsSchemaIfNotLoaded", func(t *testing.T) { - // Given a handler with schema validator but no schema loaded - handler, mocks := setup(t) - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil - handler.(*configHandler).schemaValidator = NewSchemaValidator(mocks.Shell) - handler.(*configHandler).schemaValidator.Shims = NewShims() // Use real filesystem for schema validator - - // Use real filesystem operations for this test - handler.(*configHandler).shims = NewShims() - - // Create temp directory structure - tmpDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - - // Create schema file - schemaDir := filepath.Join(tmpDir, "contexts", "_template") - if err := os.MkdirAll(schemaDir, 0755); err != nil { - t.Fatalf("Failed to create schema directory: %v", err) - } - schemaPath := filepath.Join(schemaDir, "schema.yaml") - schemaContent := ` -$schema: https://json-schema.org/draft/2020-12/schema -type: object -properties: - test_key: - type: string -` - if err := os.WriteFile(schemaPath, []byte(schemaContent), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - // Create context directory but no values.yaml - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And schema should be loaded - if handler.(*configHandler).schemaValidator.Schema == nil { - t.Error("Schema should be loaded") - } - - // And contextValues should be initialized as empty - if len(handler.(*configHandler).contextValues) != 0 { - t.Errorf("Expected empty contextValues, got: %v", handler.(*configHandler).contextValues) - } - }) - - t.Run("ErrorLoadingSchema", func(t *testing.T) { - // Given a handler with schema validator and malformed schema file - handler, mocks := setup(t) - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil - handler.(*configHandler).schemaValidator = NewSchemaValidator(mocks.Shell) - handler.(*configHandler).schemaValidator.Shims = NewShims() // Use real filesystem for schema validator - - // Use real filesystem operations for this test - handler.(*configHandler).shims = NewShims() - - // Create temp directory structure - tmpDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - - // Create malformed schema file - schemaDir := filepath.Join(tmpDir, "contexts", "_template") - if err := os.MkdirAll(schemaDir, 0755); err != nil { - t.Fatalf("Failed to create schema directory: %v", err) - } - schemaPath := filepath.Join(schemaDir, "schema.yaml") - if err := os.WriteFile(schemaPath, []byte("invalid: yaml: content:"), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error for malformed schema, got nil") - } - if !strings.Contains(err.Error(), "error loading schema") { - t.Errorf("Expected 'error loading schema', got: %v", err) - } - }) - - t.Run("LoadsValuesYamlSuccessfully", func(t *testing.T) { - // Given a standalone handler with valid values.yaml - tmpDir := t.TempDir() - injector := di.NewInjector() - - mockShell := shell.NewMockShell(injector) - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - injector.Register("shell", mockShell) - - handler := NewConfigHandler(injector) - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil // Ensure values aren't already loaded - - // Create context directory and values.yaml - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - valuesPath := filepath.Join(contextDir, "values.yaml") - valuesContent := `test_key: test_value -another_key: 123 -` - if err := os.WriteFile(valuesPath, []byte(valuesContent), 0644); err != nil { - t.Fatalf("Failed to write values.yaml: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should contain the loaded values - if handler.(*configHandler).contextValues == nil { - t.Fatal("contextValues is nil") - } - if len(handler.(*configHandler).contextValues) == 0 { - t.Fatal("contextValues is empty") - } - if handler.(*configHandler).contextValues["test_key"] != "test_value" { - t.Errorf("Expected test_key='test_value', got: %v", handler.(*configHandler).contextValues["test_key"]) - } - anotherKey, ok := handler.(*configHandler).contextValues["another_key"] - if !ok { - t.Error("Expected another_key to be present") - } else { - // YAML unmarshals numbers as different types depending on their value - // Check if it's 123 regardless of the specific integer type - switch v := anotherKey.(type) { - case int: - if v != 123 { - t.Errorf("Expected another_key=123, got: %v", v) - } - case int64: - if v != 123 { - t.Errorf("Expected another_key=123, got: %v", v) - } - case uint64: - if v != 123 { - t.Errorf("Expected another_key=123, got: %v", v) - } - default: - t.Errorf("Expected another_key to be numeric, got: %v (type: %T)", v, v) - } - } - }) - - t.Run("ErrorReadingValuesYaml", func(t *testing.T) { - // Given a standalone handler with values.yaml that cannot be read - tmpDir := t.TempDir() - injector := di.NewInjector() - - mockShell := shell.NewMockShell(injector) - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - injector.Register("shell", mockShell) - - handler := NewConfigHandler(injector) - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil // Ensure values aren't already loaded - - // Create context directory and values.yaml - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - valuesPath := filepath.Join(contextDir, "values.yaml") - if err := os.WriteFile(valuesPath, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to write values.yaml: %v", err) - } - - // Mock ReadFile to return error for values.yaml - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - if strings.Contains(filename, "values.yaml") { - return nil, fmt.Errorf("read error") - } - return os.ReadFile(filename) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error reading values.yaml") { - t.Errorf("Expected 'error reading values.yaml', got: %v", err) - } - }) - - t.Run("ErrorUnmarshallingValuesYaml", func(t *testing.T) { - // Given a standalone handler with malformed values.yaml - tmpDir := t.TempDir() - injector := di.NewInjector() - - mockShell := shell.NewMockShell(injector) - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - injector.Register("shell", mockShell) - - handler := NewConfigHandler(injector) - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil // Ensure values aren't already loaded - - // Create context directory and malformed values.yaml - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - valuesPath := filepath.Join(contextDir, "values.yaml") - if err := os.WriteFile(valuesPath, []byte("invalid: yaml: content:"), 0644); err != nil { - t.Fatalf("Failed to write values.yaml: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error unmarshalling values.yaml") { - t.Errorf("Expected 'error unmarshalling values.yaml', got: %v", err) - } - }) - - t.Run("ValidatesValuesYamlWithSchema", func(t *testing.T) { - // Given a standalone handler with schema and values.yaml - tmpDir := t.TempDir() - injector := di.NewInjector() - - mockShell := shell.NewMockShell(injector) - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - injector.Register("shell", mockShell) - - handler := NewConfigHandler(injector) - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil // Ensure values aren't already loaded - handler.(*configHandler).schemaValidator.Shims = NewShims() - - // Create schema file - schemaDir := filepath.Join(tmpDir, "contexts", "_template") - if err := os.MkdirAll(schemaDir, 0755); err != nil { - t.Fatalf("Failed to create schema directory: %v", err) - } - schemaPath := filepath.Join(schemaDir, "schema.yaml") - schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema -type: object -properties: - test_key: - type: string -additionalProperties: false -` - if err := os.WriteFile(schemaPath, []byte(schemaContent), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - // Create context directory and values.yaml with valid content - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - valuesPath := filepath.Join(contextDir, "values.yaml") - valuesContent := `test_key: test_value -` - if err := os.WriteFile(valuesPath, []byte(valuesContent), 0644); err != nil { - t.Fatalf("Failed to write values.yaml: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should contain validated values - if handler.(*configHandler).contextValues["test_key"] != "test_value" { - t.Errorf("Expected test_key='test_value', got: %v", handler.(*configHandler).contextValues["test_key"]) - } - }) - - t.Run("ValidationFailsForInvalidValuesYaml", func(t *testing.T) { - // Given a standalone handler with schema and invalid values.yaml - tmpDir := t.TempDir() - injector := di.NewInjector() - - mockShell := shell.NewMockShell(injector) - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - injector.Register("shell", mockShell) - - handler := NewConfigHandler(injector) - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil // Ensure values aren't already loaded - handler.(*configHandler).schemaValidator.Shims = NewShims() - - // Create schema file - schemaDir := filepath.Join(tmpDir, "contexts", "_template") - if err := os.MkdirAll(schemaDir, 0755); err != nil { - t.Fatalf("Failed to create schema directory: %v", err) - } - schemaPath := filepath.Join(schemaDir, "schema.yaml") - schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema -type: object -properties: - test_key: - type: string -additionalProperties: false -` - if err := os.WriteFile(schemaPath, []byte(schemaContent), 0644); err != nil { - t.Fatalf("Failed to write schema file: %v", err) - } - - // Create context directory and values.yaml with invalid content - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - valuesPath := filepath.Join(contextDir, "values.yaml") - valuesContent := `invalid_key: should_not_be_allowed -` - if err := os.WriteFile(valuesPath, []byte(valuesContent), 0644); err != nil { - t.Fatalf("Failed to write values.yaml: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then a validation error should be returned - if err == nil { - t.Fatal("Expected validation error, got nil") - } - if !strings.Contains(err.Error(), "validation failed") { - t.Errorf("Expected 'validation failed', got: %v", err) - } - }) - - t.Run("NoValuesYamlFileInitializesEmpty", func(t *testing.T) { - // Given a handler with no values.yaml file - handler, mocks := setup(t) - handler.(*configHandler).loaded = true - handler.(*configHandler).context = "test" - handler.(*configHandler).contextValues = nil - - // Use real filesystem operations for this test - handler.(*configHandler).shims = NewShims() - - // Create temp directory structure - tmpDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - - // Create context directory but NO values.yaml - contextDir := filepath.Join(tmpDir, "contexts", "test") - if err := os.MkdirAll(contextDir, 0755); err != nil { - t.Fatalf("Failed to create context directory: %v", err) - } - - // When ensureValuesYamlLoaded is called - err := handler.(*configHandler).ensureValuesYamlLoaded() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - // And contextValues should be initialized as empty - if handler.(*configHandler).contextValues == nil { - t.Error("contextValues should be initialized") - } - if len(handler.(*configHandler).contextValues) != 0 { - t.Errorf("Expected empty contextValues, got: %v", handler.(*configHandler).contextValues) - } - }) -} - -// ============================================================================= -// Additional Tests for Full Coverage (from config_handler_test.go) -// ============================================================================= - -func TestConfigHandler_IsLoaded(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - return handler - } - - t.Run("IsLoadedTrue", func(t *testing.T) { - handler := setup(t) - handler.(*configHandler).loaded = true - - isLoaded := handler.IsLoaded() - - if !isLoaded { - t.Errorf("expected IsLoaded to return true, got false") - } - }) - - t.Run("IsLoadedFalse", func(t *testing.T) { - handler := setup(t) - handler.(*configHandler).loaded = false - - isLoaded := handler.IsLoaded() - - if isLoaded { - t.Errorf("expected IsLoaded to return false, got true") - } - }) -} - -func TestConfigHandler_IsContextConfigLoaded(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - return handler - } - - t.Run("ReturnsFalseWhenBaseConfigNotLoaded", func(t *testing.T) { - handler := setup(t) - handler.(*configHandler).loaded = false - - isLoaded := handler.IsContextConfigLoaded() - - if isLoaded { - t.Errorf("expected IsContextConfigLoaded to return false when base config not loaded, got true") - } - }) - - t.Run("ReturnsFalseWhenContextNotSet", func(t *testing.T) { - handler, mocks := setup(t).(*configHandler), setupMocks(t) - handler.loaded = true - handler.shims = mocks.Shims - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.ReadFile = func(filename string) ([]byte, error) { - return []byte(""), nil - } - mocks.Shims.Getenv = func(key string) string { - return "" - } - handler.shell = mocks.Shell - - isLoaded := handler.IsContextConfigLoaded() - - if isLoaded { - t.Errorf("expected IsContextConfigLoaded to return false when context not set, got true") - } - }) - - t.Run("ReturnsTrueWhenContextExistsAndIsValid", func(t *testing.T) { - handler := setup(t) - handler.(*configHandler).loaded = true - handler.(*configHandler).config = v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "test-context": { - Cluster: &cluster.ClusterConfig{ - Workers: cluster.NodeGroupConfig{ - Volumes: []string{"/var/blah"}, - }, - }, - }, - }, - } - - mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.ReadFile = func(filename string) ([]byte, error) { - return []byte("test-context"), nil - } - mocks.Shims.Getenv = func(key string) string { - return "" - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - handler.(*configHandler).loadedContexts["test-context"] = true - - isLoaded := handler.IsContextConfigLoaded() - - if !isLoaded { - t.Errorf("expected IsContextConfigLoaded to return true when context exists and is valid, got false") - } - }) -} - -func TestConfigHandler_GetContext(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - return handler - } - - t.Run("Success", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.ReadFile = func(filename string) ([]byte, error) { - if filename == filepath.Join("/mock/project/root", ".windsor", "context") { - return []byte("test-context"), nil - } - return nil, fmt.Errorf("file not found") - } - mocks.Shims.Getenv = func(key string) string { - return "" - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - - contextValue := handler.GetContext() - - if contextValue != "test-context" { - t.Errorf("expected context 'test-context', got %s", contextValue) - } - }) - - t.Run("GetContextDefaultsToLocal", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.ReadFile = func(_ string) ([]byte, error) { - return nil, fmt.Errorf("file not found") - } - mocks.Shims.Getenv = func(key string) string { - return "" - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - - actualContext := handler.GetContext() - - expectedContext := "local" - if actualContext != expectedContext { - t.Errorf("Expected context %q, got %q", expectedContext, actualContext) - } - }) - - t.Run("ContextFromEnvironment", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "env-context" - } - return "" - } - handler.(*configHandler).shims = mocks.Shims - - actualContext := handler.GetContext() - - if actualContext != "env-context" { - t.Errorf("Expected context 'env-context', got %q", actualContext) - } - }) -} - -func TestConfigHandler_SetContext(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - return handler - } - - t.Run("Success", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { - return nil - } - mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { - return nil - } - mocks.Shims.Setenv = func(key, value string) error { - return nil - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - - err := handler.SetContext("new-context") - - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - }) - - t.Run("GetProjectRootError", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("mocked error inside GetProjectRoot") - } - handler.(*configHandler).shell = mocks.Shell - - err := handler.SetContext("new-context") - - if err == nil { - t.Fatal("expected error, got nil") - } - }) -} - -func TestConfigHandler_Clean(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - return handler - } + result := handler.convertStringByPattern("false") - t.Run("Success", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil - } - mocks.Shims.RemoveAll = func(path string) error { - return nil - } - handler.(*configHandler).shims = mocks.Shims - handler.(*configHandler).shell = mocks.Shell - - err := handler.Clean() - - if err != nil { - t.Fatalf("expected no error, got %v", err) + if result != false { + t.Errorf("Expected false, got %v", result) } }) - t.Run("ErrorGettingConfigRoot", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("error getting project root") - } - handler.(*configHandler).shell = mocks.Shell + t.Run("RecognizesInteger", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - err := handler.Clean() + result := handler.convertStringByPattern("42") - if err == nil { - t.Fatalf("expected error, got none") + if result != 42 { + t.Errorf("Expected 42, got %v", result) } }) -} -func TestConfigHandler_SetSecretsProvider(t *testing.T) { - t.Run("AddsProvider", func(t *testing.T) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) + t.Run("RecognizesFloat", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - mockProvider := secrets.NewMockSecretsProvider(mocks.Injector) + result := handler.convertStringByPattern("3.14") - handler.SetSecretsProvider(mockProvider) - - if len(handler.(*configHandler).secretsProviders) != 1 { - t.Errorf("Expected 1 secrets provider, got %d", len(handler.(*configHandler).secretsProviders)) + if result != 3.14 { + t.Errorf("Expected 3.14, got %v", result) } }) - t.Run("AddsMultipleProviders", func(t *testing.T) { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - - mockProvider1 := secrets.NewMockSecretsProvider(mocks.Injector) - mockProvider2 := secrets.NewMockSecretsProvider(mocks.Injector) + t.Run("ReturnsStringWhenNoPatternMatches", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - handler.SetSecretsProvider(mockProvider1) - handler.SetSecretsProvider(mockProvider2) + result := handler.convertStringByPattern("regular_string") - if len(handler.(*configHandler).secretsProviders) != 2 { - t.Errorf("Expected 2 secrets providers, got %d", len(handler.(*configHandler).secretsProviders)) + if result != "regular_string" { + t.Errorf("Expected 'regular_string', got %v", result) } }) } -func TestConfigHandler_LoadSchema(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - err := handler.Initialize() - if err != nil { - t.Fatalf("failed to initialize: %v", err) - } - return handler - } - - t.Run("Success", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) +func TestConfigHandler_getExpectedTypeFromSchema(t *testing.T) { + t.Run("ReturnsTypeFromSchema", func(t *testing.T) { + handler, tmpDir := setupPrivateTestHandler(t) - schemaContent := []byte(` -$schema: https://schemas.windsorcli.dev/blueprint-config/v1alpha1 + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema type: object properties: - test_key: - type: string`) - - mocks.Shims.ReadFile = func(filename string) ([]byte, error) { - return schemaContent, nil - } - handler.(*configHandler).shims = mocks.Shims - if handler.(*configHandler).schemaValidator != nil { - handler.(*configHandler).schemaValidator.Shims = mocks.Shims - } + bool_key: + type: boolean + int_key: + type: integer + num_key: + type: number +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) - err := handler.LoadSchema("/path/to/schema.yaml") - if err != nil { - t.Fatalf("expected no error, got %v", err) + boolType := handler.getExpectedTypeFromSchema("bool_key") + if boolType != "boolean" { + t.Errorf("Expected 'boolean', got '%s'", boolType) } - }) - - t.Run("ErrorReadingFile", func(t *testing.T) { - handler := setup(t) - mocks := setupMocks(t) - mocks.Shims.ReadFile = func(filename string) ([]byte, error) { - return nil, fmt.Errorf("read error") + intType := handler.getExpectedTypeFromSchema("int_key") + if intType != "integer" { + t.Errorf("Expected 'integer', got '%s'", intType) } - handler.(*configHandler).shims = mocks.Shims - err := handler.LoadSchema("/path/to/schema.yaml") - if err == nil { - t.Fatal("expected error") + numType := handler.getExpectedTypeFromSchema("num_key") + if numType != "number" { + t.Errorf("Expected 'number', got '%s'", numType) } }) -} - -func TestConfigHandler_LoadSchemaFromBytes(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - err := handler.Initialize() - if err != nil { - t.Fatalf("failed to initialize: %v", err) - } - return handler - } - t.Run("Success", func(t *testing.T) { - handler := setup(t) + t.Run("ReturnsEmptyForMissingKey", func(t *testing.T) { + handler, tmpDir := setupPrivateTestHandler(t) - schemaContent := []byte(`{ - "$schema": "https://schemas.windsorcli.dev/blueprint-config/v1alpha1", - "type": "object", - "properties": { - "test_key": { - "type": "string" - } - } - }`) - - err := handler.LoadSchemaFromBytes(schemaContent) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - }) - - t.Run("ErrorInvalidSchema", func(t *testing.T) { - handler := setup(t) + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: {} +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) - schemaContent := []byte(`invalid json`) + typeStr := handler.getExpectedTypeFromSchema("missing_key") - err := handler.LoadSchemaFromBytes(schemaContent) - if err == nil { - t.Fatal("expected error for invalid schema") + if typeStr != "" { + t.Errorf("Expected empty string for missing key, got '%s'", typeStr) } }) -} - -func TestConfigHandler_GetSchemaDefaults(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - err := handler.Initialize() - if err != nil { - t.Fatalf("failed to initialize: %v", err) - } - return handler - } - - t.Run("ReturnsDefaults", func(t *testing.T) { - handler := setup(t) - - schemaContent := []byte(`{ - "$schema": "https://schemas.windsorcli.dev/blueprint-config/v1alpha1", - "type": "object", - "properties": { - "test_key": { - "type": "string", - "default": "test_value" - } - } - }`) - - err := handler.LoadSchemaFromBytes(schemaContent) - if err != nil { - t.Fatalf("failed to load schema: %v", err) - } - - defaults, err := handler.GetSchemaDefaults() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if defaults["test_key"] != "test_value" { - t.Errorf("expected default value 'test_value', got %v", defaults["test_key"]) - } - }) + t.Run("ReturnsEmptyWhenNoSchemaLoaded", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) - t.Run("ErrorWhenSchemaNotLoaded", func(t *testing.T) { - handler := setup(t) + typeStr := handler.getExpectedTypeFromSchema("any_key") - _, err := handler.GetSchemaDefaults() - if err == nil { - t.Fatal("expected error when schema not loaded") + if typeStr != "" { + t.Errorf("Expected empty string when no schema, got '%s'", typeStr) } }) -} - -func TestConfigHandler_GetContextValues(t *testing.T) { - setup := func(t *testing.T) ConfigHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - return handler - } - t.Run("MergesConfigAndValues", func(t *testing.T) { - handler := setup(t) + t.Run("HandlesInvalidPropertiesType", func(t *testing.T) { + // Given a config handler with invalid schema properties + handler, _ := setupPrivateTestHandler(t) - h := handler.(*configHandler) - h.context = "test" - h.loaded = true - h.config.Contexts = map[string]*v1alpha1.Context{ - "test": { - Cluster: &cluster.ClusterConfig{ - Enabled: ptrBool(true), - }, + handler.schemaValidator = &SchemaValidator{ + Schema: map[string]any{ + "properties": "not_a_map", }, } - h.contextValues = map[string]any{ - "custom_key": "custom_value", - } - values, err := handler.GetContextValues() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } + // When getting type for any key + typeStr := handler.getExpectedTypeFromSchema("any_key") - if values["custom_key"] != "custom_value" { - t.Error("expected custom_value to be in merged values") + // Then empty string should be returned + if typeStr != "" { + t.Errorf("Expected empty for invalid properties, got '%s'", typeStr) } }) - t.Run("IncludesSchemaDefaults", func(t *testing.T) { - handler := setup(t) - - err := handler.Initialize() - if err != nil { - t.Fatalf("failed to initialize handler: %v", err) - } - - h := handler.(*configHandler) - h.context = "test" - h.loaded = true - h.contextValues = map[string]any{ - "override_key": "override_value", - } + t.Run("HandlesInvalidPropertySchema", func(t *testing.T) { + // Given a config handler with invalid property schema + handler, _ := setupPrivateTestHandler(t) - schemaContent := []byte(`{ - "$schema": "https://schemas.windsorcli.dev/blueprint-config/v1alpha1", - "type": "object", - "properties": { - "default_key": { - "type": "string", - "default": "default_value" - }, - "override_key": { - "type": "string", - "default": "default_override" + handler.schemaValidator = &SchemaValidator{ + Schema: map[string]any{ + "properties": map[string]any{ + "test_key": "not_a_schema", }, - "nested": { - "type": "object", - "properties": { - "nested_default": { - "type": "string", - "default": "nested_value" - } - } - } - } - }`) - - err = handler.LoadSchemaFromBytes(schemaContent) - if err != nil { - t.Fatalf("failed to load schema: %v", err) - } - - values, err := handler.GetContextValues() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if values["default_key"] != "default_value" { - t.Errorf("expected default_key from schema defaults, got %v", values["default_key"]) - } - - if values["override_key"] != "override_value" { - t.Errorf("expected override_key from values.yaml, got %v", values["override_key"]) - } - - if nested, ok := values["nested"].(map[string]any); ok { - if nested["nested_default"] != "nested_value" { - t.Errorf("expected nested.nested_default from schema, got %v", nested["nested_default"]) - } - } else { - t.Error("expected nested to be a map") - } - }) -} - -func TestConfigHandler_deepMerge(t *testing.T) { - setup := func(t *testing.T) *configHandler { - mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - return handler.(*configHandler) - } - - t.Run("MergesSimpleValues", func(t *testing.T) { - handler := setup(t) - - base := map[string]any{ - "key1": "value1", - "key2": "value2", - } - overlay := map[string]any{ - "key2": "override2", - "key3": "value3", - } - - result := handler.deepMerge(base, overlay) - - if result["key1"] != "value1" { - t.Errorf("expected key1 to remain from base") - } - if result["key2"] != "override2" { - t.Errorf("expected key2 to be overridden") - } - if result["key3"] != "value3" { - t.Errorf("expected key3 to be added from overlay") - } - }) - - t.Run("MergesNestedMaps", func(t *testing.T) { - handler := setup(t) - - base := map[string]any{ - "nested": map[string]any{ - "key1": "value1", - "key2": "value2", - }, - } - overlay := map[string]any{ - "nested": map[string]any{ - "key2": "override2", - "key3": "value3", }, } - result := handler.deepMerge(base, overlay) + // When getting type for the key + typeStr := handler.getExpectedTypeFromSchema("test_key") - nested := result["nested"].(map[string]any) - if nested["key1"] != "value1" { - t.Errorf("expected nested.key1 to remain from base") - } - if nested["key2"] != "override2" { - t.Errorf("expected nested.key2 to be overridden") - } - if nested["key3"] != "value3" { - t.Errorf("expected nested.key3 to be added from overlay") + // Then empty string should be returned + if typeStr != "" { + t.Errorf("Expected empty for invalid property schema, got '%s'", typeStr) } }) } diff --git a/pkg/config/config_handler_public_test.go b/pkg/config/config_handler_public_test.go index eac8a07ca..86d6bee0c 100644 --- a/pkg/config/config_handler_public_test.go +++ b/pkg/config/config_handler_public_test.go @@ -1,18 +1,12 @@ package config import ( - "fmt" "os" "path/filepath" - "reflect" - "strings" "testing" "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/api/v1alpha1/aws" - "github.com/windsorcli/cli/api/v1alpha1/cluster" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" ) @@ -21,1871 +15,2366 @@ import ( // ============================================================================= type Mocks struct { - Injector di.Injector - ConfigHandler *ConfigHandler - Shell *shell.MockShell - SecretsProvider *secrets.MockSecretsProvider - Shims *Shims + Injector di.Injector + Shell *shell.MockShell + Shims *Shims } type SetupOptions struct { - Injector di.Injector - ConfigHandler ConfigHandler - ConfigStr string -} - -func setupShims(t *testing.T) *Shims { - t.Helper() - shims := NewShims() - shims.Stat = func(name string) (os.FileInfo, error) { - return nil, nil - } - shims.ReadFile = func(filename string) ([]byte, error) { - return []byte("dummy: data"), nil - } - shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { - return nil - } - shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test" - } - return "" - } - return shims + ConfigStr string } func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { t.Helper() - origDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get working directory: %v", err) - } - tmpDir := t.TempDir() - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change to temp directory: %v", err) - } - os.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) + os.Setenv("WINDSOR_CONTEXT", "test-context") - var injector di.Injector - if len(opts) > 0 { - injector = opts[0].Injector - } else { - injector = di.NewInjector() - } + injector := di.NewInjector() - mockShell := shell.NewMockShell(injector) + mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } injector.Register("shell", mockShell) - mockSecretsProvider := secrets.NewMockSecretsProvider(injector) - injector.Register("secretsProvider", mockSecretsProvider) - - mockConfigHandler := NewMockConfigHandler() - mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { - return &v1alpha1.Context{} - } - injector.Register("configHandler", mockConfigHandler) - - mockShims := setupShims(t) - t.Cleanup(func() { os.Unsetenv("WINDSOR_PROJECT_ROOT") - - if err := os.Chdir(origDir); err != nil { - t.Logf("Warning: Failed to change back to original directory: %v", err) - } + os.Unsetenv("WINDSOR_CONTEXT") }) return &Mocks{ - Shell: mockShell, - SecretsProvider: mockSecretsProvider, - Injector: injector, - Shims: mockShims, + Injector: injector, + Shell: mockShell, + Shims: NewShims(), } } // ============================================================================= -// Helper Functions +// Test Public Methods // ============================================================================= -// stringPtr returns a pointer to the provided string -func stringPtr(s string) *string { - return &s -} +func TestConfigHandler_Initialize(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupMocks(t) -// ============================================================================= -// Constructor -// ============================================================================= + handler := NewConfigHandler(mocks.Injector) + + err := handler.Initialize() + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if !handler.IsLoaded() == handler.IsLoaded() { + t.Error("Expected handler to be initialized") + } + }) -func TestNewConfigHandler(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { + t.Run("InitializesDataMap", func(t *testing.T) { mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - return handler, mocks - } - t.Run("Success", func(t *testing.T) { - handler, _ := setup(t) + err := handler.Initialize() + if err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + handler.Set("test", "value") + value := handler.Get("test") - // Then the handler should be successfully created and not be nil - if handler == nil { - t.Fatal("Expected non-nil configHandler") + if value != "value" { + t.Errorf("Expected data map to be initialized and usable") } }) -} -// ============================================================================= -// Public Methods -// ============================================================================= + t.Run("ReturnsErrorWhenShellResolveFails", func(t *testing.T) { + // Given a config handler with invalid injector + injector := di.NewInjector() + + handler := NewConfigHandler(injector) + + // When initializing the handler + err := handler.Initialize() + + // Then initialization should fail + if err == nil { + t.Error("Expected error when shell cannot be resolved") + } + }) +} func TestConfigHandler_LoadConfig(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { + t.Run("LoadsRootConfigContextSection", func(t *testing.T) { mocks := setupMocks(t) handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } + handler.Initialize() + handler.SetContext("test-context") - t.Run("Success", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) - - // And a valid config path - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "config.yaml") + tmpDir, _ := mocks.Shell.GetProjectRoot() + rootConfig := `version: v1alpha1 +contexts: + test-context: + provider: local + dns: + domain: example.com +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) - // When LoadConfig is called with the valid path - err := handler.LoadConfig(configPath) + err := handler.LoadConfig() - // Then no error should be returned if err != nil { - t.Fatalf("LoadConfig() unexpected error: %v", err) + t.Fatalf("Expected no error, got %v", err) } - // And the path should be set correctly - if handler.(*configHandler).path != configPath { - t.Errorf("Expected path = %v, got = %v", configPath, handler.(*configHandler).path) + provider := handler.GetString("provider") + if provider != "local" { + t.Errorf("Expected provider='local', got '%s'", provider) + } + + domain := handler.GetString("dns.domain") + if domain != "example.com" { + t.Errorf("Expected dns.domain='example.com', got '%s'", domain) } }) - t.Run("CreateEmptyConfigFileIfNotExist", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) + t.Run("LoadsContextSpecificWindsorYaml", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // And a mocked osStat that returns ErrNotExist - handler.(*configHandler).shims.Stat = func(_ string) (os.FileInfo, error) { - return nil, os.ErrNotExist - } + tmpDir, _ := mocks.Shell.GetProjectRoot() + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + contextConfig := `provider: generic +cluster: + enabled: true + driver: talos +` + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte(contextConfig), 0644) - // When LoadConfig is called with a non-existent path - err := handler.LoadConfig("test_config.yaml") + err := handler.LoadConfig() - // Then no error should be returned if err != nil { - t.Fatalf("LoadConfig() unexpected error: %v", err) + t.Fatalf("Expected no error, got %v", err) + } + + provider := handler.GetString("provider") + if provider != "generic" { + t.Errorf("Expected provider='generic', got '%s'", provider) + } + + driver := handler.GetString("cluster.driver") + if driver != "talos" { + t.Errorf("Expected cluster.driver='talos', got '%s'", driver) } }) - t.Run("ReadFileError", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) + t.Run("LoadsValuesYaml", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + tmpDir, _ := mocks.Shell.GetProjectRoot() + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + valuesContent := `dev: true +custom_key: custom_value +nested: + key: nested_value +` + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte(valuesContent), 0644) + + err := handler.LoadConfig() - // And a mocked osReadFile that returns an error - handler.(*configHandler).shims.ReadFile = func(filename string) ([]byte, error) { - return nil, fmt.Errorf("mocked error reading file") + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - // When LoadConfig is called - err := handler.LoadConfig("mocked_config.yaml") + dev := handler.GetBool("dev") + if !dev { + t.Error("Expected dev=true") + } - // Then an error should be returned - if err == nil { - t.Fatalf("LoadConfig() expected error, got nil") + customKey := handler.GetString("custom_key") + if customKey != "custom_value" { + t.Errorf("Expected custom_key='custom_value', got '%s'", customKey) } - // And the error message should be as expected - expectedError := "error reading config file: mocked error reading file" - if err.Error() != expectedError { - t.Errorf("LoadConfig() error = %v, expected '%s'", err, expectedError) + nestedKey := handler.GetString("nested.key") + if nestedKey != "nested_value" { + t.Errorf("Expected nested.key='nested_value', got '%s'", nestedKey) } }) - t.Run("UnmarshalError", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) + t.Run("MergesAllSourcesWithCorrectPrecedence", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + tmpDir, _ := mocks.Shell.GetProjectRoot() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + test_key: + type: string + default: schema_default +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) - // And a mocked yamlUnmarshal that returns an error - handler.(*configHandler).shims.YamlUnmarshal = func(data []byte, v any) error { - return fmt.Errorf("mocked error unmarshalling yaml") - } + rootConfig := `version: v1alpha1 +contexts: + test-context: + test_key: root_value +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) - // When LoadConfig is called - err := handler.LoadConfig("mocked_path.yaml") + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + contextConfig := `test_key: context_value +` + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte(contextConfig), 0644) - // Then an error should be returned - if err == nil { - t.Fatalf("LoadConfig() expected error, got nil") + valuesContent := `test_key: values_override +` + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte(valuesContent), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - // And the error message should be as expected - expectedError := "error unmarshalling yaml: mocked error unmarshalling yaml" - if err.Error() != expectedError { - t.Errorf("LoadConfig() error = %v, expected '%s'", err, expectedError) + value := handler.GetString("test_key") + if value != "values_override" { + t.Errorf("Expected values.yaml to have highest precedence, got '%s'", value) } }) - t.Run("UnsupportedConfigVersion", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) + t.Run("LoadsSchemaWithoutErrors", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // And a mocked yamlUnmarshal that sets an unsupported version - handler.(*configHandler).shims.YamlUnmarshal = func(data []byte, v any) error { - if config, ok := v.(*v1alpha1.Config); ok { - config.Version = "unsupported_version" - } - return nil + tmpDir, _ := mocks.Shell.GetProjectRoot() + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + key: + type: string +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Errorf("Expected no error loading schema, got %v", err) } + }) - // When LoadConfig is called - err := handler.LoadConfig("mocked_path.yaml") + t.Run("SetsLoadedFlag", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // Then an error should be returned - if err == nil { - t.Fatalf("LoadConfig() expected error, got nil") + if handler.IsLoaded() { + t.Error("Expected IsLoaded=false before loading") } - // And the error message should be as expected - expectedError := "unsupported config version: unsupported_version" - if err.Error() != expectedError { - t.Errorf("LoadConfig() error = %v, expected '%s'", err, expectedError) + tmpDir, _ := mocks.Shell.GetProjectRoot() + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte("provider: local\n"), 0644) + + err := handler.LoadConfig() + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + if !handler.IsLoaded() { + t.Error("Expected IsLoaded=true after loading") } }) -} -func TestConfigHandler_Get(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { + t.Run("ValidatesValuesYamlAgainstSchema", func(t *testing.T) { mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) + handler.Initialize() + handler.SetContext("test-context") + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + valid_key: + type: string +additionalProperties: false +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + valuesContent := `invalid_key: should_fail_validation +` + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte(valuesContent), 0644) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected validation error for invalid values.yaml") } - return handler, mocks - } + }) - t.Run("KeyNotUnderContexts", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, mocks := setup(t) + t.Run("HandlesYmlExtension", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // And a mocked shell that returns a project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/mock/project/root", nil + tmpDir, _ := mocks.Shell.GetProjectRoot() + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + contextConfig := `provider: from_yml +` + os.WriteFile(filepath.Join(contextDir, "windsor.yml"), []byte(contextConfig), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - // And a mocked shims that handles context file - handler.(*configHandler).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") + provider := handler.GetString("provider") + if provider != "from_yml" { + t.Errorf("Expected provider from .yml file, got '%s'", provider) } + }) + + t.Run("ReturnsErrorWhenShellNotInitialized", func(t *testing.T) { + // Given a config handler without initialized shell + injector := di.NewInjector() + handler := NewConfigHandler(injector).(*configHandler) + + // When loading config without shell + err := handler.LoadConfig() - // And a config with proper initialization - handler.(*configHandler).config = v1alpha1.Config{ - Version: "v1alpha1", - Contexts: map[string]*v1alpha1.Context{ - "local": { - Environment: map[string]string{}, - }, - }, + // Then loading should fail + if err == nil { + t.Error("Expected error when shell not initialized") + } + if err.Error() != "shell not initialized" { + t.Errorf("Expected 'shell not initialized', got '%v'", err) } + }) - // And the context is set - handler.(*configHandler).context = "local" + t.Run("ReturnsErrorForInvalidSchemaFile", func(t *testing.T) { + // Given a config handler with invalid schema file + handler, tmpDir := setupPrivateTestHandler(t) - // When getting a key not under contexts - val := handler.Get("nonContextKey") + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + invalidSchema := `this is not: valid [yaml` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(invalidSchema), 0644) - // Then nil should be returned - if val != nil { - t.Errorf("Expected nil for non-context key, got %v", val) + // When loading config with invalid schema + err := handler.LoadConfig() + + // Then loading should fail + if err == nil { + t.Error("Expected error for invalid schema file") } }) - t.Run("InvalidPath", func(t *testing.T) { - // Given a set of safe mocks and a configHandler - handler, _ := setup(t) + t.Run("ReturnsErrorForInvalidRootConfig", func(t *testing.T) { + // Given a config handler with invalid root config + handler, tmpDir := setupPrivateTestHandler(t) - // When calling Get with an empty path - value := handler.Get("") + invalidConfig := `invalid: yaml: [[[` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(invalidConfig), 0644) - // Then nil should be returned - if value != nil { - t.Errorf("Expected nil for empty path, got %v", value) + // When loading config with invalid root config + err := handler.LoadConfig() + + // Then loading should fail + if err == nil { + t.Error("Expected error for invalid root config") } }) - t.Run("PrecedenceAndSchemaDefaults", func(t *testing.T) { - // Given a handler with schema validator and various data sources - handler, _ := setup(t) - handler.(*configHandler).context = "test" - handler.(*configHandler).loaded = true + t.Run("ReturnsErrorForInvalidContextConfig", func(t *testing.T) { + // Given a config handler with invalid context config + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + invalidConfig := `invalid: yaml: [[[` + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte(invalidConfig), 0644) + + // When loading config with invalid context config + err := handler.LoadConfig() - // Set up schema validator with defaults - handler.(*configHandler).schemaValidator = &SchemaValidator{ - Schema: map[string]any{ - "properties": map[string]any{ - "SCHEMA_KEY": map[string]any{ - "default": "schema_default_value", - }, - }, - }, + // Then loading should fail + if err == nil { + t.Error("Expected error for invalid context config") } + }) - // Test schema defaults for single-key path - value := handler.Get("SCHEMA_KEY") - expected := "schema_default_value" - if value != expected { - t.Errorf("Expected schema default value '%s', got '%v'", expected, value) + t.Run("ReturnsErrorForInvalidValuesYaml", func(t *testing.T) { + // Given a config handler with invalid values.yaml + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + invalidValues := `invalid: yaml: [[[` + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte(invalidValues), 0644) + + // When loading config with invalid values.yaml + err := handler.LoadConfig() + + // Then loading should fail + if err == nil { + t.Error("Expected error for invalid values.yaml") } + }) - // Test that multi-key paths don't use schema defaults - value = handler.Get("contexts.test.SCHEMA_KEY") - if value != nil { - t.Errorf("Expected nil for multi-key path, got '%v'", value) + t.Run("IntegrationTestWithAllSources", func(t *testing.T) { + // Given a complete configuration setup with schema, root config, context config, and values.yaml + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("integration-test") + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + schema_default: + type: string + default: from_schema + override_test: + type: string + default: schema_value +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + + rootConfig := `version: v1alpha1 +contexts: + integration-test: + provider: from_root + override_test: root_value +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) + + contextDir := filepath.Join(tmpDir, "contexts", "integration-test") + os.MkdirAll(contextDir, 0755) + contextConfig := `cluster: + enabled: true +override_test: context_value +` + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte(contextConfig), 0644) + + valuesContent := `override_test: values_final +custom_field: user_data +` + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte(valuesContent), 0644) + + // When loading the configuration + err := handler.LoadConfig() + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) } - // Test contextValues precedence - handler.(*configHandler).contextValues = map[string]any{ - "TEST_VAR": "values_value", + // Then all configuration sources should be properly merged with correct precedence + schemaDefault := handler.GetString("schema_default") + if schemaDefault != "from_schema" { + t.Errorf("Expected schema default, got '%s'", schemaDefault) } - value = handler.Get("contexts.test.TEST_VAR") - expected = "values_value" - if value != expected { - t.Errorf("Expected contextValues value '%s', got '%v'", expected, value) + + provider := handler.GetString("provider") + if provider != "from_root" { + t.Errorf("Expected provider from root, got '%s'", provider) } - // Test that contextValues are not checked when not loaded - handler.(*configHandler).loaded = false - value = handler.Get("contexts.test.TEST_VAR") - if value != nil { - t.Errorf("Expected nil when not loaded, got '%v'", value) + clusterEnabled := handler.GetBool("cluster.enabled") + if !clusterEnabled { + t.Error("Expected cluster.enabled from context config") + } + + overrideTest := handler.GetString("override_test") + if overrideTest != "values_final" { + t.Errorf("Expected values.yaml to have final say, got '%s'", overrideTest) + } + + customField := handler.GetString("custom_field") + if customField != "user_data" { + t.Errorf("Expected custom_field from values.yaml, got '%s'", customField) + } + }) + + t.Run("HandlesContextWithoutRootConfig", func(t *testing.T) { + // Given a context config without root config + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte("provider: test\n"), 0644) + + // When loading config + err := handler.LoadConfig() + + // Then it should succeed and load context config + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + provider := handler.GetString("provider") + if provider != "test" { + t.Error("Expected to load context config without root config") + } + }) + + t.Run("HandlesRootContextSectionWithoutMatch", func(t *testing.T) { + // Given a root config with different context section + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") + + rootConfig := `version: v1alpha1 +contexts: + other-context: + provider: other +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) + + // When loading config + err := handler.LoadConfig() + + // Then it should succeed without error + if err != nil { + t.Fatalf("Expected no error, got %v", err) } }) } -func TestConfigHandler_SaveConfig(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_LoadConfigString(t *testing.T) { + t.Run("ExtractsCurrentContextSection", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "test-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + mocks := setupMocks(t) handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) + handler.Initialize() + + yaml := `version: v1alpha1 +contexts: + test-context: + provider: local + dns: + domain: test + other-context: + provider: remote +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - return handler, mocks - } - t.Run("Success", func(t *testing.T) { - // Given a configHandler with a mocked shell - handler, mocks := setup(t) + provider := handler.GetString("provider") + if provider != "local" { + t.Errorf("Expected provider='local' from test-context, got '%s'", provider) + } - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + domain := handler.GetString("dns.domain") + if domain != "test" { + t.Errorf("Expected dns.domain='test', got '%s'", domain) } + }) - // And a context is set - handler.(*configHandler).context = "test-context" + t.Run("MergesFlatYamlStructure", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // And some configuration data - handler.Set("contexts.test-context.provider", "local") + yaml := `provider: generic +custom_key: custom_value +` - // When SaveConfig is called - err := handler.SaveConfig() + err := handler.LoadConfigString(yaml) - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) } - // And the root windsor.yaml should exist with only version - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(rootConfigPath); os.IsNotExist(err) { - t.Fatalf("Root config file was not created at %s", rootConfigPath) + provider := handler.GetString("provider") + if provider != "generic" { + t.Errorf("Expected provider='generic', got '%s'", provider) } - // And the context config should exist - contextConfigPath := filepath.Join(tempDir, "contexts", "test-context", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Fatalf("Context config file was not created at %s", contextConfigPath) + customKey := handler.GetString("custom_key") + if customKey != "custom_value" { + t.Errorf("Expected custom_key='custom_value', got '%s'", customKey) } }) - t.Run("WithOverwriteFalse", func(t *testing.T) { - // Given a configHandler with existing config files - handler, mocks := setup(t) + t.Run("SetsLoadedFlag", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + err := handler.LoadConfigString("provider: test\n") + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if !handler.IsLoaded() { + t.Error("Expected IsLoaded=true after LoadConfigString") } + }) - handler.(*configHandler).context = "test-context" + t.Run("HandlesUnmarshalError", func(t *testing.T) { + // Given a handler and invalid YAML string + handler, _ := setupPrivateTestHandler(t) - // Create existing files - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - os.WriteFile(rootConfigPath, []byte("existing content"), 0644) + // When loading invalid YAML + err := handler.LoadConfigString("invalid: yaml: [[[") - contextDir := filepath.Join(tempDir, "contexts", "test-context") - os.MkdirAll(contextDir, 0755) - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - os.WriteFile(contextConfigPath, []byte("existing context content"), 0644) + // Then it should return unmarshal error + if err == nil { + t.Error("Expected unmarshal error") + } + }) - // When SaveConfig is called with overwrite false - err := handler.SaveConfig(false) + t.Run("HandlesEmptyString", func(t *testing.T) { + // Given a handler and empty string + handler, _ := setupPrivateTestHandler(t) - // Then no error should be returned + // When loading empty string + err := handler.LoadConfigString("") + + // Then it should succeed without error if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("Expected no error for empty string, got %v", err) } + }) +} - // And the files should still contain the original content - rootContent, _ := os.ReadFile(rootConfigPath) - if string(rootContent) != "existing content" { - t.Errorf("Root config file was overwritten when it shouldn't have been") +func TestConfigHandler_Get(t *testing.T) { + t.Run("ReturnsValueFromData", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("simple.key", "test_value") + + value := handler.Get("simple.key") + + if value != "test_value" { + t.Errorf("Expected 'test_value', got '%v'", value) } + }) + + t.Run("ReturnsNilForEmptyPath", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - contextContent, _ := os.ReadFile(contextConfigPath) - if string(contextContent) != "existing context content" { - t.Errorf("Context config file was overwritten when it shouldn't have been") + value := handler.Get("") + + if value != nil { + t.Errorf("Expected nil for empty path, got '%v'", value) } }) - t.Run("ShellNotInitialized", func(t *testing.T) { - // Given a configHandler without initialized shell - handler, _ := setup(t) - handler.(*configHandler).shell = nil + t.Run("ReturnsNilForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // When SaveConfig is called - err := handler.SaveConfig() + value := handler.Get("nonexistent.key") - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + if value != nil { + t.Errorf("Expected nil for missing key, got '%v'", value) } - if err.Error() != "shell not initialized" { - t.Errorf("Expected 'shell not initialized' error, got %v", err) + }) + + t.Run("NavigatesNestedMaps", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("parent.child.grandchild", "nested_value") + + value := handler.Get("parent.child.grandchild") + + if value != "nested_value" { + t.Errorf("Expected 'nested_value', got '%v'", value) } }) - t.Run("GetProjectRootError", func(t *testing.T) { - // Given a configHandler with shell that fails to get project root - handler, mocks := setup(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root failed") + t.Run("FallsBackToSchemaDefaultsForTopLevelKey", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + default_key: + type: string + default: schema_default_value +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadConfig() + + value := handler.Get("default_key") + + if value != "schema_default_value" { + t.Errorf("Expected schema default 'schema_default_value', got '%v'", value) } + }) - // When SaveConfig is called - err := handler.SaveConfig() + t.Run("FallsBackToSchemaDefaultsForNestedKey", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + nested: + type: object + properties: + key: + type: string + default: nested_default +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadConfig() + + value := handler.Get("nested.key") + + if value != "nested_default" { + t.Errorf("Expected nested schema default 'nested_default', got '%v'", value) } - if !strings.Contains(err.Error(), "error retrieving project root") { - t.Errorf("Expected 'error retrieving project root' in error, got %v", err) + }) + +} + +func TestConfigHandler_GetString(t *testing.T) { + t.Run("ReturnsStringValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("key", "string_value") + + result := handler.GetString("key") + + if result != "string_value" { + t.Errorf("Expected 'string_value', got '%s'", result) } }) - t.Run("RootConfigExists_SkipsRootCreation", func(t *testing.T) { - // Given a configHandler with existing root config - handler, mocks := setup(t) + t.Run("ReturnsEmptyStringForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + result := handler.GetString("missing.key") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if result != "" { + t.Errorf("Expected empty string, got '%s'", result) } + }) - handler.(*configHandler).context = "test-context" + t.Run("ReturnsProvidedDefault", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Create existing root config - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - originalContent := "version: v1alpha1\nexisting: config" - os.WriteFile(rootConfigPath, []byte(originalContent), 0644) + result := handler.GetString("missing.key", "default_value") - // When SaveConfig is called - err := handler.SaveConfig() + if result != "default_value" { + t.Errorf("Expected 'default_value', got '%s'", result) + } + }) - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Run("ConvertsNonStringToString", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("number", 42) + + result := handler.GetString("number") + + if result != "42" { + t.Errorf("Expected '42', got '%s'", result) } + }) +} + +func TestConfigHandler_GetInt(t *testing.T) { + t.Run("ReturnsIntValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("count", 42) + + result := handler.GetInt("count") + + if result != 42 { + t.Errorf("Expected 42, got %d", result) + } + }) + + t.Run("IgnoresFloat64Values", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("count", float64(42.7)) + + result := handler.GetInt("count") - // And the root config should not be overwritten - content, _ := os.ReadFile(rootConfigPath) - if string(content) != originalContent { - t.Errorf("Root config was overwritten when it should be preserved") + if result != 0 { + t.Errorf("Expected 0 (fallback for non-integer), got %d", result) } }) - t.Run("ContextExistsInRoot_SkipsContextCreation", func(t *testing.T) { - // Given a configHandler with context existing in root config - handler, mocks := setup(t) + t.Run("ConvertsUint64ToInt", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("count", uint64(42)) + + result := handler.GetInt("count") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if result != 42 { + t.Errorf("Expected 42, got %d", result) } + }) + + t.Run("ConvertsStringToInt", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - handler.(*configHandler).context = "existing-context" + handler.Set("count", "42") - // Setup config with existing context in root - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "existing-context": { - Provider: stringPtr("local"), - }, + result := handler.GetInt("count") + + if result != 42 { + t.Errorf("Expected 42, got %d", result) } + }) - // Create existing root config file - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - os.WriteFile(rootConfigPath, []byte("version: v1alpha1"), 0644) + t.Run("ReturnsZeroForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // When SaveConfig is called - err := handler.SaveConfig() + result := handler.GetInt("missing.key") - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) + if result != 0 { + t.Errorf("Expected 0, got %d", result) } + }) - // And the context config should not be created - contextConfigPath := filepath.Join(tempDir, "contexts", "existing-context", "windsor.yaml") - if _, err := os.Stat(contextConfigPath); !os.IsNotExist(err) { - t.Errorf("Context config was created when it shouldn't have been") + t.Run("ReturnsProvidedDefault", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + result := handler.GetInt("missing.key", 99) + + if result != 99 { + t.Errorf("Expected 99, got %d", result) } }) - t.Run("ContextConfigExists_SkipsContextCreation", func(t *testing.T) { - // Given a configHandler with existing context config file - handler, mocks := setup(t) + t.Run("ConvertsInt64ToInt", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("count", int64(42)) + + result := handler.GetInt("count") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if result != 42 { + t.Errorf("Expected 42, got %d", result) } + }) - handler.(*configHandler).context = "test-context" + t.Run("ConvertsUintToInt", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Create existing context config - contextDir := filepath.Join(tempDir, "contexts", "test-context") - os.MkdirAll(contextDir, 0755) - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - originalContent := "provider: local\nexisting: config" - os.WriteFile(contextConfigPath, []byte(originalContent), 0644) + handler.Set("count", uint(42)) - // When SaveConfig is called - err := handler.SaveConfig() + result := handler.GetInt("count") - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) + if result != 42 { + t.Errorf("Expected 42, got %d", result) } + }) + + t.Run("ReturnsZeroForNonNumericValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // And the context config should not be overwritten - content, _ := os.ReadFile(contextConfigPath) - if string(content) != originalContent { - t.Errorf("Context config was overwritten when it should be preserved") + handler.Set("count", "not_a_number") + + result := handler.GetInt("count") + + if result != 0 { + t.Errorf("Expected 0 for non-numeric string, got %d", result) } }) +} + +func TestConfigHandler_GetBool(t *testing.T) { + t.Run("ReturnsBoolValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("enabled", true) - t.Run("RootConfigMarshalError", func(t *testing.T) { - // Given a configHandler with marshal error for root config - handler, mocks := setup(t) + result := handler.GetBool("enabled") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if !result { + t.Error("Expected true, got false") } + }) + + t.Run("ReturnsFalseForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - handler.(*configHandler).context = "test-context" + result := handler.GetBool("missing.key") - // Override Stat to return not exists (so files will be created) - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist + if result { + t.Error("Expected false, got true") } + }) + + t.Run("ReturnsProvidedDefault", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Mock YamlMarshal to return error - handler.(*configHandler).shims.YamlMarshal = func(v interface{}) ([]byte, error) { - return nil, fmt.Errorf("marshal error") + result := handler.GetBool("missing.key", true) + + if !result { + t.Error("Expected true, got false") } + }) +} - // When SaveConfig is called - err := handler.SaveConfig() +func TestConfigHandler_GetStringSlice(t *testing.T) { + t.Run("ReturnsStringSlice", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + handler.Set("items", []string{"a", "b", "c"}) + + result := handler.GetStringSlice("items") + + if len(result) != 3 { + t.Errorf("Expected length 3, got %d", len(result)) } - if !strings.Contains(err.Error(), "error marshalling root config") { - t.Errorf("Expected 'error marshalling root config' in error, got %v", err) + if result[0] != "a" || result[1] != "b" || result[2] != "c" { + t.Errorf("Expected [a b c], got %v", result) } }) - t.Run("RootConfigWriteError", func(t *testing.T) { - // Given a configHandler with write error for root config - handler, mocks := setup(t) + t.Run("ConvertsInterfaceSlice", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("items", []interface{}{"x", "y", "z"}) - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + result := handler.GetStringSlice("items") + + if len(result) != 3 { + t.Errorf("Expected length 3, got %d", len(result)) + } + if result[0] != "x" || result[1] != "y" || result[2] != "z" { + t.Errorf("Expected [x y z], got %v", result) } + }) + + t.Run("ReturnsEmptySliceForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - handler.(*configHandler).context = "test-context" + result := handler.GetStringSlice("missing.key") - // Override Stat to return not exists (so files will be created) - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist + if len(result) != 0 { + t.Errorf("Expected empty slice, got %v", result) } + }) + + t.Run("ReturnsProvidedDefault", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Mock WriteFile to return error - handler.(*configHandler).shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { - return fmt.Errorf("write error") + defaultSlice := []string{"default1", "default2"} + + result := handler.GetStringSlice("missing.key", defaultSlice) + + if len(result) != 2 || result[0] != "default1" || result[1] != "default2" { + t.Errorf("Expected default slice, got %v", result) } + }) +} - // When SaveConfig is called - err := handler.SaveConfig() +func TestConfigHandler_GetStringMap(t *testing.T) { + t.Run("ReturnsStringMap", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + handler.Set("environment", map[string]string{"KEY1": "value1", "KEY2": "value2"}) + + result := handler.GetStringMap("environment") + + if len(result) != 2 { + t.Errorf("Expected map with 2 entries, got %d", len(result)) } - if !strings.Contains(err.Error(), "error writing root config") { - t.Errorf("Expected 'error writing root config' in error, got %v", err) + if result["KEY1"] != "value1" || result["KEY2"] != "value2" { + t.Errorf("Expected KEY1=value1, KEY2=value2, got %v", result) } }) - t.Run("ContextDirectoryCreationError", func(t *testing.T) { - // Given a configHandler with directory creation error - handler, mocks := setup(t) + t.Run("ConvertsInterfaceMap", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } + handler.Set("environment", map[string]interface{}{"KEY": "value"}) - handler.(*configHandler).context = "test-context" + result := handler.GetStringMap("environment") - // Override Stat to return not exists (so files will be created) - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist + if result["KEY"] != "value" { + t.Errorf("Expected KEY=value, got %v", result) } + }) + + t.Run("ReturnsEmptyMapForMissingKey", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + result := handler.GetStringMap("missing.key") - // Mock MkdirAll to return error - handler.(*configHandler).shims.MkdirAll = func(path string, perm os.FileMode) error { - return fmt.Errorf("mkdir error") + if len(result) != 0 { + t.Errorf("Expected empty map, got %v", result) } + }) - // When SaveConfig is called - err := handler.SaveConfig() + t.Run("ReturnsProvidedDefault", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + defaultMap := map[string]string{"default": "value"} + + result := handler.GetStringMap("missing.key", defaultMap) + + if result["default"] != "value" { + t.Errorf("Expected default map, got %v", result) } - if !strings.Contains(err.Error(), "error creating context directory") { - t.Errorf("Expected 'error creating context directory' in error, got %v", err) + }) + + t.Run("ConvertsInterfaceKeyMap", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.Set("env", map[interface{}]interface{}{"KEY": "value"}) + + result := handler.GetStringMap("env") + + if result["KEY"] != "value" { + t.Errorf("Expected KEY=value, got %v", result) } }) - t.Run("ContextConfigMarshalError", func(t *testing.T) { - // Given a configHandler with marshal error for context config - handler, mocks := setup(t) + t.Run("ConvertsNonStringValuesToString", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + handler.Set("env", map[string]interface{}{"NUM": 42, "BOOL": true}) + + result := handler.GetStringMap("env") + + if result["NUM"] != "42" { + t.Errorf("Expected NUM='42', got '%s'", result["NUM"]) } + if result["BOOL"] != "true" { + t.Errorf("Expected BOOL='true', got '%s'", result["BOOL"]) + } + }) +} + +func TestConfigHandler_Set(t *testing.T) { + t.Run("SetsSimpleValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - handler.(*configHandler).context = "test-context" + err := handler.Set("key", "value") - // Override Stat to return not exists (so files will be created) - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist + if err != nil { + t.Errorf("Expected no error, got %v", err) } - // Track marshal calls to return error on second call (context config) - marshalCallCount := 0 - handler.(*configHandler).shims.YamlMarshal = func(v interface{}) ([]byte, error) { - marshalCallCount++ - if marshalCallCount == 2 { - return nil, fmt.Errorf("context marshal error") - } - return []byte("version: v1alpha1"), nil + result := handler.GetString("key") + if result != "value" { + t.Errorf("Expected 'value', got '%s'", result) } + }) - // When SaveConfig is called - err := handler.SaveConfig() + t.Run("SetsNestedValue", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then an error should be returned - if err == nil { - t.Fatal("Expected error, got nil") + err := handler.Set("parent.child.key", "nested_value") + + if err != nil { + t.Errorf("Expected no error, got %v", err) } - if !strings.Contains(err.Error(), "error marshalling context config") { - t.Errorf("Expected 'error marshalling context config' in error, got %v", err) + + result := handler.GetString("parent.child.key") + if result != "nested_value" { + t.Errorf("Expected 'nested_value', got '%s'", result) } }) - t.Run("ContextConfigWriteError", func(t *testing.T) { - // Given a configHandler with write error for context config - handler, mocks := setup(t) + t.Run("CreatesIntermediateMaps", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + err := handler.Set("a.b.c.d", "deep_value") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + result := handler.GetString("a.b.c.d") + if result != "deep_value" { + t.Errorf("Expected 'deep_value', got '%s'", result) } + }) + + t.Run("ValidatesDynamicFieldsAgainstSchema", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - handler.(*configHandler).context = "test-context" + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + allowed_key: + type: string +additionalProperties: false +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + err := handler.Set("disallowed_key", "value") - // Override Stat to return not exists (so files will be created) - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist + if err == nil { + t.Error("Expected validation error for disallowed key") } + }) - // Track write calls to return error on second call (context config) - writeCallCount := 0 - handler.(*configHandler).shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { - writeCallCount++ - if writeCallCount == 2 { - return fmt.Errorf("context write error") - } - return nil + t.Run("DoesNotValidateStaticFields", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +additionalProperties: false +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + err := handler.Set("provider", "generic") + + if err != nil { + t.Errorf("Expected no error for static field, got %v", err) } + }) - // When SaveConfig is called - err := handler.SaveConfig() + t.Run("ReturnsErrorForEmptyPath", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + err := handler.Set("", "value") - // Then an error should be returned if err == nil { - t.Fatal("Expected error, got nil") + t.Error("Expected error for empty path") } - if !strings.Contains(err.Error(), "error writing context config") { - t.Errorf("Expected 'error writing context config' in error, got %v", err) + }) + + t.Run("ReturnsErrorForInvalidPath", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + err := handler.Set("invalid..path", "value") + + if err == nil { + t.Error("Expected error for invalid path with double dots") } }) - t.Run("BothFilesExist_NoOperationsPerformed", func(t *testing.T) { - // Given a configHandler with both root and context configs existing - handler, mocks := setup(t) + t.Run("ConvertsStringBasedOnSchemaType", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + bool_field: + type: boolean + int_field: + type: integer + float_field: + type: number +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + handler.Set("bool_field", "true") + handler.Set("int_field", "42") + handler.Set("float_field", "3.14") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + boolVal := handler.GetBool("bool_field") + if !boolVal { + t.Error("Expected string 'true' to be converted to boolean") } - handler.(*configHandler).context = "test-context" + intVal := handler.GetInt("int_field") + if intVal != 42 { + t.Errorf("Expected string '42' to be converted to int, got %d", intVal) + } - // Create both existing files - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - originalRootContent := "version: v1alpha1\nexisting: root" - os.WriteFile(rootConfigPath, []byte(originalRootContent), 0644) + floatVal := handler.Get("float_field") + if floatVal != 3.14 { + t.Errorf("Expected string '3.14' to be converted to float, got %v", floatVal) + } + }) +} - contextDir := filepath.Join(tempDir, "contexts", "test-context") - os.MkdirAll(contextDir, 0755) - contextConfigPath := filepath.Join(contextDir, "windsor.yaml") - originalContextContent := "provider: local\nexisting: context" - os.WriteFile(contextConfigPath, []byte(originalContextContent), 0644) +func TestConfigHandler_SaveConfig(t *testing.T) { + t.Run("CreatesRootWindsorYaml", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + handler.Set("provider", "local") - // When SaveConfig is called err := handler.SaveConfig() - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) } - // And both files should remain unchanged - rootContent, _ := os.ReadFile(rootConfigPath) - if string(rootContent) != originalRootContent { - t.Errorf("Root config was modified when it shouldn't have been") + rootPath := filepath.Join(tmpDir, "windsor.yaml") + content, err := os.ReadFile(rootPath) + if err != nil { + t.Fatalf("Failed to read root config: %v", err) } - contextContent, _ := os.ReadFile(contextConfigPath) - if string(contextContent) != originalContextContent { - t.Errorf("Context config was modified when it shouldn't have been") + expected := "version: v1alpha1\n" + if string(content) != expected { + t.Errorf("Expected root config to be:\n%s\nGot:\n%s", expected, string(content)) } }) - t.Run("EmptyVersion_UsesEmptyString", func(t *testing.T) { - // Given a configHandler with empty version - handler, mocks := setup(t) + t.Run("SeparatesStaticAndDynamicFields", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + handler.Set("provider", "generic") + handler.Set("cluster.enabled", true) + handler.Set("custom_dynamic_field", "dynamic_value") + + err := handler.SaveConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - handler.(*configHandler).context = "test-context" - handler.(*configHandler).config.Version = "" + windsorPath := filepath.Join(tmpDir, "contexts", "test-context", "windsor.yaml") + windsorContent, err := os.ReadFile(windsorPath) + if err != nil { + t.Fatalf("Failed to read windsor.yaml: %v", err) + } - // Override shims to actually work with the real filesystem - handler.(*configHandler).shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { - return os.WriteFile(filename, data, perm) + windsorStr := string(windsorContent) + if !contains(windsorStr, "provider:") { + t.Error("windsor.yaml should contain provider (static field)") + } + if !contains(windsorStr, "cluster:") { + t.Error("windsor.yaml should contain cluster (static field)") } - handler.(*configHandler).shims.MkdirAll = func(path string, perm os.FileMode) error { - return os.MkdirAll(path, perm) + if contains(windsorStr, "custom_dynamic_field") { + t.Error("windsor.yaml should not contain dynamic fields") } - handler.(*configHandler).shims.Stat = func(name string) (os.FileInfo, error) { - return os.Stat(name) + + valuesPath := filepath.Join(tmpDir, "contexts", "test-context", "values.yaml") + valuesContent, err := os.ReadFile(valuesPath) + if err != nil { + t.Fatalf("Failed to read values.yaml: %v", err) } - // When SaveConfig is called + valuesStr := string(valuesContent) + if !contains(valuesStr, "custom_dynamic_field") { + t.Error("values.yaml should contain custom_dynamic_field (dynamic field)") + } + if contains(valuesStr, "provider:") { + t.Error("values.yaml should not contain provider (static field)") + } + }) + + t.Run("ExcludesFieldsWithYamlDashTag", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + handler.Set("cluster.workers.count", 2) + handler.Set("cluster.workers.nodes.worker-1.endpoint", "127.0.0.1:50001") + handler.Set("cluster.workers.nodes.worker-1.hostname", "worker-1") + err := handler.SaveConfig() - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) + } + + windsorPath := filepath.Join(tmpDir, "contexts", "test-context", "windsor.yaml") + windsorContent, err := os.ReadFile(windsorPath) + if err != nil { + t.Fatalf("Failed to read windsor.yaml: %v", err) + } + + windsorStr := string(windsorContent) + if contains(windsorStr, "nodes:") { + t.Errorf("windsor.yaml should not contain nodes (yaml:\"-\" tag), got:\n%s", windsorStr) + } + if !contains(windsorStr, "count:") { + t.Errorf("windsor.yaml should contain count field, got:\n%s", windsorStr) + } + }) + + t.Run("DoesNotSaveSchemaDefaults", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + schema_only_key: + type: string + default: should_not_be_saved +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadConfig() + + value := handler.Get("schema_only_key") + if value != "should_not_be_saved" { + t.Fatalf("Schema default should be accessible via Get, got '%v'", value) + } + + err := handler.SaveConfig() + if err != nil { + t.Fatalf("SaveConfig failed: %v", err) } - // And the root config should contain empty version - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - content, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(content), "version: \"\"") && !strings.Contains(string(content), "version:") { - t.Errorf("Expected version field in config, got: %s", string(content)) + valuesPath := filepath.Join(tmpDir, "contexts", "test-context", "values.yaml") + if _, err := os.Stat(valuesPath); err == nil { + content, _ := os.ReadFile(valuesPath) + if contains(string(content), "schema_only_key") { + t.Errorf("values.yaml should not contain schema defaults, got:\n%s", string(content)) + } } }) - t.Run("CreateContextConfigWhenNotInRootConfig", func(t *testing.T) { - // Given a configHandler with existing root config - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } - - // Create existing root config that doesn't include the current context - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1 -contexts: - different-context: - provider: local` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + t.Run("SavesOnlyUserSetDynamicValues", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - // Load the existing root config - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) - } + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // Set the current context to one not defined in root config - handler.(*configHandler).context = "new-context" - handler.Set("contexts.new-context.provider", "local") + handler.Set("user_key", "user_value") - // When SaveConfig is called err := handler.SaveConfig() - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) } - // And the context config should be created since the context is not in root config - contextConfigPath := filepath.Join(tempDir, "contexts", "new-context", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Fatalf("Context config file was not created at %s, but should have been since context is not in root config", contextConfigPath) + valuesPath := filepath.Join(tmpDir, "contexts", "test-context", "values.yaml") + content, err := os.ReadFile(valuesPath) + if err != nil { + t.Fatalf("Failed to read values.yaml: %v", err) } - // And the root config should not be overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "different-context") { - t.Errorf("Root config appears to have been overwritten") + expected := "user_key: user_value\n" + if string(content) != expected { + t.Errorf("Expected values.yaml to be:\n%s\nGot:\n%s", expected, string(content)) } }) - t.Run("CreateContextConfigWhenRootConfigExistsWithoutContexts", func(t *testing.T) { - // Given a configHandler with existing root config that has NO contexts section - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } + t.Run("SaveAndReloadPreservesData", func(t *testing.T) { + // Given a config handler with data + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("save-test") - // Create existing root config with only version (this is the most common case for user's issue) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + handler.Set("provider", "generic") + handler.Set("cluster.enabled", true) + handler.Set("cluster.workers.count", 2) + handler.Set("custom_dynamic", "dynamic_value") - // Load the existing root config - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) + // When saving and reloading config + err := handler.SaveConfig() + if err != nil { + t.Fatalf("SaveConfig failed: %v", err) } - // Set the current context to local (typical init scenario) - handler.(*configHandler).context = "local" - handler.Set("contexts.local.provider", "local") - - // When SaveConfig is called - err := handler.SaveConfig() + newHandler, _ := setupPrivateTestHandler(t) + newHandler.shell = handler.shell + newHandler.SetContext("save-test") - // Then no error should be returned + err = newHandler.LoadConfig() if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("LoadConfig failed: %v", err) } - // And the context config should be created since the context is not in root config - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Fatalf("Context config file was not created at %s, but should have been since context is not in root config", contextConfigPath) + // Then data should be preserved + provider := newHandler.GetString("provider") + if provider != "generic" { + t.Errorf("Expected provider to be preserved, got '%s'", provider) } - // And the root config should not be overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "version: v1alpha1") { - t.Errorf("Root config appears to have been overwritten") + count := newHandler.GetInt("cluster.workers.count") + if count != 2 { + t.Errorf("Expected count=2 to be preserved, got %d", count) } - }) - t.Run("SimulateInitPipelineWorkflow", func(t *testing.T) { - // Given a configHandler simulating the exact init pipeline workflow - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + customDynamic := newHandler.GetString("custom_dynamic") + if customDynamic != "dynamic_value" { + t.Errorf("Expected custom_dynamic to be preserved, got '%s'", customDynamic) } - // Create existing root config with only version (common in real scenarios) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) - - // Step 1: Load existing config like init pipeline does in BasePipeline.Initialize - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) + // And files should be properly separated + windsorPath := filepath.Join(tmpDir, "contexts", "save-test", "windsor.yaml") + windsorContent, _ := os.ReadFile(windsorPath) + if contains(string(windsorContent), "custom_dynamic") { + t.Error("windsor.yaml should not contain dynamic fields") } - // Step 2: Set context like init pipeline does - if err := handler.SetContext("local"); err != nil { - t.Fatalf("Failed to set context: %v", err) + valuesPath := filepath.Join(tmpDir, "contexts", "save-test", "values.yaml") + valuesContent, _ := os.ReadFile(valuesPath) + if contains(string(valuesContent), "provider") { + t.Error("values.yaml should not contain static fields") } + }) - // Step 3: Set default configuration like init pipeline does - if err := handler.SetDefault(DefaultConfig); err != nil { - t.Fatalf("Failed to set default config: %v", err) - } + t.Run("ReturnsErrorWhenShellNotInitialized", func(t *testing.T) { + // Given an uninitialized config handler + injector := di.NewInjector() + handler := NewConfigHandler(injector).(*configHandler) + + // When attempting to save config + err := handler.SaveConfig() - // Step 4: Generate context ID like init pipeline does - if err := handler.GenerateContextID(); err != nil { - t.Fatalf("Failed to generate context ID: %v", err) + // Then it should return an error + if err == nil { + t.Error("Expected error when shell not initialized") } + }) - // Step 5: Save config like init pipeline does + t.Run("SkipsCreatingFilesWhenNoData", func(t *testing.T) { + // Given a handler with no data to save + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("empty-test") + + // When saving config with no data err := handler.SaveConfig() - // Then no error should be returned + // Then it should succeed without creating files if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error for empty data, got %v", err) } - // And the context config should be created since context is not defined in root - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was not created at %s, this reproduces the user's issue", contextConfigPath) + windsorPath := filepath.Join(tmpDir, "contexts", "empty-test", "windsor.yaml") + if _, err := os.Stat(windsorPath); !os.IsNotExist(err) { + t.Error("Expected windsor.yaml not to be created when no static fields") } - // And the root config should not be overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "version: v1alpha1") { - t.Errorf("Root config appears to have been overwritten") + valuesPath := filepath.Join(tmpDir, "contexts", "empty-test", "values.yaml") + if _, err := os.Stat(valuesPath); !os.IsNotExist(err) { + t.Error("Expected values.yaml not to be created when no dynamic fields") } }) - t.Run("DebugSaveConfigLogic", func(t *testing.T) { - // Given a configHandler with existing root config with no contexts - handler, mocks := setup(t) + t.Run("CreatesRootConfigOnlyOnce", func(t *testing.T) { + // Given an existing root config file + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } + rootPath := filepath.Join(tmpDir, "windsor.yaml") + existingContent := "version: v1alpha1\nexisting: data\n" + os.WriteFile(rootPath, []byte(existingContent), 0644) - // Create existing root config with only version (user's scenario) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + // When saving config with new data + handler.Set("provider", "test") + handler.SaveConfig() - // Load the existing root config - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) + // Then existing root config should be preserved + content, _ := os.ReadFile(rootPath) + if string(content) != existingContent { + t.Error("Expected existing root config to be preserved") } + }) - // Set context and config values - handler.(*configHandler).context = "local" - handler.Set("contexts.local.provider", "local") + t.Run("HandlesRootConfigMarshalError", func(t *testing.T) { + // Given a handler with marshal error + handler, _ := setupPrivateTestHandler(t) + handler.SetContext("test-context") + handler.Set("provider", "test") - // Debug: Check what's in the config before SaveConfig - t.Logf("Config.Contexts before SaveConfig: %+v", handler.(*configHandler).config.Contexts) - if handler.(*configHandler).config.Contexts != nil { - if _, exists := handler.(*configHandler).config.Contexts["local"]; exists { - t.Logf("local context exists in root config") - } else { - t.Logf("local context does NOT exist in root config") - } - } else { - t.Logf("Config.Contexts is nil") + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, os.ErrInvalid } - // When SaveConfig is called + // When saving config err := handler.SaveConfig() - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Check if context config was created - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Logf("Context config file was NOT created at %s", contextConfigPath) - } else { - t.Logf("Context config file WAS created at %s", contextConfigPath) + // Then it should return marshal error + if err == nil { + t.Error("Expected marshal error") } }) - t.Run("ContextNotSetInRootConfigInitially", func(t *testing.T) { - // Given a configHandler that mimics the exact init flow - handler, mocks := setup(t) + t.Run("HandlesWriteFileError", func(t *testing.T) { + // Given a handler with write file error + handler, _ := setupPrivateTestHandler(t) + handler.SetContext("test-context") + handler.Set("provider", "test") - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + handler.shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + return os.ErrPermission } - // Create existing root config with only version (user's scenario) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + // When saving config + err := handler.SaveConfig() - // Load the existing root config - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) + // Then it should return write file error + if err == nil { + t.Error("Expected write file error") } + }) - // Set the context but DON'T call Set() to add context data yet - handler.(*configHandler).context = "local" + t.Run("OverwritesExistingContextConfig", func(t *testing.T) { + // Given an existing context config file + handler, tmpDir := setupPrivateTestHandler(t) + handler.SetContext("test-context") - // Debug: Check state before adding any context data - t.Logf("Config.Contexts before setting any context data: %+v", handler.(*configHandler).config.Contexts) + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + windsorPath := filepath.Join(contextDir, "windsor.yaml") + os.WriteFile(windsorPath, []byte("provider: old\n"), 0644) - // When SaveConfig is called without any context configuration being set - err := handler.SaveConfig() + // When saving with overwrite=true + handler.Set("provider", "new") + err := handler.SaveConfig(true) - // Then no error should be returned + // Then it should overwrite the existing config if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) } - // Check if context config was created - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was NOT created at %s - this reproduces the user's issue", contextConfigPath) - } else { - t.Logf("Context config file WAS created at %s", contextConfigPath) + content, _ := os.ReadFile(windsorPath) + if !contains(string(content), "new") { + t.Error("Expected config to be overwritten with new value") } }) +} - t.Run("ReproduceActualIssue", func(t *testing.T) { - // Given a real-world scenario where a root windsor.yaml exists with only version - handler, mocks := setup(t) +func TestConfigHandler_SetDefault(t *testing.T) { + t.Run("MergesDefaultContextIntoData", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + defaultContext := v1alpha1.Context{ + Provider: ptrString("default_provider"), } - // Create existing root config with only version (exact user scenario) - rootConfigPath := filepath.Join(tempDir, "windsor.yaml") - rootConfig := `version: v1alpha1` - os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + err := handler.SetDefault(defaultContext) - // Step 1: Load existing config like init pipeline does - if err := handler.LoadConfig(rootConfigPath); err != nil { - t.Fatalf("Failed to load root config: %v", err) + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - // Step 2: Set context - if err := handler.SetContext("local"); err != nil { - t.Fatalf("Failed to set context: %v", err) + provider := handler.GetString("provider") + if provider != "default_provider" { + t.Errorf("Expected provider='default_provider', got '%s'", provider) } + }) - // Step 3: Set default configuration (this would add context data) - if err := handler.SetDefault(DefaultConfig); err != nil { - t.Fatalf("Failed to set default config: %v", err) - } + t.Run("AllowsOverridingDefaults", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Step 4: Generate context ID - if err := handler.GenerateContextID(); err != nil { - t.Fatalf("Failed to generate context ID: %v", err) + defaultContext := v1alpha1.Context{ + Provider: ptrString("default_provider"), } + handler.SetDefault(defaultContext) - // Debug: Check config state before SaveConfig - t.Logf("Config before SaveConfig: %+v", handler.(*configHandler).config) - if handler.(*configHandler).config.Contexts != nil { - if ctx, exists := handler.(*configHandler).config.Contexts["local"]; exists { - t.Logf("local context exists in config: %+v", ctx) - } else { - t.Logf("local context does NOT exist in config") - } - } else { - t.Logf("Config.Contexts is nil") - } + handler.Set("provider", "override_provider") - // Step 5: Save config (the critical call) - err := handler.SaveConfig() - if err != nil { - t.Errorf("Expected no error, got %v", err) + provider := handler.GetString("provider") + + if provider != "override_provider" { + t.Errorf("Expected override to work, got '%s'", provider) } + }) - // Check if context config file was created - contextConfigPath := filepath.Join(tempDir, "contexts", "local", "windsor.yaml") - if _, err := handler.(*configHandler).shims.Stat(contextConfigPath); os.IsNotExist(err) { - t.Errorf("Context config file was NOT created at %s - this is the bug!", contextConfigPath) - } else { - content, _ := os.ReadFile(contextConfigPath) - t.Logf("Context config file WAS created with content: %s", string(content)) + t.Run("HandlesMarshalError", func(t *testing.T) { + // Given a config handler with failing marshal + handler, _ := setupPrivateTestHandler(t) + + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, os.ErrInvalid } - // Check root config wasn't overwritten - rootContent, _ := os.ReadFile(rootConfigPath) - if !strings.Contains(string(rootContent), "version: v1alpha1") { - t.Errorf("Root config appears to have been overwritten: %s", string(rootContent)) + // When setting default with marshal error + err := handler.SetDefault(v1alpha1.Context{}) + + // Then setting should fail + if err == nil { + t.Error("Expected marshal error") } }) - t.Run("SavesContextValuesWhenLoaded", func(t *testing.T) { - // Given a configHandler with loaded contextValues - handler, mocks := setup(t) + t.Run("HandlesUnmarshalError", func(t *testing.T) { + // Given a config handler with failing unmarshal + handler, _ := setupPrivateTestHandler(t) - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + handler.shims.YamlUnmarshal = func(data []byte, v any) error { + return os.ErrInvalid } - // Use real filesystem operations for this test - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test-context" - } - return "" - } + // When setting default with unmarshal error + err := handler.SetDefault(v1alpha1.Context{Provider: ptrString("test")}) - handler.(*configHandler).context = "test-context" - handler.(*configHandler).loaded = true - handler.(*configHandler).contextValues = map[string]any{ - "test_key": "test_value", - "number": 42, + // Then setting should fail + if err == nil { + t.Error("Expected unmarshal error") } + }) +} - // When SaveConfig is called - err := handler.SaveConfig() +func TestConfigHandler_GetConfig(t *testing.T) { + t.Run("ConvertsDataMapToContextStruct", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } + handler.Set("provider", "test_provider") + handler.Set("dns.domain", "test.local") - // And values.yaml should be created with the context values - valuesPath := filepath.Join(tempDir, "contexts", "test-context", "values.yaml") - if _, err := os.Stat(valuesPath); os.IsNotExist(err) { - t.Fatalf("values.yaml was not created at %s", valuesPath) - } + config := handler.GetConfig() - // And the content should match contextValues - content, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read values.yaml: %v", err) + if config == nil { + t.Fatal("Expected non-nil config") } - if !strings.Contains(string(content), "test_key") { - t.Errorf("values.yaml should contain 'test_key', got: %s", string(content)) + if config.Provider == nil || *config.Provider != "test_provider" { + t.Errorf("Expected provider='test_provider', got %v", config.Provider) } - if !strings.Contains(string(content), "test_value") { - t.Errorf("values.yaml should contain 'test_value', got: %s", string(content)) + if config.DNS == nil || config.DNS.Domain == nil || *config.DNS.Domain != "test.local" { + t.Errorf("Expected dns.domain='test.local', got %v", config.DNS) } }) - t.Run("SavesContextValuesEvenWhenNotLoaded", func(t *testing.T) { - handler, mocks := setup(t) + t.Run("ExcludesNodesFieldDueToYamlTag", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } + handler.Set("cluster.workers.count", 2) + handler.Set("cluster.workers.nodes.worker-1.endpoint", "127.0.0.1:50001") - contextValue := "test-context" - handler.(*configHandler).shims.WriteFile = os.WriteFile - handler.(*configHandler).shims.MkdirAll = os.MkdirAll - handler.(*configHandler).shims.Stat = os.Stat - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return contextValue - } - return "" - } - handler.(*configHandler).shims.Setenv = func(key, value string) error { - if key == "WINDSOR_CONTEXT" { - contextValue = value - } - return nil - } + config := handler.GetConfig() - if err := handler.SetContext("test-context"); err != nil { - t.Fatalf("Failed to set context: %v", err) + if config == nil || config.Cluster == nil { + t.Fatal("Expected cluster.workers to exist") } - - handler.(*configHandler).loaded = false - handler.(*configHandler).contextValues = map[string]any{ - "test_key": "test_value", + if config.Cluster.Workers.Count == nil || *config.Cluster.Workers.Count != 2 { + t.Error("Expected count=2") } + if len(config.Cluster.Workers.Nodes) > 0 { + t.Error("Expected nodes to be excluded (yaml:\"-\" tag)") + } + }) - err := handler.SaveConfig() + t.Run("HandlesMarshalError", func(t *testing.T) { + // Given a config handler with failing marshal + handler, _ := setupPrivateTestHandler(t) + handler.Set("test", "value") - if err != nil { - t.Errorf("Expected no error, got %v", err) + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, os.ErrInvalid } - valuesPath := filepath.Join(tempDir, "contexts", "test-context", "values.yaml") + // When getting config with marshal error + config := handler.GetConfig() - if _, err := os.Stat(valuesPath); os.IsNotExist(err) { - contextDir := filepath.Join(tempDir, "contexts", "test-context") - files, _ := os.ReadDir(contextDir) - t.Logf("Files in context directory: %v", files) - t.Errorf("values.yaml should have been created even when not loaded") + // Then empty config should be returned + if config == nil { + t.Error("Expected empty config on marshal error, got nil") } + }) - content, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read values.yaml: %v", err) - } + t.Run("HandlesUnmarshalError", func(t *testing.T) { + // Given a config handler with failing unmarshal + handler, _ := setupPrivateTestHandler(t) + handler.Set("test", "value") - if !strings.Contains(string(content), "test_value") { - t.Errorf("values.yaml should contain 'test_value', got: %s", string(content)) + callCount := 0 + handler.shims.YamlUnmarshal = func(data []byte, v any) error { + callCount++ + if callCount > 1 { + return os.ErrInvalid + } + return handler.shims.YamlUnmarshal(data, v) } - }) - t.Run("SkipsSavingContextValuesWhenNil", func(t *testing.T) { - // Given a configHandler with nil contextValues - handler, mocks := setup(t) + // When getting config with unmarshal error + config := handler.GetConfig() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + // Then empty config should be returned + if config == nil { + t.Error("Expected empty config on unmarshal error, got nil") } + }) +} - handler.(*configHandler).context = "test-context" - handler.(*configHandler).loaded = true - handler.(*configHandler).contextValues = nil +func TestConfigHandler_GetContextValues(t *testing.T) { + t.Run("ReturnsDataMergedWithSchemaDefaults", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - // When SaveConfig is called - err := handler.SaveConfig() + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + schema_key: + type: string + default: schema_value +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadConfig() + handler.Set("user_key", "user_value") + + values, err := handler.GetContextValues() - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Fatalf("Expected no error, got %v", err) } - // And values.yaml should NOT be created - valuesPath := filepath.Join(tempDir, "contexts", "test-context", "values.yaml") - if _, err := os.Stat(valuesPath); !os.IsNotExist(err) { - t.Errorf("values.yaml should not have been created when contextValues is nil") + if values["schema_key"] != "schema_value" { + t.Errorf("Expected schema default in context values, got '%v'", values["schema_key"]) } - }) - t.Run("SkipsSavingContextValuesWhenEmpty", func(t *testing.T) { - // Given a configHandler with empty contextValues - handler, mocks := setup(t) - - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil + if values["user_key"] != "user_value" { + t.Errorf("Expected user value in context values, got '%v'", values["user_key"]) } + }) + + t.Run("IncludesServiceCalculatedValues", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - handler.(*configHandler).context = "test-context" - handler.(*configHandler).loaded = true - handler.(*configHandler).contextValues = map[string]any{} + handler.Set("cluster.workers.nodes.worker-1.endpoint", "127.0.0.1:50001") - // When SaveConfig is called - err := handler.SaveConfig() + values, err := handler.GetContextValues() - // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And values.yaml should NOT be created - valuesPath := filepath.Join(tempDir, "contexts", "test-context", "values.yaml") - if _, err := os.Stat(valuesPath); !os.IsNotExist(err) { - t.Errorf("values.yaml should not have been created when contextValues is empty") + t.Fatalf("Expected no error, got %v", err) + } + + if cluster, ok := values["cluster"].(map[string]any); ok { + if workers, ok := cluster["workers"].(map[string]any); ok { + if nodes, ok := workers["nodes"].(map[string]any); ok { + if worker1, ok := nodes["worker-1"].(map[string]any); ok { + if endpoint := worker1["endpoint"]; endpoint != "127.0.0.1:50001" { + t.Errorf("Expected service-calculated endpoint, got '%v'", endpoint) + } + } else { + t.Error("Expected worker-1 node to be accessible") + } + } else { + t.Error("Expected nodes to be accessible in GetContextValues") + } + } } }) +} - t.Run("SaveContextValuesError", func(t *testing.T) { - // Given a configHandler with contextValues and a write error - handler, mocks := setup(t) +func TestConfigHandler_GetConfigRoot(t *testing.T) { + t.Run("ReturnsConfigRoot", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - tempDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tempDir, nil - } + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // Use real filesystem operations but mock WriteFile to fail for values.yaml - handler.(*configHandler).shims = NewShims() - handler.(*configHandler).shims.Getenv = func(key string) string { - if key == "WINDSOR_CONTEXT" { - return "test-context" - } - return "" + root, err := handler.GetConfigRoot() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - handler.(*configHandler).shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { - if strings.Contains(filename, "values.yaml") { - return fmt.Errorf("write error") - } - return os.WriteFile(filename, data, perm) + + expectedRoot := filepath.Join(tmpDir, "contexts", "test-context") + if root != expectedRoot { + t.Errorf("Expected root='%s', got '%s'", expectedRoot, root) } + }) - handler.(*configHandler).context = "test-context" - handler.(*configHandler).loaded = true - handler.(*configHandler).contextValues = map[string]any{ - "test_key": "test_value", + t.Run("ReturnsErrorWhenShellFails", func(t *testing.T) { + // Given a handler with shell that fails + injector := di.NewInjector() + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", os.ErrPermission } + injector.Register("shell", mockShell) - // When SaveConfig is called - err := handler.SaveConfig() + handler := NewConfigHandler(injector) + handler.Initialize() + handler.SetContext("test") - // Then an error should be returned + // When getting config root + _, err := handler.GetConfigRoot() + + // Then it should return error when GetProjectRoot fails if err == nil { - t.Fatal("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error saving values.yaml") { - t.Errorf("Expected 'error saving values.yaml' in error, got %v", err) + t.Error("Expected error when GetProjectRoot fails") } }) } -func TestConfigHandler_GetString(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_Clean(t *testing.T) { + t.Run("RemovesConfigDirectories", func(t *testing.T) { mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } + tmpDir, _ := mocks.Shell.GetProjectRoot() - t.Run("WithNonExistentKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") - // When getting a non-existent key - got := handler.GetString("nonExistentKey") + configRoot := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(configRoot, 0755) - // Then an empty string should be returned - expectedValue := "" - if got != expectedValue { - t.Errorf("GetString() = %v, expected %v", got, expectedValue) - } - }) + kubeDir := filepath.Join(configRoot, ".kube") + os.MkdirAll(kubeDir, 0755) + os.WriteFile(filepath.Join(kubeDir, "config"), []byte("test"), 0644) - t.Run("GetStringWithDefaultValue", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + err := handler.Clean() - // When getting a non-existent key with a default value - defaultValue := "defaultString" - value := handler.GetString("non.existent.key", defaultValue) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } - // Then the default value should be returned - if value != defaultValue { - t.Errorf("Expected value '%v', got '%v'", defaultValue, value) + if _, err := os.Stat(kubeDir); !os.IsNotExist(err) { + t.Error("Expected .kube directory to be removed") } }) - t.Run("WithExistingKey", func(t *testing.T) { - // Given a handler with a context and existing key-value pair - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config = v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "default": { - Environment: map[string]string{ - "existingKey": "existingValue", - }, - }, - }, + t.Run("ReturnsErrorWhenGetConfigRootFails", func(t *testing.T) { + // Given a handler with shell that fails + injector := di.NewInjector() + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", os.ErrPermission } + injector.Register("shell", mockShell) + + handler := NewConfigHandler(injector).(*configHandler) + handler.Initialize() + handler.SetContext("test") - // When getting an existing key - got := handler.GetString("environment.existingKey") + // When cleaning + err := handler.Clean() - // Then the value should be returned as a string - expectedValue := "existingValue" - if got != expectedValue { - t.Errorf("GetString() = %v, expected %v", got, expectedValue) + // Then it should return error when GetProjectRoot fails + if err == nil { + t.Error("Expected error when GetProjectRoot fails") } }) } -func TestConfigHandler_GetInt(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_GenerateContextID(t *testing.T) { + t.Run("GeneratesIDWhenNotSet", func(t *testing.T) { mocks := setupMocks(t) handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } + handler.Initialize() - t.Run("WithExistingNonIntegerKey", func(t *testing.T) { - // Given a handler with a context and non-integer value - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config = v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "default": { - AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("notAnInt"), - }, - }, - }, - } + err := handler.GenerateContextID() - // When getting a key with non-integer value - value := handler.GetInt("aws.aws_endpoint_url") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } - // Then the default integer value should be returned - expectedValue := 0 - if value != expectedValue { - t.Errorf("Expected value %v, got %v", expectedValue, value) + id := handler.GetString("id") + if id == "" { + t.Error("Expected ID to be generated") + } + if len(id) != 8 { + t.Errorf("Expected ID length 8, got %d", len(id)) } }) - t.Run("WithNonExistentKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" - - // When getting a non-existent key - value := handler.GetInt("nonExistentKey") + t.Run("DoesNotOverrideExistingID", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then the default integer value should be returned - expectedValue := 0 - if value != expectedValue { - t.Errorf("Expected value %v, got %v", expectedValue, value) - } - }) + handler.Set("id", "existing_id") - t.Run("WithNonExistentKeyAndDefaultValue", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + err := handler.GenerateContextID() - // When getting a non-existent key with a default value - got := handler.GetInt("nonExistentKey", 99) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } - // Then the provided default value should be returned - expectedValue := 99 - if got != expectedValue { - t.Errorf("GetInt() = %v, expected %v", got, expectedValue) + id := handler.GetString("id") + if id != "existing_id" { + t.Errorf("Expected existing ID to be preserved, got '%s'", id) } }) - t.Run("WithExistingIntegerKey", func(t *testing.T) { - // Given a handler with a context and integer value - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config = v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "default": { - Cluster: &cluster.ClusterConfig{ - ControlPlanes: cluster.NodeGroupConfig{ - Count: ptrInt(3), - }, - }, - }, - }, + t.Run("HandlesRandomGenerationError", func(t *testing.T) { + // Given a handler with random generation error + handler, _ := setupPrivateTestHandler(t) + + handler.shims.CryptoRandRead = func(b []byte) (int, error) { + return 0, os.ErrPermission } - // When getting an existing integer key - got := handler.GetInt("cluster.controlplanes.count") + // When generating context ID + err := handler.GenerateContextID() - // Then the integer value should be returned - expectedValue := 3 - if got != expectedValue { - t.Errorf("GetInt() = %v, expected %v", got, expectedValue) + // Then it should return error when random generation fails + if err == nil { + t.Error("Expected error when random generation fails") } }) } -func TestConfigHandler_GetBool(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_LoadSchema(t *testing.T) { + t.Run("LoadsSchemaSuccessfully", func(t *testing.T) { mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } + handler.Initialize() + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + test_key: + type: string + default: test_value +` + schemaPath := filepath.Join(schemaDir, "schema.yaml") + os.WriteFile(schemaPath, []byte(schemaContent), 0644) + + err := handler.LoadSchema(schemaPath) - t.Run("WithExistingBooleanKey", func(t *testing.T) { - // Given a handler with a context and boolean value - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config = v1alpha1.Config{ - Contexts: map[string]*v1alpha1.Context{ - "default": { - AWS: &aws.AWSConfig{ - Enabled: ptrBool(true), - }, - }, - }, + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - // When getting an existing boolean key - got := handler.GetBool("aws.enabled") - - // Then the boolean value should be returned - expectedValue := true - if got != expectedValue { - t.Errorf("GetBool() = %v, expected %v", got, expectedValue) + value := handler.Get("test_key") + if value != "test_value" { + t.Error("Expected schema default to be accessible after LoadSchema") } }) - t.Run("WithExistingNonBooleanKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + t.Run("ReturnsErrorForInvalidSchema", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // When setting a non-boolean value for the key - handler.Set("contexts.default.aws.aws_endpoint_url", "notABool") + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + invalidSchema := `invalid: yaml: content: [[[` + schemaPath := filepath.Join(schemaDir, "schema.yaml") + os.WriteFile(schemaPath, []byte(invalidSchema), 0644) - // When getting an existing key with a non-boolean value - value := handler.GetBool("aws.aws_endpoint_url") - expectedValue := false + err := handler.LoadSchema(schemaPath) - // Then the default boolean value should be returned - if value != expectedValue { - t.Errorf("Expected value %v, got %v", expectedValue, value) + if err == nil { + t.Error("Expected error for invalid schema") } }) - t.Run("WithNonExistentKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + t.Run("HandlesReadFileError", func(t *testing.T) { + // Given a handler and non-existent schema file + handler, _ := setupPrivateTestHandler(t) - // When getting a non-existent key - value := handler.GetBool("nonExistentKey") - expectedValue := false + // When loading non-existent schema + err := handler.LoadSchema("/nonexistent/schema.yaml") - // Then the default boolean value should be returned - if value != expectedValue { - t.Errorf("Expected value %v, got %v", expectedValue, value) + // Then it should return read file error + if err == nil { + t.Error("Expected read file error") } }) - t.Run("WithNonExistentKeyAndDefaultValue", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + t.Run("HandlesInvalidSchemaContent", func(t *testing.T) { + // Given a handler and invalid schema content + handler, tmpDir := setupPrivateTestHandler(t) - // When getting a non-existent key with a default value - got := handler.GetBool("nonExistentKey", false) + schemaPath := filepath.Join(tmpDir, "invalid_schema.yaml") + os.WriteFile(schemaPath, []byte("invalid yaml [[["), 0644) - // Then the provided default value should be returned - expectedValue := false - if got != expectedValue { - t.Errorf("GetBool() = %v, expected %v", got, expectedValue) + // When loading invalid schema + err := handler.LoadSchema(schemaPath) + + // Then it should return error for invalid schema content + if err == nil { + t.Error("Expected error for invalid schema content") } }) } -func TestConfigHandler_GetStringSlice(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_LoadSchemaFromBytes(t *testing.T) { + t.Run("LoadsSchemaFromBytes", func(t *testing.T) { mocks := setupMocks(t) handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } + handler.Initialize() - t.Run("Success", func(t *testing.T) { - // Given a handler with a context containing a slice value - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": { - Cluster: &cluster.ClusterConfig{ - Workers: cluster.NodeGroupConfig{ - HostPorts: []string{"50000:50002/tcp", "30080:8080/tcp", "30443:8443/tcp"}, - }, - }, - }, - } + schemaContent := []byte(`$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + byte_schema_key: + type: string + default: from_bytes +`) - // When retrieving the slice value using GetStringSlice - value := handler.GetStringSlice("cluster.workers.hostports") + err := handler.LoadSchemaFromBytes(schemaContent) - // Then the returned slice should match the expected slice - expectedSlice := []string{"50000:50002/tcp", "30080:8080/tcp", "30443:8443/tcp"} - if !reflect.DeepEqual(value, expectedSlice) { - t.Errorf("Expected GetStringSlice to return %v, got %v", expectedSlice, value) + if err != nil { + t.Fatalf("Expected no error, got %v", err) } - }) - - t.Run("WithNonExistentKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" - // When retrieving a non-existent key using GetStringSlice - value := handler.GetStringSlice("nonExistentKey") - - // Then the returned value should be an empty slice - if len(value) != 0 { - t.Errorf("Expected GetStringSlice with non-existent key to return an empty slice, got %v", value) + value := handler.Get("byte_schema_key") + if value != "from_bytes" { + t.Error("Expected schema default from bytes to be accessible") } }) - t.Run("WithNonExistentKeyAndDefaultValue", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" - defaultValue := []string{"default1", "default2"} + t.Run("HandlesInvalidSchemaBytes", func(t *testing.T) { + // Given a handler and invalid schema bytes + handler, _ := setupPrivateTestHandler(t) - // When retrieving a non-existent key with a default value - value := handler.GetStringSlice("nonExistentKey", defaultValue) + // When loading invalid schema bytes + err := handler.LoadSchemaFromBytes([]byte("invalid yaml [[[")) - // Then the returned value should match the default value - if !reflect.DeepEqual(value, defaultValue) { - t.Errorf("Expected GetStringSlice with default to return %v, got %v", defaultValue, value) + // Then it should return error for invalid schema bytes + if err == nil { + t.Error("Expected error for invalid schema bytes") } }) - t.Run("TypeMismatch", func(t *testing.T) { - // Given a handler where the key exists but is not a slice - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.Set("contexts.default.cluster.workers.hostports", 123) // Set an int instead of slice + t.Run("HandlesValidSchemaBytes", func(t *testing.T) { + // Given a handler and valid schema bytes + handler, _ := setupPrivateTestHandler(t) - // When retrieving the value using GetStringSlice - value := handler.GetStringSlice("cluster.workers.hostports") + schemaBytes := []byte(`$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + test: + type: string +`) - // Then the returned slice should be empty - if len(value) != 0 { - t.Errorf("Expected empty slice due to type mismatch, got %v", value) + // When loading valid schema bytes + err := handler.LoadSchemaFromBytes(schemaBytes) + + // Then it should succeed without error + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) } -func TestConfigHandler_GetStringMap(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_GetContext(t *testing.T) { + t.Run("ReturnsContextFromEnvironment", func(t *testing.T) { mocks := setupMocks(t) - handler := NewConfigHandler(mocks.Injector) - handler.(*configHandler).shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - return handler, mocks - } - t.Run("Success", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "default": { - Environment: map[string]string{ - "KEY1": "value1", - "KEY2": "value2", - }, - }, - } + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // When retrieving the map value using GetStringMap - value := handler.GetStringMap("environment") + context := handler.GetContext() - // Then the returned map should match the expected map - expectedMap := map[string]string{"KEY1": "value1", "KEY2": "value2"} - if !reflect.DeepEqual(value, expectedMap) { - t.Errorf("Expected GetStringMap to return %v, got %v", expectedMap, value) + if context != "test-context" { + t.Errorf("Expected 'test-context' from environment, got '%s'", context) } }) - t.Run("WithNonExistentKey", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" + t.Run("ReadsContextFromFile", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - // When retrieving a non-existent key using GetStringMap - value := handler.GetStringMap("nonExistentKey") + os.Unsetenv("WINDSOR_CONTEXT") + defer os.Setenv("WINDSOR_CONTEXT", "test-context") - // Then the returned value should be an empty map - if !reflect.DeepEqual(value, map[string]string{}) { - t.Errorf("Expected GetStringMap with non-existent key to return an empty map, got %v", value) - } - }) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - t.Run("WithNonExistentKeyAndDefaultValue", func(t *testing.T) { - // Given a handler with a context set - handler, _ := setup(t) - handler.(*configHandler).context = "default" - defaultValue := map[string]string{"defaultKey1": "defaultValue1", "defaultKey2": "defaultValue2"} + contextFilePath := filepath.Join(tmpDir, ".windsor", "context") + os.MkdirAll(filepath.Dir(contextFilePath), 0755) + os.WriteFile(contextFilePath, []byte("file-context"), 0644) - // When retrieving a non-existent key with a default value - value := handler.GetStringMap("nonExistentKey", defaultValue) + context := handler.GetContext() - // Then the returned value should match the default value - if !reflect.DeepEqual(value, defaultValue) { - t.Errorf("Expected GetStringMap with default to return %v, got %v", defaultValue, value) + if context != "file-context" { + t.Errorf("Expected 'file-context', got '%s'", context) } }) - t.Run("TypeMismatch", func(t *testing.T) { - // Given a handler where the key exists but is not a map[string]string - handler, _ := setup(t) - handler.(*configHandler).context = "default" - handler.Set("contexts.default.environment", 123) // Set an int instead of map + t.Run("DefaultsToLocalWhenNoContextSet", func(t *testing.T) { + mocks := setupMocks(t) + + os.Unsetenv("WINDSOR_CONTEXT") + defer os.Setenv("WINDSOR_CONTEXT", "test-context") + + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // When retrieving the value using GetStringMap - value := handler.GetStringMap("environment") + context := handler.GetContext() - // Then the returned map should be empty - if len(value) != 0 { - t.Errorf("Expected empty map due to type mismatch, got %v", value) + if context != "local" { + t.Errorf("Expected default 'local', got '%s'", context) } }) } -func TestConfigHandler_GetConfig(t *testing.T) { - setup := func(t *testing.T) (ConfigHandler, *Mocks) { +func TestConfigHandler_SetContext(t *testing.T) { + t.Run("WritesContextToFile", func(t *testing.T) { mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + handler := NewConfigHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } - handler.(*configHandler).shims = mocks.Shims - return handler, mocks - } + handler.Initialize() - t.Run("EmptyContext", func(t *testing.T) { - // Given a handler with no context set - handler, _ := setup(t) + err := handler.SetContext("new-context") - // When getting the config - config := handler.GetConfig() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } - // Then the default config should be returned - if config == nil { - t.Fatal("Expected default config, got nil") + contextFilePath := filepath.Join(tmpDir, ".windsor", "context") + content, err := os.ReadFile(contextFilePath) + if err != nil { + t.Fatalf("Failed to read context file: %v", err) + } + + if string(content) != "new-context" { + t.Errorf("Expected context file to contain 'new-context', got '%s'", string(content)) } }) - t.Run("NonExistentContext", func(t *testing.T) { - // Given a handler with a non-existent context - handler, _ := setup(t) - handler.(*configHandler).context = "nonexistent" + t.Run("HandlesMkdirAllError", func(t *testing.T) { + // Given a handler with MkdirAll error + handler, _ := setupPrivateTestHandler(t) - // When getting the config - config := handler.GetConfig() + handler.shims.MkdirAll = func(path string, perm os.FileMode) error { + return os.ErrPermission + } - // Then the default config should be returned - if config == nil { - t.Fatal("Expected default config, got nil") + // When setting context + err := handler.SetContext("new-context") + + // Then it should return MkdirAll error + if err == nil { + t.Error("Expected MkdirAll error") } }) - t.Run("ExistingContext", func(t *testing.T) { - // Given a handler with an existing context - handler, _ := setup(t) - handler.(*configHandler).context = "test" + t.Run("HandlesWriteFileError", func(t *testing.T) { + // Given a handler with WriteFile error + handler, _ := setupPrivateTestHandler(t) - // And a context with environment variables - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "test": { - Environment: map[string]string{ - "TEST_VAR": "test_value", - }, - }, + handler.shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { + return os.ErrPermission } - // And default context with different environment variables - handler.(*configHandler).defaultContextConfig = v1alpha1.Context{ - Environment: map[string]string{ - "DEFAULT_VAR": "default_value", - }, + // When setting context + err := handler.SetContext("new-context") + + // Then it should return WriteFile error + if err == nil { + t.Error("Expected WriteFile error") } + }) +} - // When getting the config - config := handler.GetConfig() +func TestConfigHandler_IsLoaded(t *testing.T) { + t.Run("ReturnsFalseBeforeLoading", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() - // Then the merged config should be returned - if config == nil { - t.Fatal("Expected merged config, got nil") - } + result := handler.IsLoaded() - // 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"]) + if result { + t.Error("Expected IsLoaded=false before loading config") } }) - t.Run("ContextOverridesDefault", func(t *testing.T) { - // Given a handler with an existing context - handler, _ := setup(t) - handler.(*configHandler).context = "test" + t.Run("ReturnsTrueAfterLoadingFiles", func(t *testing.T) { + mocks := setupMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() - // And a context with environment variables that override defaults - handler.(*configHandler).config.Contexts = map[string]*v1alpha1.Context{ - "test": { - Environment: map[string]string{ - "SHARED_VAR": "context_value", - }, - }, - } + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte("provider: local\n"), 0644) + + handler.LoadConfig() - // And default context with the same environment variable - handler.(*configHandler).defaultContextConfig = v1alpha1.Context{ - Environment: map[string]string{ - "SHARED_VAR": "default_value", - }, + result := handler.IsLoaded() + + if !result { + t.Error("Expected IsLoaded=true after loading config files") } + }) - // When getting the config - config := handler.GetConfig() + t.Run("ReturnsTrueAfterLoadConfigString", func(t *testing.T) { + mocks := setupMocks(t) + handler := NewConfigHandler(mocks.Injector) + handler.Initialize() + + handler.LoadConfigString("provider: test\n") - // 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"]) + result := handler.IsLoaded() + + if !result { + t.Error("Expected IsLoaded=true after LoadConfigString") } }) } + +// ============================================================================= +// Test Helpers +// ============================================================================= + +func contains(s, substr string) bool { + return len(s) >= len(substr) && findSubstring(s, substr) +} + +func findSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/config/mock_config_handler.go b/pkg/config/mock_config_handler.go index 95412e2bb..a706a5de0 100644 --- a/pkg/config/mock_config_handler.go +++ b/pkg/config/mock_config_handler.go @@ -4,38 +4,32 @@ import ( "fmt" "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/secrets" ) // MockConfigHandler is a mock implementation of the ConfigHandler interface type MockConfigHandler struct { - InitializeFunc func() error - LoadConfigFunc func(path string) error - LoadConfigStringFunc func(content string) error - LoadContextConfigFunc func() error - IsLoadedFunc func() bool - GetStringFunc func(key string, defaultValue ...string) string - GetIntFunc func(key string, defaultValue ...int) int - GetBoolFunc func(key string, defaultValue ...bool) bool - IsContextConfigLoadedFunc func() bool - GetStringSliceFunc func(key string, defaultValue ...[]string) []string - GetStringMapFunc func(key string, defaultValue ...map[string]string) map[string]string - SetFunc func(key string, value any) error - SetContextValueFunc func(key string, value any) error - SaveConfigFunc func(overwrite ...bool) error - GetFunc func(key string) any - SetDefaultFunc func(context v1alpha1.Context) error - GetConfigFunc func() *v1alpha1.Context - GetContextFunc func() string - SetContextFunc func(context string) error - GetConfigRootFunc func() (string, error) - CleanFunc func() error - SetSecretsProviderFunc func(provider secrets.SecretsProvider) - GenerateContextIDFunc func() error - LoadSchemaFunc func(schemaPath string) error - LoadSchemaFromBytesFunc func(schemaContent []byte) error - GetSchemaDefaultsFunc func() (map[string]any, error) - GetContextValuesFunc func() (map[string]any, error) + InitializeFunc func() error + LoadConfigFunc func() error + LoadConfigStringFunc func(content string) error + IsLoadedFunc func() bool + GetStringFunc func(key string, defaultValue ...string) string + GetIntFunc func(key string, defaultValue ...int) int + GetBoolFunc func(key string, defaultValue ...bool) bool + GetStringSliceFunc func(key string, defaultValue ...[]string) []string + GetStringMapFunc func(key string, defaultValue ...map[string]string) map[string]string + SetFunc func(key string, value any) error + SaveConfigFunc func(overwrite ...bool) error + GetFunc func(key string) any + SetDefaultFunc func(context v1alpha1.Context) error + GetConfigFunc func() *v1alpha1.Context + GetContextFunc func() string + SetContextFunc func(context string) error + GetConfigRootFunc func() (string, error) + CleanFunc func() error + GenerateContextIDFunc func() error + LoadSchemaFunc func(schemaPath string) error + LoadSchemaFromBytesFunc func(schemaContent []byte) error + GetContextValuesFunc func() (map[string]any, error) } // ============================================================================= @@ -60,9 +54,9 @@ func (m *MockConfigHandler) Initialize() error { } // LoadConfig calls the mock LoadConfigFunc if set, otherwise returns nil -func (m *MockConfigHandler) LoadConfig(path string) error { +func (m *MockConfigHandler) LoadConfig() error { if m.LoadConfigFunc != nil { - return m.LoadConfigFunc(path) + return m.LoadConfigFunc() } return nil } @@ -75,14 +69,6 @@ func (m *MockConfigHandler) LoadConfigString(content string) error { return nil } -// LoadContextConfig calls the mock LoadContextConfigFunc if set, otherwise returns nil -func (m *MockConfigHandler) LoadContextConfig() error { - if m.LoadContextConfigFunc != nil { - return m.LoadContextConfigFunc() - } - return nil -} - // IsLoaded calls the mock IsLoadedFunc if set, otherwise returns false func (m *MockConfigHandler) IsLoaded() bool { if m.IsLoadedFunc != nil { @@ -91,14 +77,6 @@ func (m *MockConfigHandler) IsLoaded() bool { return false } -// IsContextConfigLoaded calls the mock IsContextConfigLoadedFunc if set, otherwise returns false -func (m *MockConfigHandler) IsContextConfigLoaded() bool { - if m.IsContextConfigLoadedFunc != nil { - return m.IsContextConfigLoadedFunc() - } - return false -} - // GetString calls the mock GetStringFunc if set, otherwise returns a reasonable default string func (m *MockConfigHandler) GetString(key string, defaultValue ...string) string { if m.GetStringFunc != nil { @@ -162,14 +140,6 @@ func (m *MockConfigHandler) Set(key string, value any) error { return nil } -// SetContextValue calls the mock SetContextValueFunc if set, otherwise returns nil -func (m *MockConfigHandler) SetContextValue(key string, value any) error { - if m.SetContextValueFunc != nil { - return m.SetContextValueFunc(key, value) - } - return nil -} - // Get calls the mock GetFunc if set, otherwise returns a reasonable default value func (m *MockConfigHandler) Get(key string) any { if m.GetFunc != nil { @@ -234,13 +204,6 @@ func (m *MockConfigHandler) Clean() error { return nil } -// SetSecretsProvider calls the mock SetSecretsProviderFunc if set, otherwise does nothing -func (m *MockConfigHandler) SetSecretsProvider(provider secrets.SecretsProvider) { - if m.SetSecretsProviderFunc != nil { - m.SetSecretsProviderFunc(provider) - } -} - // GenerateContextID calls the mock GenerateContextIDFunc if set, otherwise returns nil func (m *MockConfigHandler) GenerateContextID() error { if m.GenerateContextIDFunc != nil { @@ -265,14 +228,6 @@ func (m *MockConfigHandler) LoadSchemaFromBytes(schemaContent []byte) error { return fmt.Errorf("LoadSchemaFromBytesFunc not set") } -// GetSchemaDefaults calls the mock GetSchemaDefaultsFunc if set, otherwise returns an error -func (m *MockConfigHandler) GetSchemaDefaults() (map[string]any, error) { - if m.GetSchemaDefaultsFunc != nil { - return m.GetSchemaDefaultsFunc() - } - return nil, fmt.Errorf("GetSchemaDefaultsFunc not set") -} - // GetContextValues calls the mock GetContextValuesFunc if set, otherwise returns an error func (m *MockConfigHandler) GetContextValues() (map[string]any, error) { if m.GetContextValuesFunc != nil { diff --git a/pkg/config/mock_config_handler_test.go b/pkg/config/mock_config_handler_test.go index 197638b20..2553f30d4 100644 --- a/pkg/config/mock_config_handler_test.go +++ b/pkg/config/mock_config_handler_test.go @@ -2,1070 +2,1254 @@ package config import ( "fmt" - "reflect" "testing" "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/secrets" ) +// The MockConfigHandlerTest is a test suite for the MockConfigHandler implementation. +// It provides comprehensive test coverage for mock config handler operations, +// ensuring reliable testing of config-dependent functionality. +// The MockConfigHandlerTest validates the mock implementation's behavior. + +// ============================================================================= +// Test Setup +// ============================================================================= + +// stringPtr returns a pointer to the given string value +func stringPtr(s string) *string { + return &s +} + +// setupMockConfigHandlerMocks creates a new set of mocks for testing MockConfigHandler +func setupMockConfigHandlerMocks(t *testing.T) *MockConfigHandler { + t.Helper() + + // Create mock config handler + mockConfigHandler := NewMockConfigHandler() + + return mockConfigHandler +} + +// ============================================================================= +// Test Public Methods +// ============================================================================= + +// TestMockConfigHandler_NewMockConfigHandler tests the constructor for MockConfigHandler +func TestMockConfigHandler_NewMockConfigHandler(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given + mockConfigHandler := setupMockConfigHandlerMocks(t) + + // Then the mock config handler should be created successfully + if mockConfigHandler == nil { + t.Errorf("Expected mockConfigHandler, got nil") + } + }) +} + +// TestMockConfigHandler_Initialize tests the Initialize method of MockConfigHandler func TestMockConfigHandler_Initialize(t *testing.T) { - mockInitializeErr := fmt.Errorf("mock initialize error") + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with InitializeFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.InitializeFunc = func() error { + return nil + } + + // When calling Initialize + err := mockConfigHandler.Initialize() - t.Run("WithFuncSet", func(t *testing.T) { + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("Error", func(t *testing.T) { // Given a mock config handler with InitializeFunc set to return an error - handler := NewMockConfigHandler() - handler.InitializeFunc = func() error { return mockInitializeErr } + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock initialize error") + mockConfigHandler.InitializeFunc = func() error { + return expectedError + } - // When Initialize is called - err := handler.Initialize() + // When calling Initialize + err := mockConfigHandler.Initialize() - // Then the error should match the expected mock error - if err != mockInitializeErr { - t.Errorf("Expected error = %v, got = %v", mockInitializeErr, err) + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without InitializeFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with InitializeFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When Initialize is called - err := handler.Initialize() + // When calling Initialize + err := mockConfigHandler.Initialize() - // Then no error should be returned + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) } +// TestMockConfigHandler_LoadConfig tests the LoadConfig method of MockConfigHandler func TestMockConfigHandler_LoadConfig(t *testing.T) { - mockLoadErr := fmt.Errorf("mock load config error") + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with LoadConfigFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.LoadConfigFunc = func() error { + return nil + } + + // When calling LoadConfig + err := mockConfigHandler.LoadConfig() + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) - t.Run("WithPath", func(t *testing.T) { + t.Run("Error", func(t *testing.T) { // Given a mock config handler with LoadConfigFunc set to return an error - handler := NewMockConfigHandler() - handler.LoadConfigFunc = func(path string) error { - return mockLoadErr + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock load config error") + mockConfigHandler.LoadConfigFunc = func() error { + return expectedError } - // When LoadConfig is called with a path - err := handler.LoadConfig("some/path") + // When calling LoadConfig + err := mockConfigHandler.LoadConfig() - // Then the error should match the expected mock error - if err != mockLoadErr { - t.Errorf("Expected error = %v, got = %v", mockLoadErr, err) + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without LoadConfigFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with LoadConfigFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When LoadConfig is called with a path - err := handler.LoadConfig("some/path") + // When calling LoadConfig + err := mockConfigHandler.LoadConfig() - // Then no error should be returned + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_GetString(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetStringFunc set to return an empty string - handler := NewMockConfigHandler() - handler.GetStringFunc = func(key string, defaultValue ...string) string { return "" } +// TestMockConfigHandler_LoadConfigString tests the LoadConfigString method of MockConfigHandler +func TestMockConfigHandler_LoadConfigString(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with LoadConfigStringFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.LoadConfigStringFunc = func(content string) error { + return nil + } - // When GetString is called with a key - value := handler.GetString("someKey") + // When calling LoadConfigString + err := mockConfigHandler.LoadConfigString("test content") - // Then the returned value should be an empty string - if value != "" { - t.Errorf("Expected GetString with key to return empty string, got %v", value) + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetStringFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with LoadConfigStringFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock load config string error") + mockConfigHandler.LoadConfigStringFunc = func(content string) error { + return expectedError + } - // When GetString is called with a key - value := handler.GetString("someKey") + // When calling LoadConfigString + err := mockConfigHandler.LoadConfigString("test content") - // Then the returned value should be 'mock-string' - if value != "mock-string" { - t.Errorf("Expected GetString with no func set to return 'mock-string', got %v", value) + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("WithDefaultValue", func(t *testing.T) { - // Given a mock config handler - handler := NewMockConfigHandler() - defaultValue := "default" + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with LoadConfigStringFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetString is called with a key and a default value - value := handler.GetString("someKey", defaultValue) + // When calling LoadConfigString + err := mockConfigHandler.LoadConfigString("test content") - // Then the returned value should match the default value - if value != defaultValue { - t.Errorf("Expected GetString with default to return %v, got %v", defaultValue, value) + // Then no error should be returned (default implementation) + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_GetInt(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetIntFunc set to return 0 - handler := NewMockConfigHandler() - handler.GetIntFunc = func(key string, defaultValue ...int) int { return 0 } +// TestMockConfigHandler_IsLoaded tests the IsLoaded method of MockConfigHandler +func TestMockConfigHandler_IsLoaded(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with IsLoadedFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := true + mockConfigHandler.IsLoadedFunc = func() bool { + return expectedResult + } - // When GetInt is called with a key - value := handler.GetInt("someKey") + // When calling IsLoaded + result := mockConfigHandler.IsLoaded() - // Then the returned value should be 0 - if value != 0 { - t.Errorf("Expected GetInt with key to return 0, got %v", value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetIntFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with IsLoadedFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetInt is called with a key - value := handler.GetInt("someKey") + // When calling IsLoaded + result := mockConfigHandler.IsLoaded() - // Then the returned value should be 42 - if value != 42 { - t.Errorf("Expected GetInt with no func set to return 42, got %v", value) + // Then false should be returned (default implementation) + if result != false { + t.Errorf("Expected false, got %v", result) } }) +} - t.Run("WithDefaultValue", func(t *testing.T) { - // Given a mock config handler - handler := NewMockConfigHandler() - defaultValue := 42 +// TestMockConfigHandler_GetString tests the GetString method of MockConfigHandler +func TestMockConfigHandler_GetString(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetStringFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := "mock-string-value" + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return expectedResult + } - // When GetInt is called with a key and a default value - value := handler.GetInt("someKey", defaultValue) + // When calling GetString + result := mockConfigHandler.GetString("test-key") - // Then the returned value should match the default value - if value != defaultValue { - t.Errorf("Expected GetInt with default to return %v, got %v", defaultValue, value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) -} -func TestMockConfigHandler_GetBool(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetBoolFunc set to return false - handler := NewMockConfigHandler() - handler.GetBoolFunc = func(key string, defaultValue ...bool) bool { return false } + t.Run("WithDefaultValue", func(t *testing.T) { + // Given a mock config handler with GetStringFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := "mock-string-value" + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return expectedResult + } - // When GetBool is called with a key - value := handler.GetBool("someKey") + // When calling GetString with default value + result := mockConfigHandler.GetString("test-key", "default-value") - // Then the returned value should be false - if value != false { - t.Errorf("Expected GetBool with key to return false, got %v", value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetBoolFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetStringFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetBool is called with a key - value := handler.GetBool("someKey") + // When calling GetString + result := mockConfigHandler.GetString("test-key") - // Then the returned value should be true - if value != true { - t.Errorf("Expected GetBool with no func set to return true, got %v", value) + // Then the default mock string should be returned + expectedResult := "mock-string" + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithDefaultValue", func(t *testing.T) { - // Given a mock config handler - handler := NewMockConfigHandler() - defaultValue := true + t.Run("NotImplementedWithDefault", func(t *testing.T) { + // Given a mock config handler with GetStringFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetBool is called with a key and a default value - value := handler.GetBool("someKey", defaultValue) + // When calling GetString with default value + result := mockConfigHandler.GetString("test-key", "custom-default") - // Then the returned value should match the default value - if value != defaultValue { - t.Errorf("Expected GetBool with default to return %v, got %v", defaultValue, value) + // Then the custom default should be returned + expectedResult := "custom-default" + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) } -func TestMockConfigHandler_GetStringSlice(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetStringSliceFunc set to return a specific slice - handler := NewMockConfigHandler() - expectedSlice := []string{"value1", "value2"} - handler.GetStringSliceFunc = func(key string, defaultValue ...[]string) []string { return expectedSlice } +// TestMockConfigHandler_GetInt tests the GetInt method of MockConfigHandler +func TestMockConfigHandler_GetInt(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetIntFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := 123 + mockConfigHandler.GetIntFunc = func(key string, defaultValue ...int) int { + return expectedResult + } - // When GetStringSlice is called with a key - value := handler.GetStringSlice("someKey") + // When calling GetInt + result := mockConfigHandler.GetInt("test-key") - // Then the returned value should match the expected slice - if !reflect.DeepEqual(value, expectedSlice) { - t.Errorf("Expected GetStringSlice with key to return %v, got %v", expectedSlice, value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetStringSliceFunc set - handler := NewMockConfigHandler() + t.Run("WithDefaultValue", func(t *testing.T) { + // Given a mock config handler with GetIntFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := 456 + mockConfigHandler.GetIntFunc = func(key string, defaultValue ...int) int { + return expectedResult + } - // When GetStringSlice is called with a key - value := handler.GetStringSlice("someKey") + // When calling GetInt with default value + result := mockConfigHandler.GetInt("test-key", 999) - // Then the returned value should be the default empty slice - if len(value) != 0 { - t.Errorf("Expected GetStringSlice with no func set to return an empty slice, got %v", value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithDefaultValue", func(t *testing.T) { - // Given a mock config handler - handler := NewMockConfigHandler() - defaultValue := []string{"default1", "default2"} + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetIntFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetStringSlice is called with a key and a default value - value := handler.GetStringSlice("someKey", defaultValue) + // When calling GetInt + result := mockConfigHandler.GetInt("test-key") - // Then the returned value should match the default value - if !reflect.DeepEqual(value, defaultValue) { - t.Errorf("Expected GetStringSlice with default to return %v, got %v", defaultValue, value) + // Then the default mock int should be returned + expectedResult := 42 + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) -} -func TestMockConfigHandler_GetStringMap(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetStringMapFunc set to return a specific map - handler := NewMockConfigHandler() - expectedMap := map[string]string{"key1": "value1", "key2": "value2"} - handler.GetStringMapFunc = func(key string, defaultValue ...map[string]string) map[string]string { return expectedMap } + t.Run("NotImplementedWithDefault", func(t *testing.T) { + // Given a mock config handler with GetIntFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetStringMap is called with a key - value := handler.GetStringMap("someKey") + // When calling GetInt with default value + result := mockConfigHandler.GetInt("test-key", 999) - // Then the returned value should match the expected map - if !reflect.DeepEqual(value, expectedMap) { - t.Errorf("Expected GetStringMap with key to return %v, got %v", expectedMap, value) + // Then the custom default should be returned + expectedResult := 999 + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) +} - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetStringMapFunc set - handler := NewMockConfigHandler() +// TestMockConfigHandler_GetBool tests the GetBool method of MockConfigHandler +func TestMockConfigHandler_GetBool(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetBoolFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := false + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + return expectedResult + } - // When GetStringMap is called with a key - value := handler.GetStringMap("someKey") + // When calling GetBool + result := mockConfigHandler.GetBool("test-key") - // Then the returned value should be the default empty map - if len(value) != 0 { - t.Errorf("Expected GetStringMap with no func set to return an empty map, got %v", value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) t.Run("WithDefaultValue", func(t *testing.T) { - // Given a mock config handler - handler := NewMockConfigHandler() - defaultValue := map[string]string{"defaultKey1": "defaultValue1", "defaultKey2": "defaultValue2"} + // Given a mock config handler with GetBoolFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := false + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + return expectedResult + } - // When GetStringMap is called with a key and a default value - value := handler.GetStringMap("someKey", defaultValue) + // When calling GetBool with default value + result := mockConfigHandler.GetBool("test-key", true) - // Then the returned value should match the default value - if !reflect.DeepEqual(value, defaultValue) { - t.Errorf("Expected GetStringMap with default to return %v, got %v", defaultValue, value) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) -} -func TestMockConfigHandler_Set(t *testing.T) { - t.Run("WithKeyAndValue", func(t *testing.T) { - // Given a mock config handler with SetFunc set to do nothing - handler := NewMockConfigHandler() - handler.SetFunc = func(key string, value any) error { return nil } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetBoolFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When Set is called with a key and a value - handler.Set("someKey", "someValue") + // When calling GetBool + result := mockConfigHandler.GetBool("test-key") - // Then no error should occur + // Then the default mock bool should be returned + expectedResult := true + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) + } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without SetFunc set - handler := NewMockConfigHandler() + t.Run("NotImplementedWithDefault", func(t *testing.T) { + // Given a mock config handler with GetBoolFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When Set is called with a key and a value - handler.Set("someKey", "someValue") + // When calling GetBool with default value + result := mockConfigHandler.GetBool("test-key", false) - // Then no error should occur + // Then the custom default should be returned + expectedResult := false + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) + } }) } -func TestMockConfigHandler_SetContextValue(t *testing.T) { - t.Run("WithKeyAndValue", func(t *testing.T) { - // Given a mock config handler with SetContextValueFunc set to do nothing - handler := NewMockConfigHandler() - handler.SetContextValueFunc = func(key string, value any) error { return nil } +// TestMockConfigHandler_GetStringSlice tests the GetStringSlice method of MockConfigHandler +func TestMockConfigHandler_GetStringSlice(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetStringSliceFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := []string{"item1", "item2"} + mockConfigHandler.GetStringSliceFunc = func(key string, defaultValue ...[]string) []string { + return expectedResult + } - // When SetContextValue is called with a key and a value - err := handler.SetContextValue("someKey", "someValue") + // When calling GetStringSlice + result := mockConfigHandler.GetStringSlice("test-key") - // Then no error should be returned - if err != nil { - t.Errorf("Expected SetContextValue to return nil, got %v", err) + // Then the expected result should be returned + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result[0] != expectedResult[0] || result[1] != expectedResult[1] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without SetContextValueFunc set - handler := NewMockConfigHandler() + t.Run("WithDefaultValue", func(t *testing.T) { + // Given a mock config handler with GetStringSliceFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := []string{"custom1", "custom2"} + mockConfigHandler.GetStringSliceFunc = func(key string, defaultValue ...[]string) []string { + return expectedResult + } - // When SetContextValue is called with a key and a value - err := handler.SetContextValue("someKey", "someValue") + // When calling GetStringSlice with default value + result := mockConfigHandler.GetStringSlice("test-key", []string{"default1", "default2"}) - // Then no error should be returned - if err != nil { - t.Errorf("Expected SetContextValue to return nil, got %v", err) + // Then the expected result should be returned + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result[0] != expectedResult[0] || result[1] != expectedResult[1] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) -} -func TestMockConfigHandler_SaveConfig(t *testing.T) { - mockSaveErr := fmt.Errorf("mock save config error") - - t.Run("WithPath", func(t *testing.T) { - // Given a mock config handler with SaveConfigFunc set to return an error - handler := NewMockConfigHandler() - handler.SaveConfigFunc = func(overwrite ...bool) error { return mockSaveErr } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetStringSliceFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When SaveConfig is called - err := handler.SaveConfig() + // When calling GetStringSlice + result := mockConfigHandler.GetStringSlice("test-key") - // Then the error should match the expected mock error - if err != mockSaveErr { - t.Errorf("Expected error = %v, got = %v", mockSaveErr, err) + // Then an empty slice should be returned (default implementation) + if len(result) != 0 { + t.Errorf("Expected empty slice, got %v", result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without SaveConfigFunc set - handler := NewMockConfigHandler() + t.Run("NotImplementedWithDefault", func(t *testing.T) { + // Given a mock config handler with GetStringSliceFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When SaveConfig is called - err := handler.SaveConfig() + // When calling GetStringSlice with default value + result := mockConfigHandler.GetStringSlice("test-key", []string{"default1", "default2"}) - // Then no error should be returned - if err != nil { - t.Errorf("Expected SaveConfig to return nil, got %v", err) + // Then the custom default should be returned + expectedResult := []string{"default1", "default2"} + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result[0] != expectedResult[0] || result[1] != expectedResult[1] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) } -func TestMockConfigHandler_Get(t *testing.T) { - t.Run("WithKey", func(t *testing.T) { - // Given a mock config handler with GetFunc set to return 'mock-value' - handler := NewMockConfigHandler() - handler.GetFunc = func(key string) any { return "mock-value" } +// TestMockConfigHandler_GetStringMap tests the GetStringMap method of MockConfigHandler +func TestMockConfigHandler_GetStringMap(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetStringMapFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := map[string]string{"key1": "value1", "key2": "value2"} + mockConfigHandler.GetStringMapFunc = func(key string, defaultValue ...map[string]string) map[string]string { + return expectedResult + } - // When Get is called with a key - value := handler.Get("someKey") + // When calling GetStringMap + result := mockConfigHandler.GetStringMap("test-key") - // Then the returned value should be 'mock-value' - if value != "mock-value" { - t.Errorf("Expected Get to return 'mock-value', got %v", value) + // Then the expected result should be returned + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result["key1"] != expectedResult["key1"] || result["key2"] != expectedResult["key2"] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetFunc set - handler := NewMockConfigHandler() + t.Run("WithDefaultValue", func(t *testing.T) { + // Given a mock config handler with GetStringMapFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := map[string]string{"custom1": "custom2"} + mockConfigHandler.GetStringMapFunc = func(key string, defaultValue ...map[string]string) map[string]string { + return expectedResult + } - // When Get is called with a key - value := handler.Get("someKey") + // When calling GetStringMap with default value + result := mockConfigHandler.GetStringMap("test-key", map[string]string{"default1": "default2"}) - // Then the returned value should be 'mock-value' - if value != "mock-value" { - t.Errorf("Expected Get to return 'mock-value', got %v", value) + // Then the expected result should be returned + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result["custom1"] != expectedResult["custom1"] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) -} -func TestMockConfigHandler_SetDefault(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with SetDefaultFunc set to verify parameters - mockHandler := NewMockConfigHandler() - called := false - - // And SetDefaultFunc updates the flag and checks the parameters - mockHandler.SetDefaultFunc = func(context v1alpha1.Context) error { - called = true - if !reflect.DeepEqual(context, DefaultConfig_Localhost) { - t.Errorf("Expected value %v, got %v", DefaultConfig_Localhost, context) - } - return nil - } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetStringMapFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When SetDefault is called - mockHandler.SetDefault(DefaultConfig_Localhost) + // When calling GetStringMap + result := mockConfigHandler.GetStringMap("test-key") - // Then the function should be called - if !called { - t.Error("Expected SetDefaultFunc to be called") + // Then an empty map should be returned (default implementation) + if len(result) != 0 { + t.Errorf("Expected empty map, got %v", result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without SetDefaultFunc set - mockHandler := NewMockConfigHandler() + t.Run("NotImplementedWithDefault", func(t *testing.T) { + // Given a mock config handler with GetStringMapFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When SetDefault is called - mockHandler.SetDefault(DefaultConfig_Localhost) + // When calling GetStringMap with default value + result := mockConfigHandler.GetStringMap("test-key", map[string]string{"default1": "default2"}) - // Then no error should occur + // Then the custom default should be returned + expectedResult := map[string]string{"default1": "default2"} + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result["default1"] != expectedResult["default1"] { + t.Errorf("Expected result %v, got %v", expectedResult, result) + } }) } -func TestMockConfigHandler_GetConfig(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with GetConfigFunc set to return a mock context - mockHandler := NewMockConfigHandler() - called := false - - // And GetConfigFunc updates the flag and returns a mock context - mockContext := &v1alpha1.Context{} - mockHandler.GetConfigFunc = func() *v1alpha1.Context { - called = true - return mockContext +// TestMockConfigHandler_Set tests the Set method of MockConfigHandler +func TestMockConfigHandler_Set(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with SetFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.SetFunc = func(key string, value any) error { + return nil } - // When GetConfig is called - config := mockHandler.GetConfig() + // When calling Set + err := mockConfigHandler.Set("test-key", "test-value") + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) - // Then the returned config should match the mock context - if !reflect.DeepEqual(config, mockContext) { - t.Errorf("Expected GetConfig to return %v, got %v", mockContext, config) + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with SetFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock set error") + mockConfigHandler.SetFunc = func(key string, value any) error { + return expectedError } - // And the function should be called - if !called { - t.Error("Expected GetConfigFunc to be called") + // When calling Set + err := mockConfigHandler.Set("test-key", "test-value") + + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("NoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetConfigFunc set - mockHandler := NewMockConfigHandler() - mockHandler.GetConfigFunc = nil + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with SetFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetConfig is called - config := mockHandler.GetConfig() + // When calling Set + err := mockConfigHandler.Set("test-key", "test-value") - // Then an empty Context should be returned - if !reflect.DeepEqual(config, &v1alpha1.Context{}) { - t.Errorf("Expected GetConfig to return empty Context, got %v", config) + // Then no error should be returned (default implementation) + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_GetContext(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler - handler := NewMockConfigHandler() - - // And the GetContextFunc is set to return a specific mock context string - handler.GetContextFunc = func() string { - return "mock-context" +// TestMockConfigHandler_Get tests the Get method of MockConfigHandler +func TestMockConfigHandler_Get(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := "mock-get-value" + mockConfigHandler.GetFunc = func(key string) any { + return expectedResult } - // When GetContext is called to retrieve the context - context := handler.GetContext() + // When calling Get + result := mockConfigHandler.Get("test-key") - // Then the returned context should match the expected mock context - if context != "mock-context" { - t.Errorf("Expected GetContext to return 'mock-context', got %v", context) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without setting GetContextFunc - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetContext is called to retrieve the context - context := handler.GetContext() + // When calling Get + result := mockConfigHandler.Get("test-key") - // Then the returned context should match the default mock context - if context != "mock-context" { - t.Errorf("Expected GetContext to return 'mock-context', got %v", context) + // Then the default mock value should be returned + expectedResult := "mock-value" + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) } -func TestMockConfigHandler_SetContext(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler - handler := NewMockConfigHandler() - - // And the SetContextFunc is set to a function that returns no error - handler.SetContextFunc = func(context string) error { +// TestMockConfigHandler_SaveConfig tests the SaveConfig method of MockConfigHandler +func TestMockConfigHandler_SaveConfig(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with SaveConfigFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.SaveConfigFunc = func(overwrite ...bool) error { return nil } - // When SetContext is called with a mock context string - err := handler.SetContext("mock-context") + // When calling SaveConfig + err := mockConfigHandler.SaveConfig() // Then no error should be returned if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without setting SetContextFunc - handler := NewMockConfigHandler() + t.Run("WithOverwrite", func(t *testing.T) { + // Given a mock config handler with SaveConfigFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.SaveConfigFunc = func(overwrite ...bool) error { + return nil + } - // When SetContext is called with a mock context string - err := handler.SetContext("mock-context") + // When calling SaveConfig with overwrite + err := mockConfigHandler.SaveConfig(true) // Then no error should be returned if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) -} -func TestMockConfigHandler_GetConfigRoot(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler with GetConfigRootFunc set - handler := NewMockConfigHandler() - handler.GetConfigRootFunc = func() (string, error) { return "mock-config-root", nil } + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with SaveConfigFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock save config error") + mockConfigHandler.SaveConfigFunc = func(overwrite ...bool) error { + return expectedError + } - // When GetConfigRoot is called - root, err := handler.GetConfigRoot() + // When calling SaveConfig + err := mockConfigHandler.SaveConfig() - // Then no error should be returned - if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") } - - // And the root should be 'mock-config-root' - if root != "mock-config-root" { - t.Errorf("Expected GetConfigRoot to return 'mock-config-root', got %v", root) + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without GetConfigRootFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with SaveConfigFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetConfigRoot is called - root, err := handler.GetConfigRoot() + // When calling SaveConfig + err := mockConfigHandler.SaveConfig() - // Then no error should be returned + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) - } - - // And the root should be 'mock-config-root' - if root != "mock-config-root" { - t.Errorf("Expected GetConfigRoot to return 'mock-config-root', got %v", root) + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_Clean(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler with CleanFunc set - handler := NewMockConfigHandler() - handler.CleanFunc = func() error { return nil } +// TestMockConfigHandler_SetDefault tests the SetDefault method of MockConfigHandler +func TestMockConfigHandler_SetDefault(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with SetDefaultFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.SetDefaultFunc = func(context v1alpha1.Context) error { + return nil + } - // When Clean is called - err := handler.Clean() + // When calling SetDefault + err := mockConfigHandler.SetDefault(v1alpha1.Context{}) // Then no error should be returned if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without CleanFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with SetDefaultFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock set default error") + mockConfigHandler.SetDefaultFunc = func(context v1alpha1.Context) error { + return expectedError + } - // When Clean is called - err := handler.Clean() + // When calling SetDefault + err := mockConfigHandler.SetDefault(v1alpha1.Context{}) - // Then no error should be returned + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) + } + }) + + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with SetDefaultFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) + + // When calling SetDefault + err := mockConfigHandler.SetDefault(v1alpha1.Context{}) + + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_IsLoaded(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler with IsLoadedFunc set - handler := NewMockConfigHandler() - handler.IsLoadedFunc = func() bool { return true } +// TestMockConfigHandler_GetConfig tests the GetConfig method of MockConfigHandler +func TestMockConfigHandler_GetConfig(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetConfigFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := &v1alpha1.Context{ID: stringPtr("test-context")} + mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return expectedResult + } - // When IsLoaded is called - loaded := handler.IsLoaded() + // When calling GetConfig + result := mockConfigHandler.GetConfig() - // Then the returned value should be true - if !loaded { - t.Errorf("Expected IsLoaded to return true, got %v", loaded) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without IsLoadedFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetConfigFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When IsLoaded is called - loaded := handler.IsLoaded() + // When calling GetConfig + result := mockConfigHandler.GetConfig() - // Then the returned value should be false - if loaded { - t.Errorf("Expected IsLoaded to return false, got %v", loaded) + // Then an empty context should be returned (default implementation) + if result == nil { + t.Error("Expected non-nil result, got nil") + } + if result.ID != nil { + t.Errorf("Expected empty context, got %v", result) } }) } -func TestMockConfigHandler_IsContextConfigLoaded(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a new mock config handler with IsContextConfigLoadedFunc set - handler := NewMockConfigHandler() - handler.IsContextConfigLoadedFunc = func() bool { return true } +// TestMockConfigHandler_GetContext tests the GetContext method of MockConfigHandler +func TestMockConfigHandler_GetContext(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetContextFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := "test-context" + mockConfigHandler.GetContextFunc = func() string { + return expectedResult + } - // When IsContextConfigLoaded is called - loaded := handler.IsContextConfigLoaded() + // When calling GetContext + result := mockConfigHandler.GetContext() - // Then the returned value should be true - if !loaded { - t.Errorf("Expected IsContextConfigLoaded to return true, got %v", loaded) + // Then the expected result should be returned + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a new mock config handler without IsContextConfigLoadedFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetContextFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When IsContextConfigLoaded is called - loaded := handler.IsContextConfigLoaded() + // When calling GetContext + result := mockConfigHandler.GetContext() - // Then the returned value should be false - if loaded { - t.Errorf("Expected IsContextConfigLoaded to return false, got %v", loaded) + // Then the default mock context should be returned + expectedResult := "mock-context" + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) } -func TestMockConfigHandler_LoadConfigString(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with LoadConfigStringFunc set - handler := NewMockConfigHandler() - mockErr := fmt.Errorf("mock load config string error") - handler.LoadConfigStringFunc = func(content string) error { return mockErr } +// TestMockConfigHandler_SetContext tests the SetContext method of MockConfigHandler +func TestMockConfigHandler_SetContext(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with SetContextFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.SetContextFunc = func(context string) error { + return nil + } - // When LoadConfigString is called - err := handler.LoadConfigString("some content") + // When calling SetContext + err := mockConfigHandler.SetContext("test-context") - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without LoadConfigStringFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with SetContextFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock set context error") + mockConfigHandler.SetContextFunc = func(context string) error { + return expectedError + } - // When LoadConfigString is called - err := handler.LoadConfigString("some content") + // When calling SetContext + err := mockConfigHandler.SetContext("test-context") - // Then no error should be returned + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) + } + }) + + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with SetContextFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) + + // When calling SetContext + err := mockConfigHandler.SetContext("test-context") + + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_SetSecretsProvider(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with SetSecretsProviderFunc set - handler := NewMockConfigHandler() - var calledProvider secrets.SecretsProvider - handler.SetSecretsProviderFunc = func(provider secrets.SecretsProvider) { - calledProvider = provider +// TestMockConfigHandler_GetConfigRoot tests the GetConfigRoot method of MockConfigHandler +func TestMockConfigHandler_GetConfigRoot(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetConfigRootFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := "/mock/config/root" + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return expectedResult, nil } - // And a mock secrets provider - mockProvider := secrets.NewMockSecretsProvider(nil) - - // When setting the secrets provider - handler.SetSecretsProvider(mockProvider) + // When calling GetConfigRoot + result, err := mockConfigHandler.GetConfigRoot() - // Then the function should be called with the provider - if calledProvider != mockProvider { - t.Errorf("Expected SetSecretsProviderFunc to be called with %v, got %v", mockProvider, calledProvider) + // Then no error should be returned and the result should match + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without SetSecretsProviderFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with GetConfigRootFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock get config root error") + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", expectedError + } - // And a mock secrets provider - mockProvider := secrets.NewMockSecretsProvider(nil) + // When calling GetConfigRoot + result, err := mockConfigHandler.GetConfigRoot() - // When setting the secrets provider - // Then it should not panic - handler.SetSecretsProvider(mockProvider) + // Then the expected error should be returned and result should be empty + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) + } + if result != "" { + t.Errorf("Expected empty result, got %v", result) + } }) -} -func TestMockConfigHandler_GenerateContextID(t *testing.T) { - t.Run("WithMockFunction", func(t *testing.T) { - // Given a mock config handler with GenerateContextIDFunc set - handler := NewMockConfigHandler() - mockErr := fmt.Errorf("mock generate context ID error") - handler.GenerateContextIDFunc = func() error { return mockErr } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetConfigRootFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GenerateContextID is called - err := handler.GenerateContextID() + // When calling GetConfigRoot + result, err := mockConfigHandler.GetConfigRoot() - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) + // Then no error should be returned and result should be default (default implementation) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + expectedResult := "mock-config-root" + if result != expectedResult { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) +} - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GenerateContextIDFunc set - handler := NewMockConfigHandler() +// TestMockConfigHandler_Clean tests the Clean method of MockConfigHandler +func TestMockConfigHandler_Clean(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with CleanFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.CleanFunc = func() error { + return nil + } - // When GenerateContextID is called - err := handler.GenerateContextID() + // When calling Clean + err := mockConfigHandler.Clean() // Then no error should be returned if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) + t.Errorf("Expected no error, got %v", err) } }) -} -func TestMockConfigHandler_LoadContextConfig(t *testing.T) { - t.Run("WithFuncSet", func(t *testing.T) { - // Given a MockConfigHandler with LoadContextConfigFunc set - mockHandler := NewMockConfigHandler() - expectedError := fmt.Errorf("mocked load context config error") - mockHandler.LoadContextConfigFunc = func() error { + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with CleanFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock clean error") + mockConfigHandler.CleanFunc = func() error { return expectedError } - // When LoadContextConfig is called - err := mockHandler.LoadContextConfig() + // When calling Clean + err := mockConfigHandler.Clean() - // Then it should return the mocked error - if err != expectedError { - t.Errorf("LoadContextConfig() error = %v, expected %v", err, expectedError) + // Then the expected error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a MockConfigHandler with no LoadContextConfigFunc set - mockHandler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with CleanFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When LoadContextConfig is called - err := mockHandler.LoadContextConfig() + // When calling Clean + err := mockConfigHandler.Clean() - // Then it should return nil + // Then no error should be returned (default implementation) if err != nil { - t.Errorf("LoadContextConfig() error = %v, expected nil", err) + t.Errorf("Expected no error, got %v", err) } }) } -func TestMockConfigHandler_LoadSchema(t *testing.T) { - mockErr := fmt.Errorf("mock load schema error") - - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with LoadSchemaFunc set to return an error - handler := NewMockConfigHandler() - handler.LoadSchemaFunc = func(path string) error { - return mockErr - } - - // When LoadSchema is called - err := handler.LoadSchema("/path/to/schema.yaml") - - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) - } - }) - - t.Run("WithFuncSetSuccess", func(t *testing.T) { - // Given a mock config handler with LoadSchemaFunc set to return nil - handler := NewMockConfigHandler() - var capturedPath string - handler.LoadSchemaFunc = func(path string) error { - capturedPath = path +// TestMockConfigHandler_GenerateContextID tests the GenerateContextID method of MockConfigHandler +func TestMockConfigHandler_GenerateContextID(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GenerateContextIDFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.GenerateContextIDFunc = func() error { return nil } - // When LoadSchema is called with a specific path - testPath := "/test/schema.yaml" - err := handler.LoadSchema(testPath) + // When calling GenerateContextID + err := mockConfigHandler.GenerateContextID() // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got = %v", err) - } - - // And the path should be captured correctly - if capturedPath != testPath { - t.Errorf("Expected path = %s, got = %s", testPath, capturedPath) + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without LoadSchemaFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with GenerateContextIDFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock generate context id error") + mockConfigHandler.GenerateContextIDFunc = func() error { + return expectedError + } - // When LoadSchema is called - err := handler.LoadSchema("/path/to/schema.yaml") + // When calling GenerateContextID + err := mockConfigHandler.GenerateContextID() - // Then an error should be returned + // Then the expected error should be returned if err == nil { - t.Error("Expected error when LoadSchemaFunc not set, got nil") + t.Error("Expected error, got nil") } - expectedErr := "LoadSchemaFunc not set" - if err.Error() != expectedErr { - t.Errorf("Expected error message = %s, got = %s", expectedErr, err.Error()) + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) -} -func TestMockConfigHandler_LoadSchemaFromBytes(t *testing.T) { - mockErr := fmt.Errorf("mock load schema from bytes error") - - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with LoadSchemaFromBytesFunc set to return an error - handler := NewMockConfigHandler() - handler.LoadSchemaFromBytesFunc = func(content []byte) error { - return mockErr - } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GenerateContextIDFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When LoadSchemaFromBytes is called - err := handler.LoadSchemaFromBytes([]byte("schema content")) + // When calling GenerateContextID + err := mockConfigHandler.GenerateContextID() - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) + // Then no error should be returned (default implementation) + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) +} - t.Run("WithFuncSetSuccess", func(t *testing.T) { - // Given a mock config handler with LoadSchemaFromBytesFunc set to return nil - handler := NewMockConfigHandler() - var capturedContent []byte - handler.LoadSchemaFromBytesFunc = func(content []byte) error { - capturedContent = content +// TestMockConfigHandler_LoadSchema tests the LoadSchema method of MockConfigHandler +func TestMockConfigHandler_LoadSchema(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.LoadSchemaFunc = func(schemaPath string) error { return nil } - // When LoadSchemaFromBytes is called with specific content - testContent := []byte("test schema content") - err := handler.LoadSchemaFromBytes(testContent) + // When calling LoadSchema + err := mockConfigHandler.LoadSchema("/mock/schema.yaml") // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got = %v", err) - } - - // And the content should be captured correctly - if string(capturedContent) != string(testContent) { - t.Errorf("Expected content = %s, got = %s", string(testContent), string(capturedContent)) + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without LoadSchemaFromBytesFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock load schema error") + mockConfigHandler.LoadSchemaFunc = func(schemaPath string) error { + return expectedError + } - // When LoadSchemaFromBytes is called - err := handler.LoadSchemaFromBytes([]byte("schema content")) + // When calling LoadSchema + err := mockConfigHandler.LoadSchema("/mock/schema.yaml") - // Then an error should be returned + // Then the expected error should be returned if err == nil { - t.Error("Expected error when LoadSchemaFromBytesFunc not set, got nil") + t.Error("Expected error, got nil") } - expectedErr := "LoadSchemaFromBytesFunc not set" - if err.Error() != expectedErr { - t.Errorf("Expected error message = %s, got = %s", expectedErr, err.Error()) + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) -} - -func TestMockConfigHandler_GetSchemaDefaults(t *testing.T) { - mockErr := fmt.Errorf("mock get schema defaults error") - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with GetSchemaDefaultsFunc set to return an error - handler := NewMockConfigHandler() - handler.GetSchemaDefaultsFunc = func() (map[string]any, error) { - return nil, mockErr - } + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetSchemaDefaults is called - defaults, err := handler.GetSchemaDefaults() + // When calling LoadSchema + err := mockConfigHandler.LoadSchema("/mock/schema.yaml") - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) + // Then an error should be returned (default implementation) + if err == nil { + t.Error("Expected error, got nil") } - - // And defaults should be nil - if defaults != nil { - t.Errorf("Expected defaults = nil, got = %v", defaults) + expectedError := "LoadSchemaFunc not set" + if err.Error() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) +} - t.Run("WithFuncSetSuccess", func(t *testing.T) { - // Given a mock config handler with GetSchemaDefaultsFunc set to return defaults - handler := NewMockConfigHandler() - expectedDefaults := map[string]any{ - "key1": "value1", - "key2": 42, - "key3": true, - } - handler.GetSchemaDefaultsFunc = func() (map[string]any, error) { - return expectedDefaults, nil +// TestMockConfigHandler_LoadSchemaFromBytes tests the LoadSchemaFromBytes method of MockConfigHandler +func TestMockConfigHandler_LoadSchemaFromBytes(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFromBytesFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + mockConfigHandler.LoadSchemaFromBytesFunc = func(schemaContent []byte) error { + return nil } - // When GetSchemaDefaults is called - defaults, err := handler.GetSchemaDefaults() + // When calling LoadSchemaFromBytes + err := mockConfigHandler.LoadSchemaFromBytes([]byte("schema content")) // Then no error should be returned if err != nil { - t.Errorf("Expected no error, got = %v", err) - } - - // And the defaults should match the expected values - if !reflect.DeepEqual(defaults, expectedDefaults) { - t.Errorf("Expected defaults = %v, got = %v", expectedDefaults, defaults) + t.Errorf("Expected no error, got %v", err) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetSchemaDefaultsFunc set - handler := NewMockConfigHandler() + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFromBytesFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock load schema from bytes error") + mockConfigHandler.LoadSchemaFromBytesFunc = func(schemaContent []byte) error { + return expectedError + } - // When GetSchemaDefaults is called - defaults, err := handler.GetSchemaDefaults() + // When calling LoadSchemaFromBytes + err := mockConfigHandler.LoadSchemaFromBytes([]byte("schema content")) - // Then an error should be returned + // Then the expected error should be returned if err == nil { - t.Error("Expected error when GetSchemaDefaultsFunc not set, got nil") + t.Error("Expected error, got nil") } - expectedErr := "GetSchemaDefaultsFunc not set" - if err.Error() != expectedErr { - t.Errorf("Expected error message = %s, got = %s", expectedErr, err.Error()) + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) } + }) + + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with LoadSchemaFromBytesFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // And defaults should be nil - if defaults != nil { - t.Errorf("Expected defaults = nil, got = %v", defaults) + // When calling LoadSchemaFromBytes + err := mockConfigHandler.LoadSchemaFromBytes([]byte("schema content")) + + // Then an error should be returned (default implementation) + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "LoadSchemaFromBytesFunc not set" + if err.Error() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, err) } }) } +// TestMockConfigHandler_GetContextValues tests the GetContextValues method of MockConfigHandler func TestMockConfigHandler_GetContextValues(t *testing.T) { - mockErr := fmt.Errorf("mock get context values error") - - t.Run("WithFuncSet", func(t *testing.T) { - // Given a mock config handler with GetContextValuesFunc set to return an error - handler := NewMockConfigHandler() - handler.GetContextValuesFunc = func() (map[string]any, error) { - return nil, mockErr + t.Run("Success", func(t *testing.T) { + // Given a mock config handler with GetContextValuesFunc set + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedResult := map[string]any{"key1": "value1", "key2": 42} + mockConfigHandler.GetContextValuesFunc = func() (map[string]any, error) { + return expectedResult, nil } - // When GetContextValues is called - values, err := handler.GetContextValues() + // When calling GetContextValues + result, err := mockConfigHandler.GetContextValues() - // Then the error should match the expected mock error - if err != mockErr { - t.Errorf("Expected error = %v, got = %v", mockErr, err) + // Then no error should be returned and the result should match + if err != nil { + t.Errorf("Expected no error, got %v", err) } - - // And values should be nil - if values != nil { - t.Errorf("Expected values = nil, got = %v", values) + if len(result) != len(expectedResult) { + t.Errorf("Expected result length %v, got %v", len(expectedResult), len(result)) + } + if result["key1"] != expectedResult["key1"] || result["key2"] != expectedResult["key2"] { + t.Errorf("Expected result %v, got %v", expectedResult, result) } }) - t.Run("WithFuncSetSuccess", func(t *testing.T) { - // Given a mock config handler with GetContextValuesFunc set to return values - handler := NewMockConfigHandler() - expectedValues := map[string]any{ - "environment": map[string]any{ - "VAR1": "value1", - "VAR2": "value2", - }, - "cluster": map[string]any{ - "enabled": true, - "nodes": 3, - }, - } - handler.GetContextValuesFunc = func() (map[string]any, error) { - return expectedValues, nil + t.Run("Error", func(t *testing.T) { + // Given a mock config handler with GetContextValuesFunc set to return an error + mockConfigHandler := setupMockConfigHandlerMocks(t) + expectedError := fmt.Errorf("mock get context values error") + mockConfigHandler.GetContextValuesFunc = func() (map[string]any, error) { + return nil, expectedError } - // When GetContextValues is called - values, err := handler.GetContextValues() + // When calling GetContextValues + result, err := mockConfigHandler.GetContextValues() - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got = %v", err) + // Then the expected error should be returned and result should be nil + if err == nil { + t.Error("Expected error, got nil") } - - // And the values should match the expected values - if !reflect.DeepEqual(values, expectedValues) { - t.Errorf("Expected values = %v, got = %v", expectedValues, values) + if err.Error() != expectedError.Error() { + t.Errorf("Expected error %v, got %v", expectedError, err) + } + if result != nil { + t.Errorf("Expected nil result, got %v", result) } }) - t.Run("WithNoFuncSet", func(t *testing.T) { - // Given a mock config handler without GetContextValuesFunc set - handler := NewMockConfigHandler() + t.Run("NotImplemented", func(t *testing.T) { + // Given a mock config handler with GetContextValuesFunc not set + mockConfigHandler := setupMockConfigHandlerMocks(t) - // When GetContextValues is called - values, err := handler.GetContextValues() + // When calling GetContextValues + result, err := mockConfigHandler.GetContextValues() - // Then an error should be returned + // Then an error should be returned and result should be nil (default implementation) if err == nil { - t.Error("Expected error when GetContextValuesFunc not set, got nil") + t.Error("Expected error, got nil") } - expectedErr := "GetContextValuesFunc not set" - if err.Error() != expectedErr { - t.Errorf("Expected error message = %s, got = %s", expectedErr, err.Error()) + expectedError := "GetContextValuesFunc not set" + if err.Error() != expectedError { + t.Errorf("Expected error %v, got %v", expectedError, err) } - - // And values should be nil - if values != nil { - t.Errorf("Expected values = nil, got = %v", values) + if result != nil { + t.Errorf("Expected nil result, got %v", result) } }) } diff --git a/pkg/env/azure_env_test.go b/pkg/env/azure_env_test.go index cfc796567..ad55015e4 100644 --- a/pkg/env/azure_env_test.go +++ b/pkg/env/azure_env_test.go @@ -31,6 +31,9 @@ contexts: environment: "test-environment" ` } + if opts[0].Context == "" { + opts[0].Context = "mock-context" + } mocks := setupMocks(t, opts[0]) // Mock stat function to make Azure config directory exist @@ -105,14 +108,15 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { }) t.Run("MissingConfiguration", func(t *testing.T) { - printer, mocks := setup(t) - if err := mocks.ConfigHandler.LoadConfigString(` + // Setup without default Azure config + printer, _ := setup(t, &SetupOptions{ + ConfigStr: ` version: v1alpha1 contexts: mock-context: {} -`); err != nil { - t.Fatalf("Failed to load config: %v", err) - } +`, + Context: "mock-context", + }) envVars, err := printer.GetEnvVars() if err != nil { t.Errorf("Expected no error, got %v", err) diff --git a/pkg/env/docker_env_test.go b/pkg/env/docker_env_test.go index a3ad6b020..1e1be16b1 100644 --- a/pkg/env/docker_env_test.go +++ b/pkg/env/docker_env_test.go @@ -727,7 +727,7 @@ contexts: }) // And the registry URL is set in the context - printer.configHandler.SetContextValue("docker.registry_url", "registry.example.com:5000") + printer.configHandler.Set("docker.registry_url", "registry.example.com:5000") // When getting the registry URL url, err := printer.getRegistryURL() @@ -760,7 +760,7 @@ contexts: }) // And the registry URL is set in the context - printer.configHandler.SetContextValue("docker.registry_url", "registry.example.com") + printer.configHandler.Set("docker.registry_url", "registry.example.com") // When getting the registry URL url, err := printer.getRegistryURL() @@ -849,7 +849,7 @@ contexts: }) // And the registry URL is set in the context - printer.configHandler.SetContextValue("docker.registry_url", "registry.example.com") + printer.configHandler.Set("docker.registry_url", "registry.example.com") // When getting the registry URL url, err := printer.getRegistryURL() diff --git a/pkg/env/env_test.go b/pkg/env/env_test.go index b99a0f32c..107f7a6d6 100644 --- a/pkg/env/env_test.go +++ b/pkg/env/env_test.go @@ -26,6 +26,7 @@ type SetupOptions struct { Injector di.Injector ConfigHandler config.ConfigHandler ConfigStr string + Context string } // setupShims creates a new Shims instance with default implementations @@ -60,7 +61,6 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Set project root environment variable os.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) - os.Setenv("WINDSOR_CONTEXT", "mock-context") // Process options with defaults options := &SetupOptions{} @@ -68,6 +68,13 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { options = opts[0] } + // Set context from options or default to test-context + if options.Context != "" { + os.Setenv("WINDSOR_CONTEXT", options.Context) + } else { + os.Setenv("WINDSOR_CONTEXT", "test-context") + } + // Create injector var injector di.Injector if options.Injector == nil { diff --git a/pkg/env/terraform_env_test.go b/pkg/env/terraform_env_test.go index ccbdf21af..55fbd23ae 100644 --- a/pkg/env/terraform_env_test.go +++ b/pkg/env/terraform_env_test.go @@ -47,7 +47,7 @@ func setupTerraformEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return nil, nil } - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "local") + mocks.ConfigHandler.Set("terraform.backend.type", "local") mocks.Shims.Stat = func(name string) (os.FileInfo, error) { // Convert paths to slash format for consistent comparison @@ -411,7 +411,7 @@ func TestTerraformEnv_PostEnvHook(t *testing.T) { t.Run("UnsupportedBackend", func(t *testing.T) { printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "unsupported") + mocks.ConfigHandler.Set("terraform.backend.type", "unsupported") // When the PostEnvHook function is called err := printer.PostEnvHook() @@ -739,7 +739,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("S3Backend", func(t *testing.T) { // Given a TerraformEnvPrinter with S3 backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "s3") + mocks.ConfigHandler.Set("terraform.backend.type", "s3") var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { @@ -766,7 +766,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("KubernetesBackend", func(t *testing.T) { // Given a TerraformEnvPrinter with Kubernetes backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "kubernetes") + mocks.ConfigHandler.Set("terraform.backend.type", "kubernetes") var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { @@ -793,7 +793,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("AzureRMBackend", func(t *testing.T) { // Given a TerraformEnvPrinter with AzureRM backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "azurerm") + mocks.ConfigHandler.Set("terraform.backend.type", "azurerm") var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { @@ -820,7 +820,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("UnsupportedBackend", func(t *testing.T) { // Given a TerraformEnvPrinter with unsupported backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "unsupported") + mocks.ConfigHandler.Set("terraform.backend.type", "unsupported") // When generateBackendOverrideTf is called err := printer.generateBackendOverrideTf() @@ -853,7 +853,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("NoneBackend", func(t *testing.T) { // Given a TerraformEnvPrinter with "none" backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "none") + mocks.ConfigHandler.Set("terraform.backend.type", "none") // Mock Stat and Remove to verify file deletion fileExists := true @@ -892,7 +892,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("NoneBackendFileNotExists", func(t *testing.T) { // Given a TerraformEnvPrinter with "none" backend configuration and no existing file printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "none") + mocks.ConfigHandler.Set("terraform.backend.type", "none") // Mock Stat to return file not exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { @@ -926,7 +926,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("NoneBackendRemoveError", func(t *testing.T) { // Given a TerraformEnvPrinter with "none" backend configuration and failing Remove printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "none") + mocks.ConfigHandler.Set("terraform.backend.type", "none") // Mock Stat to return file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { @@ -1054,7 +1054,7 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("LocalBackendWithPrefix", func(t *testing.T) { // Given a TerraformEnvPrinter with local backend and prefix configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.prefix", "mock-prefix/") + mocks.ConfigHandler.Set("terraform.backend.prefix", "mock-prefix/") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1078,11 +1078,11 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("S3BackendWithPrefix", func(t *testing.T) { // Given a TerraformEnvPrinter with S3 backend and prefix configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "s3") - mocks.ConfigHandler.SetContextValue("terraform.backend.prefix", "mock-prefix/") - mocks.ConfigHandler.SetContextValue("terraform.backend.s3.bucket", "mock-bucket") - mocks.ConfigHandler.SetContextValue("terraform.backend.s3.region", "mock-region") - mocks.ConfigHandler.SetContextValue("terraform.backend.s3.secret_key", "mock-secret-key") + mocks.ConfigHandler.Set("terraform.backend.type", "s3") + mocks.ConfigHandler.Set("terraform.backend.prefix", "mock-prefix/") + mocks.ConfigHandler.Set("terraform.backend.s3.bucket", "mock-bucket") + mocks.ConfigHandler.Set("terraform.backend.s3.region", "mock-region") + mocks.ConfigHandler.Set("terraform.backend.s3.secret_key", "mock-secret-key") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1109,9 +1109,9 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("KubernetesBackendWithPrefix", func(t *testing.T) { // Given a TerraformEnvPrinter with Kubernetes backend and prefix configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "kubernetes") - mocks.ConfigHandler.SetContextValue("terraform.backend.prefix", "mock-prefix") - mocks.ConfigHandler.SetContextValue("terraform.backend.kubernetes.namespace", "mock-namespace") + mocks.ConfigHandler.Set("terraform.backend.type", "kubernetes") + mocks.ConfigHandler.Set("terraform.backend.prefix", "mock-prefix") + mocks.ConfigHandler.Set("terraform.backend.kubernetes.namespace", "mock-namespace") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1136,8 +1136,8 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("BackendTfvarsFileExistsWithPrefix", func(t *testing.T) { // Given a TerraformEnvPrinter with backend tfvars file and prefix configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.prefix", "mock-prefix/") - mocks.ConfigHandler.SetContextValue("context", "mock-context") + mocks.ConfigHandler.Set("terraform.backend.prefix", "mock-prefix/") + mocks.ConfigHandler.Set("context", "mock-context") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1161,7 +1161,7 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("BackendTfvarsFileExists", func(t *testing.T) { // Given a TerraformEnvPrinter with a backend.tfvars file printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("context", "mock-context") + mocks.ConfigHandler.Set("context", "mock-context") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1195,7 +1195,7 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("BackendTfvarsFileDoesNotExist", func(t *testing.T) { // Given a TerraformEnvPrinter without a backend.tfvars file printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("context", "mock-context") + mocks.ConfigHandler.Set("context", "mock-context") projectPath := "project/path" configRoot := "/mock/config/root" @@ -1228,11 +1228,11 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("AzureRMBackendWithPrefix", func(t *testing.T) { // Given a TerraformEnvPrinter with AzureRM backend and prefix configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "azurerm") - mocks.ConfigHandler.SetContextValue("terraform.backend.prefix", "mock-prefix/") - mocks.ConfigHandler.SetContextValue("terraform.backend.azurerm.storage_account_name", "mock-storage") - mocks.ConfigHandler.SetContextValue("terraform.backend.azurerm.container_name", "mock-container") - mocks.ConfigHandler.SetContextValue("terraform.backend.azurerm.use_azuread", true) + mocks.ConfigHandler.Set("terraform.backend.type", "azurerm") + mocks.ConfigHandler.Set("terraform.backend.prefix", "mock-prefix/") + mocks.ConfigHandler.Set("terraform.backend.azurerm.storage_account_name", "mock-storage") + mocks.ConfigHandler.Set("terraform.backend.azurerm.container_name", "mock-container") + mocks.ConfigHandler.Set("terraform.backend.azurerm.use_azuread", true) projectPath := "project/path" configRoot := "/mock/config/root" @@ -1259,7 +1259,7 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { t.Run("UnsupportedBackendType", func(t *testing.T) { // Given a TerraformEnvPrinter with unsupported backend configuration printer, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "unsupported") + mocks.ConfigHandler.Set("terraform.backend.type", "unsupported") projectPath := "project/path" configRoot := "/mock/config/root" diff --git a/pkg/env/windsor_env_test.go b/pkg/env/windsor_env_test.go index 1204480b4..726c47820 100644 --- a/pkg/env/windsor_env_test.go +++ b/pkg/env/windsor_env_test.go @@ -31,6 +31,9 @@ contexts: SECRET_VAR: "{{secret_name}}" ` } + if opts[0].Context == "" { + opts[0].Context = "mock-context" + } mocks := setupMocks(t, opts[0]) // Get the temp dir that was set up in setupMocks @@ -385,7 +388,21 @@ contexts: }) t.Run("NoEnvironmentVarsInConfig", func(t *testing.T) { - printer, mocks := setup(t) + // Setup with empty environment + mocks := setupWindsorEnvMocks(t, &SetupOptions{ + ConfigStr: ` +version: v1alpha1 +contexts: + mock-context: + environment: {} +`, + Context: "mock-context", + }) + printer := NewWindsorEnvPrinter(mocks.Injector) + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize env: %v", err) + } + printer.shims = mocks.Shims // Given a WindsorEnvPrinter with empty environment configuration projectRoot, err := mocks.Shell.GetProjectRoot() @@ -397,16 +414,6 @@ contexts: printer.managedEnv = []string{} printer.managedAlias = []string{} - // And empty environment map in config - if err := mocks.ConfigHandler.LoadConfigString(` -version: v1alpha1 -contexts: - test-context: - environment: {} -`); err != nil { - t.Fatalf("LoadConfigString returned error: %v", err) - } - // When GetEnvVars is called envVars, err := printer.GetEnvVars() if err != nil { diff --git a/pkg/pipelines/check_test.go b/pkg/pipelines/check_test.go index 810f68817..b0c85c647 100644 --- a/pkg/pipelines/check_test.go +++ b/pkg/pipelines/check_test.go @@ -252,7 +252,7 @@ func TestCheckPipeline_Initialize(t *testing.T) { injector := di.NewInjector() mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.InitializeFunc = func() error { return nil } - mockConfigHandler.LoadConfigFunc = func(path string) error { + mockConfigHandler.LoadConfigFunc = func() error { return fmt.Errorf("config loading failed") } injector.Register("configHandler", mockConfigHandler) @@ -281,7 +281,7 @@ func TestCheckPipeline_Initialize(t *testing.T) { if err == nil { t.Fatal("Expected error, got nil") } - if err.Error() != "failed to load base config: error loading config file: config loading failed" { + if err.Error() != "failed to load context config: config loading failed" { t.Errorf("Expected config loading error, got: %v", err) } }) diff --git a/pkg/pipelines/env_test.go b/pkg/pipelines/env_test.go index 316091205..b2e268159 100644 --- a/pkg/pipelines/env_test.go +++ b/pkg/pipelines/env_test.go @@ -167,8 +167,14 @@ func TestEnvPipeline_Initialize(t *testing.T) { return "", fmt.Errorf("project root error") } + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.InitializeFunc = func() error { return nil } + mockConfigHandler.LoadConfigFunc = func() error { + return fmt.Errorf("error retrieving project root: project root error") + } + setupOptions := &SetupOptions{ - ConfigHandler: config.NewMockConfigHandler(), + ConfigHandler: mockConfigHandler, } pipeline, mocks := setup(t, setupOptions) mocks.Injector.Register("shell", mockShell) @@ -180,7 +186,7 @@ func TestEnvPipeline_Initialize(t *testing.T) { if err == nil { t.Fatal("Expected error, got nil") } - if err.Error() != "failed to load base config: error retrieving project root: project root error" { + if err.Error() != "failed to load context config: error retrieving project root: project root error" { t.Errorf("Expected load config error, got: %v", err) } }) diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go index 7d09befa6..2d844e07b 100644 --- a/pkg/pipelines/init.go +++ b/pkg/pipelines/init.go @@ -84,7 +84,8 @@ func (p *InitPipeline) Initialize(injector di.Injector, ctx context.Context) err return fmt.Errorf("Error setting context value: %w", err) } - if !p.configHandler.IsContextConfigLoaded() { + isLoaded := p.configHandler.IsLoaded() + if !isLoaded { if err := p.setDefaultConfiguration(ctx, contextName); err != nil { return err } @@ -102,6 +103,11 @@ func (p *InitPipeline) Initialize(injector di.Injector, ctx context.Context) err return fmt.Errorf("Error saving config file: %w", err) } + // Reload config to ensure everything is synchronized in memory + if err := p.configHandler.LoadConfig(); err != nil { + return fmt.Errorf("failed to reload context config: %w", err) + } + // Component Collection Phase kubernetesClient := p.withKubernetesClient() @@ -376,14 +382,14 @@ func (p *InitPipeline) setDefaultConfiguration(_ context.Context, contextName st } if isLocalContext && p.configHandler.GetString("vm.driver") == "" && vmDriver != "" { - if err := p.configHandler.SetContextValue("vm.driver", vmDriver); err != nil { + if err := p.configHandler.Set("vm.driver", vmDriver); err != nil { return fmt.Errorf("Error setting vm.driver: %w", err) } } if existingProvider == "" { if contextName == "local" || strings.HasPrefix(contextName, "local-") { - if err := p.configHandler.SetContextValue("provider", "generic"); err != nil { + if err := p.configHandler.Set("provider", "generic"); err != nil { return fmt.Errorf("Error setting provider from context name: %w", err) } } @@ -406,21 +412,21 @@ func (p *InitPipeline) processPlatformConfiguration(_ context.Context) error { switch provider { case "aws": - if err := p.configHandler.SetContextValue("aws.enabled", true); err != nil { + if err := p.configHandler.Set("aws.enabled", true); err != nil { return fmt.Errorf("Error setting aws.enabled: %w", err) } - if err := p.configHandler.SetContextValue("cluster.driver", "eks"); err != nil { + if err := p.configHandler.Set("cluster.driver", "eks"); err != nil { return fmt.Errorf("Error setting cluster.driver: %w", err) } case "azure": - if err := p.configHandler.SetContextValue("azure.enabled", true); err != nil { + if err := p.configHandler.Set("azure.enabled", true); err != nil { return fmt.Errorf("Error setting azure.enabled: %w", err) } - if err := p.configHandler.SetContextValue("cluster.driver", "aks"); err != nil { + if err := p.configHandler.Set("cluster.driver", "aks"); err != nil { return fmt.Errorf("Error setting cluster.driver: %w", err) } case "generic": - if err := p.configHandler.SetContextValue("cluster.driver", "talos"); err != nil { + if err := p.configHandler.Set("cluster.driver", "talos"); err != nil { return fmt.Errorf("Error setting cluster.driver: %w", err) } } diff --git a/pkg/pipelines/init_test.go b/pkg/pipelines/init_test.go index bff32871c..890ed5676 100644 --- a/pkg/pipelines/init_test.go +++ b/pkg/pipelines/init_test.go @@ -480,7 +480,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { mockConfigHandler.SetDefaultFunc = func(defaultConfig v1alpha1.Context) error { return nil } - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return nil } pipeline.configHandler = mockConfigHandler @@ -521,7 +521,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { // Given a pipeline with no provider set and "local" context name pipeline, mockConfigHandler := setup(t, "", "") providerSet := false - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { providerSet = true } @@ -544,7 +544,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { // Given a pipeline with no provider set and "aws" context name pipeline, mockConfigHandler := setup(t, "", "") providerSet := false - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { providerSet = true } @@ -621,7 +621,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { // Given a pipeline with no provider set and "local" context name pipeline, mockConfigHandler := setup(t, "", "") var setProvider string - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { setProvider = value.(string) } @@ -644,7 +644,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { // Given a pipeline with provider already set to "aws" pipeline, mockConfigHandler := setup(t, "", "aws") providerSetCount := 0 - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { providerSetCount++ } @@ -667,7 +667,7 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { // Given a pipeline with no provider set and unknown context name pipeline, mockConfigHandler := setup(t, "", "") providerSet := false - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { providerSet = true } @@ -708,9 +708,9 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { }) t.Run("ReturnsErrorWhenSetProviderFromContextNameFails", func(t *testing.T) { - // Given a pipeline with config handler that fails on SetContextValue for provider + // Given a pipeline with config handler that fails on Set for provider pipeline, mockConfigHandler := setup(t, "", "") - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { if key == "provider" { return fmt.Errorf("set provider failed") } @@ -748,10 +748,10 @@ func TestInitPipeline_setDefaultConfiguration(t *testing.T) { } }) - t.Run("ReturnsErrorWhenSetContextValueFails", func(t *testing.T) { - // Given a pipeline with config handler that fails on SetContextValue + t.Run("ReturnsErrorWhenSetFails", func(t *testing.T) { + // Given a pipeline with config handler that fails on Set pipeline, mockConfigHandler := setup(t, "", "") - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("set context value failed") } @@ -780,7 +780,7 @@ func TestInitPipeline_processPlatformConfiguration(t *testing.T) { } return "" } - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return nil } pipeline.configHandler = mockConfigHandler @@ -813,10 +813,10 @@ func TestInitPipeline_processPlatformConfiguration(t *testing.T) { }) } - t.Run("ReturnsErrorWhenSetContextValueFails", func(t *testing.T) { + t.Run("ReturnsErrorWhenSetFails", func(t *testing.T) { // Given a pipeline with platform configuration that fails pipeline, mockConfigHandler := setup(t, "aws") - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return fmt.Errorf("config error") } @@ -1135,7 +1135,7 @@ func TestInitPipeline_setDefaultConfiguration_HostPortsValidation(t *testing.T) setDefaultConfig = defaultConfig return nil } - mockConfigHandler.SetContextValueFunc = func(key string, value interface{}) error { + mockConfigHandler.SetFunc = func(key string, value interface{}) error { return nil } diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index 32ce14697..bcdc9e1df 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -163,9 +163,14 @@ func (p *BasePipeline) Initialize(injector di.Injector, ctx context.Context) err } } - // Load context config after context is set - if err := p.configHandler.LoadContextConfig(); err != nil { - return fmt.Errorf("failed to load context config: %w", err) + // Load context config after context is set (only if not in init pipeline) + // Init pipeline doesn't load config because files don't exist yet + isInit, _ := ctx.Value("initPipeline").(bool) + if !isInit { + if err := p.configHandler.LoadConfig(); err != nil { + return fmt.Errorf("failed to load context config: %w", err) + } + } else { } return nil @@ -488,47 +493,6 @@ func (p *BasePipeline) handleSessionReset() error { return nil } -// loadConfig loads the windsor.yaml config file from the project root into the config handler. -// This is a common operation that most pipelines will need, so it's provided in the base pipeline. -func (p *BasePipeline) loadConfig() error { - if p.shell == nil { - return fmt.Errorf("shell not initialized") - } - if p.configHandler == nil { - return fmt.Errorf("config handler not initialized") - } - if p.shims == nil { - return fmt.Errorf("shims not initialized") - } - - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } - - yamlPath := filepath.Join(projectRoot, "windsor.yaml") - ymlPath := filepath.Join(projectRoot, "windsor.yml") - - var cliConfigPath string - if _, err := p.shims.Stat(yamlPath); err == nil { - cliConfigPath = yamlPath - } else if _, err := p.shims.Stat(ymlPath); err == nil { - cliConfigPath = ymlPath - } - - if cliConfigPath != "" { - if err := p.configHandler.LoadConfig(cliConfigPath); err != nil { - return fmt.Errorf("error loading config file: %w", err) - } - } - - if err := p.configHandler.LoadContextConfig(); err != nil { - return fmt.Errorf("error loading context config: %w", err) - } - - return nil -} - // loadBaseConfig loads only the base configuration file (windsor.yaml) without loading context config func (p *BasePipeline) loadBaseConfig() error { if p.shell == nil { @@ -541,26 +505,8 @@ func (p *BasePipeline) loadBaseConfig() error { return fmt.Errorf("shims not initialized") } - projectRoot, err := p.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error retrieving project root: %w", err) - } - - yamlPath := filepath.Join(projectRoot, "windsor.yaml") - ymlPath := filepath.Join(projectRoot, "windsor.yml") - - var cliConfigPath string - if _, err := p.shims.Stat(yamlPath); err == nil { - cliConfigPath = yamlPath - } else if _, err := p.shims.Stat(ymlPath); err == nil { - cliConfigPath = ymlPath - } - - if cliConfigPath != "" { - if err := p.configHandler.LoadConfig(cliConfigPath); err != nil { - return fmt.Errorf("error loading config file: %w", err) - } - } + // Config is now loaded via LoadConfig() which loads from standard paths + // Root windsor.yaml loading is handled by LoadConfig() return nil } @@ -641,8 +587,7 @@ func (p *BasePipeline) withSecretsProviders() ([]secrets.SecretsProvider, error) } } - contextName := p.configHandler.GetContext() - vaults, ok := p.configHandler.Get(fmt.Sprintf("contexts.%s.secrets.onepassword.vaults", contextName)).(map[string]secretsConfigType.OnePasswordVault) + vaults, ok := p.configHandler.Get("secrets.onepassword.vaults").(map[string]secretsConfigType.OnePasswordVault) if ok && len(vaults) > 0 { useSDK := p.shims.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index 1938a52ef..9d2e2bfe1 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -175,10 +175,7 @@ network: t.Fatalf("Failed to write context config: %v", err) } - // Load context config to set loaded flag - if err := configHandler.LoadContextConfig(); err != nil { - t.Fatalf("Failed to load context config: %v", err) - } + // Config will be loaded by pipeline initialization // Register shims shims := setupShims(t) @@ -1064,201 +1061,8 @@ func TestBasePipeline_handleSessionReset(t *testing.T) { }) } -func TestBasePipeline_loadConfig(t *testing.T) { - t.Run("ReturnsErrorWhenShellIsNil", func(t *testing.T) { - // Given a BasePipeline with nil shell - pipeline := NewBasePipeline() - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then an error should be returned - if err == nil { - t.Error("Expected error when shell is nil") - } - if err.Error() != "shell not initialized" { - t.Errorf("Expected 'shell not initialized' error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerIsNil", func(t *testing.T) { - // Given a BasePipeline with shell but nil config handler - pipeline := NewBasePipeline() - pipeline.shell = shell.NewMockShell() - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then an error should be returned - if err == nil { - t.Error("Expected error when config handler is nil") - } - if err.Error() != "config handler not initialized" { - t.Errorf("Expected 'config handler not initialized' error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenShimsIsNil", func(t *testing.T) { - // Given a BasePipeline with shell and config handler but nil shims - pipeline := NewBasePipeline() - pipeline.shell = shell.NewMockShell() - pipeline.configHandler = config.NewMockConfigHandler() - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then an error should be returned - if err == nil { - t.Error("Expected error when shims is nil") - } - if err.Error() != "shims not initialized" { - t.Errorf("Expected 'shims not initialized' error, got %v", err) - } - }) - - t.Run("LoadsConfigSuccessfully", func(t *testing.T) { - // Given a BasePipeline with shell, config handler, and shims - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - projectRoot := t.TempDir() - mockShell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } - pipeline.shell = mockShell - - mockConfigHandler := config.NewMockConfigHandler() - loadConfigCalled := false - mockConfigHandler.LoadConfigFunc = func(path string) error { - loadConfigCalled = true - expectedPath := filepath.Join(projectRoot, "windsor.yaml") - if path != expectedPath { - t.Errorf("Expected config path %q, got %q", expectedPath, path) - } - return nil - } - pipeline.configHandler = mockConfigHandler - - pipeline.shims = NewShims() - - // Create a test config file - configPath := filepath.Join(projectRoot, "windsor.yaml") - if err := os.WriteFile(configPath, []byte("test: config"), 0644); err != nil { - t.Fatalf("Failed to create test config file: %v", err) - } - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then no error should be returned and config should be loaded - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !loadConfigCalled { - t.Error("Expected loadConfig to be called on config handler") - } - }) - - t.Run("ReturnsErrorWhenGetProjectRootFails", func(t *testing.T) { - // Given a BasePipeline with failing shell - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root error") - } - pipeline.shell = mockShell - - mockConfigHandler := config.NewMockConfigHandler() - pipeline.configHandler = mockConfigHandler - - pipeline.shims = NewShims() - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then an error should be returned - if err == nil { - t.Error("Expected error when GetProjectRoot fails") - } - if !strings.Contains(err.Error(), "error retrieving project root") { - t.Errorf("Expected 'error retrieving project root' in error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenLoadConfigFails", func(t *testing.T) { - // Given a BasePipeline with config handler that fails to load - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - projectRoot := t.TempDir() - mockShell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } - pipeline.shell = mockShell - - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.LoadConfigFunc = func(path string) error { - return fmt.Errorf("load config error") - } - pipeline.configHandler = mockConfigHandler - - pipeline.shims = NewShims() - - // Create a test config file - configPath := filepath.Join(projectRoot, "windsor.yaml") - if err := os.WriteFile(configPath, []byte("test: config"), 0644); err != nil { - t.Fatalf("Failed to create test config file: %v", err) - } - - // When loadConfig is called - err := pipeline.loadConfig() - - // Then an error should be returned - if err == nil { - t.Error("Expected error when loadConfig fails") - } - if !strings.Contains(err.Error(), "error loading config file") { - t.Errorf("Expected 'error loading config file' in error, got %v", err) - } - }) - - t.Run("SkipsLoadingWhenNoConfigFileExists", func(t *testing.T) { - // Given a BasePipeline with no config file - pipeline := NewBasePipeline() - - mockShell := shell.NewMockShell() - projectRoot := t.TempDir() - mockShell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } - pipeline.shell = mockShell - - mockConfigHandler := config.NewMockConfigHandler() - loadConfigCalled := false - mockConfigHandler.LoadConfigFunc = func(path string) error { - loadConfigCalled = true - return nil - } - pipeline.configHandler = mockConfigHandler - - pipeline.shims = NewShims() - - // When loadConfig is called (no config file exists) - err := pipeline.loadConfig() - - // Then no error should be returned and loadConfig should not be called - if err != nil { - t.Errorf("Expected no error when no config file exists, got %v", err) - } - if loadConfigCalled { - t.Error("Expected loadConfig not to be called when no config file exists") - } - }) -} - // ============================================================================= -// Test Private Methods - withEnvPrinters +// Test Private Methods // ============================================================================= func TestBasePipeline_withEnvPrinters(t *testing.T) { @@ -1557,10 +1361,6 @@ func TestBasePipeline_withEnvPrinters(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withSecretsProviders -// ============================================================================= - func TestBasePipeline_withSecretsProviders(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks, string) { pipeline := NewBasePipeline() @@ -1690,7 +1490,7 @@ func TestBasePipeline_withSecretsProviders(t *testing.T) { // Configure OnePassword vaults mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetFunc = func(key string) any { - if key == "contexts.test-context.secrets.onepassword.vaults" { + if key == "secrets.onepassword.vaults" { return map[string]secretsConfigType.OnePasswordVault{ "vault1": { Name: "test-vault", @@ -1727,7 +1527,7 @@ func TestBasePipeline_withSecretsProviders(t *testing.T) { // Configure OnePassword vaults mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetFunc = func(key string) any { - if key == "contexts.test-context.secrets.onepassword.vaults" { + if key == "secrets.onepassword.vaults" { return map[string]secretsConfigType.OnePasswordVault{ "vault1": { Name: "test-vault", @@ -1782,10 +1582,6 @@ func TestBasePipeline_withSecretsProviders(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withBlueprintHandler -// ============================================================================= - func TestBasePipeline_withBlueprintHandler(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -1842,10 +1638,6 @@ func TestBasePipeline_withBlueprintHandler(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withStack -// ============================================================================= - func TestBasePipeline_withStack(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -1902,10 +1694,6 @@ func TestBasePipeline_withStack(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withArtifactBuilder -// ============================================================================= - func TestBasePipeline_withArtifactBuilder(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -1962,10 +1750,6 @@ func TestBasePipeline_withArtifactBuilder(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withVirtualMachine -// ============================================================================= - func TestBasePipeline_withVirtualMachine(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -2093,10 +1877,6 @@ func TestBasePipeline_withVirtualMachine(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withContainerRuntime -// ============================================================================= - func TestBasePipeline_withContainerRuntime(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -2196,10 +1976,6 @@ func TestBasePipeline_withContainerRuntime(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withKubernetesClient -// ============================================================================= - func TestBasePipeline_withKubernetesClient(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -2256,10 +2032,6 @@ func TestBasePipeline_withKubernetesClient(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withKubernetesManager -// ============================================================================= - func TestBasePipeline_withKubernetesManager(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -2316,10 +2088,6 @@ func TestBasePipeline_withKubernetesManager(t *testing.T) { }) } -// ============================================================================= -// Test Private Methods - withServices -// ============================================================================= - func TestBasePipeline_withServices(t *testing.T) { t.Run("ReturnsEmptyWhenDockerDisabled", func(t *testing.T) { // Given a base pipeline with Docker disabled diff --git a/pkg/stack/windsor_stack_test.go b/pkg/stack/windsor_stack_test.go index 0a0d7bacf..f2bdf7a90 100644 --- a/pkg/stack/windsor_stack_test.go +++ b/pkg/stack/windsor_stack_test.go @@ -232,7 +232,7 @@ func TestWindsorStack_Up(t *testing.T) { t.Run("ErrorGeneratingTerraformArgs", func(t *testing.T) { stack, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "unsupported") + mocks.ConfigHandler.Set("terraform.backend.type", "unsupported") // And when Up is called err := stack.Up() @@ -363,7 +363,7 @@ func TestWindsorStack_Down(t *testing.T) { t.Run("ErrorGeneratingTerraformArgs", func(t *testing.T) { stack, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("terraform.backend.type", "unsupported") + mocks.ConfigHandler.Set("terraform.backend.type", "unsupported") // And when Down is called err := stack.Down() diff --git a/pkg/tools/tools_manager.go b/pkg/tools/tools_manager.go index c680da699..3eb7e78ce 100644 --- a/pkg/tools/tools_manager.go +++ b/pkg/tools/tools_manager.go @@ -128,7 +128,7 @@ func (t *BaseToolsManager) Check() error { } } - if vaults := t.configHandler.Get(fmt.Sprintf("contexts.%s.secrets.onepassword.vaults", t.configHandler.GetContext())); vaults != nil { + if vaults := t.configHandler.Get("secrets.onepassword.vaults"); vaults != nil { if err := t.checkOnePassword(); err != nil { spin.Stop() fmt.Fprintf(os.Stderr, "\033[31m✗ %s - Failed\033[0m\n", message) diff --git a/pkg/tools/tools_manager_test.go b/pkg/tools/tools_manager_test.go index c341f8601..2370765aa 100644 --- a/pkg/tools/tools_manager_test.go +++ b/pkg/tools/tools_manager_test.go @@ -296,7 +296,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("DockerDisabled", func(t *testing.T) { // When docker is disabled in config mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("docker.enabled", false) + mocks.ConfigHandler.Set("docker.enabled", false) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "docker" || name == "docker-compose" || name == "docker-cli-plugin-docker-compose" { @@ -314,7 +314,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("ClusterDisabled", func(t *testing.T) { // When cluster is disabled in config mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("cluster.enabled", false) + mocks.ConfigHandler.Set("cluster.enabled", false) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "kubectl" { @@ -332,8 +332,8 @@ func TestToolsManager_Check(t *testing.T) { t.Run("AllToolsDisabled", func(t *testing.T) { // When all tools are disabled in config mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("docker.enabled", false) - mocks.ConfigHandler.SetContextValue("cluster.enabled", false) + mocks.ConfigHandler.Set("docker.enabled", false) + mocks.ConfigHandler.Set("cluster.enabled", false) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "docker" || name == "docker-compose" || name == "docker-cli-plugin-docker-compose" || name == "kubectl" { @@ -351,7 +351,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("DockerEnabledButNotAvailable", func(t *testing.T) { // When docker is enabled but not available in PATH mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("docker.enabled", true) + mocks.ConfigHandler.Set("docker.enabled", true) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "docker" || name == "docker-compose" || name == "docker-cli-plugin-docker-compose" { @@ -369,7 +369,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("ClusterEnabledButNotAvailable", func(t *testing.T) { // When cluster is enabled but kubectl not available in PATH mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("cluster.enabled", true) + mocks.ConfigHandler.Set("cluster.enabled", true) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "kubectl" { @@ -387,7 +387,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("TerraformEnabledButNotAvailable", func(t *testing.T) { // When terraform is enabled but not available in PATH mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("terraform.enabled", true) + mocks.ConfigHandler.Set("terraform.enabled", true) originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "terraform" { @@ -405,7 +405,7 @@ func TestToolsManager_Check(t *testing.T) { t.Run("ColimaEnabledButNotAvailable", func(t *testing.T) { // When colima is enabled but not available in PATH mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("vm.driver", "colima") + mocks.ConfigHandler.Set("vm.driver", "colima") originalExecLookPath := execLookPath execLookPath = func(name string) (string, error) { if name == "colima" { @@ -462,9 +462,9 @@ contexts: t.Run("MultipleToolFailures", func(t *testing.T) { // Given multiple tools are enabled but fail checks mocks, toolsManager := setup(t, defaultConfig) - mocks.ConfigHandler.SetContextValue("docker.enabled", true) - mocks.ConfigHandler.SetContextValue("aws.enabled", true) - mocks.ConfigHandler.SetContextValue("cluster.enabled", true) + mocks.ConfigHandler.Set("docker.enabled", true) + mocks.ConfigHandler.Set("aws.enabled", true) + mocks.ConfigHandler.Set("cluster.enabled", true) // Mock failures for multiple tools originalExecLookPath := execLookPath diff --git a/pkg/workstation/network/colima_network.go b/pkg/workstation/network/colima_network.go index 4e2c4f00a..655a61913 100644 --- a/pkg/workstation/network/colima_network.go +++ b/pkg/workstation/network/colima_network.go @@ -72,7 +72,7 @@ func (n *ColimaNetworkManager) Initialize() error { // Set docker.NetworkCIDR to the default value if it's not set if n.configHandler.GetString("network.cidr_block") == "" { - return n.configHandler.SetContextValue("network.cidr_block", constants.DEFAULT_NETWORK_CIDR) + return n.configHandler.Set("network.cidr_block", constants.DEFAULT_NETWORK_CIDR) } return nil diff --git a/pkg/workstation/network/colima_network_test.go b/pkg/workstation/network/colima_network_test.go index 567d300db..a5ae227a0 100644 --- a/pkg/workstation/network/colima_network_test.go +++ b/pkg/workstation/network/colima_network_test.go @@ -104,7 +104,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { t.Run("NoNetworkCIDRConfigured", func(t *testing.T) { // Given a network manager with no CIDR configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "") + mocks.ConfigHandler.Set("network.cidr_block", "") // And configuring the guest err := manager.ConfigureGuest() @@ -122,7 +122,7 @@ func TestColimaNetworkManager_ConfigureGuest(t *testing.T) { t.Run("NoGuestIPConfigured", func(t *testing.T) { // Given a network manager with no guest IP configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.address", "") + mocks.ConfigHandler.Set("vm.address", "") // And configuring the guest err := manager.ConfigureGuest() @@ -397,7 +397,7 @@ func TestColimaNetworkManager_getHostIP(t *testing.T) { t.Run("NoGuestAddressSet", func(t *testing.T) { // Given a network manager with no guest address manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.address", "") + mocks.ConfigHandler.Set("vm.address", "") // And getting the host IP hostIP, err := manager.getHostIP() @@ -422,7 +422,7 @@ func TestColimaNetworkManager_getHostIP(t *testing.T) { t.Run("ErrorParsingGuestIP", func(t *testing.T) { // Given a network manager with invalid guest IP manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.address", "invalid_ip_address") + mocks.ConfigHandler.Set("vm.address", "invalid_ip_address") // And getting the host IP hostIP, err := manager.getHostIP() diff --git a/pkg/workstation/network/darwin_network_test.go b/pkg/workstation/network/darwin_network_test.go index 044ace808..c16f92502 100644 --- a/pkg/workstation/network/darwin_network_test.go +++ b/pkg/workstation/network/darwin_network_test.go @@ -40,7 +40,7 @@ func TestDarwinNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with no CIDR configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "") + mocks.ConfigHandler.Set("network.cidr_block", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -59,8 +59,8 @@ func TestDarwinNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with no guest IP configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.address", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -79,8 +79,8 @@ func TestDarwinNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with existing route manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.1.10") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.1.10") originalExecSilentFunc := mocks.Shell.ExecSilentFunc mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { @@ -106,8 +106,8 @@ func TestDarwinNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with route check error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.1.10") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.1.10") originalExecSilentFunc := mocks.Shell.ExecSilentFunc mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { @@ -174,8 +174,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a properly configured network manager manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And configuring DNS err := manager.ConfigureDNS() @@ -190,8 +190,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { // Given a network manager in localhost mode manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("dns.domain", "example.com") // And capturing resolver file content var capturedContent []byte @@ -220,7 +220,7 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking empty DNS domain - mocks.ConfigHandler.SetContextValue("dns.domain", "") + mocks.ConfigHandler.Set("dns.domain", "") // And configuring DNS err := manager.ConfigureDNS() @@ -239,8 +239,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { // Given a network manager with no DNS address manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "") // And configuring DNS err := manager.ConfigureDNS() @@ -258,8 +258,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("ResolverFileAlreadyExists", func(t *testing.T) { // Given a network manager with existing resolver file manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking existing resolver file mocks.Shims.ReadFile = func(filename string) ([]byte, error) { @@ -281,8 +281,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("CreateResolverDirectoryError", func(t *testing.T) { // Given a network manager with resolver directory error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking resolver directory error mocks.Shims.Stat = func(name string) (os.FileInfo, error) { @@ -315,8 +315,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("WriteFileError", func(t *testing.T) { // Given a network manager with file write error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking file write error mocks.Shims.WriteFile = func(_ string, _ []byte, _ os.FileMode) error { @@ -339,8 +339,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("MoveResolverFileError", func(t *testing.T) { // Given a network manager with file move error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking successful write but failed move mocks.Shims.WriteFile = func(_ string, _ []byte, _ os.FileMode) error { @@ -370,8 +370,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("FlushDNSCacheError", func(t *testing.T) { // Given a network manager with DNS cache flush error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") mocks.Shell.ExecSudoFunc = func(message string, command string, args ...string) (string, error) { if command == "dscacheutil" && args[0] == "-flushcache" { @@ -396,8 +396,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("RestartMDNSResponderError", func(t *testing.T) { // Given a network manager with mDNSResponder restart error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") mocks.Shell.ExecSudoFunc = func(message string, command string, args ...string) (string, error) { if command == "killall" && args[0] == "-HUP" { @@ -422,8 +422,8 @@ func TestDarwinNetworkManager_ConfigureDNS(t *testing.T) { t.Run("IsLocalhostScenario", func(t *testing.T) { // Given a network manager in localhost mode manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "127.0.0.1") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "127.0.0.1") // And configuring DNS err := manager.ConfigureDNS() diff --git a/pkg/workstation/network/linux_network_test.go b/pkg/workstation/network/linux_network_test.go index f20adc522..f3c9dc437 100644 --- a/pkg/workstation/network/linux_network_test.go +++ b/pkg/workstation/network/linux_network_test.go @@ -40,7 +40,7 @@ func TestLinuxNetworkManager_ConfigureHostRoute(t *testing.T) { t.Run("NoNetworkCIDRConfigured", func(t *testing.T) { // Given a network manager with no CIDR configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "") + mocks.ConfigHandler.Set("network.cidr_block", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -81,8 +81,8 @@ func TestLinuxNetworkManager_ConfigureHostRoute(t *testing.T) { t.Run("NoGuestIPConfigured", func(t *testing.T) { // Given a network manager with no guest IP configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.5.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.5.0/24") + mocks.ConfigHandler.Set("vm.address", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -106,8 +106,8 @@ func TestLinuxNetworkManager_ConfigureHostRoute(t *testing.T) { } return "", nil } - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.5.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.1.2") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.5.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.1.2") // And configuring the host route err := manager.ConfigureHostRoute() @@ -127,8 +127,8 @@ func TestLinuxNetworkManager_ConfigureHostRoute(t *testing.T) { } return "", nil } - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.5.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.5.100") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.5.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.5.100") // And configuring the host route err := manager.ConfigureHostRoute() @@ -194,8 +194,8 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("SuccessLocalhostMode", func(t *testing.T) { // Given a network manager in localhost mode manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("dns.domain", "example.com") // And configuring DNS err := manager.ConfigureDNS() @@ -249,7 +249,7 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("DomainNotConfigured", func(t *testing.T) { // Given a network manager with no DNS domain manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "") + mocks.ConfigHandler.Set("dns.domain", "") // And configuring DNS err := manager.ConfigureDNS() @@ -267,9 +267,9 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("NoDNSAddressConfigured", func(t *testing.T) { // Given a network manager with no DNS address manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "") - mocks.ConfigHandler.SetContextValue("vm.driver", "colima") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "") + mocks.ConfigHandler.Set("vm.driver", "colima") // And mocking systemd-resolved being in use mocks.Shims.ReadLink = func(_ string) (string, error) { @@ -337,8 +337,8 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("ErrorCreatingDropInDirectory", func(t *testing.T) { // Given a network manager with drop-in directory creation error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking systemd-resolved being in use mocks.Shims.ReadLink = func(_ string) (string, error) { @@ -374,8 +374,8 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("ErrorWritingDNSConfig", func(t *testing.T) { // Given a network manager with DNS config writing error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking systemd-resolved being in use mocks.Shims.ReadLink = func(_ string) (string, error) { @@ -411,8 +411,8 @@ func TestLinuxNetworkManager_ConfigureDNS(t *testing.T) { t.Run("ErrorRestartingSystemdResolved", func(t *testing.T) { // Given a network manager with systemd-resolved restart error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "1.2.3.4") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "1.2.3.4") // And mocking systemd-resolved being in use mocks.Shims.ReadLink = func(_ string) (string, error) { diff --git a/pkg/workstation/network/network.go b/pkg/workstation/network/network.go index eb94a1a14..f3663d640 100644 --- a/pkg/workstation/network/network.go +++ b/pkg/workstation/network/network.go @@ -92,7 +92,7 @@ func (n *BaseNetworkManager) Initialize() error { networkCIDR := n.configHandler.GetString("network.cidr_block") if networkCIDR == "" { networkCIDR = constants.DEFAULT_NETWORK_CIDR - if err := n.configHandler.SetContextValue("network.cidr_block", networkCIDR); err != nil { + if err := n.configHandler.Set("network.cidr_block", networkCIDR); err != nil { return fmt.Errorf("error setting default network CIDR: %w", err) } } diff --git a/pkg/workstation/network/windows_network_test.go b/pkg/workstation/network/windows_network_test.go index 8ac5f9fa4..12e69aaa3 100644 --- a/pkg/workstation/network/windows_network_test.go +++ b/pkg/workstation/network/windows_network_test.go @@ -46,7 +46,7 @@ func TestWindowsNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with no CIDR configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "") + mocks.ConfigHandler.Set("network.cidr_block", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -65,7 +65,7 @@ func TestWindowsNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with no guest IP configured manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.address", "") + mocks.ConfigHandler.Set("vm.address", "") // And configuring the host route err := manager.ConfigureHostRoute() @@ -84,8 +84,8 @@ func TestWindowsNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with route check error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.1.2") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.1.2") mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "powershell" && args[0] == "-Command" { @@ -113,8 +113,8 @@ func TestWindowsNetworkManager_ConfigureHostRoute(t *testing.T) { // Given a network manager with route addition error manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") - mocks.ConfigHandler.SetContextValue("vm.address", "192.168.1.2") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.address", "192.168.1.2") mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "powershell" && args[0] == "-Command" { @@ -157,8 +157,8 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking DNS configuration - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "8.8.8.8") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "8.8.8.8") // And configuring DNS err := manager.ConfigureDNS() @@ -174,9 +174,9 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking localhost mode configuration - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "") - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") // And configuring DNS err := manager.ConfigureDNS() @@ -192,7 +192,7 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking missing DNS domain - mocks.ConfigHandler.SetContextValue("dns.domain", "") + mocks.ConfigHandler.Set("dns.domain", "") // And configuring DNS err := manager.ConfigureDNS() @@ -212,7 +212,7 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking missing DNS address - mocks.ConfigHandler.SetContextValue("dns.address", "") + mocks.ConfigHandler.Set("dns.address", "") // And configuring DNS err := manager.ConfigureDNS() @@ -232,8 +232,8 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking DNS check error - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "192.168.1.1") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "192.168.1.1") // And configuring DNS err := manager.ConfigureDNS() @@ -249,9 +249,9 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking localhost mode configuration - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "") - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") // And configuring DNS err := manager.ConfigureDNS() @@ -316,7 +316,7 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking missing DNS domain - mocks.ConfigHandler.SetContextValue("dns.domain", "") + mocks.ConfigHandler.Set("dns.domain", "") // And configuring DNS err := manager.ConfigureDNS() @@ -336,7 +336,7 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { manager, mocks := setup(t) // And mocking missing DNS address - mocks.ConfigHandler.SetContextValue("dns.address", "") + mocks.ConfigHandler.Set("dns.address", "") // And configuring DNS err := manager.ConfigureDNS() @@ -422,9 +422,9 @@ func TestWindowsNetworkManager_ConfigureDNS(t *testing.T) { // Given a network manager with no DNS address manager, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("dns.domain", "example.com") - mocks.ConfigHandler.SetContextValue("dns.address", "") - mocks.ConfigHandler.SetContextValue("vm.driver", "hyperv") + mocks.ConfigHandler.Set("dns.domain", "example.com") + mocks.ConfigHandler.Set("dns.address", "") + mocks.ConfigHandler.Set("vm.driver", "hyperv") // And configuring DNS err := manager.ConfigureDNS() diff --git a/pkg/workstation/services/dns_service.go b/pkg/workstation/services/dns_service.go index d18db03bb..b7a53f95d 100644 --- a/pkg/workstation/services/dns_service.go +++ b/pkg/workstation/services/dns_service.go @@ -58,7 +58,7 @@ func (s *DNSService) Initialize() error { // SetAddress updates DNS address in config and calls BaseService's SetAddress. func (s *DNSService) SetAddress(address string) error { - err := s.configHandler.SetContextValue("dns.address", address) + err := s.configHandler.Set("dns.address", address) if err != nil { return fmt.Errorf("error setting DNS address: %w", err) } diff --git a/pkg/workstation/services/dns_service_test.go b/pkg/workstation/services/dns_service_test.go index a4f183ca6..11fbfa27e 100644 --- a/pkg/workstation/services/dns_service_test.go +++ b/pkg/workstation/services/dns_service_test.go @@ -168,7 +168,7 @@ func TestDNSService_SetAddress(t *testing.T) { t.Run("ErrorSettingAddress", func(t *testing.T) { mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.SetContextValueFunc = func(key string, value any) error { + mockConfigHandler.SetFunc = func(key string, value any) error { return fmt.Errorf("mocked error setting address") } mocks := setupDnsMocks(t, &SetupOptions{ @@ -232,9 +232,9 @@ func TestDNSService_GetComposeConfig(t *testing.T) { service, mocks := setup(t) // Set vm.driver to docker-desktop to simulate localhost mode - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") - mocks.ConfigHandler.SetContextValue("dns.domain", "test") - mocks.ConfigHandler.SetContextValue("network.cidr_block", "192.168.1.0/24") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("dns.domain", "test") + mocks.ConfigHandler.Set("network.cidr_block", "192.168.1.0/24") // When GetComposeConfig is called cfg, err := service.GetComposeConfig() @@ -332,7 +332,7 @@ func TestDNSService_WriteConfig(t *testing.T) { service.SetAddress("127.0.0.1") // Set the DNS domain - mocks.ConfigHandler.SetContextValue("dns.domain", "test") + mocks.ConfigHandler.Set("dns.domain", "test") // Mock the writeFile function to capture the content written var writtenContent []byte @@ -375,7 +375,7 @@ func TestDNSService_WriteConfig(t *testing.T) { t.Run("SuccessLocalhostMode", func(t *testing.T) { // Setup service, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") var writtenContent []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { @@ -634,7 +634,7 @@ func TestDNSService_WriteConfig(t *testing.T) { t.Run("SuccessLocalhostModeWithWildcard", func(t *testing.T) { // Setup service, mocks := setup(t) - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") var writtenContent []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { @@ -818,7 +818,7 @@ func TestDNSService_GetHostname(t *testing.T) { service.SetName("test") // Set the dns.domain configuration value - mocks.ConfigHandler.SetContextValue("dns.domain", "test") + mocks.ConfigHandler.Set("dns.domain", "test") return service, mocks } @@ -841,7 +841,7 @@ func TestDNSService_GetHostname(t *testing.T) { // Given a DNSService with no name set service, mocks := setup(t) service.SetName("") // Clear the name - mocks.ConfigHandler.SetContextValue("dns.domain", "") // Clear the domain + mocks.ConfigHandler.Set("dns.domain", "") // Clear the domain // When GetHostname is called hostname := service.GetHostname() diff --git a/pkg/workstation/services/git_livereload_service_test.go b/pkg/workstation/services/git_livereload_service_test.go index c90dcb7ef..87a087e5a 100644 --- a/pkg/workstation/services/git_livereload_service_test.go +++ b/pkg/workstation/services/git_livereload_service_test.go @@ -112,7 +112,7 @@ func TestGitLivereloadService_GetComposeConfig(t *testing.T) { gitLivereloadService.SetName("git") // Configure rsync_include in the mock config - mocks.ConfigHandler.SetContextValue("git.livereload.rsync_include", "kustomize") + mocks.ConfigHandler.Set("git.livereload.rsync_include", "kustomize") // When GetComposeConfig is called composeConfig, err := gitLivereloadService.GetComposeConfig() diff --git a/pkg/workstation/services/localstack_service_test.go b/pkg/workstation/services/localstack_service_test.go index 5613e8d9a..dfe53afd8 100644 --- a/pkg/workstation/services/localstack_service_test.go +++ b/pkg/workstation/services/localstack_service_test.go @@ -26,11 +26,11 @@ func TestLocalstackService_GetComposeConfig(t *testing.T) { service, mocks := setup(t) // Mock configuration for Localstack - err := mocks.ConfigHandler.SetContextValue("aws.localstack.enabled", true) + err := mocks.ConfigHandler.Set("aws.localstack.enabled", true) if err != nil { t.Fatalf("failed to set localstack enabled: %v", err) } - err = mocks.ConfigHandler.SetContextValue("aws.localstack.services", []string{"s3", "dynamodb"}) + err = mocks.ConfigHandler.Set("aws.localstack.services", []string{"s3", "dynamodb"}) if err != nil { t.Fatalf("failed to set localstack services: %v", err) } @@ -71,11 +71,11 @@ func TestLocalstackService_GetComposeConfig(t *testing.T) { service, mocks := setup(t) // Mock configuration for Localstack - err := mocks.ConfigHandler.SetContextValue("aws.localstack.enabled", true) + err := mocks.ConfigHandler.Set("aws.localstack.enabled", true) if err != nil { t.Fatalf("failed to set localstack enabled: %v", err) } - err = mocks.ConfigHandler.SetContextValue("aws.localstack.services", []string{"s3", "dynamodb"}) + err = mocks.ConfigHandler.Set("aws.localstack.services", []string{"s3", "dynamodb"}) if err != nil { t.Fatalf("failed to set localstack services: %v", err) } diff --git a/pkg/workstation/services/registry_service.go b/pkg/workstation/services/registry_service.go index 905a24ef8..e208f33fc 100644 --- a/pkg/workstation/services/registry_service.go +++ b/pkg/workstation/services/registry_service.go @@ -76,7 +76,7 @@ func (s *RegistryService) SetAddress(address string) error { hostName := s.GetHostname() - err := s.configHandler.SetContextValue(fmt.Sprintf("docker.registries[%s].hostname", s.name), hostName) + err := s.configHandler.Set(fmt.Sprintf("docker.registries[%s].hostname", s.name), hostName) if err != nil { return fmt.Errorf("failed to set hostname for registry %s: %w", s.name, err) } @@ -93,7 +93,7 @@ func (s *RegistryService) SetAddress(address string) error { if localRegistry == nil { localRegistry = s hostPort = constants.REGISTRY_DEFAULT_HOST_PORT - err = s.configHandler.SetContextValue("docker.registry_url", hostName) + err = s.configHandler.Set("docker.registry_url", hostName) if err != nil { return fmt.Errorf("failed to set registry URL for registry %s: %w", s.name, err) } @@ -105,7 +105,7 @@ func (s *RegistryService) SetAddress(address string) error { if hostPort != 0 { s.hostPort = hostPort - err := s.configHandler.SetContextValue(fmt.Sprintf("docker.registries[%s].hostport", s.name), hostPort) + err := s.configHandler.Set(fmt.Sprintf("docker.registries[%s].hostport", s.name), hostPort) if err != nil { return fmt.Errorf("failed to set host port for registry %s: %w", s.name, err) } diff --git a/pkg/workstation/services/registry_service_test.go b/pkg/workstation/services/registry_service_test.go index f57d63015..dde4e8115 100644 --- a/pkg/workstation/services/registry_service_test.go +++ b/pkg/workstation/services/registry_service_test.go @@ -153,8 +153,8 @@ func TestRegistryService_GetComposeConfig(t *testing.T) { service, mocks := setup(t) // Set up the registry configuration - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") - mocks.ConfigHandler.SetContextValue("docker.registries.local-registry.hostport", 5000) + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("docker.registries.local-registry.hostport", 5000) // Configure service for local registry testing service.address = "localhost" @@ -199,20 +199,33 @@ func TestRegistryService_SetAddress(t *testing.T) { mocks := setupMocks(t) // Load initial config configYAML := ` -apiVersion: v1alpha1 +version: v1alpha1 contexts: mock-context: dns: domain: test docker: registries: - registry: {} - registry1: {} - registry2: {} + registry: + remote: "" + local: "" + registry1: + remote: "" + local: "" + registry2: + remote: "" + local: "" ` if err := mocks.ConfigHandler.LoadConfigString(configYAML); err != nil { t.Fatalf("Failed to load config: %v", err) } + + // Verify config loaded correctly + domain := mocks.ConfigHandler.GetString("dns.domain") + if domain != "test" { + t.Fatalf("Config not loaded correctly, dns.domain = '%s', expected 'test'", domain) + } + service := NewRegistryService(mocks.Injector) service.shims = mocks.Shims service.Initialize() @@ -228,10 +241,23 @@ contexts: service, mocks := setup(t) // And localhost mode - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } + // Verify vm.driver was set + if vmDriver := mocks.ConfigHandler.GetString("vm.driver"); vmDriver != "docker-desktop" { + t.Fatalf("vm.driver not set correctly, got '%s'", vmDriver) + } + + // Manually set the expected values to verify Get/Set works + if err := mocks.ConfigHandler.Set("docker.registries.registry.hostname", "manual.test"); err != nil { + t.Fatalf("Manual set failed: %v", err) + } + if manual := mocks.ConfigHandler.GetString("docker.registries.registry.hostname"); manual != "manual.test" { + t.Fatalf("Manual get failed, expected 'manual.test', got '%s'", manual) + } + // When SetAddress is called with localhost err := service.SetAddress("localhost") @@ -267,12 +293,12 @@ contexts: service, mocks := setup(t) // And remote registry configuration - if err := mocks.ConfigHandler.SetContextValue("docker.registries.registry.remote", "remote.registry:5000"); err != nil { + if err := mocks.ConfigHandler.Set("docker.registries.registry.remote", "remote.registry:5000"); err != nil { t.Fatalf("Failed to set remote registry: %v", err) } // And localhost mode - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } @@ -303,13 +329,13 @@ contexts: service, mocks := setup(t) // And localhost mode - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } // And custom host port customHostPort := 5001 - if err := mocks.ConfigHandler.SetContextValue("docker.registries.registry.hostport", customHostPort); err != nil { + if err := mocks.ConfigHandler.Set("docker.registries.registry.hostport", customHostPort); err != nil { t.Fatalf("Failed to set custom host port: %v", err) } @@ -333,7 +359,7 @@ contexts: service1, mocks := setup(t) // And localhost mode - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } @@ -409,7 +435,7 @@ contexts: service.SetName("registry") // And mock error when setting hostname - mockConfigHandler.SetContextValueFunc = func(key string, value any) error { + mockConfigHandler.SetFunc = func(key string, value any) error { return fmt.Errorf("mock error setting hostname") } @@ -448,7 +474,7 @@ contexts: return "" } - mockConfigHandler.SetContextValueFunc = func(key string, value any) error { + mockConfigHandler.SetFunc = func(key string, value any) error { if strings.Contains(key, "hostport") { return fmt.Errorf("mock error setting host port") } @@ -504,7 +530,7 @@ contexts: return "" } - mockConfigHandler.SetContextValueFunc = func(key string, value any) error { + mockConfigHandler.SetFunc = func(key string, value any) error { if key == "docker.registry_url" { return fmt.Errorf("mock error setting registry URL") } diff --git a/pkg/workstation/services/service_test.go b/pkg/workstation/services/service_test.go index 98b304485..4c228e66f 100644 --- a/pkg/workstation/services/service_test.go +++ b/pkg/workstation/services/service_test.go @@ -105,6 +105,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Set project root environment variable t.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) + t.Setenv("WINDSOR_CONTEXT", "mock-context") // Register cleanup to restore original state t.Cleanup(func() { @@ -147,7 +148,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Load config configYAML := ` -apiVersion: v1alpha1 +version: v1alpha1 contexts: mock-context: dns: @@ -407,7 +408,7 @@ func TestBaseService_IsLocalhostMode(t *testing.T) { service, mocks := setup(t) // And mock behavior for docker-desktop driver - mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop") + mocks.ConfigHandler.Set("vm.driver", "docker-desktop") // When isLocalhostMode is called isLocal := service.isLocalhostMode() @@ -423,7 +424,7 @@ func TestBaseService_IsLocalhostMode(t *testing.T) { service, mocks := setup(t) // And mock behavior for non-docker-desktop driver - mocks.ConfigHandler.SetContextValue("vm.driver", "other-driver") + mocks.ConfigHandler.Set("vm.driver", "other-driver") // When isLocalhostMode is called isLocal := service.isLocalhostMode() diff --git a/pkg/workstation/services/talos_service.go b/pkg/workstation/services/talos_service.go index 2f2cfb28b..1c13ac139 100644 --- a/pkg/workstation/services/talos_service.go +++ b/pkg/workstation/services/talos_service.go @@ -27,7 +27,6 @@ var ( nextAPIPort = constants.DEFAULT_TALOS_API_PORT + 1 defaultAPIPort = constants.DEFAULT_TALOS_API_PORT portLock sync.Mutex - extraPortIndex = 0 controlPlaneLeader *TalosService usedHostPorts = make(map[uint32]bool) ) @@ -82,10 +81,10 @@ func (s *TalosService) SetAddress(address string) error { nodeType = "controlplanes" } - if err := s.configHandler.SetContextValue(fmt.Sprintf("cluster.%s.nodes.%s.hostname", nodeType, s.name), s.GetHostname()); err != nil { + if err := s.configHandler.Set(fmt.Sprintf("cluster.%s.nodes.%s.hostname", nodeType, s.name), s.GetHostname()); err != nil { return err } - if err := s.configHandler.SetContextValue(fmt.Sprintf("cluster.%s.nodes.%s.node", nodeType, s.name), s.GetHostname()); err != nil { + if err := s.configHandler.Set(fmt.Sprintf("cluster.%s.nodes.%s.node", nodeType, s.name), s.GetHostname()); err != nil { return err } @@ -106,7 +105,8 @@ func (s *TalosService) SetAddress(address string) error { } endpoint := fmt.Sprintf("%s:%d", endpointAddress, port) - if err := s.configHandler.SetContextValue(fmt.Sprintf("cluster.%s.nodes.%s.endpoint", nodeType, s.name), endpoint); err != nil { + nodePath := fmt.Sprintf("cluster.%s.nodes.%s.endpoint", nodeType, s.name) + if err := s.configHandler.Set(nodePath, endpoint); err != nil { return err } @@ -130,7 +130,7 @@ func (s *TalosService) SetAddress(address string) error { hostPortsCopy[i] = fmt.Sprintf("%d:%d/%s", hostPort, nodePort, protocol) } - if err := s.configHandler.SetContextValue(fmt.Sprintf("cluster.%s.nodes.%s.hostports", nodeType, s.name), hostPortsCopy); err != nil { + if err := s.configHandler.Set(fmt.Sprintf("cluster.%s.nodes.%s.hostports", nodeType, s.name), hostPortsCopy); err != nil { return err } diff --git a/pkg/workstation/services/talos_service_test.go b/pkg/workstation/services/talos_service_test.go index c3e2c663f..19e95467f 100644 --- a/pkg/workstation/services/talos_service_test.go +++ b/pkg/workstation/services/talos_service_test.go @@ -28,9 +28,13 @@ func setupTalosServiceMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Create base mocks using setupMocks mocks := setupMocks(t, opts...) - // Load config - configYAML := fmt.Sprintf(` -apiVersion: v1alpha1 + // Load config - use provided config if available, otherwise use default + var configToLoad string + if len(opts) > 0 && opts[0].ConfigStr != "" { + configToLoad = opts[0].ConfigStr + } else { + configToLoad = fmt.Sprintf(` +version: v1alpha1 contexts: mock-context: dns: @@ -72,18 +76,12 @@ contexts: cpu: %d memory: %d `, constants.DEFAULT_TALOS_API_PORT, - constants.DEFAULT_TALOS_WORKER_CPU, - constants.DEFAULT_TALOS_WORKER_RAM) - - if err := mocks.ConfigHandler.LoadConfigString(configYAML); err != nil { - t.Fatalf("Failed to load config: %v", err) + constants.DEFAULT_TALOS_WORKER_CPU, + constants.DEFAULT_TALOS_WORKER_RAM) } - // Load optional config if provided - if len(opts) > 0 && opts[0].ConfigStr != "" { - if err := mocks.ConfigHandler.LoadConfigString(opts[0].ConfigStr); err != nil { - t.Fatalf("Failed to load config string: %v", err) - } + if err := mocks.ConfigHandler.LoadConfigString(configToLoad); err != nil { + t.Fatalf("Failed to load config: %v", err) } mocks.Shell.GetProjectRootFunc = func() (string, error) { @@ -286,7 +284,7 @@ func TestTalosService_SetAddress(t *testing.T) { "30001:30001/udp", "30002:30002/tcp", } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -338,7 +336,7 @@ func TestTalosService_SetAddress(t *testing.T) { } // And invalid host port format in config - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", []string{"invalid:format:extra"}); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"invalid:format:extra"}); err != nil { t.Fatalf("Failed to set invalid host port format: %v", err) } @@ -371,7 +369,7 @@ func TestTalosService_SetAddress(t *testing.T) { } // And invalid protocol in config - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", []string{"30000:30000/invalid"}); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000/invalid"}); err != nil { t.Fatalf("Failed to set invalid protocol: %v", err) } @@ -404,7 +402,7 @@ func TestTalosService_SetAddress(t *testing.T) { } // Set host ports for first worker - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", []string{"30000:30000"}); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000"}); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -421,7 +419,7 @@ func TestTalosService_SetAddress(t *testing.T) { } // Set same host ports for second worker - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", []string{"30000:30000"}); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", []string{"30000:30000"}); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -451,7 +449,7 @@ func TestTalosService_SetAddress(t *testing.T) { usedHostPorts = make(map[uint32]bool) // And a custom TLD - if err := mocks.ConfigHandler.SetContextValue("dns.domain", "custom.local"); err != nil { + if err := mocks.ConfigHandler.Set("dns.domain", "custom.local"); err != nil { t.Fatalf("Failed to set custom TLD: %v", err) } @@ -505,7 +503,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts := []string{ "abc:30000", // Non-numeric host port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -524,7 +522,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts = []string{ "30000:xyz", // Non-numeric node port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -543,7 +541,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts = []string{ "xyz", // Non-numeric single port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -581,7 +579,7 @@ func TestTalosService_SetAddress(t *testing.T) { "30001:30001", "30002:30002", } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts1); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts1); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -603,7 +601,7 @@ func TestTalosService_SetAddress(t *testing.T) { "30002:30002", // Overlaps with first worker "30003:30003", // New port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts2); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts2); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -642,7 +640,7 @@ func TestTalosService_SetAddress(t *testing.T) { "30000:30000", // Overlaps with first worker "30003:30004", // Overlaps with second worker's incremented port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts3); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts3); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -703,7 +701,7 @@ func TestTalosService_SetAddress(t *testing.T) { "30000:30000", "30000:30001", // Intentional conflict with first port } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -757,7 +755,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts := []string{ "30000:30000/invalid", } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -793,7 +791,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts := []string{ "30000:30000:30000", // Too many colons } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -829,7 +827,7 @@ func TestTalosService_SetAddress(t *testing.T) { hostPorts := []string{ "invalid:30000", } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -1065,7 +1063,7 @@ func TestTalosService_GetComposeConfig(t *testing.T) { // And a custom image is configured customImage := "custom/talos:latest" - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.nodes.controlplane1.image", customImage); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.nodes.controlplane1.image", customImage); err != nil { t.Fatalf("Failed to set custom image: %v", err) } @@ -1096,7 +1094,7 @@ func TestTalosService_GetComposeConfig(t *testing.T) { "/data/controlplane1:/mnt/data", "/logs/controlplane1:/mnt/logs", } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.volumes", volumes); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.volumes", volumes); err != nil { t.Fatalf("Failed to set volumes: %v", err) } @@ -1128,20 +1126,24 @@ func TestTalosService_GetComposeConfig(t *testing.T) { }) t.Run("SuccessEmptyConfig", func(t *testing.T) { - // Given a TalosService with mock components - service, mocks := setup(t) - - // And an empty cluster config - if err := mocks.ConfigHandler.LoadConfigString(` -apiVersion: v1alpha1 + // Given a TalosService with mock components and empty cluster config + emptyConfig := &SetupOptions{ + ConfigStr: ` +version: v1alpha1 contexts: mock-context: dns: domain: test vm: driver: docker-desktop -`); err != nil { - t.Fatalf("Failed to load empty config: %v", err) +`, + } + mocks := setupTalosServiceMocks(t, emptyConfig) + service := NewTalosService(mocks.Injector, "controlplane") + service.shims = mocks.Shims + service.SetName("controlplane1") + if err := service.Initialize(); err != nil { + t.Fatalf("Failed to initialize service: %v", err) } // When GetComposeConfig is called @@ -1162,20 +1164,24 @@ contexts: }) t.Run("EmptyConfig", func(t *testing.T) { - // Given a TalosService with mock components - service, mocks := setup(t) - - // And an empty cluster config - if err := mocks.ConfigHandler.LoadConfigString(` -apiVersion: v1alpha1 + // Given a TalosService with mock components and empty cluster config + emptyConfig := &SetupOptions{ + ConfigStr: ` +version: v1alpha1 contexts: mock-context: dns: domain: test vm: driver: docker-desktop -`); err != nil { - t.Fatalf("Failed to load empty config: %v", err) +`, + } + mocks := setupTalosServiceMocks(t, emptyConfig) + service := NewTalosService(mocks.Injector, "controlplane") + service.shims = mocks.Shims + service.SetName("controlplane1") + if err := service.Initialize(); err != nil { + t.Fatalf("Failed to initialize service: %v", err) } // When GetComposeConfig is called @@ -1200,13 +1206,13 @@ contexts: service, mocks := setup(t) // And custom images at different levels - if err := mocks.ConfigHandler.SetContextValue("cluster.image", "cluster-wide:latest"); err != nil { + if err := mocks.ConfigHandler.Set("cluster.image", "cluster-wide:latest"); err != nil { t.Fatalf("Failed to set cluster-wide image: %v", err) } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.image", "group-specific:latest"); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.image", "group-specific:latest"); err != nil { t.Fatalf("Failed to set group-specific image: %v", err) } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.nodes.controlplane1.image", "node-specific:latest"); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.nodes.controlplane1.image", "node-specific:latest"); err != nil { t.Fatalf("Failed to set node-specific image: %v", err) } @@ -1237,7 +1243,7 @@ contexts: "/data/controlplane1:/mnt/data", "/logs/controlplane1:/mnt/logs", } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.volumes", customVolumes); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.volumes", customVolumes); err != nil { t.Fatalf("Failed to set custom volumes: %v", err) } @@ -1273,7 +1279,7 @@ contexts: service, mocks := setup(t) // And invalid volume format in config - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.volumes", []string{"invalid:format:extra"}); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.volumes", []string{"invalid:format:extra"}); err != nil { t.Fatalf("Failed to set invalid volume format: %v", err) } @@ -1294,8 +1300,8 @@ contexts: mocks := setupTalosServiceMocks(t) // And DNS configuration is set - mocks.ConfigHandler.SetContextValue("dns.domain", "test") - mocks.ConfigHandler.SetContextValue("dns.address", "192.168.1.1") + mocks.ConfigHandler.Set("dns.domain", "test") + mocks.ConfigHandler.Set("dns.address", "192.168.1.1") // Create a worker node service := NewTalosService(mocks.Injector, "worker") @@ -1342,7 +1348,7 @@ contexts: service, mocks := setup(t) // And localhost mode is enabled - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set VM driver: %v", err) } @@ -1351,7 +1357,7 @@ contexts: "30000:30000/tcp", "30001:30001/udp", } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.nodes.controlplane1.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.nodes.controlplane1.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -1422,7 +1428,7 @@ contexts: service, mocks := setup(t) // And localhost mode is enabled - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set VM driver: %v", err) } @@ -1430,7 +1436,7 @@ contexts: hostPorts := []string{ fmt.Sprintf("%d:30000/tcp", math.MaxUint32+1), // Port too large } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.nodes.controlplane1.hostports", hostPorts); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.nodes.controlplane1.hostports", hostPorts); err != nil { t.Fatalf("Failed to set host ports: %v", err) } @@ -1451,7 +1457,7 @@ contexts: service, mocks := setup(t) // And localhost mode is enabled - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "docker-desktop"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "docker-desktop"); err != nil { t.Fatalf("Failed to set VM driver: %v", err) } @@ -1486,7 +1492,7 @@ contexts: "${TEST_DATA_DIR}/controlplane1:/mnt/data", "/logs/controlplane1:/mnt/logs", } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.volumes", volumes); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.volumes", volumes); err != nil { t.Fatalf("Failed to set volumes: %v", err) } @@ -1552,10 +1558,10 @@ contexts: // And custom CPU and RAM settings customCPU := 4 customRAM := 8 - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.cpu", customCPU); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.cpu", customCPU); err != nil { t.Fatalf("Failed to set control plane CPU: %v", err) } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.memory", customRAM); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.memory", customRAM); err != nil { t.Fatalf("Failed to set control plane RAM: %v", err) } @@ -1585,10 +1591,10 @@ contexts: // And custom CPU and RAM settings customWorkerCPU := 2 customWorkerRAM := 4 - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.cpu", customWorkerCPU); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.cpu", customWorkerCPU); err != nil { t.Fatalf("Failed to set worker CPU: %v", err) } - if err := mocks.ConfigHandler.SetContextValue("cluster.workers.memory", customWorkerRAM); err != nil { + if err := mocks.ConfigHandler.Set("cluster.workers.memory", customWorkerRAM); err != nil { t.Fatalf("Failed to set worker RAM: %v", err) } @@ -1621,7 +1627,7 @@ contexts: volumes := []string{ "/data/controlplane1:/mnt/data", } - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.volumes", volumes); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.volumes", volumes); err != nil { t.Fatalf("Failed to set volumes: %v", err) } @@ -1691,7 +1697,7 @@ contexts: service, mocks := setup(t) // And DNS configuration - if err := mocks.ConfigHandler.SetContextValue("dns.address", "8.8.8.8"); err != nil { + if err := mocks.ConfigHandler.Set("dns.address", "8.8.8.8"); err != nil { t.Fatalf("Failed to set DNS address: %v", err) } @@ -1723,7 +1729,7 @@ contexts: service, mocks := setup(t) // And an invalid port value in config - if err := mocks.ConfigHandler.SetContextValue("cluster.controlplanes.nodes.controlplane1.endpoint", "controlplane1.test:invalid"); err != nil { + if err := mocks.ConfigHandler.Set("cluster.controlplanes.nodes.controlplane1.endpoint", "controlplane1.test:invalid"); err != nil { t.Fatalf("Failed to set invalid port value: %v", err) } diff --git a/pkg/workstation/virt/colima_virt.go b/pkg/workstation/virt/colima_virt.go index 6b17d53ef..55b10339f 100644 --- a/pkg/workstation/virt/colima_virt.go +++ b/pkg/workstation/virt/colima_virt.go @@ -59,7 +59,7 @@ func (v *ColimaVirt) Up() error { return fmt.Errorf("failed to start Colima VM: %w", err) } - if err := v.configHandler.SetContextValue("vm.address", info.Address); err != nil { + if err := v.configHandler.Set("vm.address", info.Address); err != nil { return fmt.Errorf("failed to set VM address in config handler: %w", err) } diff --git a/pkg/workstation/virt/colima_virt_test.go b/pkg/workstation/virt/colima_virt_test.go index 93e1062c0..800307b63 100644 --- a/pkg/workstation/virt/colima_virt_test.go +++ b/pkg/workstation/virt/colima_virt_test.go @@ -189,7 +189,7 @@ func TestColimaVirt_WriteConfig(t *testing.T) { mocks := setupColimaMocks(t) // Ensure vm.driver is explicitly set to colima - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "colima"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "colima"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } @@ -338,7 +338,7 @@ func TestColimaVirt_WriteConfig(t *testing.T) { } // And vm.driver is not colima - if err := mocks.ConfigHandler.SetContextValue("vm.driver", "other"); err != nil { + if err := mocks.ConfigHandler.Set("vm.driver", "other"); err != nil { t.Fatalf("Failed to set vm.driver: %v", err) } @@ -677,8 +677,8 @@ func TestColimaVirt_Up(t *testing.T) { return mocks.ConfigHandler.GetInt(key, defaultValues...) } - // Override just the SetContextValue to return an error - mockConfigHandler.SetContextValueFunc = func(key string, _ any) error { + // Override just the Set to return an error + mockConfigHandler.SetFunc = func(key string, _ any) error { if key == "vm.address" { return fmt.Errorf("mock set context value error") } diff --git a/pkg/workstation/virt/docker_virt_test.go b/pkg/workstation/virt/docker_virt_test.go index 53cb47f11..affad6123 100644 --- a/pkg/workstation/virt/docker_virt_test.go +++ b/pkg/workstation/virt/docker_virt_test.go @@ -217,7 +217,7 @@ func TestDockerVirt_Initialize(t *testing.T) { t.Run("ErrorDockerNotEnabled", func(t *testing.T) { // Given a docker virt instance with docker disabled dockerVirt, mocks := setup(t) - if err := mocks.ConfigHandler.SetContextValue("docker.enabled", false); err != nil { + if err := mocks.ConfigHandler.Set("docker.enabled", false); err != nil { t.Fatalf("Failed to set docker.enabled: %v", err) }