This allows a check function to be set to determine whether it is allowed to be set. This also ensures that if null is allowed it is either the only value or not set.
526 lines
12 KiB
Go
526 lines
12 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/boundary/internal/cmd/base"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPopulateAttrFlags tests common patterns we'll actually be using. Note
|
|
// that this is not an exhaustive test of the full CombinationSliceVar
|
|
// functionality, it's a bit higher level test based on what we'll actually need
|
|
// and what we'll actually have set.
|
|
func TestPopulateAttrFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
expected []base.CombinedSliceFlagValue
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "strings-only",
|
|
args: []string{"-string-attr", "foo=bar", "-string-attr", `bar="baz"`},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "string-attr",
|
|
Keys: []string{"foo"},
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: "string-attr",
|
|
Keys: []string{"bar"},
|
|
Value: `"baz"`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nums-only",
|
|
args: []string{"-num-attr", "foo=-1.2", "-num-attr", "bar=5"},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-attr",
|
|
Keys: []string{"foo"},
|
|
Value: "-1.2",
|
|
},
|
|
{
|
|
Name: "num-attr",
|
|
Keys: []string{"bar"},
|
|
Value: "5",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "bools-only",
|
|
args: []string{"-bool-attr", "foo=true", "-bool-attr", "bar=false"},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "bool-attr",
|
|
Keys: []string{"foo"},
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "bool-attr",
|
|
Keys: []string{"bar"},
|
|
Value: "false",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "mixed",
|
|
args: []string{"-num-attr", "foo=9820", "-string-attr", "bar=9820", "-attr", "baz=9820"},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-attr",
|
|
Keys: []string{"foo"},
|
|
Value: "9820",
|
|
},
|
|
{
|
|
Name: "string-attr",
|
|
Keys: []string{"bar"},
|
|
Value: "9820",
|
|
},
|
|
{
|
|
Name: "attr",
|
|
Keys: []string{"baz"},
|
|
Value: "9820",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "mixed-segments",
|
|
args: []string{"-num-attr", "foo.bar.baz=9820", "-string-attr", "bar.baz.foo=9820", "-attr", "baz.foo.bar=9820"},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-attr",
|
|
Keys: []string{"foo", "bar", "baz"},
|
|
Value: "9820",
|
|
},
|
|
{
|
|
Name: "string-attr",
|
|
Keys: []string{"bar", "baz", "foo"},
|
|
Value: "9820",
|
|
},
|
|
{
|
|
Name: "attr",
|
|
Keys: []string{"baz", "foo", "bar"},
|
|
Value: "9820",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "bad-key-name",
|
|
args: []string{"-num-attr", "fo-oo=5"},
|
|
expectedErr: "invalid value",
|
|
},
|
|
{
|
|
name: "bad-key-name-in-segment",
|
|
args: []string{"-num-attr", "fo.oo-o.o=5"},
|
|
expectedErr: "invalid value",
|
|
},
|
|
{
|
|
name: "colon-in-segment",
|
|
args: []string{"-attr", "filter=tagName eq 'application:south-seas'"},
|
|
expected: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "attr",
|
|
Keys: []string{"filter"},
|
|
Value: "tagName eq 'application:south-seas'",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
// Note: we do the setup on each run to make sure we aren't carrying
|
|
// state over; just like in the real CLI where each run would have
|
|
// pristine state.
|
|
c := new(base.Command)
|
|
flagSet := c.FlagSet(base.FlagSetNone)
|
|
f := flagSet.NewFlagSet("Attribute Options")
|
|
cmd := "create"
|
|
flagNames := map[string][]string{
|
|
cmd: {
|
|
"attributes",
|
|
"attr",
|
|
"string-attr",
|
|
"bool-attr",
|
|
"num-attr",
|
|
},
|
|
}
|
|
|
|
PopulateAttributeFlags(c, f, flagNames, cmd)
|
|
err := flagSet.Parse(tt.args)
|
|
if tt.expectedErr != "" {
|
|
require.Error(err)
|
|
assert.Contains(err.Error(), tt.expectedErr)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
assert.Equal(tt.expected, c.FlagAttrs)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHandleAttributeFlags tests the function that parses types based on
|
|
// incoming data. It assumes we're coming in with CombinedSliceFlagValues and
|
|
// validates what comes out -- whether nil func was called or the map function
|
|
// was called (and its contents).
|
|
func TestHandleAttributeFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []base.CombinedSliceFlagValue
|
|
expectedMap map[string]interface{}
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "strings-only",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "string-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: "string-%s",
|
|
Keys: []string{"bar"},
|
|
Value: `"baz"`,
|
|
},
|
|
},
|
|
expectedMap: map[string]interface{}{
|
|
"foo": "bar",
|
|
"bar": "baz",
|
|
},
|
|
},
|
|
{
|
|
name: "nums-only",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "-1.2",
|
|
},
|
|
{
|
|
Name: "num-%s",
|
|
Keys: []string{"bar"},
|
|
Value: "5",
|
|
},
|
|
},
|
|
expectedMap: map[string]interface{}{
|
|
"foo": float64(-1.2),
|
|
"bar": int64(5),
|
|
},
|
|
},
|
|
{
|
|
name: "bad-float-num",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "-15d.2",
|
|
},
|
|
},
|
|
expectedErr: "as a float",
|
|
},
|
|
{
|
|
name: "bad-int-num",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "num-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "-15d3",
|
|
},
|
|
},
|
|
expectedErr: "as an int",
|
|
},
|
|
{
|
|
name: "bools-only",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "bool-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "bool-%s",
|
|
Keys: []string{"bar"},
|
|
Value: "false",
|
|
},
|
|
},
|
|
expectedMap: map[string]interface{}{
|
|
"foo": true,
|
|
"bar": false,
|
|
},
|
|
},
|
|
{
|
|
name: "bad-bool",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "bool-%s",
|
|
Keys: []string{"foo"},
|
|
Value: "t",
|
|
},
|
|
},
|
|
expectedErr: "as a bool",
|
|
},
|
|
{
|
|
name: "attr-only",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"b1"},
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"b2"},
|
|
Value: "false",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"s1"},
|
|
Value: "scoopde",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"s2"},
|
|
Value: `"woop"`,
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"n1"},
|
|
Value: "-1.2",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"n2"},
|
|
Value: "5",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"a"},
|
|
Value: `["foo", 1.5, true, ["bar"], {"hip": "hop"}]`,
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"nil"},
|
|
Value: "null",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"m"},
|
|
Value: `{"b": true, "n": 6, "s": "scoopde", "a": ["bar"], "m": {"hip": "hop"}}`,
|
|
},
|
|
},
|
|
expectedMap: map[string]interface{}{
|
|
"b1": true,
|
|
"b2": false,
|
|
"s1": "scoopde",
|
|
"s2": "woop",
|
|
"n1": float64(-1.2),
|
|
"n2": int64(5),
|
|
"a": []interface{}{
|
|
"foo",
|
|
json.Number("1.5"),
|
|
true,
|
|
[]interface{}{"bar"},
|
|
map[string]interface{}{"hip": "hop"},
|
|
},
|
|
"m": map[string]interface{}{
|
|
"b": true,
|
|
"n": json.Number("6"),
|
|
"s": "scoopde",
|
|
"a": []interface{}{"bar"},
|
|
"m": map[string]interface{}{"hip": "hop"},
|
|
},
|
|
"nil": nil,
|
|
},
|
|
},
|
|
{
|
|
name: "map-array-structure",
|
|
args: []base.CombinedSliceFlagValue{
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"bools"},
|
|
Value: "true",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"bools"},
|
|
Value: "false",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"strings", "s1"},
|
|
Value: "scoopde",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"strings", "s2"},
|
|
Value: `"woop"`,
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"numbers", "reps"},
|
|
Value: "-1.2",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"numbers", "reps"},
|
|
Value: "5",
|
|
},
|
|
{
|
|
Name: "%s",
|
|
Keys: []string{"strings", "s2"}, // This will overwrite above!
|
|
Value: "null",
|
|
},
|
|
},
|
|
expectedMap: map[string]interface{}{
|
|
"bools": []interface{}{true, false},
|
|
"strings": map[string]interface{}{
|
|
"s1": "scoopde",
|
|
"s2": nil,
|
|
},
|
|
"numbers": map[string]interface{}{
|
|
"reps": []interface{}{float64(-1.2), int64(5)},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
for _, typ := range []string{"attr", "secret"} {
|
|
t.Run(fmt.Sprintf("%s-%s", tt.name, typ), func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
// Note: we do the setup on each run to make sure we aren't carrying
|
|
// state over; just like in the real CLI where each run would have
|
|
// pristine state.
|
|
c := new(base.Command)
|
|
var outMap map[string]interface{}
|
|
|
|
args := make([]base.CombinedSliceFlagValue, 0, len(tt.args))
|
|
for _, arg := range tt.args {
|
|
arg.Name = fmt.Sprintf(arg.Name, typ)
|
|
args = append(args, arg)
|
|
}
|
|
|
|
err := HandleAttributeFlags(c, typ, "", args, func() {}, func(in map[string]interface{}) { outMap = in })
|
|
if tt.expectedErr != "" {
|
|
require.Error(err)
|
|
assert.Contains(err.Error(), tt.expectedErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(err)
|
|
assert.Equal(tt.expectedMap, outMap)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNullableStringSlice(t *testing.T) {
|
|
makeStringSlicePointer := func(in ...string) *[]string {
|
|
return &in
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
cmd string
|
|
args []string
|
|
expected base.StringSliceVar
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "not-set-no-null",
|
|
cmd: "add-values",
|
|
args: []string{"-val", "foobar", "-val", "barfoo", "-val", "boobaz", "-val", "bazboo"},
|
|
expected: base.StringSliceVar{
|
|
Target: makeStringSlicePointer("foobar", "barfoo", "boobaz", "bazboo"),
|
|
},
|
|
},
|
|
{
|
|
name: "not-set-null",
|
|
cmd: "add-values",
|
|
args: []string{"-val", "foobar", "-val", "null", "-val", "boobaz", "-val", "bazboo"},
|
|
expectedErr: `"null" is not an allowed value`,
|
|
},
|
|
{
|
|
name: "set-no-null",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "foobar", "-val", "barfoo", "-val", "boobaz", "-val", "bazboo"},
|
|
expected: base.StringSliceVar{
|
|
Target: makeStringSlicePointer("foobar", "barfoo", "boobaz", "bazboo"),
|
|
},
|
|
},
|
|
{
|
|
name: "set-only-null",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "null"},
|
|
expected: base.StringSliceVar{
|
|
Target: makeStringSlicePointer("null"),
|
|
},
|
|
},
|
|
{
|
|
name: "set-null-and-others-beginning",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "null", "-val", "barfoo", "-val", "boobaz", "-val", "bazboo"},
|
|
expectedErr: `"null" cannot be combined with other values`,
|
|
},
|
|
{
|
|
name: "set-null-and-others-middle",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "foobar", "-val", "null", "-val", "boobaz", "-val", "bazboo"},
|
|
expectedErr: `"null" cannot be combined with other values`,
|
|
},
|
|
{
|
|
name: "set-null-and-others-end",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "foobar", "-val", "barfoo", "-val", "boobaz", "-val", "null"},
|
|
expectedErr: `"null" cannot be combined with other values`,
|
|
},
|
|
{
|
|
name: "set-null-multiple",
|
|
cmd: "set-values",
|
|
args: []string{"-val", "null", "-val", "null"},
|
|
expected: base.StringSliceVar{
|
|
Target: makeStringSlicePointer("null"),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
// Note: we do the setup on each run to make sure we aren't carrying
|
|
// state over; just like in the real CLI where each run would have
|
|
// pristine state.
|
|
c := new(base.Command)
|
|
flagSet := c.FlagSet(base.FlagSetNone)
|
|
f := flagSet.NewFlagSet("Stringsssss")
|
|
var target []string
|
|
ssVar := &base.StringSliceVar{
|
|
Name: "val",
|
|
Target: &target,
|
|
NullCheck: func() bool {
|
|
return strings.HasPrefix(tt.cmd, "set-")
|
|
},
|
|
}
|
|
f.StringSliceVar(ssVar)
|
|
|
|
err := flagSet.Parse(tt.args)
|
|
if tt.expectedErr != "" {
|
|
require.Error(err)
|
|
assert.Contains(err.Error(), tt.expectedErr)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
assert.Equal(*tt.expected.Target, *ssVar.Target)
|
|
})
|
|
}
|
|
}
|