// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package query import ( "errors" "fmt" "net/url" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" ) // test that Values(input) matches want. If not, report an error on t. func testValue(t *testing.T, input interface{}, want url.Values) { v, err := Values(input) if err != nil { t.Errorf("Values(%q) returned error: %v", input, err) } if diff := cmp.Diff(want, v); diff != "" { t.Errorf("Values(%#v) mismatch:\n%s", input, diff) } } func TestValues_BasicTypes(t *testing.T) { tests := []struct { input interface{} want url.Values }{ // zero values {struct{ V string }{}, url.Values{"V": {""}}}, {struct{ V int }{}, url.Values{"V": {"0"}}}, {struct{ V uint }{}, url.Values{"V": {"0"}}}, {struct{ V float32 }{}, url.Values{"V": {"0"}}}, {struct{ V bool }{}, url.Values{"V": {"false"}}}, // simple non-zero values {struct{ V string }{"v"}, url.Values{"V": {"v"}}}, {struct{ V int }{1}, url.Values{"V": {"1"}}}, {struct{ V uint }{1}, url.Values{"V": {"1"}}}, {struct{ V float32 }{0.1}, url.Values{"V": {"0.1"}}}, {struct{ V bool }{true}, url.Values{"V": {"true"}}}, // bool-specific options { struct { V bool `url:",int"` }{false}, url.Values{"V": {"0"}}, }, { struct { V bool `url:",int"` }{true}, url.Values{"V": {"1"}}, }, // time values { struct { V time.Time }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"2000-01-01T12:34:56Z"}}, }, { struct { V time.Time `url:",unix"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096"}}, }, { struct { V time.Time `url:",unixmilli"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096000"}}, }, { struct { V time.Time `url:",unixnano"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096000000000"}}, }, { struct { V time.Time `layout:"2006-01-02"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"2000-01-01"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_Pointers(t *testing.T) { str := "s" strPtr := &str tests := []struct { input interface{} want url.Values }{ // nil pointers (zero values) {struct{ V *string }{}, url.Values{"V": {""}}}, {struct{ V *int }{}, url.Values{"V": {""}}}, // non-zero pointer values {struct{ V *string }{&str}, url.Values{"V": {"s"}}}, {struct{ V **string }{&strPtr}, url.Values{"V": {"s"}}}, // slices of pointer values {struct{ V []*string }{}, url.Values{}}, {struct{ V []*string }{[]*string{&str, &str}}, url.Values{"V": {"s", "s"}}}, // pointer to slice {struct{ V *[]string }{}, url.Values{"V": {""}}}, {struct{ V *[]string }{&[]string{"a", "b"}}, url.Values{"V": {"a", "b"}}}, // pointer values for the input struct itself {(*struct{})(nil), url.Values{}}, {&struct{}{}, url.Values{}}, {&struct{ V string }{}, url.Values{"V": {""}}}, {&struct{ V string }{"v"}, url.Values{"V": {"v"}}}, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_Slices(t *testing.T) { tests := []struct { input interface{} want url.Values }{ // slices of strings { struct{ V []string }{}, url.Values{}, }, { struct{ V []string }{[]string{"a", "b"}}, url.Values{"V": {"a", "b"}}, }, { struct { V []string `url:",comma"` }{[]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V []string `url:",space"` }{[]string{"a", "b"}}, url.Values{"V": {"a b"}}, }, { struct { V []string `url:",semicolon"` }{[]string{"a", "b"}}, url.Values{"V": {"a;b"}}, }, { struct { V []string `url:",brackets"` }{[]string{"a", "b"}}, url.Values{"V[]": {"a", "b"}}, }, { struct { V []string `url:",numbered"` }{[]string{"a", "b"}}, url.Values{"V0": {"a"}, "V1": {"b"}}, }, // arrays of strings { struct{ V [2]string }{}, url.Values{"V": {"", ""}}, }, { struct{ V [2]string }{[2]string{"a", "b"}}, url.Values{"V": {"a", "b"}}, }, { struct { V [2]string `url:",comma"` }{[2]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V [2]string `url:",space"` }{[2]string{"a", "b"}}, url.Values{"V": {"a b"}}, }, { struct { V [2]string `url:",semicolon"` }{[2]string{"a", "b"}}, url.Values{"V": {"a;b"}}, }, { struct { V [2]string `url:",brackets"` }{[2]string{"a", "b"}}, url.Values{"V[]": {"a", "b"}}, }, { struct { V [2]string `url:",numbered"` }{[2]string{"a", "b"}}, url.Values{"V0": {"a"}, "V1": {"b"}}, }, // custom delimiters { struct { V []string `del:","` }{[]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V []string `del:"|"` }{[]string{"a", "b"}}, url.Values{"V": {"a|b"}}, }, { struct { V []string `del:"🥑"` }{[]string{"a", "b"}}, url.Values{"V": {"a🥑b"}}, }, // slice of bools with additional options { struct { V []bool `url:",space,int"` }{[]bool{true, false}}, url.Values{"V": {"1 0"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_NestedTypes(t *testing.T) { type SubNested struct { Value string `url:"value"` } type Nested struct { A SubNested `url:"a"` B *SubNested `url:"b"` Ptr *SubNested `url:"ptr,omitempty"` } tests := []struct { input interface{} want url.Values }{ { struct { Nest Nested `url:"nest"` }{ Nested{ A: SubNested{ Value: "v", }, }, }, url.Values{ "nest[a][value]": {"v"}, "nest[b]": {""}, }, }, { struct { Nest Nested `url:"nest"` }{ Nested{ Ptr: &SubNested{ Value: "v", }, }, }, url.Values{ "nest[a][value]": {""}, "nest[b]": {""}, "nest[ptr][value]": {"v"}, }, }, { nil, url.Values{}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_OmitEmpty(t *testing.T) { str := "" tests := []struct { input interface{} want url.Values }{ {struct{ v string }{}, url.Values{}}, // non-exported field { struct { V string `url:",omitempty"` }{}, url.Values{}, }, { struct { V string `url:"-"` }{}, url.Values{}, }, { struct { V string `url:"omitempty"` // actually named omitempty }{}, url.Values{"omitempty": {""}}, }, { // include value for a non-nil pointer to an empty value struct { V *string `url:",omitempty"` }{&str}, url.Values{"V": {""}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_EmbeddedStructs(t *testing.T) { type Inner struct { V string } type Outer struct { Inner } type OuterPtr struct { *Inner } type Mixed struct { Inner V string } type unexported struct { Inner V string } type Exported struct { unexported } tests := []struct { input interface{} want url.Values }{ { Outer{Inner{V: "a"}}, url.Values{"V": {"a"}}, }, { OuterPtr{&Inner{V: "a"}}, url.Values{"V": {"a"}}, }, { Mixed{Inner: Inner{V: "a"}, V: "b"}, url.Values{"V": {"b", "a"}}, }, { // values from unexported embed are still included Exported{ unexported{ Inner: Inner{V: "bar"}, V: "foo", }, }, url.Values{"V": {"foo", "bar"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_InvalidInput(t *testing.T) { _, err := Values("") if err == nil { t.Errorf("expected Values() to return an error on invalid input") } } // customEncodedStrings is a slice of strings with a custom URL encoding type customEncodedStrings []string // EncodeValues using key name of the form "{key}.N" where N increments with // each value. A value of "err" will return an error. func (m customEncodedStrings) EncodeValues(key string, v *url.Values) error { for i, arg := range m { if arg == "err" { return errors.New("encoding error") } v.Set(fmt.Sprintf("%s.%d", key, i), arg) } return nil } func TestValues_CustomEncodingSlice(t *testing.T) { tests := []struct { input interface{} want url.Values }{ { struct { V customEncodedStrings `url:"v"` }{}, url.Values{}, }, { struct { V customEncodedStrings `url:"v"` }{[]string{"a", "b"}}, url.Values{"v.0": {"a"}, "v.1": {"b"}}, }, // pointers to custom encoded types { struct { V *customEncodedStrings `url:"v"` }{}, url.Values{}, }, { struct { V *customEncodedStrings `url:"v"` }{(*customEncodedStrings)(&[]string{"a", "b"})}, url.Values{"v.0": {"a"}, "v.1": {"b"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } // One of the few ways reflectValues will return an error is if a custom // encoder returns an error. Test all of the various ways that can happen. func TestValues_CustomEncoding_Error(t *testing.T) { type st struct { V customEncodedStrings } tests := []struct { input interface{} }{ { st{[]string{"err"}}, }, { // struct field struct{ S st }{st{[]string{"err"}}}, }, { // embedded struct struct{ st }{st{[]string{"err"}}}, }, } for _, tt := range tests { _, err := Values(tt.input) if err == nil { t.Errorf("Values(%q) did not return expected encoding error", tt.input) } } } // customEncodedInt is an int with a custom URL encoding type customEncodedInt int // EncodeValues encodes values with leading underscores func (m customEncodedInt) EncodeValues(key string, v *url.Values) error { v.Set(key, fmt.Sprintf("_%d", m)) return nil } func TestValues_CustomEncodingInt(t *testing.T) { var zero customEncodedInt = 0 var one customEncodedInt = 1 tests := []struct { input interface{} want url.Values }{ { struct { V customEncodedInt `url:"v"` }{}, url.Values{"v": {"_0"}}, }, { struct { V customEncodedInt `url:"v,omitempty"` }{zero}, url.Values{}, }, { struct { V customEncodedInt `url:"v"` }{one}, url.Values{"v": {"_1"}}, }, // pointers to custom encoded types { struct { V *customEncodedInt `url:"v"` }{}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedInt `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V *customEncodedInt `url:"v,omitempty"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedInt `url:"v"` }{&one}, url.Values{"v": {"_1"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } // customEncodedInt is an int with a custom URL encoding defined on its pointer // value. type customEncodedIntPtr int // EncodeValues encodes a 0 as false, 1 as true, and nil as unknown. All other // values cause an error. func (m *customEncodedIntPtr) EncodeValues(key string, v *url.Values) error { if m == nil { v.Set(key, "undefined") } else { v.Set(key, fmt.Sprintf("_%d", *m)) } return nil } // Test behavior when encoding is defined for a pointer of a custom type. // Custom type should be able to encode values for nil pointers. func TestValues_CustomEncodingPointer(t *testing.T) { var zero customEncodedIntPtr = 0 var one customEncodedIntPtr = 1 tests := []struct { input interface{} want url.Values }{ // non-pointer values do not get the custom encoding because // they don't implement the encoder interface. { struct { V customEncodedIntPtr `url:"v"` }{}, url.Values{"v": {"0"}}, }, { struct { V customEncodedIntPtr `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V customEncodedIntPtr `url:"v"` }{one}, url.Values{"v": {"1"}}, }, // pointers to custom encoded types. { struct { V *customEncodedIntPtr `url:"v"` }{}, url.Values{"v": {"undefined"}}, }, { struct { V *customEncodedIntPtr `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V *customEncodedIntPtr `url:"v"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedIntPtr `url:"v,omitempty"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedIntPtr `url:"v"` }{&one}, url.Values{"v": {"_1"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestIsEmptyValue(t *testing.T) { str := "string" tests := []struct { value interface{} empty bool }{ // slices, arrays, and maps {[]int{}, true}, {[]int{0}, false}, {[0]int{}, true}, {[3]int{}, false}, {[3]int{1}, false}, {map[string]string{}, true}, {map[string]string{"a": "b"}, false}, // strings {"", true}, {" ", false}, {"a", false}, // bool {true, false}, {false, true}, // ints of various types {(int)(0), true}, {(int)(1), false}, {(int)(-1), false}, {(int8)(0), true}, {(int8)(1), false}, {(int8)(-1), false}, {(int16)(0), true}, {(int16)(1), false}, {(int16)(-1), false}, {(int32)(0), true}, {(int32)(1), false}, {(int32)(-1), false}, {(int64)(0), true}, {(int64)(1), false}, {(int64)(-1), false}, {(uint)(0), true}, {(uint)(1), false}, {(uint8)(0), true}, {(uint8)(1), false}, {(uint16)(0), true}, {(uint16)(1), false}, {(uint32)(0), true}, {(uint32)(1), false}, {(uint64)(0), true}, {(uint64)(1), false}, // floats {(float32)(0), true}, {(float32)(0.0), true}, {(float32)(0.1), false}, {(float64)(0), true}, {(float64)(0.0), true}, {(float64)(0.1), false}, // pointers {(*int)(nil), true}, {new([]int), false}, {&str, false}, // time {time.Time{}, true}, {time.Now(), false}, // unknown type - always false unless a nil pointer, which are always empty. {(*struct{ int })(nil), true}, {struct{ int }{}, false}, {struct{ int }{0}, false}, {struct{ int }{1}, false}, } for _, tt := range tests { got := isEmptyValue(reflect.ValueOf(tt.value)) want := tt.empty if got != want { t.Errorf("isEmptyValue(%v) returned %t; want %t", tt.value, got, want) } } } func TestParseTag(t *testing.T) { name, opts := parseTag("field,foobar,foo") if name != "field" { t.Fatalf("name = %q, want field", name) } for _, tt := range []struct { opt string want bool }{ {"foobar", true}, {"foo", true}, {"bar", false}, {"field", false}, } { if opts.Contains(tt.opt) != tt.want { t.Errorf("Contains(%q) = %v", tt.opt, !tt.want) } } }