Files
threshold/internal/libs/patchstruct/patchstruct_test.go
T
Jeff Mitchell 1c39691023 Fix setting host catalog attributes to nil (#1743)
Internally attributes are not allowed to be the null value. However, as
currently implemented, this then breaks our normal model of sending an
explicit `null` in the JSON to reset/clear a field. This is a problem
because the CLI doesn't have a way to say "set this but to empty"
without a lot of hoops, and causes some issues for API users and TF as
well.

This changes PatchBytes and PatchStruct to return an empty value (or
marshaled empty value in the case of PatchBytes) if the source is null
or empty. In the host catalog update function we then always set this
value if it's in the db mask (removing the nil check). Together this
means a null value ends up coming out as an empty value (and thus valid
for the DB) from PatchStruct/PatchBytes, and our API translation
function already returns nothing for attributes if the length of the
fields are zero.
2021-11-26 16:58:19 -05:00

230 lines
4.3 KiB
Go

package patchstruct
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/internal/host"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
type testCase struct {
name string
dst map[string]interface{}
src map[string]interface{}
expected map[string]interface{}
}
var testCases = []testCase{
{
name: "merge",
dst: map[string]interface{}{
"foo": "bar",
},
src: map[string]interface{}{
"baz": "qux",
},
expected: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
},
{
name: "overwrite",
dst: map[string]interface{}{
"foo": "bar",
},
src: map[string]interface{}{
"foo": "baz",
},
expected: map[string]interface{}{
"foo": "baz",
},
},
{
name: "delete",
dst: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
src: map[string]interface{}{
"baz": nil,
},
expected: map[string]interface{}{
"foo": "bar",
},
},
{
name: "recursive",
dst: map[string]interface{}{
"nested": map[string]interface{}{
"a": "b",
},
"foo": "bar",
},
src: map[string]interface{}{
"nested": map[string]interface{}{
"c": "d",
},
},
expected: map[string]interface{}{
"nested": map[string]interface{}{
"a": "b",
"c": "d",
},
"foo": "bar",
},
},
{
name: "nested with nil",
dst: nil,
src: map[string]interface{}{
"a": "b",
"nested": map[string]interface{}{
"c": "d",
"e": nil,
},
"f": nil,
},
expected: map[string]interface{}{
"a": "b",
"nested": map[string]interface{}{
"c": "d",
},
},
},
{
name: "overwrite with map in src",
dst: map[string]interface{}{
"foo": "bar",
},
src: map[string]interface{}{
"foo": map[string]interface{}{
"a": "b",
},
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"a": "b",
},
},
},
{
name: "nil src",
dst: map[string]interface{}{
"foo": "bar",
},
src: nil,
expected: nil,
},
{
name: "nil dst",
dst: nil,
src: map[string]interface{}{
"foo": "bar",
},
expected: map[string]interface{}{
"foo": "bar",
},
},
{
name: "nil dst with src nil value",
dst: nil,
src: map[string]interface{}{
"foo": "bar",
"fiz": nil,
},
expected: map[string]interface{}{
"foo": "bar",
},
},
}
func TestPatchStruct(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
dst, src := mustStruct(tc.dst), mustStruct(tc.src)
dstOrig, srcOrig := mustStruct(tc.dst), mustStruct(tc.src)
if tc.dst == nil {
dst = nil
dstOrig = nil
}
if tc.src == nil {
src = nil
srcOrig = nil
}
actual := PatchStruct(dst, src)
require.Empty(cmp.Diff(mustStruct(tc.expected), actual,
cmpopts.IgnoreUnexported(structpb.Struct{}, structpb.Value{}),
cmpopts.SortSlices(func(x, y interface{}) bool {
return x.(*host.IpAddress).Address < y.(*host.IpAddress).Address
})))
require.Equal(dstOrig, dst)
require.Equal(srcOrig, src)
})
}
}
func TestPatchBytes(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
dst, src := mustMarshal(tc.dst), mustMarshal(tc.src)
actual, err := PatchBytes(dst, src)
require.NoError(err)
requireEqualEncoded(t, mustMarshal(tc.expected), actual)
})
}
}
func TestPatchBytesErr(t *testing.T) {
t.Run("src", func(t *testing.T) {
require := require.New(t)
_, err := PatchBytes(nil, []byte("foo"))
require.ErrorIs(err, proto.Error)
require.True(strings.HasPrefix(err.Error(), "error reading source data: "))
})
}
func mustStruct(in map[string]interface{}) *structpb.Struct {
out, err := structpb.NewStruct(in)
if err != nil {
panic(err)
}
return out
}
func mustMarshal(in map[string]interface{}) []byte {
b, err := proto.Marshal(mustStruct(in))
if err != nil {
panic(err)
}
return b
}
func requireEqualEncoded(t *testing.T, expected, actual []byte) {
t.Helper()
require := require.New(t)
expectedpb, actualpb := new(structpb.Struct), new(structpb.Struct)
err := proto.Unmarshal(expected, expectedpb)
require.NoError(err)
err = proto.Unmarshal(actual, actualpb)
require.NoError(err)
require.Equal(expectedpb, actualpb)
}