package config import ( "os" "path/filepath" "testing" "github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/parser" "github.com/goccy/go-yaml/token" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_setValueInYAML(t *testing.T) { tests := []struct { name string input string key string value string expected string wantErr string }{ { name: "set top-level bool to true", input: "transitive: true\nparanoid: false\n", key: "paranoid", value: "true", expected: "transitive: true\nparanoid: true\n", }, { name: "set top-level bool to false", input: "transitive: true\nparanoid: true\n", key: "paranoid", value: "false", expected: "transitive: true\nparanoid: false\n", }, { name: "set top-level integer", input: "transitive_depth: 5\n", key: "transitive_depth", value: "10", expected: "transitive_depth: 10\n", }, { name: "set top-level string", input: "verbosity: normal\n", key: "verbosity", value: "verbose", expected: "verbosity: verbose\n", }, { name: "set nested key", input: "cloud:\n enabled: false\n endpoint_id: \"\"\n", key: "cloud.enabled", value: "true", expected: "cloud:\n enabled: true\n endpoint_id: \"\"\n", }, { name: "set deeply nested key", input: "dependency_cooldown:\n enabled: true\n days: 5\n", key: "dependency_cooldown.days", value: "10", expected: "dependency_cooldown:\n enabled: true\n days: 10\n", }, { name: "preserve comments", input: "# Important setting\ntransitive: true\n# Paranoid mode\nparanoid: false\n", key: "paranoid", value: "true", expected: "# Important setting\ntransitive: true\n# Paranoid mode\nparanoid: true\n", }, { name: "set string with spaces", input: "verbosity: normal\n", key: "verbosity", value: "my custom value", expected: "verbosity: my custom value\n", }, { name: "set same value is idempotent", input: "paranoid: false\n", key: "paranoid", value: "false", expected: "paranoid: false\n", }, { name: "error on empty key", input: "transitive: true\n", key: "", value: "false", wantErr: "key cannot be empty", }, { name: "error on nonexistent key", input: "transitive: true\n", key: "nonexistent", value: "false", wantErr: "key not found", }, { name: "error on nonexistent nested key", input: "cloud:\n enabled: false\n", key: "cloud.nonexistent", value: "true", wantErr: "key not found", }, { name: "error when setting non-leaf node", input: "cloud:\n enabled: false\n", key: "cloud", value: "true", wantErr: "cannot set value on non-scalar node", }, { name: "error when intermediate key is not a mapping", input: "transitive: true\n", key: "transitive.nested", value: "true", wantErr: "intermediate key", }, { name: "error on sequence node target", input: "trusted_packages:\n - purl: pkg:npm/foo\n reason: test\n", key: "trusted_packages", value: "true", wantErr: "cannot set value on non-scalar node", }, { name: "error on invalid bool value", input: "paranoid: false\n", key: "paranoid", value: "falce", wantErr: "invalid value", }, { name: "error on non-integer for integer field", input: "transitive_depth: 5\n", key: "transitive_depth", value: "abc", wantErr: "invalid value", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := setValueInYAML([]byte(tt.input), tt.key, tt.value) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) return } require.NoError(t, err) assert.Equal(t, tt.expected, string(result)) }) } } func Test_createScalarNode(t *testing.T) { pos := &token.Position{Line: 1, Column: 1, Offset: 0} tests := []struct { name string value string expectedType ast.NodeType }{ {name: "true is bool", value: "true", expectedType: ast.BoolType}, {name: "false is bool", value: "false", expectedType: ast.BoolType}, {name: "positive int", value: "42", expectedType: ast.IntegerType}, {name: "zero is int", value: "0", expectedType: ast.IntegerType}, {name: "negative int", value: "-5", expectedType: ast.IntegerType}, {name: "plain string", value: "hello", expectedType: ast.StringType}, {name: "float-like is string", value: "3.14", expectedType: ast.StringType}, {name: "True (capitalized) is string", value: "True", expectedType: ast.StringType}, {name: "empty string", value: "", expectedType: ast.StringType}, {name: "numeric-prefix string", value: "123abc", expectedType: ast.StringType}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { node, err := createScalarNode(tt.value, pos) require.NoError(t, err) assert.Equal(t, tt.expectedType, node.Type()) }) } } func TestSetConfigValue(t *testing.T) { t.Run("creates config from template and sets value", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) initConfig() err := SetConfigValue("paranoid", "true") require.NoError(t, err) data, err := os.ReadFile(filepath.Join(tmpDir, "config.yml")) require.NoError(t, err) assert.Contains(t, string(data), "paranoid: true") }) t.Run("updates existing config preserving other values", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) initConfig() configPath := filepath.Join(tmpDir, "config.yml") err := os.WriteFile(configPath, []byte("transitive: true\nparanoid: false\n"), 0o644) require.NoError(t, err) err = SetConfigValue("paranoid", "true") require.NoError(t, err) data, err := os.ReadFile(configPath) require.NoError(t, err) assert.Contains(t, string(data), "transitive: true") assert.Contains(t, string(data), "paranoid: true") }) t.Run("returns error for nonexistent key", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) initConfig() err := os.WriteFile(filepath.Join(tmpDir, "config.yml"), []byte("paranoid: false\n"), 0o644) require.NoError(t, err) err = SetConfigValue("nonexistent", "true") require.Error(t, err) assert.Contains(t, err.Error(), "key not found") }) } func TestGetConfigValue(t *testing.T) { configYAML := "paranoid: true\ntransitive: false\ntransitive_depth: 10\nverbosity: verbose\n" + "cloud:\n enabled: true\n endpoint_id: ep-123\n" + "dependency_cooldown:\n enabled: true\n days: 7\n" + "proxy:\n enabled: false\n install_only: true\n" + "sandbox:\n enabled: true\n enforce_always: false\n" setupConfig := func(t *testing.T) { t.Helper() tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) err := os.WriteFile(filepath.Join(tmpDir, "config.yml"), []byte(configYAML), 0o644) require.NoError(t, err) initConfig() } tests := []struct { name string key string expected any wantErr string }{ {name: "top-level bool true", key: "paranoid", expected: true}, {name: "top-level bool false", key: "transitive", expected: false}, {name: "top-level integer", key: "transitive_depth", expected: 10}, {name: "top-level string", key: "verbosity", expected: "verbose"}, {name: "nested bool", key: "cloud.enabled", expected: true}, {name: "nested string", key: "cloud.endpoint_id", expected: "ep-123"}, {name: "nested integer", key: "dependency_cooldown.days", expected: 7}, {name: "nested bool under proxy", key: "proxy.enabled", expected: false}, {name: "nested bool under proxy install_only", key: "proxy.install_only", expected: true}, {name: "nested bool under sandbox", key: "sandbox.enabled", expected: true}, {name: "nested bool under sandbox enforce_always", key: "sandbox.enforce_always", expected: false}, {name: "error on empty key", key: "", wantErr: "key cannot be empty"}, {name: "error on unknown top-level key", key: "totally_bogus", wantErr: "unknown config key"}, {name: "error on unknown nested key", key: "cloud.nonexistent", wantErr: "unknown config key"}, {name: "error on too-deep key", key: "cloud.enabled.deep", wantErr: "unknown config key"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setupConfig(t) val, err := GetConfigValue(tt.key) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) return } require.NoError(t, err) assert.Equal(t, tt.expected, val) }) } t.Run("returns nested object as map", func(t *testing.T) { setupConfig(t) val, err := GetConfigValue("cloud") require.NoError(t, err) m, ok := val.(map[string]any) require.True(t, ok, "expected map[string]any, got %T", val) assert.Equal(t, true, m["enabled"]) assert.Equal(t, "ep-123", m["endpoint_id"]) }) t.Run("returns defaults when no config file exists", func(t *testing.T) { t.Setenv("PMG_CONFIG_DIR", "/tmp/pmg-test/random-does-not-exist") initConfig() val, err := GetConfigValue("transitive") require.NoError(t, err) assert.Equal(t, true, val) val, err = GetConfigValue("paranoid") require.NoError(t, err) assert.Equal(t, false, val) }) t.Run("env var overrides config file", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) t.Setenv("PMG_PARANOID", "true") err := os.WriteFile(filepath.Join(tmpDir, "config.yml"), []byte("paranoid: false\n"), 0o644) require.NoError(t, err) initConfig() val, err := GetConfigValue("paranoid") require.NoError(t, err) // Viper returns env var values as strings assert.Equal(t, "true", val) }) } func TestSetStringFieldPreservesType(t *testing.T) { tests := []struct { name string input string key string value string }{ { name: "string field set to bool-like value stays string", input: "verbosity: normal\n", key: "verbosity", value: "true", }, { name: "string field set to integer-like value stays string", input: "verbosity: normal\n", key: "verbosity", value: "42", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := setValueInYAML([]byte(tt.input), tt.key, tt.value) require.NoError(t, err) file, err := parser.ParseBytes(result, parser.ParseComments) require.NoError(t, err) root := file.Docs[0].Body.(*ast.MappingNode) for _, mv := range root.Values { if mv.Key.String() == tt.key { assert.Equal(t, ast.StringType, mv.Value.Type(), "expected StringType but got %s", mv.Value.Type()) return } } t.Fatalf("key %q not found in result", tt.key) }) } } func Test_needsQuoting(t *testing.T) { tests := []struct { value string expected bool }{ {"true", true}, {"false", true}, {"True", true}, {"False", true}, {"yes", true}, {"no", true}, {"null", true}, {"42", true}, {"-5", true}, {"0", true}, {"3.14", true}, {"hello", false}, {"normal", false}, {"verbose", false}, {"", true}, {"123abc", false}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { assert.Equal(t, tt.expected, needsQuoting(tt.value)) }) } } func TestSetThenGetRoundTrip(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) configPath := filepath.Join(tmpDir, "config.yml") err := os.WriteFile(configPath, []byte("paranoid: false\ntransitive_depth: 5\nverbosity: normal\n"), 0o644) require.NoError(t, err) initConfig() err = SetConfigValue("paranoid", "true") require.NoError(t, err) err = SetConfigValue("transitive_depth", "20") require.NoError(t, err) err = SetConfigValue("verbosity", "silent") require.NoError(t, err) // Reload global config from file to pick up changes initConfig() val, err := GetConfigValue("paranoid") require.NoError(t, err) assert.Equal(t, true, val) val, err = GetConfigValue("transitive_depth") require.NoError(t, err) assert.Equal(t, 20, val) val, err = GetConfigValue("verbosity") require.NoError(t, err) assert.Equal(t, "silent", val) }