diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 4bceb95..2c80455 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -4,8 +4,19 @@ import ( "encoding/json" "fmt" "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 // that can be of type URL, GET, or FORM (multipart, json, urlencoded) type Parameter struct { @@ -22,16 +33,23 @@ type Parameter struct { } // Parse parameter (json-like) if not already done -func (i *Parameter) Parse() { +func (i *Parameter) Parse() error { /* (1) Stop if already parsed or nil*/ if i.Parsed || i.Value == nil { - return + return nil } /* (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 @@ -39,7 +57,7 @@ func (i *Parameter) Parse() { // - size = 1 : return json of first element // - size > 1 : return array of json elements // - string : return json if valid, else return raw string -func parseParameter(data interface{}) interface{} { +func parseParameter(data interface{}) (interface{}, error) { dtype := reflect.TypeOf(data) dvalue := reflect.ValueOf(data) @@ -48,23 +66,12 @@ func parseParameter(data interface{}) interface{} { /* (1) []string -> recursive */ case reflect.Slice: - // 1. Return nothing if empty + // 1. ignore empty if dvalue.Len() == 0 { - return nil + return data, nil } - // 2. only return first element if alone - 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 + // 2. parse each element recursively result := make([]interface{}, dvalue.Len()) for i, l := 0, dvalue.Len(); i < l; i++ { @@ -72,12 +79,17 @@ func parseParameter(data interface{}) interface{} { // ignore non-string if element.Kind() != reflect.String { + result[i] = element.Interface() continue } - result[i] = parseParameter(element.String()) + parsed, err := parseParameter(element.String()) + if err != nil { + return data, err + } + result[i] = parsed } - return result + return result, nil /* (2) string -> parse */ case reflect.String: @@ -94,23 +106,23 @@ func parseParameter(data interface{}) interface{} { mapval, ok := result.(map[string]interface{}) if !ok { - return dvalue.String() + return dvalue.String(), ErrInvalidRootType } wrapped, ok := mapval["wrapped"] if !ok { - return dvalue.String() + return dvalue.String(), ErrInvalidJSON } - return wrapped + return wrapped, nil } // else return as string - return dvalue.String() + return dvalue.String(), nil } /* (3) NIL if unknown type */ - return dvalue + return dvalue.Interface(), nil } diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go new file mode 100644 index 0000000..29bcbec --- /dev/null +++ b/internal/reqdata/parameter_test.go @@ -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 + } + + } + +} diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 6866a8f..0805b84 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -1,11 +1,12 @@ package reqdata import ( - "bytes" "fmt" "net/http" "net/http/httptest" + "net/url" "reflect" + "strings" "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) { tests := []struct { URLEncoded string @@ -301,7 +323,7 @@ func TestStoreWithUrlEncodedForm(t *testing.T) { for i, test := range tests { 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.Header.Add("Content-Type", "application/x-www-form-urlencoded") defer req.Body.Close() @@ -474,7 +496,7 @@ func TestJsonParameters(t *testing.T) { for i, test := range tests { 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.Header.Add("Content-Type", "application/json") 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() + } + + }) + + } + }) + } + +} diff --git a/typecheck/builtin/string_test.go b/typecheck/builtin/string_test.go index 048485b..d23b7bf 100644 --- a/typecheck/builtin/string_test.go +++ b/typecheck/builtin/string_test.go @@ -41,6 +41,18 @@ func TestString_AvailableTypes(t *testing.T) { {"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)", false}, diff --git a/typecheck/builtin/uint_test.go b/typecheck/builtin/uint_test.go index c1d26aa..4a6ceb8 100644 --- a/typecheck/builtin/uint_test.go +++ b/typecheck/builtin/uint_test.go @@ -96,6 +96,11 @@ func TestUint_Values(t *testing.T) { // strane offset because of how precision works {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}, {[]byte("bytes"), false}, {-0.1, false},