Files
helmfile/pkg/config/config_test.go
T
000a6fa660 fix: support escaped commas in --state-values-set (#2813)
* feat: support comma escape for values overwrites

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>

* fix: harden state-values-set parsing and support \, in state-values-set-string

- Return a descriptive error instead of panicking when a --state-values-set
  assignment is missing '=', has an empty key, or ends with a trailing comma
- Support escaped commas in --state-values-set-string and preserve other
  backslashes (including trailing ones), aligning it with --state-values-set
  and Helm's --set/--set-string; quoted values keep working
- Allow empty values in --state-values-set-string for parity with
  --state-values-set
- Restructure parsing into guard-clause helpers with single-level
  conditionals; parse assignments via strings.Cut
- Extend table-driven tests: error cases, --state-values-set-string
  coverage, combined flags, and unit tests for the split/unescape helpers

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: zhengbayi <zhengbaiyi@sensetime.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-03 15:31:04 +08:00

243 lines
7.1 KiB
Go

package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCLIConfigImplStateValuesSet(t *testing.T) {
tests := []struct {
name string
set []string
want map[string]any
wantErr string
}{
{
name: "comma separated assignments retain their types",
set: []string{"count=2,enabled=true,disabled=false,empty=null,zero=0,code=001"},
want: map[string]any{
"count": int64(2), "enabled": true, "disabled": false,
"empty": nil, "zero": int64(0), "code": "001",
},
},
{
name: "escaped commas within multiple values",
set: []string{`a=1,b=2\,3,c=9\,10\,11`},
want: map[string]any{"a": int64(1), "b": "2,3", "c": "9,10,11"},
},
{
name: "escaped commas at the start and end of a value",
set: []string{`message=\,hello\,`},
want: map[string]any{"message": ",hello,"},
},
{
name: "repeated flags overwrite earlier values",
set: []string{`message=first\,second`, `message=third\,fourth,count=2`},
want: map[string]any{"message": "third,fourth", "count": int64(2)},
},
{
name: "equals signs remain within values",
set: []string{`message=first\,second=value,count=2`},
want: map[string]any{"message": "first,second=value", "count": int64(2)},
},
{
name: "unicode values",
set: []string{`message=你好\,世界`},
want: map[string]any{"message": "你好,世界"},
},
{
name: "empty value",
set: []string{"message=,count=2"},
want: map[string]any{"message": "", "count": int64(2)},
},
{
name: "backslashes before other characters are preserved",
set: []string{`path=C:\tmp\config`},
want: map[string]any{"path": `C:\tmp\config`},
},
{
name: "trailing backslash is preserved",
set: []string{`path=C:\tmp\`},
want: map[string]any{"path": `C:\tmp\`},
},
{
name: "escaped dots in keys are preserved for key parsing",
set: []string{`annotations.example\.com/key=first\,second`},
want: map[string]any{
"annotations": map[string]any{"example.com/key": "first,second"},
},
},
{
name: "nested array keys",
set: []string{`servers[0].host=east\,west,enabled=true`},
want: map[string]any{
"servers": []any{map[string]any{"host": "east,west"}},
"enabled": true,
},
},
{
name: "even backslashes leave comma as an assignment separator",
set: []string{`path=C:\\,count=2`},
want: map[string]any{"path": `C:\\`, "count": int64(2)},
},
{
name: "odd backslashes escape the comma",
set: []string{`message=first\\\,second`},
want: map[string]any{"message": `first\\,second`},
},
{
name: "assignment without equals sign is rejected",
set: []string{"message"},
wantErr: `invalid assignment "message": expected <key>=<value>`,
},
{
name: "trailing comma is rejected",
set: []string{"message=hello,"},
wantErr: `invalid assignment "": expected <key>=<value>`,
},
{
name: "empty key is rejected",
set: []string{"=hello"},
wantErr: `invalid assignment "=hello": expected <key>=<value>`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGlobalImpl(&GlobalOptions{StateValuesSet: tt.set})
err := NewCLIConfigImpl(g)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, g.StateValuesSet())
})
}
}
func TestNewCLIConfigImplStateValuesSetString(t *testing.T) {
tests := []struct {
name string
set []string
want map[string]any
wantErr string
}{
{
name: "values are kept as strings without type conversion",
set: []string{"count=2,enabled=true"},
want: map[string]any{"count": "2", "enabled": "true"},
},
{
name: "quoted values may contain commas",
set: []string{`zone="zone1,zone2",imageTag=1.23.3`},
want: map[string]any{"zone": `"zone1,zone2"`, "imageTag": "1.23.3"},
},
{
name: "escaped commas within values",
set: []string{`zone=zone1\,zone2,imageTag=1.23.3`},
want: map[string]any{"zone": "zone1,zone2", "imageTag": "1.23.3"},
},
{
name: "backslashes and trailing backslash are preserved",
set: []string{`path=C:\tmp\config,trailing=C:\tmp\`},
want: map[string]any{"path": `C:\tmp\config`, "trailing": `C:\tmp\`},
},
{
name: "even backslashes leave comma as an assignment separator",
set: []string{`path=C:\\,count=2`},
want: map[string]any{"path": `C:\\`, "count": "2"},
},
{
name: "empty value",
set: []string{"message=,count=2"},
want: map[string]any{"message": "", "count": "2"},
},
{
name: "repeated flags overwrite earlier values",
set: []string{`zone=a\,b`, `zone=c\,d`},
want: map[string]any{"zone": "c,d"},
},
{
name: "escaped dots in keys are preserved for key parsing",
set: []string{`annotations.example\.com/key=first\,second`},
want: map[string]any{
"annotations": map[string]any{"example.com/key": "first,second"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGlobalImpl(&GlobalOptions{StateValuesSetString: tt.set})
err := NewCLIConfigImpl(g)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, g.StateValuesSet())
})
}
}
func TestNewCLIConfigImplBothSetFlags(t *testing.T) {
g := NewGlobalImpl(&GlobalOptions{
StateValuesSet: []string{"count=2"},
StateValuesSetString: []string{"imageTag=1.23.3"},
})
require.NoError(t, NewCLIConfigImpl(g))
want := map[string]any{
"count": int64(2),
"imageTag": "1.23.3",
}
assert.Equal(t, want, g.StateValuesSet())
}
func TestSplitOnUnescapedCommas(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{name: "no commas", input: "a=1", want: []string{"a=1"}},
{name: "plain separators", input: "a=1,b=2", want: []string{"a=1", "b=2"}},
{name: "escaped separator", input: `a=1\,2,b=3`, want: []string{`a=1\,2`, "b=3"}},
{name: "escaped separator only", input: `a=1\,2`, want: []string{`a=1\,2`}},
{name: "even backslashes unescape the separator", input: `a=1\\,b=2`, want: []string{`a=1\\`, "b=2"}},
{name: "trailing separator", input: "a=1,", want: []string{"a=1", ""}},
{name: "trailing backslash", input: `a=1\`, want: []string{`a=1\`}},
{name: "unicode", input: `a=你好\,世界,b=2`, want: []string{`a=你好\,世界`, "b=2"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, splitOnUnescapedCommas(tt.input))
})
}
}
func TestUnescapeCommas(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "no escapes", input: "a=1,b=2", want: "a=1,b=2"},
{name: "escaped comma", input: `a=1\,2`, want: "a=1,2"},
{name: "escaped dot preserved", input: `a\.b=1`, want: `a\.b=1`},
{name: "even backslashes preserved", input: `a=1\\`, want: `a=1\\`},
{name: "trailing backslash preserved", input: `a=1\`, want: `a=1\`},
{name: "unicode preserved", input: `a=你好\,世界`, want: "a=你好,世界"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, unescapeCommas(tt.input))
})
}
}