crowdsec/pkg/yamlpatch/merge_test.go

238 lines
6.9 KiB
Go

// Copyright (c) 2018 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package yamlpatch
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
yaml "gopkg.in/yaml.v2"
)
func trimcr(s string) string {
return strings.ReplaceAll(s, "\r\n", "\n")
}
func mustRead(t testing.TB, fname string) []byte {
contents, err := os.ReadFile(fname)
require.NoError(t, err, "failed to read file: %s", fname)
return contents
}
func dump(t testing.TB, actual, expected string) {
// It's impossible to debug YAML if the actual and expected values are
// printed on a single line.
t.Logf("Actual:\n\n%s\n\n", actual)
t.Logf("Expected:\n\n%s\n\n", expected)
}
func strip(s string) string {
// It's difficult to write string constants that are valid YAML. Normalize
// strings for ease of testing.
s = strings.TrimSpace(s)
s = strings.Replace(s, "\t", " ", -1)
return s
}
func canonicalize(t testing.TB, s string) string {
// round-trip to canonicalize formatting
var i interface{}
require.NoError(t,
yaml.Unmarshal([]byte(strip(s)), &i),
"canonicalize: couldn't unmarshal YAML",
)
formatted, err := yaml.Marshal(i)
require.NoError(t, err, "canonicalize: couldn't marshal YAML")
return string(bytes.TrimSpace(formatted))
}
func unmarshal(t testing.TB, s string) interface{} {
var i interface{}
require.NoError(t, yaml.Unmarshal([]byte(strip(s)), &i), "unmarshaling failed")
return i
}
func succeeds(t testing.TB, strict bool, left, right, expect string) {
l, r := unmarshal(t, left), unmarshal(t, right)
m, err := merge(l, r, strict)
require.NoError(t, err, "merge failed")
actualBytes, err := yaml.Marshal(m)
require.NoError(t, err, "couldn't marshal merged structure")
actual := canonicalize(t, string(actualBytes))
expect = canonicalize(t, expect)
if !assert.Equal(t, expect, actual) {
dump(t, actual, expect)
}
}
func fails(t testing.TB, strict bool, left, right string) {
_, err := merge(unmarshal(t, left), unmarshal(t, right), strict)
assert.Error(t, err, "merge succeeded")
}
func TestIntegration(t *testing.T) {
base := mustRead(t, "testdata/base.yaml")
prod := mustRead(t, "testdata/production.yaml")
expect := mustRead(t, "testdata/expect.yaml")
merged, err := YAML([][]byte{base, prod}, true /* strict */)
require.NoError(t, err, "merge failed")
if !assert.Equal(t, trimcr(string(expect)), merged.String(), "unexpected contents") {
dump(t, merged.String(), string(expect))
}
}
func TestEmpty(t *testing.T) {
full := []byte("foo: bar\n")
null := []byte("~")
tests := []struct {
desc string
sources [][]byte
expect string
}{
{"empty base", [][]byte{nil, full}, string(full)},
{"empty override", [][]byte{full, nil}, string(full)},
{"both empty", [][]byte{nil, nil}, ""},
{"null base", [][]byte{null, full}, string(full)},
{"null override", [][]byte{full, null}, "null\n"},
{"empty base and null override", [][]byte{nil, null}, "null\n"},
{"null base and empty override", [][]byte{null, nil}, "null\n"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
merged, err := YAML(tt.sources, true /* strict */)
require.NoError(t, err, "merge failed")
assert.Equal(t, tt.expect, merged.String(), "wrong contents after merge")
})
}
}
func TestSuccess(t *testing.T) {
left := `
fun: [maserati, porsche]
practical: {toyota: camry, honda: accord}
occupants:
honda: {driver: jane, backseat: [nate]}
`
right := `
fun: [lamborghini, porsche]
practical: {honda: civic, nissan: altima}
occupants:
honda: {passenger: arthur, backseat: [nora]}
`
expect := `
fun: [lamborghini, porsche]
practical: {toyota: camry, honda: civic, nissan: altima}
occupants:
honda: {passenger: arthur, driver: jane, backseat: [nora]}
`
succeeds(t, true, left, right, expect)
succeeds(t, false, left, right, expect)
}
func TestErrors(t *testing.T) {
check := func(t testing.TB, strict bool, sources ...[]byte) error {
_, err := YAML(sources, strict)
return err
}
t.Run("tabs in source", func(t *testing.T) {
src := []byte("foo:\n\tbar:baz")
assert.Error(t, check(t, false, src), "expected error in permissive mode")
assert.Error(t, check(t, true, src), "expected error in strict mode")
})
t.Run("duplicated keys", func(t *testing.T) {
src := []byte("{foo: bar, foo: baz}")
assert.NoError(t, check(t, false, src), "expected success in permissive mode")
assert.Error(t, check(t, true, src), "expected error in permissive mode")
})
t.Run("merge error", func(t *testing.T) {
left := []byte("foo: [1, 2]")
right := []byte("foo: {bar: baz}")
assert.NoError(t, check(t, false, left, right), "expected success in permissive mode")
assert.Error(t, check(t, true, left, right), "expected error in strict mode")
})
}
func TestMismatchedTypes(t *testing.T) {
tests := []struct {
desc string
left, right string
}{
{"sequence and mapping", "[one, two]", "{foo: bar}"},
{"sequence and scalar", "[one, two]", "foo"},
{"mapping and scalar", "{foo: bar}", "foo"},
{"nested", "{foo: [one, two]}", "{foo: bar}"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc+" strict", func(t *testing.T) {
fails(t, true, tt.left, tt.right)
})
t.Run(tt.desc+" permissive", func(t *testing.T) {
// prefer the higher-priority value
succeeds(t, false, tt.left, tt.right, tt.right)
})
}
}
func TestBooleans(t *testing.T) {
// YAML helpfully interprets many strings as Booleans.
tests := []struct {
in, out string
}{
{"yes", "true"},
{"YES", "true"},
{"on", "true"},
{"ON", "true"},
{"no", "false"},
{"NO", "false"},
{"off", "false"},
{"OFF", "false"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.in, func(t *testing.T) {
succeeds(t, true, "", tt.in, tt.out)
succeeds(t, false, "", tt.in, tt.out)
})
}
}
func TestExplicitNil(t *testing.T) {
base := `foo: {one: two}`
override := `foo: ~`
expect := `foo: ~`
succeeds(t, true, base, override, expect)
succeeds(t, false, base, override, expect)
}