From c40fb03aa7c7dae80113b5f227ec1f25894745de Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:14:50 +0100 Subject: [PATCH 01/43] merge parseParameter --- internal/reqdata/param_reflect.go | 88 ------------------------------- internal/reqdata/parameter.go | 87 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 88 deletions(-) delete mode 100644 internal/reqdata/param_reflect.go diff --git a/internal/reqdata/param_reflect.go b/internal/reqdata/param_reflect.go deleted file mode 100644 index 6cbd403..0000000 --- a/internal/reqdata/param_reflect.go +++ /dev/null @@ -1,88 +0,0 @@ -package reqdata - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// parseParameter parses http GET/POST data -// - []string -// - 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{} { - dtype := reflect.TypeOf(data) - dvalue := reflect.ValueOf(data) - - switch dtype.Kind() { - - /* (1) []string -> recursive */ - case reflect.Slice: - - // 1. Return nothing if empty - if dvalue.Len() == 0 { - return 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 - result := make([]interface{}, dvalue.Len()) - - for i, l := 0, dvalue.Len(); i < l; i++ { - element := dvalue.Index(i) - - // ignore non-string - if element.Kind() != reflect.String { - continue - } - - result[i] = parseParameter(element.String()) - } - return result - - /* (2) string -> parse */ - case reflect.String: - - // build json wrapper - wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) - - // try to parse as json - var result interface{} - err := json.Unmarshal([]byte(wrapper), &result) - - // return if success - if err == nil { - - mapval, ok := result.(map[string]interface{}) - if !ok { - return dvalue.String() - } - - wrapped, ok := mapval["wrapped"] - if !ok { - return dvalue.String() - } - - return wrapped - } - - // else return as string - return dvalue.String() - - } - - /* (3) NIL if unknown type */ - return dvalue - -} diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 79201b9..4bceb95 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -1,5 +1,11 @@ package reqdata +import ( + "encoding/json" + "fmt" + "reflect" +) + // Parameter represents an http request parameter // that can be of type URL, GET, or FORM (multipart, json, urlencoded) type Parameter struct { @@ -27,3 +33,84 @@ func (i *Parameter) Parse() { i.Value = parseParameter(i.Value) } + +// parseParameter parses http GET/POST data +// - []string +// - 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{} { + dtype := reflect.TypeOf(data) + dvalue := reflect.ValueOf(data) + + switch dtype.Kind() { + + /* (1) []string -> recursive */ + case reflect.Slice: + + // 1. Return nothing if empty + if dvalue.Len() == 0 { + return 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 + result := make([]interface{}, dvalue.Len()) + + for i, l := 0, dvalue.Len(); i < l; i++ { + element := dvalue.Index(i) + + // ignore non-string + if element.Kind() != reflect.String { + continue + } + + result[i] = parseParameter(element.String()) + } + return result + + /* (2) string -> parse */ + case reflect.String: + + // build json wrapper + wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) + + // try to parse as json + var result interface{} + err := json.Unmarshal([]byte(wrapper), &result) + + // return if success + if err == nil { + + mapval, ok := result.(map[string]interface{}) + if !ok { + return dvalue.String() + } + + wrapped, ok := mapval["wrapped"] + if !ok { + return dvalue.String() + } + + return wrapped + } + + // else return as string + return dvalue.String() + + } + + /* (3) NIL if unknown type */ + return dvalue + +} From 613233faef8e4d20fbfa75732dde3d7b6bf91eaf Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:15:26 +0100 Subject: [PATCH 02/43] Test empty store -> fix --- internal/reqdata/store.go | 5 +++++ internal/reqdata/store_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 internal/reqdata/store_test.go diff --git a/internal/reqdata/store.go b/internal/reqdata/store.go index 514d4d4..0a4d81e 100644 --- a/internal/reqdata/store.go +++ b/internal/reqdata/store.go @@ -57,6 +57,11 @@ func New(uriParams []string, req *http.Request) *Store { // 1. set URI parameters ds.setURIParams(uriParams) + // ignore nil requests + if req == nil { + return ds + } + // 2. GET (query) data ds.readQuery(req) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go new file mode 100644 index 0000000..c973268 --- /dev/null +++ b/internal/reqdata/store_test.go @@ -0,0 +1,34 @@ +package reqdata + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestEmptyStore(t *testing.T) { + store := New(nil, nil) + + if store.URI == nil { + t.Errorf("store 'URI' list should be initialized") + t.Fail() + } + if len(store.URI) != 0 { + t.Errorf("store 'URI' list should be empty") + t.Fail() + } + + if store.Get == nil { + t.Errorf("store 'Get' map should be initialized") + t.Fail() + } + if store.Form == nil { + t.Errorf("store 'Form' map should be initialized") + t.Fail() + } + if store.Set == nil { + t.Errorf("store 'Set' map should be initialized") + t.Fail() + } +} From 44d19e8b65c7058f8d74802e86934ee6cb4c431c Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:15:44 +0100 Subject: [PATCH 03/43] Test store with uri arguments --- internal/reqdata/store_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index c973268..6a15342 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -32,3 +32,37 @@ func TestEmptyStore(t *testing.T) { t.Fail() } } + +func TestStoreWithUri(t *testing.T) { + urilist := []string{"abc", "def"} + store := New(urilist, nil) + + if len(store.URI) != len(urilist) { + t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.URI)) + t.Fail() + } + if len(store.Set) != len(urilist) { + t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.Set)) + t.Fail() + } + + for i, value := range urilist { + + t.Run(fmt.Sprintf("URL#%d='%s'", i, value), func(t *testing.T) { + key := fmt.Sprintf("URL#%d", i) + element, isset := store.Set[key] + + if !isset { + t.Errorf("store should contain element with key '%s'", key) + t.Failed() + } + + if element.Value != value { + t.Errorf("store[%s] should return '%s' (got '%s')", key, value, element.Value) + t.Failed() + } + }) + + } + +} From 58974613233a5d9e46a36c91698a6313f0070b52 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:15:53 +0100 Subject: [PATCH 04/43] Test store agains Query parameters --- internal/reqdata/store_test.go | 154 +++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 6a15342..1f16410 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -66,3 +66,157 @@ func TestStoreWithUri(t *testing.T) { } } + +func TestStoreWithGet(t *testing.T) { + tests := []struct { + Query string + + InvalidNames []string + ParamNames []string + ParamValues [][]string + }{ + { + Query: "", + InvalidNames: []string{}, + ParamNames: []string{}, + ParamValues: [][]string{}, + }, + { + Query: "a", + InvalidNames: []string{}, + ParamNames: []string{"a"}, + ParamValues: [][]string{[]string{""}}, + }, + { + Query: "a&b", + InvalidNames: []string{}, + ParamNames: []string{"a", "b"}, + ParamValues: [][]string{[]string{""}, []string{""}}, + }, + { + Query: "a=", + InvalidNames: []string{}, + ParamNames: []string{"a"}, + ParamValues: [][]string{[]string{""}}, + }, + { + Query: "a=&b=x", + InvalidNames: []string{}, + ParamNames: []string{"a", "b"}, + ParamValues: [][]string{[]string{""}, []string{"x"}}, + }, + { + Query: "a=b&c=d", + InvalidNames: []string{}, + ParamNames: []string{"a", "c"}, + ParamValues: [][]string{[]string{"b"}, []string{"d"}}, + }, + { + Query: "a=b&c=d&a=x", + InvalidNames: []string{}, + ParamNames: []string{"a", "c"}, + ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}}, + }, + { + Query: "a=b&_invalid=x", + InvalidNames: []string{"_invalid"}, + ParamNames: []string{"a", "_invalid"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { + Query: "a=b&invalid_=x", + InvalidNames: []string{"invalid_"}, + ParamNames: []string{"a", "invalid_"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { + Query: "a=b&GET@injection=x", + InvalidNames: []string{"GET@injection"}, + ParamNames: []string{"a", "GET@injection"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { // not really useful as all after '#' should be ignored by http clients + Query: "a=b&URL#injection=x", + InvalidNames: []string{"URL#injection"}, + ParamNames: []string{"a", "URL#injection"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil) + store := New(nil, req) + + if test.ParamNames == nil || test.ParamValues == nil { + if len(store.Set) != 0 { + t.Errorf("expected no GET 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 := fmt.Sprintf("GET@%s", pName) + values := 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() + } + + cast, canCast := param.Value.([]string) + + if !canCast { + t.Errorf("should return a []string (got '%v')", cast) + t.Failed() + } + + if len(cast) != len(values) { + t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast)) + t.Failed() + } + + for vi, value := range values { + + t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) { + if value != cast[vi] { + t.Errorf("should return '%s' (got '%s')", value, cast[vi]) + t.Failed() + } + }) + } + }) + + } + }) + } + +} From ac7adcbd1f04f68a14dd4e351884a1fcd61ad772 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:53:02 +0100 Subject: [PATCH 05/43] Test with url encoded body parameters --- internal/reqdata/store_test.go | 157 +++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 1f16410..77aa930 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -1,6 +1,7 @@ package reqdata import ( + "bytes" "fmt" "net/http" "net/http/httptest" @@ -220,3 +221,159 @@ func TestStoreWithGet(t *testing.T) { } } + +func TestStoreWithUrlEncodedForm(t *testing.T) { + tests := []struct { + URLEncoded string + + InvalidNames []string + ParamNames []string + ParamValues [][]string + }{ + { + URLEncoded: "", + InvalidNames: []string{}, + ParamNames: []string{}, + ParamValues: [][]string{}, + }, + { + URLEncoded: "a", + InvalidNames: []string{}, + ParamNames: []string{"a"}, + ParamValues: [][]string{[]string{""}}, + }, + { + URLEncoded: "a&b", + InvalidNames: []string{}, + ParamNames: []string{"a", "b"}, + ParamValues: [][]string{[]string{""}, []string{""}}, + }, + { + URLEncoded: "a=", + InvalidNames: []string{}, + ParamNames: []string{"a"}, + ParamValues: [][]string{[]string{""}}, + }, + { + URLEncoded: "a=&b=x", + InvalidNames: []string{}, + ParamNames: []string{"a", "b"}, + ParamValues: [][]string{[]string{""}, []string{"x"}}, + }, + { + URLEncoded: "a=b&c=d", + InvalidNames: []string{}, + ParamNames: []string{"a", "c"}, + ParamValues: [][]string{[]string{"b"}, []string{"d"}}, + }, + { + URLEncoded: "a=b&c=d&a=x", + InvalidNames: []string{}, + ParamNames: []string{"a", "c"}, + ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}}, + }, + { + URLEncoded: "a=b&_invalid=x", + InvalidNames: []string{"_invalid"}, + ParamNames: []string{"a", "_invalid"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { + URLEncoded: "a=b&invalid_=x", + InvalidNames: []string{"invalid_"}, + ParamNames: []string{"a", "invalid_"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { + URLEncoded: "a=b&GET@injection=x", + InvalidNames: []string{"GET@injection"}, + ParamNames: []string{"a", "GET@injection"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + { + URLEncoded: "a=b&URL#injection=x", + InvalidNames: []string{"URL#injection"}, + ParamNames: []string{"a", "URL#injection"}, + ParamValues: [][]string{[]string{"b"}, []string{""}}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { + body := bytes.NewBufferString(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() + store := New(nil, req) + + if test.ParamNames == nil || test.ParamValues == nil { + if len(store.Set) != 0 { + t.Errorf("expected no FORM 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 + values := 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() + } + + cast, canCast := param.Value.([]string) + + if !canCast { + t.Errorf("should return a []string (got '%v')", cast) + t.Failed() + } + + if len(cast) != len(values) { + t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast)) + t.Failed() + } + + for vi, value := range values { + + t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) { + if value != cast[vi] { + t.Errorf("should return '%s' (got '%s')", value, cast[vi]) + t.Failed() + } + }) + } + }) + + } + }) + } + +} From 5f1d76a0a829045f0164f3b93d824851bb6c7858 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 16:57:23 +0100 Subject: [PATCH 06/43] Test json body parameters --- internal/reqdata/store_test.go | 161 +++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 77aa930..7327f5b 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "reflect" "testing" ) @@ -377,3 +378,163 @@ func TestStoreWithUrlEncodedForm(t *testing.T) { } } + +func TestJsonParameters(t *testing.T) { + tests := []struct { + RawJson string + + InvalidNames []string + ParamNames []string + ParamValues []interface{} + }{ + // no need to fully check json because it is parsed with the standard library + { + RawJson: "", + InvalidNames: []string{}, + ParamNames: []string{}, + ParamValues: []interface{}{}, + }, + { + RawJson: "{}", + InvalidNames: []string{}, + ParamNames: []string{}, + ParamValues: []interface{}{}, + }, + { + RawJson: "{ \"a\": \"b\" }", + InvalidNames: []string{}, + ParamNames: []string{"a"}, + ParamValues: []interface{}{"b"}, + }, + { + RawJson: "{ \"a\": \"b\", \"c\": \"d\" }", + InvalidNames: []string{}, + ParamNames: []string{"a", "c"}, + ParamValues: []interface{}{"b", "d"}, + }, + { + RawJson: "{ \"_invalid\": \"x\" }", + InvalidNames: []string{"_invalid"}, + ParamNames: []string{"_invalid"}, + ParamValues: []interface{}{nil}, + }, + { + RawJson: "{ \"a\": \"b\", \"_invalid\": \"x\" }", + InvalidNames: []string{"_invalid"}, + ParamNames: []string{"a", "_invalid"}, + ParamValues: []interface{}{"b", nil}, + }, + + { + RawJson: "{ \"invalid_\": \"x\" }", + InvalidNames: []string{"invalid_"}, + ParamNames: []string{"invalid_"}, + ParamValues: []interface{}{nil}, + }, + { + RawJson: "{ \"a\": \"b\", \"invalid_\": \"x\" }", + InvalidNames: []string{"invalid_"}, + ParamNames: []string{"a", "invalid_"}, + ParamValues: []interface{}{"b", nil}, + }, + + { + RawJson: "{ \"GET@injection\": \"x\" }", + InvalidNames: []string{"GET@injection"}, + ParamNames: []string{"GET@injection"}, + ParamValues: []interface{}{nil}, + }, + { + RawJson: "{ \"a\": \"b\", \"GET@injection\": \"x\" }", + InvalidNames: []string{"GET@injection"}, + ParamNames: []string{"a", "GET@injection"}, + ParamValues: []interface{}{"b", nil}, + }, + + { + RawJson: "{ \"URL#injection\": \"x\" }", + InvalidNames: []string{"URL#injection"}, + ParamNames: []string{"URL#injection"}, + ParamValues: []interface{}{nil}, + }, + { + RawJson: "{ \"a\": \"b\", \"URL#injection\": \"x\" }", + InvalidNames: []string{"URL#injection"}, + ParamNames: []string{"a", "URL#injection"}, + ParamValues: []interface{}{"b", nil}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { + body := bytes.NewBufferString(test.RawJson) + req := httptest.NewRequest(http.MethodPost, "http://host.com", body) + req.Header.Add("Content-Type", "application/json") + 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() + } + + }) + + } + }) + } + +} From d1d581bd758749e9026412f63d8e7641d24048e6 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 17:00:11 +0100 Subject: [PATCH 07/43] Test json: add a check with invalid json (no parameter is parsed) --- internal/reqdata/store_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 7327f5b..6866a8f 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -463,6 +463,13 @@ func TestJsonParameters(t *testing.T) { ParamNames: []string{"a", "URL#injection"}, ParamValues: []interface{}{"b", nil}, }, + // json parse error + { + RawJson: "{ \"a\": \"b\", }", + InvalidNames: []string{}, + ParamNames: []string{}, + ParamValues: []interface{}{}, + }, } for i, test := range tests { From 6bc8160f993a9cb9ffcf998bbcb674c85207b2ee Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 19:19:55 +0100 Subject: [PATCH 08/43] Prettify logs --- server.go | 6 +++--- util.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index a94c55a..b43809e 100644 --- a/server.go +++ b/server.go @@ -46,7 +46,7 @@ func New(configPath string) (*Server, error) { } // 4. log configuration services - log.Printf("=== Aicra configuration ===\n") + log.Printf("🔧 Reading configuration '%s'\n", configPath) logService(*i.services, "") return i, nil @@ -68,9 +68,9 @@ func (s *Server) Handle(handler *api.Handler) { func (s Server) HTTP() httpServer { // 1. log available handlers - log.Printf("=== Mapped handlers ===\n") + log.Printf("🔗 Mapping handlers\n") for i := 0; i < len(s.handlers); i++ { - log.Printf("* [rest] %s\t'%s'\n", s.handlers[i].GetMethod(), s.handlers[i].GetPath()) + log.Printf(" ->\t%s\t'%s'\n", s.handlers[i].GetMethod(), s.handlers[i].GetPath()) } // 2. cast to http server diff --git a/util.go b/util.go index 2672f76..0a2bf12 100644 --- a/util.go +++ b/util.go @@ -90,9 +90,9 @@ func logService(s config.Service, path string) { for _, method := range handledMethods { if m := s.Method(method); m != nil { if path == "" { - log.Printf("* [rest] %s\t'/'\n", method) + log.Printf(" ->\t%s\t'/'\n", method) } else { - log.Printf("* [rest] %s\t'%s'\n", method, path) + log.Printf(" ->\t%s\t'%s'\n", method, path) } } } From d44df7eee0ae9a0ccc69593a0f5c56d09c6a25f6 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 19:24:57 +0100 Subject: [PATCH 09/43] fix readme usage & changelog --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8507ec9..ba6312a 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ func main() { server.Checkers.Add( builtin.NewFloat64() ); // 3. bind your implementations - server.HandleFunc(http.MethodGet, func(req api.Request, res *api.Response){ + server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){ // ... process stuff ... res.SetError(api.ErrorSuccess()); }) @@ -210,6 +210,6 @@ In this example we want 3 arguments : - [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*) - [x] generic controllers implementation (shared objects) - [x] response interface -- [ ] log bound resources when building the aicra server +- [x] log bound resources when building the aicra server - [ ] fail on check for unimplemented resources at server boot. - [ ] fail on check for unavailable types in api.json at server boot. From 5cdcfd21ca1013630729685e094b7267bc291e8e Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 19 Nov 2019 19:25:38 +0100 Subject: [PATCH 10/43] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba6312a..bd0a7d1 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ func main() { }) // 4. launch server - log.Fatal( http.ListenAndServer("localhost:8181", server) ) + log.Fatal( http.ListenAndServe("localhost:8181", server) ) } ``` From dec7fd9a62dd093b3f6ddee8f90dd6869ed72b11 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 20:42:05 +0100 Subject: [PATCH 11/43] test internal/cerr package + add WrapString() to use a raw string --- internal/cerr/cerr.go | 8 ++++++ internal/cerr/cerr_test.go | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 internal/cerr/cerr_test.go diff --git a/internal/cerr/cerr.go b/internal/cerr/cerr.go index 57f4431..19a8e6d 100644 --- a/internal/cerr/cerr.go +++ b/internal/cerr/cerr.go @@ -16,6 +16,14 @@ func (err Error) Wrap(e error) *WrapError { } } +// WrapString returns a new error which wraps a new error created from a string. +func (err Error) WrapString(e string) *WrapError { + return &WrapError{ + base: err, + wrap: Error(e), + } +} + // WrapError is way to wrap errors recursively. type WrapError struct { base error diff --git a/internal/cerr/cerr_test.go b/internal/cerr/cerr_test.go new file mode 100644 index 0000000..476996c --- /dev/null +++ b/internal/cerr/cerr_test.go @@ -0,0 +1,57 @@ +package cerr + +import ( + "errors" + "fmt" + "testing" +) + +func TestConstError(t *testing.T) { + const cerr1 = Error("some-string") + const cerr2 = Error("some-other-string") + const cerr3 = Error("some-string") // same const value as @cerr1 + + if cerr1.Error() == cerr2.Error() { + t.Errorf("cerr1 should not be equal to cerr2 ('%s', '%s')", cerr1.Error(), cerr2.Error()) + } + if cerr2.Error() == cerr3.Error() { + t.Errorf("cerr2 should not be equal to cerr3 ('%s', '%s')", cerr2.Error(), cerr3.Error()) + } + if cerr1.Error() != cerr3.Error() { + t.Errorf("cerr1 should be equal to cerr3 ('%s', '%s')", cerr1.Error(), cerr3.Error()) + } +} + +func TestWrappedConstError(t *testing.T) { + const parent = Error("file error") + + const readErrorConst = Error("cannot read file") + var wrappedReadError = parent.Wrap(readErrorConst) + + expectedWrappedReadError := fmt.Sprintf("%s: %s", parent.Error(), readErrorConst.Error()) + if wrappedReadError.Error() != expectedWrappedReadError { + t.Errorf("expected '%s' (got '%s')", wrappedReadError.Error(), expectedWrappedReadError) + } +} +func TestWrappedStandardError(t *testing.T) { + const parent = Error("file error") + + var writeErrorStandard error = errors.New("cannot write file") + var wrappedWriteError = parent.Wrap(writeErrorStandard) + + expectedWrappedWriteError := fmt.Sprintf("%s: %s", parent.Error(), writeErrorStandard.Error()) + if wrappedWriteError.Error() != expectedWrappedWriteError { + t.Errorf("expected '%s' (got '%s')", wrappedWriteError.Error(), expectedWrappedWriteError) + } +} +func TestWrappedStringError(t *testing.T) { + const parent = Error("file error") + + var closeErrorString string = "cannot close file" + var wrappedCloseError = parent.WrapString(closeErrorString) + + expectedWrappedCloseError := fmt.Sprintf("%s: %s", parent.Error(), closeErrorString) + if wrappedCloseError.Error() != expectedWrappedCloseError { + t.Errorf("expected '%s' (got '%s')", wrappedCloseError.Error(), expectedWrappedCloseError) + } +} From 4accad7286fa26e5a89fa9bbc2f6ca4813ddb508 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 21:02:48 +0100 Subject: [PATCH 12/43] refactor internal/config - create cerr (constant errors) with wrapped context (service name, method, parameter name) - fix comments numbering - remove duplicate check --- internal/config/errors.go | 27 +++++++++++++++++++++++++++ internal/config/method.go | 36 +++++++++++++++--------------------- internal/config/service.go | 19 +++++-------------- 3 files changed, 47 insertions(+), 35 deletions(-) create mode 100644 internal/config/errors.go diff --git a/internal/config/errors.go b/internal/config/errors.go new file mode 100644 index 0000000..ef413e8 --- /dev/null +++ b/internal/config/errors.go @@ -0,0 +1,27 @@ +package config + +import "git.xdrm.io/go/aicra/internal/cerr" + +// ErrRead - a problem ocurred when trying to read the configuration file +const ErrRead = cerr.Error("cannot read config") + +// ErrFormat - a invalid format has been detected +const ErrFormat = cerr.Error("invalid config format") + +// ErrIllegalServiceName - an illegal character has been found in a service name +const ErrIllegalServiceName = cerr.Error("service must not contain any slash '/' nor '-' symbols") + +// ErrMissingMethodDesc - a method is missing its description +const ErrMissingMethodDesc = cerr.Error("missing method description") + +// ErrMissingParamDesc - a parameter is missing its description +const ErrMissingParamDesc = cerr.Error("missing parameter description") + +// ErrIllegalParamName - a parameter has an illegal name +const ErrIllegalParamName = cerr.Error("illegal parameter name (must not begin/end with '_')") + +// ErrMissingParamType - a parameter has an illegal type +const ErrMissingParamType = cerr.Error("missing parameter type") + +// ErrParamNameConflict - a parameter has a conflict with its name/rename field +const ErrParamNameConflict = cerr.Error("name conflict for parameter") diff --git a/internal/config/method.go b/internal/config/method.go index f570b84..1cb2a02 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "strings" ) @@ -10,7 +9,7 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e // 1. fail on missing description if len(methodDef.Description) < 1 { - return fmt.Errorf("missing %s.%s description", servicePath, httpMethod) + return ErrMissingMethodDesc.WrapString(httpMethod + " " + servicePath) } // 2. stop if no parameter @@ -22,16 +21,16 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e // 3. for each parameter for pName, pData := range methodDef.Parameters { - // check name + // 3.1. check name if strings.Trim(pName, "_") != pName { - return fmt.Errorf("invalid name '%s' must not begin/end with '_'", pName) + return ErrIllegalParamName.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } if len(pData.Rename) < 1 { pData.Rename = pName } - // 5. Check for name/rename conflict + // 3.2. Check for name/rename conflict for paramName, param := range methodDef.Parameters { // ignore self @@ -39,39 +38,34 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e continue } - // 1. Same rename field + // 3.2.1. Same rename field if pData.Rename == param.Rename { - return fmt.Errorf("rename conflict for %s.%s parameter '%s'", servicePath, httpMethod, pData.Rename) + return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pData.Rename + "}") } - // 2. Not-renamed field matches a renamed field + // 3.2.2. Not-renamed field matches a renamed field if pName == param.Rename { - return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName) + return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } - // 3. Renamed field matches name + // 3.2.3. Renamed field matches name if pData.Rename == paramName { - return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName) + return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } } - // 6. Manage invalid type - if len(pData.Type) < 1 { - return fmt.Errorf("invalid type for %s.%s parameter '%s'", servicePath, httpMethod, pName) - } - - // 7. Fail on missing description + // 3.3. Fail on missing description if len(pData.Description) < 1 { - return fmt.Errorf("missing description for %s.%s parameter '%s'", servicePath, httpMethod, pName) + return ErrMissingParamDesc.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } - // 8. Fail on missing type + // 3.4. Manage invalid type if len(pData.Type) < 1 { - return fmt.Errorf("missing type for %s.%s parameter '%s'", servicePath, httpMethod, pName) + return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } - // 9. Set optional + type + // 3.5. Set optional + type if pData.Type[0] == '?' { pData.Optional = true pData.Type = pData.Type[1:] diff --git a/internal/config/service.go b/internal/config/service.go index cf223fe..1c97701 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -2,20 +2,11 @@ package config import ( "encoding/json" - "fmt" "io" "net/http" "strings" - - "git.xdrm.io/go/aicra/internal/cerr" ) -// ErrRead - a problem ocurred when trying to read the configuration file -const ErrRead = cerr.Error("cannot read config") - -// ErrFormat - a invalid format has been detected -const ErrFormat = cerr.Error("invalid config format") - // Parse builds a service from a json reader and checks for most format errors. func Parse(r io.Reader) (*Service, error) { receiver := &Service{} @@ -87,20 +78,20 @@ func (svc *Service) checkAndFormat(servicePath string) error { } } - // 1. stop if no child */ + // 2. stop if no child */ if svc.Children == nil || len(svc.Children) < 1 { return nil } - // 2. for each service */ + // 3. for each service */ for childService, ctl := range svc.Children { - // 3. invalid name */ + // 3.1. invalid name */ if strings.ContainsAny(childService, "/-") { - return fmt.Errorf("service '%s' must not contain any slash '/' nor '-' symbols", childService) + return ErrIllegalServiceName.WrapString(childService) } - // 4. check recursively */ + // 3.2. check recursively */ err := ctl.checkAndFormat(childService) if err != nil { return err From 5ac3b81f6d20051afc0d6492f9224ad9aff2ba84 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 21:34:25 +0100 Subject: [PATCH 13/43] parameter name conflict: on rename conflicting rename, return original name --- internal/config/method.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/method.go b/internal/config/method.go index 1cb2a02..5131fdc 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -40,7 +40,7 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e // 3.2.1. Same rename field if pData.Rename == param.Rename { - return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pData.Rename + "}") + return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } // 3.2.2. Not-renamed field matches a renamed field From dd0e321cbdb7794e65a50fee92c2205f60dbf90f Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 21:48:24 +0100 Subject: [PATCH 14/43] test internal/config parsing and illegal/missing fields --- internal/config/config_test.go | 388 +++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 internal/config/config_test.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..fc0f209 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,388 @@ +package config + +import ( + "fmt" + "net/http" + "strings" + "testing" +) + +func TestLegalServiceName(t *testing.T) { + tests := []struct { + Raw string + Error error + }{ + { + `{ + "/": { + "invalid/service-name": { + + } + } + }`, + ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service-name")), + }, + { + `{ + "/": { + "invalid/service/name": { + + } + } + }`, + ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service/name")), + }, + { + `{ + "/": { + "invalid-service-name": { + + } + } + }`, + ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid-service-name")), + }, + + { + `{ + "/": { + "valid.service_name": { + } + } + }`, + nil, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if err == nil && test.Error != nil { + t.Errorf("expected an error: '%s'", test.Error.Error()) + t.FailNow() + } + if err != nil && test.Error == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && test.Error != nil { + if err.Error() != test.Error.Error() { + t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) + t.FailNow() + } + } + }) + } +} +func TestAvailableMethods(t *testing.T) { + reader := strings.NewReader(`{ + "GET": { "info": "info" }, + "POST": { "info": "info" }, + "PUT": { "info": "info" }, + "DELETE": { "info": "info" } + }`) + srv, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error (got '%s')", err) + t.FailNow() + } + + if srv.Method(http.MethodGet) == nil { + t.Errorf("expected method GET to be available") + t.Fail() + } + if srv.Method(http.MethodPost) == nil { + t.Errorf("expected method POST to be available") + t.Fail() + } + if srv.Method(http.MethodPut) == nil { + t.Errorf("expected method PUT to be available") + t.Fail() + } + if srv.Method(http.MethodDelete) == nil { + t.Errorf("expected method DELETE to be available") + t.Fail() + } + + if srv.Method(http.MethodPatch) != nil { + t.Errorf("expected method PATH to be UNavailable") + t.Fail() + } +} +func TestParseEmpty(t *testing.T) { + reader := strings.NewReader(`{}`) + _, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error (got '%s')", err) + t.FailNow() + } +} +func TestParseJsonError(t *testing.T) { + reader := strings.NewReader(`{ + "GET": { + "info": "info + }, + }`) // trailing ',' is invalid JSON + _, err := Parse(reader) + if err == nil { + t.Errorf("expected error") + t.FailNow() + } +} + +func TestParseMissingMethodDescription(t *testing.T) { + tests := []struct { + Raw string + Error error + }{ + { // missing description + `{ + "GET": { + + } + }`, + ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")), + }, + { // empty description + `{ + "GET": { + "info": "" + } + }`, + ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")), + }, + { // valid description + `{ + "GET": { + "info": "a" + } + }`, + nil, + }, + { // valid description + `{ + "GET": { + "info": "some description" + } + }`, + nil, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if err == nil && test.Error != nil { + t.Errorf("expected an error: '%s'", test.Error.Error()) + t.FailNow() + } + if err != nil && test.Error == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && test.Error != nil { + if err.Error() != test.Error.Error() { + t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) + t.FailNow() + } + } + }) + } + +} + +func TestParseParameters(t *testing.T) { + tests := []struct { + Raw string + Error error + ErrorAlternative error + }{ + { // invalid param name prefix + `{ + "GET": { + "info": "info", + "in": { + "_param1": {} + } + } + }`, + ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {_param1}")), + nil, + }, + { // invalid param name suffix + `{ + "GET": { + "info": "info", + "in": { + "param1_": {} + } + } + }`, + ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {param1_}")), + nil, + }, + + { // missing param description + `{ + "GET": { + "info": "info", + "in": { + "param1": {} + } + } + }`, + ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")), + nil, + }, + { // empty param description + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "" + } + } + } + }`, + ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")), + nil, + }, + + { // missing param type + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "valid" + } + } + } + }`, + ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), + nil, + }, + { // empty param type + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "valid", + "type": "" + } + } + } + }`, + ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), + nil, + }, + { // valid description + valid type + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "valid", + "type": "valid" + } + } + } + }`, + nil, + nil, + }, + + { // name conflict with rename + `{ + "GET": { + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid" }, + "param2": { "info": "valid", "type": "valid", "name": "param1" } + + } + } + }`, + // 2 possible errors as map order is not deterministic + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), + }, + { // rename conflict with name + `{ + "GET": { + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "param2" }, + "param2": { "info": "valid", "type": "valid" } + + } + } + }`, + // 2 possible errors as map order is not deterministic + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), + }, + { // rename conflict with rename + `{ + "GET": { + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "conflict" }, + "param2": { "info": "valid", "type": "valid", "name": "conflict" } + + } + } + }`, + // 2 possible errors as map order is not deterministic + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")), + ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")), + }, + + { // both renamed with no conflict + `{ + "GET": { + "info": "info", + "in": { + "param1": { "info": "valid", "type": "valid", "name": "freename" }, + "param2": { "info": "valid", "type": "valid", "name": "freename2" } + + } + } + }`, + nil, + nil, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + _, err := Parse(strings.NewReader(test.Raw)) + + if err == nil && test.Error != nil { + t.Errorf("expected an error: '%s'", test.Error.Error()) + t.FailNow() + } + if err != nil && test.Error == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && test.Error != nil { + if err.Error() != test.Error.Error() && err.Error() != test.ErrorAlternative.Error() { + t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) + t.FailNow() + } + } + }) + } + +} From 61b58a9f98ae8d78565074254795d748a61603a7 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:00:48 +0100 Subject: [PATCH 15/43] add internal/config test for the Browse() method --- internal/config/config_test.go | 134 +++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fc0f209..08e183c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -386,3 +386,137 @@ func TestParseParameters(t *testing.T) { } } + +func TestBrowseSimple(t *testing.T) { + tests := []struct { + Raw string + Path []string + BrowseDepth int + ValidDepth bool + }{ + { // false positive -1 + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + } + } + } + }`, + []string{"parent", "subdir"}, + 1, + false, + }, + { // false positive +1 + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + } + } + } + }`, + []string{"parent", "subdir"}, + 3, + false, + }, + + { + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + } + } + } + }`, + []string{"parent", "subdir"}, + 2, + true, + }, + { // unknown path + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + } + } + } + }`, + []string{"x", "y"}, + 2, + false, + }, + { // unknown path + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + } + } + } + }`, + []string{"parent", "y"}, + 1, + true, + }, + { // Warning: this case is important to understand the precedence of service paths over + // the value of some variables. Here if we send a string parameter in the GET method that + // unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead + // of the service /parent with its parameter set to the value 'subdir'. + `{ + "/" : { + "parent": { + "/": { + "subdir": {} + }, + "GET": { + "info": "valid-desc", + "in": { + "some-value": { + "info": "valid-desc", + "type": "valid-type" + } + } + } + } + } + }`, + []string{"parent", "subdir"}, + 2, + true, + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + srv, err := Parse(strings.NewReader(test.Raw)) + + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + _, depth := srv.Browse(test.Path) + if test.ValidDepth { + if depth != test.BrowseDepth { + t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth) + t.FailNow() + } + } else { + if depth == test.BrowseDepth { + t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth) + t.FailNow() + } + + } + }) + } + +} From e9c7160d2eabc4a7c8e4458151b991168138ead3 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:18:00 +0100 Subject: [PATCH 16/43] expand internal/config test coverage - make parameter type "?" invalid as it marks it as optional only - check optional vs. required parameters - test subservice in method description error check --- internal/config/config_test.go | 85 +++++++++++++++++++++++++++++++++- internal/config/method.go | 2 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 08e183c..3d0c19c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -146,6 +146,18 @@ func TestParseMissingMethodDescription(t *testing.T) { }`, ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")), }, + { // missing description + `{ + "/": { + "subservice": { + "GET": { + + } + } + } + }`, + ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET subservice")), + }, { // empty description `{ "GET": { @@ -197,6 +209,46 @@ func TestParseMissingMethodDescription(t *testing.T) { } +func TestOptionalParam(t *testing.T) { + reader := strings.NewReader(`{ + "GET": { + "info": "info", + "in": { + "optional": { "info": "valid-desc", "type": "?optional-type" }, + "required": { "info": "valid-desc", "type": "required-type" }, + "required2": { "info": "valid-desc", "type": "a" }, + "optional2": { "info": "valid-desc", "type": "?a" } + } + } + }`) + srv, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + method := srv.Method(http.MethodGet) + if method == nil { + t.Errorf("expected GET method not to be nil") + t.FailNow() + } + for pName, param := range method.Parameters { + + if pName == "optional" || pName == "optional2" { + if !param.Optional { + t.Errorf("expected parameter '%s' to be optional", pName) + t.Failed() + } + } + if pName == "required" || pName == "required2" { + if param.Optional { + t.Errorf("expected parameter '%s' to be required", pName) + t.Failed() + } + } + } + +} func TestParseParameters(t *testing.T) { tests := []struct { Raw string @@ -284,6 +336,22 @@ func TestParseParameters(t *testing.T) { ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), nil, }, + { // invalid type (optional mark only) + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "valid", + "type": "?" + } + } + } + }`, + + ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), + ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")), + }, { // valid description + valid type `{ "GET": { @@ -291,7 +359,22 @@ func TestParseParameters(t *testing.T) { "in": { "param1": { "info": "valid", - "type": "valid" + "type": "a" + } + } + } + }`, + nil, + nil, + }, + { // valid description + valid OPTIONAL type + `{ + "GET": { + "info": "info", + "in": { + "param1": { + "info": "valid", + "type": "?valid" } } } diff --git a/internal/config/method.go b/internal/config/method.go index 5131fdc..a17df24 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -61,7 +61,7 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e } // 3.4. Manage invalid type - if len(pData.Type) < 1 { + if len(pData.Type) < 1 || pData.Type == "?" { return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } From aba95d41637ead0eca336121f6659200f45da348 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:20:12 +0100 Subject: [PATCH 17/43] remove dead code --- internal/config/method.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/config/method.go b/internal/config/method.go index a17df24..802766d 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -75,13 +75,3 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e return nil } - -// scopeHasPermission returns whether the permission fulfills a given scope -func scopeHasPermission(permission string, scope []string) bool { - for _, s := range scope { - if permission == s { - return true - } - } - return false -} From 228f5b9a02d8216bbfd02d1b8172cdec6e6a67c6 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:27:57 +0100 Subject: [PATCH 18/43] test internal/config empty parameter rename should not rename --- internal/config/config_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3d0c19c..466cec2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -209,6 +209,35 @@ func TestParseMissingMethodDescription(t *testing.T) { } +func TestParamEmptyRenameNoRename(t *testing.T) { + reader := strings.NewReader(`{ + "GET": { + "info": "info", + "in": { + "original": { "info": "valid-desc", "type": "valid-type", "name": "" } + } + } + }`) + srv, err := Parse(reader) + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + method := srv.Method(http.MethodGet) + if method == nil { + t.Errorf("expected GET method not to be nil") + t.FailNow() + } + for _, param := range method.Parameters { + + if param.Rename != "original" { + t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename) + t.FailNow() + } + } + +} func TestOptionalParam(t *testing.T) { reader := strings.NewReader(`{ "GET": { From 638f5e44c85df2de4fba1ac7ad76f5f5f13f8480 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:29:58 +0100 Subject: [PATCH 19/43] test internal/config trick to have a 100% coverage (as conflict check is undeterministic, merge all conditions) --- internal/config/config_test.go | 4 +++- internal/config/method.go | 10 +--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 466cec2..deb3170 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -490,7 +490,9 @@ func TestParseParameters(t *testing.T) { if err != nil && test.Error != nil { if err.Error() != test.Error.Error() && err.Error() != test.ErrorAlternative.Error() { - t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error()) + t.Errorf("got the error: '%s'", err.Error()) + t.Errorf("expected error (alternative 1): '%s'", test.Error.Error()) + t.Errorf("expected error (alternative 2): '%s'", test.ErrorAlternative.Error()) t.FailNow() } } diff --git a/internal/config/method.go b/internal/config/method.go index 802766d..14c3539 100644 --- a/internal/config/method.go +++ b/internal/config/method.go @@ -39,17 +39,9 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e } // 3.2.1. Same rename field - if pData.Rename == param.Rename { - return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - // 3.2.2. Not-renamed field matches a renamed field - if pName == param.Rename { - return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") - } - // 3.2.3. Renamed field matches name - if pData.Rename == paramName { + if pData.Rename == param.Rename || pName == param.Rename || pData.Rename == paramName { return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}") } From f2423bd71a30a7dfc44319ae287f9fad514f6465 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:35:00 +0100 Subject: [PATCH 20/43] make internal/multipart use internal/cerr constant errors - instead of defining its own --- internal/multipart/types.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/internal/multipart/types.go b/internal/multipart/types.go index 6860af4..86fda25 100644 --- a/internal/multipart/types.go +++ b/internal/multipart/types.go @@ -1,21 +1,15 @@ package multipart -// ConstError is a wrapper to set constant errors -type ConstError string - -// Error implements error -func (err ConstError) Error() string { - return string(err) -} +import "git.xdrm.io/go/aicra/internal/cerr" // ErrMissingDataName is set when a multipart variable/file has no name="..." -const ErrMissingDataName = ConstError("data has no name") +const ErrMissingDataName = cerr.Error("data has no name") // ErrDataNameConflict is set when a multipart variable/file name is already used -const ErrDataNameConflict = ConstError("data name conflict") +const ErrDataNameConflict = cerr.Error("data name conflict") // ErrNoHeader is set when a multipart variable/file has no (valid) header -const ErrNoHeader = ConstError("data has no header") +const ErrNoHeader = cerr.Error("data has no header") // Component represents a multipart variable/file type Component struct { From 727737ae67ba0c3857c216440074a94d6d5dc327 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 22:58:03 +0100 Subject: [PATCH 21/43] test multipart body paramters --- internal/reqdata/store_test.go | 235 +++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 6866a8f..2694e6b 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -545,3 +545,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 := bytes.NewBufferString(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() + } + + }) + + } + }) + } + +} From f076f3a88abb4ea6c51beb6d79271fdb37212b7a Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 23:00:42 +0100 Subject: [PATCH 22/43] replace bytes.NewBufferString() with strings.NewReader() --- internal/reqdata/store_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 2694e6b..7216753 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -1,11 +1,11 @@ package reqdata import ( - "bytes" "fmt" "net/http" "net/http/httptest" "reflect" + "strings" "testing" ) @@ -301,7 +301,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 +474,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() @@ -709,7 +709,7 @@ x for i, test := range tests { t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) { - body := bytes.NewBufferString(test.RawMultipart) + 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() From edc49d99151c4360d96a335a3f602ec4e26291ff Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 21 Nov 2019 23:21:59 +0100 Subject: [PATCH 23/43] expand store.go coverage to 100% with a tricky test - force http.Request.ParseForm to fail --- internal/reqdata/store_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/reqdata/store_test.go b/internal/reqdata/store_test.go index 7216753..0805b84 100644 --- a/internal/reqdata/store_test.go +++ b/internal/reqdata/store_test.go @@ -4,6 +4,7 @@ import ( "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 From b890fa0cccb8f4cf26b0bc140b7385f15a564f35 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Fri, 28 Feb 2020 17:50:55 +0100 Subject: [PATCH 24/43] return error when parsing parameters --- internal/reqdata/parameter.go | 56 +++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 4bceb95..ba07fb0 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,22 @@ 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.Value = parsed + + return nil } // parseParameter parses http GET/POST data @@ -39,7 +56,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) @@ -50,17 +67,21 @@ func parseParameter(data interface{}) interface{} { // 1. Return nothing if 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 + + // try to parse if a string (containing json) + if element.Kind() == reflect.String { + return parseParameter(element.String()) } - return parseParameter(element.String()) + + // already typed + return data, nil } @@ -72,12 +93,17 @@ func parseParameter(data interface{}) interface{} { // ignore non-string if element.Kind() != reflect.String { + result[i] = nil 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 +120,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, ErrUnknownType } From fab09b2a5b37e35ee87e63a53a9aadd06edbdb96 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Fri, 28 Feb 2020 18:41:05 +0100 Subject: [PATCH 25/43] fix: mark parameter as parsed after successful parsing - avoid parsing multiple times in a row ; it has no side-effect but is useless - actually does not cause any issue, but for consistency sake! --- internal/reqdata/parameter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index ba07fb0..2e9aec7 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -42,10 +42,11 @@ func (i *Parameter) Parse() error { /* (2) Try to parse value */ parsed, err := parseParameter(i.Value) - if err != nil { return err } + + i.Parsed = true i.Value = parsed return nil From 98878eb12734c560c2e611c16f01c91f66f2244a Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 1 Mar 2020 21:34:14 +0100 Subject: [PATCH 26/43] test simple string parameter --- internal/reqdata/parameter_test.go | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 internal/reqdata/parameter_test.go diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go new file mode 100644 index 0000000..f63f33e --- /dev/null +++ b/internal/reqdata/parameter_test.go @@ -0,0 +1,58 @@ +package reqdata + +import ( + "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 TestStringSlice(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() + } +} From 8fbe3dc1784bb7e2da40061b68a2ba949e6f1600 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 1 Mar 2020 21:41:20 +0100 Subject: [PATCH 27/43] test regex for builtin string typecheck --- typecheck/builtin/string_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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}, From 43fb3bb7c9b27ef42fff249a01ef646fc5809fc1 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 1 Mar 2020 21:43:28 +0100 Subject: [PATCH 28/43] test uint builtin typecheck overflow values for []byte --- typecheck/builtin/uint_test.go | 5 +++++ 1 file changed, 5 insertions(+) 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}, From 4d663fc56c142e4f9aecb9a13051e5f20c0244e7 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 21:51:06 +0100 Subject: [PATCH 29/43] test string slice as string ; invalid json but valid when wrapped --- internal/reqdata/parameter_test.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index f63f33e..ea5b328 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -31,7 +31,7 @@ func TestSimpleString(t *testing.T) { } } func TestStringSlice(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: "some-string"} + p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`} err := p.Parse() @@ -45,14 +45,31 @@ func TestStringSlice(t *testing.T) { t.FailNow() } - cast, canCast := p.Value.(string) + slice, canCast := p.Value.([]interface{}) if !canCast { - t.Errorf("expected parameter to be a string") + t.Errorf("expected parameter to be a []interface{}") t.FailNow() } - if cast != "some-string" { - t.Errorf("expected parameter to equal 'some-string', got '%s'", cast) + 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 + } + + } + } From 0395d763d6078ddc37deb275c18ce16da79db11f Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 21:56:05 +0100 Subject: [PATCH 30/43] test json invalid boolean primitives ; only valid when wrapped --- internal/reqdata/parameter_test.go | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index ea5b328..38b3419 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -30,6 +30,7 @@ func TestSimpleString(t *testing.T) { t.FailNow() } } + func TestStringSlice(t *testing.T) { p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`} @@ -73,3 +74,42 @@ func TestStringSlice(t *testing.T) { } } + +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() + } + }) + } + +} From 45675713e73d015267d7082c0500ebaecf568d5d Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:00:39 +0100 Subject: [PATCH 31/43] fix formatting type --- internal/reqdata/parameter_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index 38b3419..974991e 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -106,7 +106,13 @@ func TestJsonPrimitiveBool(t *testing.T) { } if cast != tcase.BoolValue { - t.Errorf("expected a value of %T, got %T", tcase.BoolValue, cast) + t.Errorf("expected a value of %t, got %t", tcase.BoolValue, cast) + t.FailNow() + } + }) + } + +} t.FailNow() } }) From 41d166529c729f1582546293da9a74069e16feee Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:01:16 +0100 Subject: [PATCH 32/43] test json invalid float primitives ; only valid when wrapped --- internal/reqdata/parameter_test.go | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index 974991e..c4a9ce4 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -1,6 +1,7 @@ package reqdata import ( + "math" "testing" ) @@ -113,6 +114,48 @@ func TestJsonPrimitiveBool(t *testing.T) { } } + +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() } }) From e132a5af42db83f2b6173b878fe7921aa32b81fd Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:19:28 +0100 Subject: [PATCH 33/43] test conversion from 1-sized slice to first element (bool vs json boolean primitive) --- internal/reqdata/parameter_test.go | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index c4a9ce4..7e35532 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -162,3 +162,109 @@ func TestJsonPrimitiveFloat(t *testing.T) { } } + +func TestOneSliceStringToString(t *testing.T) { + p := Parameter{Parsed: false, File: false, Value: []string{"lonely-string"}} + + 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.(string) + if !canCast { + t.Errorf("expected parameter to be a string") + t.FailNow() + } + + if cast != "lonely-string" { + t.Errorf("expected a value of '%s', got '%s'", "lonely-string", cast) + t.FailNow() + } +} + +func TestOneSliceBoolToBool(t *testing.T) { + tcases := []struct { + Raw bool + }{ + {true}, + {false}, + } + + for i, tcase := range tcases { + + t.Run("case "+string(i), func(t *testing.T) { + + p := Parameter{Parsed: false, File: false, Value: []bool{tcase.Raw}} + + 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.Raw { + t.Errorf("expected a value of '%t', got '%t'", tcase.Raw, cast) + t.FailNow() + } + + }) + } + +} + +func TestOneSliceJsonBoolToBool(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: []string{tcase.Raw}} + + 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.BoolValue { + t.Errorf("expected a value of '%t', got '%t'", tcase.BoolValue, cast) + t.FailNow() + } + + }) + } + +} From 7e7eb3ac295b3a94904c4acc14630b03cb4b4621 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:20:28 +0100 Subject: [PATCH 34/43] fix returning first element of 1-sized slice instead of the slice --- internal/reqdata/parameter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 2e9aec7..09d48a3 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -82,7 +82,7 @@ func parseParameter(data interface{}) (interface{}, error) { } // already typed - return data, nil + return element.Interface(), nil } From 5741ec597b1fe4d9b5bcc68c810da23f1cad9117 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:24:36 +0100 Subject: [PATCH 35/43] [breaking] do not return first element of 1-sized slices as it, return a slice - it is more consistent and does not rely of a "hidden" assomption. - for consistency, it is also a better practice to always the same type when waiting to receive a slice ; the 1 element case should not break anything --- internal/reqdata/parameter.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 09d48a3..033cbfe 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -66,27 +66,12 @@ func parseParameter(data interface{}) (interface{}, error) { /* (1) []string -> recursive */ case reflect.Slice: - // 1. Return nothing if empty + // 1. ignore empty if dvalue.Len() == 0 { return data, nil } - // 2. only return first element if alone - if dvalue.Len() == 1 { - - element := dvalue.Index(0) - - // try to parse if a string (containing json) - if element.Kind() == reflect.String { - return parseParameter(element.String()) - } - - // already typed - return element.Interface(), nil - - } - - // 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++ { From 313b91bb7ff11602340ff0b8720cef7568f8bd77 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:34:34 +0100 Subject: [PATCH 36/43] remove outdated tests --- internal/reqdata/parameter_test.go | 106 ----------------------------- 1 file changed, 106 deletions(-) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index 7e35532..c4a9ce4 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -162,109 +162,3 @@ func TestJsonPrimitiveFloat(t *testing.T) { } } - -func TestOneSliceStringToString(t *testing.T) { - p := Parameter{Parsed: false, File: false, Value: []string{"lonely-string"}} - - 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.(string) - if !canCast { - t.Errorf("expected parameter to be a string") - t.FailNow() - } - - if cast != "lonely-string" { - t.Errorf("expected a value of '%s', got '%s'", "lonely-string", cast) - t.FailNow() - } -} - -func TestOneSliceBoolToBool(t *testing.T) { - tcases := []struct { - Raw bool - }{ - {true}, - {false}, - } - - for i, tcase := range tcases { - - t.Run("case "+string(i), func(t *testing.T) { - - p := Parameter{Parsed: false, File: false, Value: []bool{tcase.Raw}} - - 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.Raw { - t.Errorf("expected a value of '%t', got '%t'", tcase.Raw, cast) - t.FailNow() - } - - }) - } - -} - -func TestOneSliceJsonBoolToBool(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: []string{tcase.Raw}} - - 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.BoolValue { - t.Errorf("expected a value of '%t', got '%t'", tcase.BoolValue, cast) - t.FailNow() - } - - }) - } - -} From 4cb62ea1223505039ed61e99c02d220a9b5b4d5f Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:42:17 +0100 Subject: [PATCH 37/43] fix parsing non-string slice values - only string were parsed using wrapped json - now we also keep primitive types --- internal/reqdata/parameter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 033cbfe..315d59f 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -79,7 +79,7 @@ func parseParameter(data interface{}) (interface{}, error) { // ignore non-string if element.Kind() != reflect.String { - result[i] = nil + result[i] = element.Interface() continue } From 5dad0ecc3909a678ccf9277fca99abab2c71e20a Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:42:45 +0100 Subject: [PATCH 38/43] test string slice vs. json string slice --- internal/reqdata/parameter_test.go | 46 +++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index c4a9ce4..5e0c753 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -32,7 +32,7 @@ func TestSimpleString(t *testing.T) { } } -func TestStringSlice(t *testing.T) { +func TestJsonStringSlice(t *testing.T) { p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`} err := p.Parse() @@ -76,6 +76,50 @@ func TestStringSlice(t *testing.T) { } +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 From ccf6577bc5326010fa0d0dd60c415c3f0b5a6658 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:42:59 +0100 Subject: [PATCH 39/43] test bool slice vs. json bool slice --- internal/reqdata/parameter_test.go | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index 5e0c753..c77ded2 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -206,3 +206,91 @@ func TestJsonPrimitiveFloat(t *testing.T) { } } + +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 + } + + } + +} From ec152aec4b62ba0a4ae7c835287cd1b4fb290bcd Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:45:58 +0100 Subject: [PATCH 40/43] fix parsing primitive types instead of erroring unknown type --- internal/reqdata/parameter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 315d59f..2c80455 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -123,6 +123,6 @@ func parseParameter(data interface{}) (interface{}, error) { } /* (3) NIL if unknown type */ - return dvalue, ErrUnknownType + return dvalue.Interface(), nil } From 682e20a959dfe41b854092a65ee1c58ce5af15d7 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 2 Mar 2020 22:49:17 +0100 Subject: [PATCH 41/43] test primitive types : bool, float64 --- internal/reqdata/parameter_test.go | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/internal/reqdata/parameter_test.go b/internal/reqdata/parameter_test.go index c77ded2..29bcbec 100644 --- a/internal/reqdata/parameter_test.go +++ b/internal/reqdata/parameter_test.go @@ -32,6 +32,68 @@ func TestSimpleString(t *testing.T) { } } +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"]`} From 1225e3b1f251e06f78ee1ae048424c66b8ed15c2 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Tue, 3 Mar 2020 18:36:52 +0100 Subject: [PATCH 42/43] refactor server handler, rename for readability --- http.go | 87 ++++++++++++++++++++++++++++--------------------------- server.go | 8 ++--- util.go | 2 +- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/http.go b/http.go index dc6ad88..44f8acc 100644 --- a/http.go +++ b/http.go @@ -13,100 +13,103 @@ import ( type httpServer Server // ServeHTTP implements http.Handler and has to be called on each request -func (s httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (server httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - // 1. build API request from HTTP request + /* (1) create api.Request from http.Request + ---------------------------------------------------------*/ request, err := api.NewRequest(r) if err != nil { log.Fatal(err) } // 2. find a matching service for this path in the config - serviceDef, pathIndex := s.services.Browse(request.URI) - if serviceDef == nil { + serviceConf, pathIndex := server.config.Browse(request.URI) + if serviceConf == nil { return } + + // 3. extract the service path from request URI servicePath := strings.Join(request.URI[:pathIndex], "/") if !strings.HasPrefix(servicePath, "/") { servicePath = "/" + servicePath } - // 3. check if matching methodDef exists in config */ - var methodDef = serviceDef.Method(r.Method) - if methodDef == nil { - response := api.NewResponse(api.ErrorUnknownMethod()) - response.ServeHTTP(w, r) - logError(response) + // 4. find method configuration from http method */ + var methodConf = serviceConf.Method(r.Method) + if methodConf == nil { + res := api.NewResponse(api.ErrorUnknownMethod()) + res.ServeHTTP(w, r) + logError(res) return } - // 4. parse every input data from the request - store := reqdata.New(request.URI[pathIndex:], r) + // 5. parse data from the request (uri, query, form, json) + data := reqdata.New(request.URI[pathIndex:], r) - /* (4) Check parameters + /* (2) check parameters ---------------------------------------------------------*/ - parameters, paramError := s.extractParameters(store, methodDef.Parameters) + parameters, paramError := server.extractParameters(data, methodConf.Parameters) // Fail if argument check failed if paramError.Code != api.ErrorSuccess().Code { - response := api.NewResponse(paramError) - response.ServeHTTP(w, r) - logError(response) + res := api.NewResponse(paramError) + res.ServeHTTP(w, r) + logError(res) return } request.Param = parameters - /* (5) Search a matching handler + /* (3) search for the handler ---------------------------------------------------------*/ - var serviceHandler *api.Handler - var serviceFound bool + var foundHandler *api.Handler + var found bool - for _, handler := range s.handlers { + for _, handler := range server.handlers { if handler.GetPath() == servicePath { - serviceFound = true + found = true if handler.GetMethod() == r.Method { - serviceHandler = handler + foundHandler = handler } } } // fail if found no handler - if serviceHandler == nil { - if serviceFound { - response := api.NewResponse() - response.SetError(api.ErrorUncallableMethod(), servicePath, r.Method) - response.ServeHTTP(w, r) - logError(response) + if foundHandler == nil { + if found { + res := api.NewResponse() + res.SetError(api.ErrorUncallableMethod(), servicePath, r.Method) + res.ServeHTTP(w, r) + logError(res) return } - response := api.NewResponse() - response.SetError(api.ErrorUncallableService(), servicePath) - response.ServeHTTP(w, r) - logError(response) + res := api.NewResponse() + res.SetError(api.ErrorUncallableService(), servicePath) + res.ServeHTTP(w, r) + logError(res) return } - /* (6) Execute handler and return response + /* (4) execute handler and return response ---------------------------------------------------------*/ // 1. feed request with configuration scope - request.Scope = methodDef.Scope + request.Scope = methodConf.Scope - // 1. execute - response := api.NewResponse() - serviceHandler.Handle(*request, response) + // 2. execute + res := api.NewResponse() + foundHandler.Handle(*request, res) - // 2. apply headers - for key, values := range response.Headers { + // 3. apply headers + for key, values := range res.Headers { for _, value := range values { w.Header().Add(key, value) } } - // 3. write to response - response.ServeHTTP(w, r) + // 4. write to response + res.ServeHTTP(w, r) return } diff --git a/server.go b/server.go index b43809e..4c40977 100644 --- a/server.go +++ b/server.go @@ -13,7 +13,7 @@ import ( // Server represents an AICRA instance featuring: type checkers, services type Server struct { - services *config.Service + config *config.Service Checkers *checker.Set handlers []*api.Handler } @@ -27,7 +27,7 @@ func New(configPath string) (*Server, error) { // 1. init instance var i = &Server{ - services: nil, + config: nil, Checkers: checker.New(), handlers: make([]*api.Handler, 0), } @@ -40,14 +40,14 @@ func New(configPath string) (*Server, error) { defer configFile.Close() // 3. load configuration - i.services, err = config.Parse(configFile) + i.config, err = config.Parse(configFile) if err != nil { return nil, err } // 4. log configuration services log.Printf("🔧 Reading configuration '%s'\n", configPath) - logService(*i.services, "") + logService(*i.config, "") return i, nil diff --git a/util.go b/util.go index 0a2bf12..949acb7 100644 --- a/util.go +++ b/util.go @@ -58,7 +58,7 @@ func (s *httpServer) extractParameters(store *reqdata.Store, methodParam map[str return nil, apiErr } - // 6. do not check if file + // 6. ignore type check if file if gotFile { parameters[param.Rename] = p.Value continue From 9103d752c368ffebaef2a3f1cdf3abfbd5d82bcc Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 8 Mar 2020 16:17:55 +0100 Subject: [PATCH 43/43] upgrade go version to 1.14 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c885d02..0adcac2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.xdrm.io/go/aicra -go 1.12 +go 1.14