blob: 84878588d11c100e52e72e1d7591ccec6e6fac64 [file] [log] [blame] [edit]
// 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)
}
}
}