helmfile/pkg/yaml/append_processor_test.go

537 lines
10 KiB
Go

package yaml
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAppendProcessor_MergeWithAppend(t *testing.T) {
processor := NewAppendProcessor()
tests := []struct {
name string
dest map[string]any
src map[string]any
expected map[string]any
}{
{
name: "append to existing slice",
dest: map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
map[string]any{
"key": "b",
},
},
},
src: map[string]any{
"values+": []any{
map[string]any{
"key": "c",
},
},
},
expected: map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
map[string]any{
"key": "b",
},
map[string]any{
"key": "c",
},
},
},
},
{
name: "nested append",
dest: map[string]any{
"config": map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
},
},
},
src: map[string]any{
"config": map[string]any{
"values+": []any{
map[string]any{
"key": "b",
},
},
},
},
expected: map[string]any{
"config": map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
map[string]any{
"key": "b",
},
},
},
},
},
{
name: "create new slice if not exists",
dest: map[string]any{},
src: map[string]any{
"values+": []any{
map[string]any{
"key": "a",
},
},
},
expected: map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
},
},
},
{
name: "nested append with non-existent parent",
dest: map[string]any{},
src: map[string]any{
"kube-state-metrics": map[string]any{
"prometheus": map[string]any{
"monitor": map[string]any{
"metricRelabelings+": []any{
map[string]any{
"action": "labeldrop",
"regex": "info_.*",
},
},
},
},
},
},
expected: map[string]any{
"kube-state-metrics": map[string]any{
"prometheus": map[string]any{
"monitor": map[string]any{
"metricRelabelings": []any{
map[string]any{
"action": "labeldrop",
"regex": "info_.*",
},
},
},
},
},
},
},
{
name: "overwrite non-slice with append",
dest: map[string]any{
"values": "not a slice",
},
src: map[string]any{
"values+": []any{
map[string]any{
"key": "a",
},
},
},
expected: map[string]any{
"values": []any{
map[string]any{
"key": "a",
},
},
},
},
{
name: "type collision - []string vs []any",
dest: map[string]any{
"values": []string{"a", "b"},
},
src: map[string]any{
"values+": []any{"c", "d"},
},
expected: map[string]any{
"values": []any{"c", "d"},
},
},
{
name: "type collision - []int vs []any",
dest: map[string]any{
"values": []int{1, 2},
},
src: map[string]any{
"values+": []any{3, 4},
},
expected: map[string]any{
"values": []any{3, 4},
},
},
{
name: "append on map - should overwrite",
dest: map[string]any{
"config": map[string]any{"key": "value"},
},
src: map[string]any{
"config+": []any{"new", "values"},
},
expected: map[string]any{
"config": []any{"new", "values"},
},
},
{
name: "append on scalar - should overwrite",
dest: map[string]any{
"version": "1.0.0",
},
src: map[string]any{
"version+": []any{"2.0.0", "3.0.0"},
},
expected: map[string]any{
"version": []any{"2.0.0", "3.0.0"},
},
},
{
name: "nil slice in destination",
dest: map[string]any{
"values": nil,
},
src: map[string]any{
"values+": []any{"a", "b"},
},
expected: map[string]any{
"values": []any{"a", "b"},
},
},
{
name: "empty slice in destination",
dest: map[string]any{
"values": []any{},
},
src: map[string]any{
"values+": []any{"a", "b"},
},
expected: map[string]any{
"values": []any{"a", "b"},
},
},
{
name: "nil slice in source",
dest: map[string]any{
"values": []any{"existing"},
},
src: map[string]any{
"values+": nil,
},
expected: map[string]any{
"values": nil,
},
},
{
name: "mixed types in slices",
dest: map[string]any{
"data": []any{"string", 42, true},
},
src: map[string]any{
"data+": []any{"new", 100, false},
},
expected: map[string]any{
"data": []any{"string", 42, true, "new", 100, false},
},
},
{
name: "multiple append keys",
dest: map[string]any{
"list1": []any{"a"},
"list2": []any{"x"},
},
src: map[string]any{
"list1+": []any{"b"},
"list2+": []any{"y"},
},
expected: map[string]any{
"list1": []any{"a", "b"},
"list2": []any{"x", "y"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srcCopy := make(map[string]any)
for k, v := range tt.src {
srcCopy[k] = v
}
err := processor.MergeWithAppend(tt.dest, tt.src)
require.NoError(t, err)
assert.Equal(t, tt.expected, tt.dest)
assert.Equal(t, srcCopy, tt.src, "source map should not be mutated")
})
}
}
func TestAppendProcessor_EdgeCases(t *testing.T) {
processor := NewAppendProcessor()
t.Run("empty maps", func(t *testing.T) {
dest := make(map[string]any)
src := make(map[string]any)
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Empty(t, dest)
})
t.Run("nil maps", func(t *testing.T) {
var dest map[string]any
var src map[string]any
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Nil(t, dest)
})
t.Run("deep nested with append", func(t *testing.T) {
dest := map[string]any{
"level1": map[string]any{
"level2": map[string]any{
"level3": map[string]any{
"values": []any{"deep"},
},
},
},
}
src := map[string]any{
"level1": map[string]any{
"level2": map[string]any{
"level3": map[string]any{
"values+": []any{"nested"},
},
},
},
}
expected := map[string]any{
"level1": map[string]any{
"level2": map[string]any{
"level3": map[string]any{
"values": []any{"deep", "nested"},
},
},
},
}
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Equal(t, expected, dest)
})
t.Run("complex nested structure", func(t *testing.T) {
dest := map[string]any{
"config": map[string]any{
"services": []any{
map[string]any{"name": "service1"},
},
"settings": map[string]any{
"timeout": 30,
},
},
}
src := map[string]any{
"config": map[string]any{
"services+": []any{
map[string]any{"name": "service2"},
},
"settings": map[string]any{
"retries": 3,
},
},
}
expected := map[string]any{
"config": map[string]any{
"services": []any{
map[string]any{"name": "service1"},
map[string]any{"name": "service2"},
},
"settings": map[string]any{
"timeout": 30,
"retries": 3,
},
},
}
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Equal(t, expected, dest)
})
}
func TestAppendProcessor_TypeConversions(t *testing.T) {
processor := NewAppendProcessor()
t.Run("map[any]any to map[string]any conversion", func(t *testing.T) {
dest := map[string]any{
"values": []any{"a"},
}
src := map[any]any{
"values+": []any{"b"},
}
srcConverted := make(map[string]any)
for k, v := range src {
if ks, ok := k.(string); ok {
srcConverted[ks] = v
}
}
err := processor.MergeWithAppend(dest, srcConverted)
require.NoError(t, err)
assert.Equal(t, []any{"a", "b"}, dest["values"])
})
t.Run("mixed key types", func(t *testing.T) {
dest := map[string]any{
"values": []any{"a"},
}
src := map[any]any{
"values+": []any{"b"},
"other": "value",
42: "number_key",
}
srcConverted := make(map[string]any)
for k, v := range src {
if ks, ok := k.(string); ok {
srcConverted[ks] = v
}
}
err := processor.MergeWithAppend(dest, srcConverted)
require.NoError(t, err)
assert.Equal(t, []any{"a", "b"}, dest["values"])
assert.Equal(t, "value", dest["other"])
assert.NotContains(t, dest, "42")
})
}
func TestAppendProcessor_PropertyBased(t *testing.T) {
processor := NewAppendProcessor()
t.Run("idempotent regular merge", func(t *testing.T) {
dest := map[string]any{
"key1": "value1",
"key2": []any{"a", "b"},
}
src := map[string]any{
"key1": "value1",
"key3": "value3",
}
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
err = processor.MergeWithAppend(dest, src)
require.NoError(t, err)
expected := map[string]any{
"key1": "value1",
"key2": []any{"a", "b"},
"key3": "value3",
}
assert.Equal(t, expected, dest)
})
t.Run("append is not idempotent", func(t *testing.T) {
dest := map[string]any{
"values": []any{"a"},
}
src := map[string]any{
"values+": []any{"b"},
}
err := processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Equal(t, []any{"a", "b"}, dest["values"])
err = processor.MergeWithAppend(dest, src)
require.NoError(t, err)
assert.Equal(t, []any{"a", "b", "b"}, dest["values"])
})
t.Run("merge is not commutative", func(t *testing.T) {
map1 := map[string]any{"a": 1, "b": 2}
map2 := map[string]any{"b": 3, "c": 4}
result1 := make(map[string]any)
for k, v := range map1 {
result1[k] = v
}
err := processor.MergeWithAppend(result1, map2)
require.NoError(t, err)
result2 := make(map[string]any)
for k, v := range map2 {
result2[k] = v
}
err = processor.MergeWithAppend(result2, map1)
require.NoError(t, err)
assert.NotEqual(t, result1, result2)
expected1 := map[string]any{"a": 1, "b": 3, "c": 4}
expected2 := map[string]any{"a": 1, "b": 2, "c": 4}
assert.Equal(t, expected1, result1)
assert.Equal(t, expected2, result2)
})
}
func TestIsAppendKey(t *testing.T) {
tests := []struct {
key string
expected bool
}{
{"key+", true},
{"key", false},
{"key++", true},
{"+key", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
result := IsAppendKey(tt.key)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetBaseKey(t *testing.T) {
tests := []struct {
key string
expected string
}{
{"key+", "key"},
{"key", "key"},
{"key++", "key+"},
{"+key", "+key"},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
result := GetBaseKey(tt.key)
assert.Equal(t, tt.expected, result)
})
}
}