Add API Framework Revel Source Files
[iec.git] / src / foundation / api / revel / binder_test.go
diff --git a/src/foundation/api/revel/binder_test.go b/src/foundation/api/revel/binder_test.go
new file mode 100644 (file)
index 0000000..5ffd5cd
--- /dev/null
@@ -0,0 +1,419 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "reflect"
+       "sort"
+       "strings"
+       "testing"
+       "time"
+)
+
+type A struct {
+       ID      int
+       Name    string
+       B       B
+       private int
+}
+
+type B struct {
+       Extra string
+}
+
+var (
+       ParamTestValues = map[string][]string{
+               "int":                            {"1"},
+               "int8":                           {"1"},
+               "int16":                          {"1"},
+               "int32":                          {"1"},
+               "int64":                          {"1"},
+               "uint":                           {"1"},
+               "uint8":                          {"1"},
+               "uint16":                         {"1"},
+               "uint32":                         {"1"},
+               "uint64":                         {"1"},
+               "float32":                        {"1.000000"},
+               "float64":                        {"1.000000"},
+               "str":                            {"hello"},
+               "bool-true":                      {"true"},
+               "bool-1":                         {"1"},
+               "bool-on":                        {"on"},
+               "bool-false":                     {"false"},
+               "bool-0":                         {"0"},
+               "bool-0.0":                       {"0.0"},
+               "bool-off":                       {"off"},
+               "bool-f":                         {"f"},
+               "date":                           {"1982-07-09"},
+               "datetime":                       {"1982-07-09 21:30"},
+               "customDate":                     {"07/09/1982"},
+               "arr[0]":                         {"1"},
+               "arr[1]":                         {"2"},
+               "arr[3]":                         {"3"},
+               "uarr[]":                         {"1", "2"},
+               "arruarr[0][]":                   {"1", "2"},
+               "arruarr[1][]":                   {"3", "4"},
+               "2darr[0][0]":                    {"0"},
+               "2darr[0][1]":                    {"1"},
+               "2darr[1][0]":                    {"10"},
+               "2darr[1][1]":                    {"11"},
+               "A.ID":                           {"123"},
+               "A.Name":                         {"rob"},
+               "B.ID":                           {"123"},
+               "B.Name":                         {"rob"},
+               "B.B.Extra":                      {"hello"},
+               "pB.ID":                          {"123"},
+               "pB.Name":                        {"rob"},
+               "pB.B.Extra":                     {"hello"},
+               "priv.private":                   {"123"},
+               "arrC[0].ID":                     {"5"},
+               "arrC[0].Name":                   {"rob"},
+               "arrC[0].B.Extra":                {"foo"},
+               "arrC[1].ID":                     {"8"},
+               "arrC[1].Name":                   {"bill"},
+               "m[a]":                           {"foo"},
+               "m[b]":                           {"bar"},
+               "m2[1]":                          {"foo"},
+               "m2[2]":                          {"bar"},
+               "m3[a]":                          {"1"},
+               "m3[b]":                          {"2"},
+               "m4[a].ID":                       {"1"},
+               "m4[a].Name":                     {"foo"},
+               "m4[b].ID":                       {"2"},
+               "m4[b].Name":                     {"bar"},
+               "mapWithAMuchLongerName[a].ID":   {"1"},
+               "mapWithAMuchLongerName[a].Name": {"foo"},
+               "mapWithAMuchLongerName[b].ID":   {"2"},
+               "mapWithAMuchLongerName[b].Name": {"bar"},
+               "invalidInt":                     {"xyz"},
+               "invalidInt2":                    {""},
+               "invalidBool":                    {"xyz"},
+               "invalidArr":                     {"xyz"},
+               "int8-overflow":                  {"1024"},
+               "uint8-overflow":                 {"1024"},
+       }
+
+       testDate     = time.Date(1982, time.July, 9, 0, 0, 0, 0, time.UTC)
+       testDatetime = time.Date(1982, time.July, 9, 21, 30, 0, 0, time.UTC)
+)
+
+var binderTestCases = map[string]interface{}{
+       "int":        1,
+       "int8":       int8(1),
+       "int16":      int16(1),
+       "int32":      int32(1),
+       "int64":      int64(1),
+       "uint":       1,
+       "uint8":      uint8(1),
+       "uint16":     uint16(1),
+       "uint32":     uint32(1),
+       "uint64":     uint64(1),
+       "float32":    float32(1.0),
+       "float64":    float64(1.0),
+       "str":        "hello",
+       "bool-true":  true,
+       "bool-1":     true,
+       "bool-on":    true,
+       "bool-false": false,
+       "bool-0":     false,
+       "bool-0.0":   false,
+       "bool-off":   false,
+       "bool-f":     false,
+       "date":       testDate,
+       "datetime":   testDatetime,
+       "customDate": testDate,
+       "arr":        []int{1, 2, 0, 3},
+       "uarr":       []int{1, 2},
+       "arruarr":    [][]int{{1, 2}, {3, 4}},
+       "2darr":      [][]int{{0, 1}, {10, 11}},
+       "A":          A{ID: 123, Name: "rob"},
+       "B":          A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "pB":         &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "arrC": []A{
+               {
+                       ID:   5,
+                       Name: "rob",
+                       B:    B{"foo"},
+               },
+               {
+                       ID:   8,
+                       Name: "bill",
+               },
+       },
+       "m":  map[string]string{"a": "foo", "b": "bar"},
+       "m2": map[int]string{1: "foo", 2: "bar"},
+       "m3": map[string]int{"a": 1, "b": 2},
+       "m4": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+       // NOTE: We also include a map with a longer name than the others since this has caused problems
+       // described in github issue #1285, resolved in pull request #1344. This test case should
+       // prevent regression.
+       "mapWithAMuchLongerName": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+       // TODO: Tests that use TypeBinders
+
+       // Invalid value tests (the result should always be the zero value for that type)
+       // The point of these is to ensure that invalid user input does not cause panics.
+       "invalidInt":     0,
+       "invalidInt2":    0,
+       "invalidBool":    true,
+       "invalidArr":     []int{},
+       "priv":           A{},
+       "int8-overflow":  int8(0),
+       "uint8-overflow": uint8(0),
+}
+
+// Types that files may be bound to, and a func that can read the content from
+// that type.
+// TODO: Is there any way to create a slice, given only the element Type?
+var fileBindings = []struct{ val, arrval, f interface{} }{
+       {(**os.File)(nil), []*os.File{}, ioutil.ReadAll},
+       {(*[]byte)(nil), [][]byte{}, func(b []byte) []byte { return b }},
+       {(*io.Reader)(nil), []io.Reader{}, ioutil.ReadAll},
+       {(*io.ReadSeeker)(nil), []io.ReadSeeker{}, ioutil.ReadAll},
+}
+
+func TestJsonBinder(t *testing.T) {
+       // create a structure to be populated
+       {
+               d, _ := json.Marshal(map[string]int{"a": 1})
+               params := &Params{JSON: d}
+               foo := struct{ A int }{}
+               c := NewTestController(nil, getMultipartRequest())
+
+               ParseParams(params, NewRequest(c.Request.In))
+               actual := Bind(params, "test", reflect.TypeOf(foo))
+               valEq(t, "TestJsonBinder", reflect.ValueOf(actual.Interface().(struct{ A int }).A), reflect.ValueOf(1))
+       }
+       {
+               d, _ := json.Marshal(map[string]interface{}{"a": map[string]int{"b": 45}})
+               params := &Params{JSON: d}
+               testMap := map[string]interface{}{}
+               actual := Bind(params, "test", reflect.TypeOf(testMap)).Interface().(map[string]interface{})
+               if actual["a"].(map[string]interface{})["b"].(float64) != 45 {
+                       t.Errorf("Failed to fetch map value %#v", actual["a"])
+               }
+               // Check to see if a named map works
+               actualb := Bind(params, "test", reflect.TypeOf(map[string]map[string]float64{})).Interface().(map[string]map[string]float64)
+               if actualb["a"]["b"] != 45 {
+                       t.Errorf("Failed to fetch map value %#v", actual["a"])
+               }
+
+       }
+}
+
+func TestBinder(t *testing.T) {
+       // Reuse the mvc_test.go multipart request to test the binder.
+       params := &Params{}
+       c := NewTestController(nil, getMultipartRequest())
+       ParseParams(params, NewRequest(c.Request.In))
+       params.Values = ParamTestValues
+
+       // Values
+       for k, v := range binderTestCases {
+               actual := Bind(params, k, reflect.TypeOf(v))
+               expected := reflect.ValueOf(v)
+               valEq(t, k, actual, expected)
+       }
+
+       // Files
+
+       // Get the keys in sorted order to make the expectation right.
+       keys := []string{}
+       for k := range expectedFiles {
+               keys = append(keys, k)
+       }
+       sort.Strings(keys)
+
+       expectedBoundFiles := make(map[string][]fh)
+       for _, k := range keys {
+               fhs := expectedFiles[k]
+               k := nextKey(k)
+               expectedBoundFiles[k] = append(expectedBoundFiles[k], fhs...)
+       }
+
+       for k, fhs := range expectedBoundFiles {
+
+               if len(fhs) == 1 {
+                       // Test binding single files to: *os.File, []byte, io.Reader, io.ReadSeeker
+                       for _, binding := range fileBindings {
+                               typ := reflect.TypeOf(binding.val).Elem()
+                               actual := Bind(params, k, typ)
+                               if !actual.IsValid() || (actual.Kind() == reflect.Interface && actual.IsNil()) {
+                                       t.Errorf("%s (%s) - Returned nil.", k, typ)
+                                       continue
+                               }
+                               returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual})
+                               valEq(t, k, returns[0], reflect.ValueOf(fhs[0].content))
+                       }
+
+               } else {
+                       // Test binding multi to:
+                       // []*os.File, [][]byte, []io.Reader, []io.ReadSeeker
+                       for _, binding := range fileBindings {
+                               typ := reflect.TypeOf(binding.arrval)
+                               actual := Bind(params, k, typ)
+                               if actual.Len() != len(fhs) {
+                                       t.Fatalf("%s (%s) - Number of files: (expected) %d != %d (actual)",
+                                               k, typ, len(fhs), actual.Len())
+                               }
+                               for i := range fhs {
+                                       returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual.Index(i)})
+                                       if !returns[0].IsValid() {
+                                               t.Errorf("%s (%s) - Returned nil.", k, typ)
+                                               continue
+                                       }
+                                       valEq(t, k, returns[0], reflect.ValueOf(fhs[i].content))
+                               }
+                       }
+               }
+       }
+}
+
+// Unbinding tests
+
+var unbinderTestCases = map[string]interface{}{
+       "int":        1,
+       "int8":       int8(1),
+       "int16":      int16(1),
+       "int32":      int32(1),
+       "int64":      int64(1),
+       "uint":       1,
+       "uint8":      uint8(1),
+       "uint16":     uint16(1),
+       "uint32":     uint32(1),
+       "uint64":     uint64(1),
+       "float32":    float32(1.0),
+       "float64":    float64(1.0),
+       "str":        "hello",
+       "bool-true":  true,
+       "bool-false": false,
+       "date":       testDate,
+       "datetime":   testDatetime,
+       "arr":        []int{1, 2, 0, 3},
+       "2darr":      [][]int{{0, 1}, {10, 11}},
+       "A":          A{ID: 123, Name: "rob"},
+       "B":          A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "pB":         &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "arrC": []A{
+               {
+                       ID:   5,
+                       Name: "rob",
+                       B:    B{"foo"},
+               },
+               {
+                       ID:   8,
+                       Name: "bill",
+               },
+       },
+       "m":  map[string]string{"a": "foo", "b": "bar"},
+       "m2": map[int]string{1: "foo", 2: "bar"},
+       "m3": map[string]int{"a": 1, "b": 2},
+}
+
+// Some of the unbinding results are not exactly what is in ParamTestValues, since it
+// serializes implicit zero values explicitly.
+var unbinderOverrideAnswers = map[string]map[string]string{
+       "arr": {
+               "arr[0]": "1",
+               "arr[1]": "2",
+               "arr[2]": "0",
+               "arr[3]": "3",
+       },
+       "A": {
+               "A.ID":      "123",
+               "A.Name":    "rob",
+               "A.B.Extra": "",
+       },
+       "arrC": {
+               "arrC[0].ID":      "5",
+               "arrC[0].Name":    "rob",
+               "arrC[0].B.Extra": "foo",
+               "arrC[1].ID":      "8",
+               "arrC[1].Name":    "bill",
+               "arrC[1].B.Extra": "",
+       },
+       "m":  {"m[a]": "foo", "m[b]": "bar"},
+       "m2": {"m2[1]": "foo", "m2[2]": "bar"},
+       "m3": {"m3[a]": "1", "m3[b]": "2"},
+}
+
+func TestUnbinder(t *testing.T) {
+       for k, v := range unbinderTestCases {
+               actual := make(map[string]string)
+               Unbind(actual, k, v)
+
+               // Get the expected key/values.
+               expected, ok := unbinderOverrideAnswers[k]
+               if !ok {
+                       expected = make(map[string]string)
+                       for k2, v2 := range ParamTestValues {
+                               if k == k2 || strings.HasPrefix(k2, k+".") || strings.HasPrefix(k2, k+"[") {
+                                       expected[k2] = v2[0]
+                               }
+                       }
+               }
+
+               // Compare length and values.
+               if len(actual) != len(expected) {
+                       t.Errorf("Length mismatch\nExpected length %d, actual %d\nExpected: %s\nActual: %s",
+                               len(expected), len(actual), expected, actual)
+               }
+               for k, v := range actual {
+                       if expected[k] != v {
+                               t.Errorf("Value mismatch.\nExpected: %s\nActual: %s", expected, actual)
+                       }
+               }
+       }
+}
+
+// Helpers
+
+func valEq(t *testing.T, name string, actual, expected reflect.Value) {
+       switch expected.Kind() {
+       case reflect.Slice:
+               // Check the type/length/element type
+               if !eq(t, name+" (type)", actual.Kind(), expected.Kind()) ||
+                       !eq(t, name+" (len)", actual.Len(), expected.Len()) ||
+                       !eq(t, name+" (elem)", actual.Type().Elem(), expected.Type().Elem()) {
+                       return
+               }
+
+               // Check value equality for each element.
+               for i := 0; i < actual.Len(); i++ {
+                       valEq(t, fmt.Sprintf("%s[%d]", name, i), actual.Index(i), expected.Index(i))
+               }
+
+       case reflect.Ptr:
+               // Check equality on the element type.
+               valEq(t, name, actual.Elem(), expected.Elem())
+       case reflect.Map:
+               if !eq(t, name+" (len)", actual.Len(), expected.Len()) {
+                       return
+               }
+               for _, key := range expected.MapKeys() {
+                       expectedValue := expected.MapIndex(key)
+                       actualValue := actual.MapIndex(key)
+                       if actualValue.IsValid() {
+                               valEq(t, fmt.Sprintf("%s[%s]", name, key), actualValue, expectedValue)
+                       } else {
+                               t.Errorf("Expected key %s not found", key)
+                       }
+               }
+       default:
+               eq(t, name, actual.Interface(), expected.Interface())
+       }
+}
+
+func init() {
+       DateFormat = DefaultDateFormat
+       DateTimeFormat = DefaultDateTimeFormat
+       TimeFormats = append(TimeFormats, DefaultDateFormat, DefaultDateTimeFormat, "01/02/2006")
+}