test and fix internal/reqdata #8

Manually merged
xdrm-brackets merged 21 commits from test/internal/reqdata into 0.2.0 2020-03-02 21:52:08 +00:00
5 changed files with 672 additions and 28 deletions

View File

@ -4,8 +4,19 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"git.xdrm.io/go/aicra/internal/cerr"
) )
// ErrUnknownType is returned when encountering an unknown type
const ErrUnknownType = cerr.Error("unknown type")
// ErrInvalidJSON is returned when json parse failed
const ErrInvalidJSON = cerr.Error("invalid json")
// ErrInvalidRootType is returned when json is a map
const ErrInvalidRootType = cerr.Error("invalid json root type")
// Parameter represents an http request parameter // Parameter represents an http request parameter
// that can be of type URL, GET, or FORM (multipart, json, urlencoded) // that can be of type URL, GET, or FORM (multipart, json, urlencoded)
type Parameter struct { type Parameter struct {
@ -22,16 +33,23 @@ type Parameter struct {
} }
// Parse parameter (json-like) if not already done // Parse parameter (json-like) if not already done
func (i *Parameter) Parse() { func (i *Parameter) Parse() error {
/* (1) Stop if already parsed or nil*/ /* (1) Stop if already parsed or nil*/
if i.Parsed || i.Value == nil { if i.Parsed || i.Value == nil {
return return nil
} }
/* (2) Try to parse value */ /* (2) Try to parse value */
i.Value = parseParameter(i.Value) parsed, err := parseParameter(i.Value)
if err != nil {
return err
}
i.Parsed = true
i.Value = parsed
return nil
} }
// parseParameter parses http GET/POST data // parseParameter parses http GET/POST data
@ -39,7 +57,7 @@ func (i *Parameter) Parse() {
// - size = 1 : return json of first element // - size = 1 : return json of first element
// - size > 1 : return array of json elements // - size > 1 : return array of json elements
// - string : return json if valid, else return raw string // - string : return json if valid, else return raw string
func parseParameter(data interface{}) interface{} { func parseParameter(data interface{}) (interface{}, error) {
dtype := reflect.TypeOf(data) dtype := reflect.TypeOf(data)
dvalue := reflect.ValueOf(data) dvalue := reflect.ValueOf(data)
@ -48,23 +66,12 @@ func parseParameter(data interface{}) interface{} {
/* (1) []string -> recursive */ /* (1) []string -> recursive */
case reflect.Slice: case reflect.Slice:
// 1. Return nothing if empty // 1. ignore empty
if dvalue.Len() == 0 { if dvalue.Len() == 0 {
return nil return data, nil
} }
// 2. only return first element if alone // 2. parse each element recursively
if dvalue.Len() == 1 {
element := dvalue.Index(0)
if element.Kind() != reflect.String {
return nil
}
return parseParameter(element.String())
}
// 3. Return all elements if more than 1
result := make([]interface{}, dvalue.Len()) result := make([]interface{}, dvalue.Len())
for i, l := 0, dvalue.Len(); i < l; i++ { for i, l := 0, dvalue.Len(); i < l; i++ {
@ -72,12 +79,17 @@ func parseParameter(data interface{}) interface{} {
// ignore non-string // ignore non-string
if element.Kind() != reflect.String { if element.Kind() != reflect.String {
result[i] = element.Interface()
continue continue
} }
result[i] = parseParameter(element.String()) parsed, err := parseParameter(element.String())
if err != nil {
return data, err
} }
return result result[i] = parsed
}
return result, nil
/* (2) string -> parse */ /* (2) string -> parse */
case reflect.String: case reflect.String:
@ -94,23 +106,23 @@ func parseParameter(data interface{}) interface{} {
mapval, ok := result.(map[string]interface{}) mapval, ok := result.(map[string]interface{})
if !ok { if !ok {
return dvalue.String() return dvalue.String(), ErrInvalidRootType
} }
wrapped, ok := mapval["wrapped"] wrapped, ok := mapval["wrapped"]
if !ok { if !ok {
return dvalue.String() return dvalue.String(), ErrInvalidJSON
} }
return wrapped return wrapped, nil
} }
// else return as string // else return as string
return dvalue.String() return dvalue.String(), nil
} }
/* (3) NIL if unknown type */ /* (3) NIL if unknown type */
return dvalue return dvalue.Interface(), nil
} }

View File

@ -0,0 +1,358 @@
package reqdata
import (
"math"
"testing"
)
func TestSimpleString(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: "some-string"}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(string)
if !canCast {
t.Errorf("expected parameter to be a string")
t.FailNow()
}
if cast != "some-string" {
t.Errorf("expected parameter to equal 'some-string', got '%s'", cast)
t.FailNow()
}
}
func TestSimpleFloat(t *testing.T) {
tcases := []float64{12.3456789, -12.3456789, 0.0000001, -0.0000001}
for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase}
if err := p.Parse(); err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(float64)
if !canCast {
t.Errorf("expected parameter to be a float64")
t.FailNow()
}
if math.Abs(cast-tcase) > 0.00000001 {
t.Errorf("expected parameter to equal '%f', got '%f'", tcase, cast)
t.FailNow()
}
})
}
}
func TestSimpleBool(t *testing.T) {
tcases := []bool{true, false}
for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase}
if err := p.Parse(); err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(bool)
if !canCast {
t.Errorf("expected parameter to be a bool")
t.FailNow()
}
if cast != tcase {
t.Errorf("expected parameter to equal '%t', got '%t'", tcase, cast)
t.FailNow()
}
})
}
}
func TestJsonStringSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast {
t.Errorf("expected parameter to be a []interface{}")
t.FailNow()
}
if len(slice) != 2 {
t.Errorf("expected 2 values, got %d", len(slice))
t.FailNow()
}
results := []string{"str1", "str2"}
for i, res := range results {
cast, canCast := slice[i].(string)
if !canCast {
t.Errorf("expected parameter %d to be a []string", i)
continue
}
if cast != res {
t.Errorf("expected first value to be '%s', got '%s'", res, cast)
continue
}
}
}
func TestStringSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []string{"str1", "str2"}}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast {
t.Errorf("expected parameter to be a []interface{}")
t.FailNow()
}
if len(slice) != 2 {
t.Errorf("expected 2 values, got %d", len(slice))
t.FailNow()
}
results := []string{"str1", "str2"}
for i, res := range results {
cast, canCast := slice[i].(string)
if !canCast {
t.Errorf("expected parameter %d to be a []string", i)
continue
}
if cast != res {
t.Errorf("expected first value to be '%s', got '%s'", res, cast)
continue
}
}
}
func TestJsonPrimitiveBool(t *testing.T) {
tcases := []struct {
Raw string
BoolValue bool
}{
{"true", true},
{"false", false},
}
for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(bool)
if !canCast {
t.Errorf("expected parameter to be a bool")
t.FailNow()
}
if cast != tcase.BoolValue {
t.Errorf("expected a value of %t, got %t", tcase.BoolValue, cast)
t.FailNow()
}
})
}
}
func TestJsonPrimitiveFloat(t *testing.T) {
tcases := []struct {
Raw string
FloatValue float64
}{
{"1", 1},
{"-1", -1},
{"0.001", 0.001},
{"-0.001", -0.001},
{"1.9992", 1.9992},
{"-1.9992", -1.9992},
{"19992", 19992},
{"-19992", -19992},
}
for i, tcase := range tcases {
t.Run("case "+string(i), func(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
cast, canCast := p.Value.(float64)
if !canCast {
t.Errorf("expected parameter to be a float64")
t.FailNow()
}
if math.Abs(cast-tcase.FloatValue) > 0.00001 {
t.Errorf("expected a value of %f, got %f", tcase.FloatValue, cast)
t.FailNow()
}
})
}
}
func TestJsonBoolSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []string{"true", "false"}}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast {
t.Errorf("expected parameter to be a []interface{}")
t.FailNow()
}
if len(slice) != 2 {
t.Errorf("expected 2 values, got %d", len(slice))
t.FailNow()
}
results := []bool{true, false}
for i, res := range results {
cast, canCast := slice[i].(bool)
if !canCast {
t.Errorf("expected parameter %d to be a []bool", i)
continue
}
if cast != res {
t.Errorf("expected first value to be '%t', got '%t'", res, cast)
continue
}
}
}
func TestBoolSlice(t *testing.T) {
p := Parameter{Parsed: false, File: false, Value: []bool{true, false}}
err := p.Parse()
if err != nil {
t.Errorf("unexpected error: <%s>", err)
t.FailNow()
}
if !p.Parsed {
t.Errorf("expected parameter to be parsed")
t.FailNow()
}
slice, canCast := p.Value.([]interface{})
if !canCast {
t.Errorf("expected parameter to be a []interface{}")
t.FailNow()
}
if len(slice) != 2 {
t.Errorf("expected 2 values, got %d", len(slice))
t.FailNow()
}
results := []bool{true, false}
for i, res := range results {
cast, canCast := slice[i].(bool)
if !canCast {
t.Errorf("expected parameter %d to be a bool, got %v", i, slice[i])
continue
}
if cast != res {
t.Errorf("expected first value to be '%t', got '%t'", res, cast)
continue
}
}
}

View File

@ -1,11 +1,12 @@
package reqdata package reqdata
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"reflect" "reflect"
"strings"
"testing" "testing"
) )
@ -222,7 +223,28 @@ func TestStoreWithGet(t *testing.T) {
} }
} }
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
// http.Request.ParseForm() fails when:
// - http.Request.Method is one of [POST,PUT,PATCH]
// - http.Request.Form is not nil (created manually)
// - http.Request.PostForm is nil (deleted manually)
// - http.Request.Body is nil (deleted manually)
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// break everything
req.Body = nil
req.Form = make(url.Values)
req.PostForm = nil
// defer req.Body.Close()
store := New(nil, req)
if len(store.Form) > 0 {
t.Errorf("expected malformed urlencoded to have failed being parsed (got %d elements)", len(store.Form))
t.FailNow()
}
}
func TestStoreWithUrlEncodedForm(t *testing.T) { func TestStoreWithUrlEncodedForm(t *testing.T) {
tests := []struct { tests := []struct {
URLEncoded string URLEncoded string
@ -301,7 +323,7 @@ func TestStoreWithUrlEncodedForm(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
body := bytes.NewBufferString(test.URLEncoded) body := strings.NewReader(test.URLEncoded)
req := httptest.NewRequest(http.MethodPost, "http://host.com", body) req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
defer req.Body.Close() defer req.Body.Close()
@ -474,7 +496,7 @@ func TestJsonParameters(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
body := bytes.NewBufferString(test.RawJson) body := strings.NewReader(test.RawJson)
req := httptest.NewRequest(http.MethodPost, "http://host.com", body) req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
defer req.Body.Close() defer req.Body.Close()
@ -545,3 +567,238 @@ func TestJsonParameters(t *testing.T) {
} }
} }
func TestMultipartParameters(t *testing.T) {
tests := []struct {
RawMultipart string
InvalidNames []string
ParamNames []string
ParamValues []interface{}
}{
// no need to fully check json because it is parsed with the standard library
{
RawMultipart: ``,
InvalidNames: []string{},
ParamNames: []string{},
ParamValues: []interface{}{},
},
{
RawMultipart: `--xxx
`,
InvalidNames: []string{},
ParamNames: []string{},
ParamValues: []interface{}{},
},
{
RawMultipart: `--xxx
--xxx--`,
InvalidNames: []string{},
ParamNames: []string{},
ParamValues: []interface{}{},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx--`,
InvalidNames: []string{},
ParamNames: []string{"a"},
ParamValues: []interface{}{"b"},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx
Content-Disposition: form-data; name="c"
d
--xxx--`,
InvalidNames: []string{},
ParamNames: []string{"a", "c"},
ParamValues: []interface{}{"b", "d"},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="_invalid"
x
--xxx--`,
InvalidNames: []string{"_invalid"},
ParamNames: []string{"_invalid"},
ParamValues: []interface{}{nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx
Content-Disposition: form-data; name="_invalid"
x
--xxx--`,
InvalidNames: []string{"_invalid"},
ParamNames: []string{"a", "_invalid"},
ParamValues: []interface{}{"b", nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="invalid_"
x
--xxx--`,
InvalidNames: []string{"invalid_"},
ParamNames: []string{"invalid_"},
ParamValues: []interface{}{nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx
Content-Disposition: form-data; name="invalid_"
x
--xxx--`,
InvalidNames: []string{"invalid_"},
ParamNames: []string{"a", "invalid_"},
ParamValues: []interface{}{"b", nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="GET@injection"
x
--xxx--`,
InvalidNames: []string{"GET@injection"},
ParamNames: []string{"GET@injection"},
ParamValues: []interface{}{nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx
Content-Disposition: form-data; name="GET@injection"
x
--xxx--`,
InvalidNames: []string{"GET@injection"},
ParamNames: []string{"a", "GET@injection"},
ParamValues: []interface{}{"b", nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="URL#injection"
x
--xxx--`,
InvalidNames: []string{"URL#injection"},
ParamNames: []string{"URL#injection"},
ParamValues: []interface{}{nil},
},
{
RawMultipart: `--xxx
Content-Disposition: form-data; name="a"
b
--xxx
Content-Disposition: form-data; name="URL#injection"
x
--xxx--`,
InvalidNames: []string{"URL#injection"},
ParamNames: []string{"a", "URL#injection"},
ParamValues: []interface{}{"b", nil},
},
// json parse error
{
RawMultipart: "{ \"a\": \"b\", }",
InvalidNames: []string{},
ParamNames: []string{},
ParamValues: []interface{}{},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
body := strings.NewReader(test.RawMultipart)
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
defer req.Body.Close()
store := New(nil, req)
if test.ParamNames == nil || test.ParamValues == nil {
if len(store.Set) != 0 {
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
t.Failed()
}
// no param to check
return
}
if len(test.ParamNames) != len(test.ParamValues) {
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
t.Failed()
}
for pi, pName := range test.ParamNames {
key := pName
value := test.ParamValues[pi]
isNameValid := true
for _, invalid := range test.InvalidNames {
if pName == invalid {
isNameValid = false
}
}
t.Run(key, func(t *testing.T) {
param, isset := store.Set[key]
if !isset {
if isNameValid {
t.Errorf("store should contain element with key '%s'", key)
t.Failed()
}
return
}
// if should be invalid
if isset && !isNameValid {
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
t.Failed()
}
valueType := reflect.TypeOf(value)
paramValue := param.Value
paramValueType := reflect.TypeOf(param.Value)
if valueType != paramValueType {
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
t.Failed()
}
if paramValue != value {
t.Errorf("should return %v (got '%v')", value, paramValue)
t.Failed()
}
})
}
})
}
}

View File

@ -41,6 +41,18 @@ func TestString_AvailableTypes(t *testing.T) {
{"string(1 )", false}, {"string(1 )", false},
{"string( 1 )", false}, {"string( 1 )", false},
{"string()", false},
{"string(a)", false},
{"string(-1)", false},
{"string(,)", false},
{"string(1,b)", false},
{"string(a,b)", false},
{"string(a,1)", false},
{"string(-1,1)", false},
{"string(1,-1)", false},
{"string(-1,-1)", false},
{"string(1,2)", true}, {"string(1,2)", true},
{"string(1, 2)", true}, {"string(1, 2)", true},
{"string(1, 2)", false}, {"string(1, 2)", false},

View File

@ -96,6 +96,11 @@ func TestUint_Values(t *testing.T) {
// strane offset because of how precision works // strane offset because of how precision works
{fmt.Sprintf("%f", float64(math.MaxUint64+1024*3)), false}, {fmt.Sprintf("%f", float64(math.MaxUint64+1024*3)), false},
{[]byte(fmt.Sprintf("%d", math.MaxInt64)), true},
{[]byte(fmt.Sprintf("%d", uint(math.MaxUint64))), true},
// strane offset because of how precision works
{[]byte(fmt.Sprintf("%f", float64(math.MaxUint64+1024*3))), false},
{"string", false}, {"string", false},
{[]byte("bytes"), false}, {[]byte("bytes"), false},
{-0.1, false}, {-0.1, false},