From 6aae5b3b64f7fe993d9ab73c1162e2467b05bd3b Mon Sep 17 00:00:00 2001 From: Jonny Huxtable Date: Tue, 16 Apr 2019 00:15:23 +0100 Subject: [PATCH 1/4] Improve test coverage, remove redundant post form --- bridge/bridge.go | 38 +++++++++------------------ bridge/bridge_test.go | 48 ++++++++++++++++++++++++++++++++++ examples/apiaggregator/main.go | 6 ++--- examples/wolframalpha/main.go | 2 +- json/rapidapi.json | 4 +-- main.go | 5 ++-- 6 files changed, 68 insertions(+), 35 deletions(-) diff --git a/bridge/bridge.go b/bridge/bridge.go index 9dc26e0..b2a58b6 100755 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -25,8 +25,7 @@ const ( type Opts struct { Name string `json:"name"` Path string `json:"path"` - Lambda bool `json:"lambda"` - Port int `json:"port"` + Lambda bool `json:"Lambda"` } // Result represents a Chainlink JobRun @@ -130,7 +129,7 @@ type Bridge interface { } // Server holds pointers to the bridges indexed by their paths -// and the bridge to be mounted in lambda. +// and the bridge to be mounted in Lambda. type Server struct { pathMap map[string]Bridge ldaBridge Bridge @@ -178,11 +177,11 @@ func NewServer(bridges ...Bridge) *Server { // If the inbuilt http server is being used, bridges can specify many external adaptors // as long if exclusive paths are given. // -// If multiple adaptors are included with lambda/gcp enabled, then the first bridge that +// If multiple adaptors are included with Lambda/gcp enabled, then the first bridge that // has it enabled will be given as the Handler. func (s *Server) Start(port int) { if len(os.Getenv("LAMBDA")) > 0 { - lambda.Start(s.lambda) + lambda.Start(s.Lambda) } else { logrus.WithField("port", port).Info("Starting the bridge server") logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), s.Mux())) @@ -246,7 +245,7 @@ func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) lambda(r *Result) (interface{}, error) { +func (s *Server) Lambda(r *Result) (interface{}, error) { if obj, err := s.ldaBridge.Run(NewHelper(r.Data)); err != nil { r.SetErrored(err) } else if data, err := ParseInterface(obj); err != nil { @@ -300,9 +299,8 @@ func (h *Helper) GetIntParam(key string) int64 { // CallOpts are the options given into a http call method type CallOpts struct { Auth Auth `json:"-"` - Param map[string]interface{} `json:"param"` - ParamPassthrough bool `json:"paramPassthrough"` - FormPassthrough bool `json:"formPassthrough"` + Query map[string]interface{} `json:"query"` + QueryPassthrough bool `json:"queryPassthrough"` Body string `json:"body"` PostForm url.Values `json:"postForm"` ExpectedCode int `json:"expectedCode"` @@ -327,8 +325,8 @@ func (h *Helper) HTTPCallWithOpts(method, url string, obj interface{}, opts Call // HTTPCallRawWithOpts performs a HTTP call with any method and returns the raw byte body and any error // Supported options: // - Authentication methods for the API (query param, headers) -// - Query parameters via `opts.Param` -// - Passthrough through all json keys within the request `data` object via `opts.ParamPassthrough` +// - Query parameters via `opts.Query` +// - Passthrough through all json keys within the request `data` object via `opts.QueryPassthrough` // - Pass in a body to send with the request via `opts.Body` // - Send in post form kv via `opts.PostForm` // - Return an error if the returning http status code is different to `opts.ExpectedCode` @@ -339,25 +337,13 @@ func (h *Helper) HTTPCallRawWithOpts(method, url string, opts CallOpts) ([]byte, } req.Header.Add("Content-Type", "application/json") - req.PostForm = opts.PostForm - if opts.FormPassthrough { - for k, v := range h.Data.Map() { - if v.IsArray() { - for _, v2 := range v.Array() { - req.PostForm[k] = append(req.PostForm[k], v2.String()) - } - } else { - req.PostForm[k] = append(req.PostForm[k], v.String()) - } - } - } q := req.URL.Query() - if opts.ParamPassthrough { + if opts.QueryPassthrough { for k, v := range h.Data.Map() { q.Add(k, fmt.Sprintf("%s", v)) } } else { - for k, v := range opts.Param { + for k, v := range opts.Query { q.Add(k, fmt.Sprintf("%s", v)) } } @@ -398,7 +384,7 @@ func NewAuth(authType string, key string, value string) Auth { return a } -// Param is the Auth implementation that requires GET param set +// Query is the Auth implementation that requires GET param set type Param struct { Key string Value string diff --git a/bridge/bridge_test.go b/bridge/bridge_test.go index 08ff9da..bdd281c 100755 --- a/bridge/bridge_test.go +++ b/bridge/bridge_test.go @@ -11,6 +11,23 @@ import ( "testing" ) +func TestJSON_Merge(t *testing.T) { + j1, err := ParseInterface(map[string]string{ + "alice": "bob", + "carl": "dennis", + }) + assert.Nil(t, err) + j2, err := ParseInterface(map[string]string{ + "alice": "bob", + "eric": "fred", + "geoff": "harry", + }) + j3, err := j1.Merge(j2) + assert.Nil(t, err) + assert.Equal(t, "bob", j3.Get("alice").String()) + assert.Equal(t, "fred", j3.Get("eric").String()) +} + // Copied from the example to use as a test fixture type CryptoCompare struct{} @@ -231,4 +248,35 @@ func TestServer_Mux_CryptoCompare(t *testing.T) { assert.True(t, ok) _, ok = data["EUR"] assert.True(t, ok) +} + +func TestServer_Lambda_CryptoCompare(t *testing.T) { + s := NewServer(&CryptoCompare{}) + + r := &Result{} + r.JobRunID = "1234" + + obj, err := s.Lambda(r) + assert.Nil(t, err) + json, err := ParseInterface(obj) + assert.Nil(t, err) + + assert.Equal(t, "1234", json.Get("jobRunId").String()) + + data := json.Get("data").Map() + _, ok := data["USD"] + assert.True(t, ok) + _, ok = data["JPY"] + assert.True(t, ok) + _, ok = data["EUR"] + assert.True(t, ok) +} + +func TestAuth_Header(t *testing.T) { + a := NewAuth(AuthHeader, "API-KEY", "key") + req, err := http.NewRequest(http.MethodGet, "http://test", nil) + assert.Nil(t, err) + a.Authenticate(req) + + assert.Equal(t, "key", req.Header.Get("API-KEY")) } \ No newline at end of file diff --git a/examples/apiaggregator/main.go b/examples/apiaggregator/main.go index 5407c62..e223e5c 100755 --- a/examples/apiaggregator/main.go +++ b/examples/apiaggregator/main.go @@ -31,11 +31,11 @@ type Result struct { // - `type` string Aggregation type to use // For example: // { -// "api": [ +// "api": [ // "https://www.bitstamp.net/api/v2/ticker/btcusd/", // "https://api.pro.coinbase.com/products/btc-usd/ticker" -// ], -// "paths": ["$.last", "$.price"], +// ], +// "paths": ["$.last", "$.price"], // "type": "median" // } type APIAggregator struct{} diff --git a/examples/wolframalpha/main.go b/examples/wolframalpha/main.go index 473ed13..9003876 100755 --- a/examples/wolframalpha/main.go +++ b/examples/wolframalpha/main.go @@ -35,7 +35,7 @@ func (cc *WolframAlpha) Run(h *bridge.Helper) (interface{}, error) { "https://api.wolframalpha.com/v1/result", bridge.CallOpts{ Auth: bridge.NewAuth(bridge.AuthParam, "appid", os.Getenv("APP_ID")), - Param: map[string]interface{}{ + Query: map[string]interface{}{ "i": h.GetParam("query"), }, }, diff --git a/json/rapidapi.json b/json/rapidapi.json index 0455250..7bc21ac 100755 --- a/json/rapidapi.json +++ b/json/rapidapi.json @@ -9,7 +9,7 @@ "env": "API_KEY" }, "opts": { - "paramPassthrough": true + "queryPassthrough": true } }, { @@ -22,7 +22,7 @@ "env": "API_KEY" }, "opts": { - "formPassthrough": true + "queryPassthrough": true } } ] \ No newline at end of file diff --git a/main.go b/main.go index 951624a..9efeb43 100755 --- a/main.go +++ b/main.go @@ -69,10 +69,10 @@ func (ja *JSON) Run(h *bridge.Helper) (interface{}, error) { r := make(map[string]interface{}) p := make(map[string]interface{}) f := make(map[string][]string) - for k, v := range ja.bridge.Opts.Param { + for k, v := range ja.bridge.Opts.Query { p[k] = h.GetParam(fmt.Sprintf("%s", v)) } - ja.bridge.Opts.Param = p + ja.bridge.Opts.Query = p for k, s := range ja.bridge.Opts.PostForm { for _, v := range s { f[k] = append(f[k], h.GetParam(v)) @@ -95,7 +95,6 @@ func (ja *JSON) Opts() *bridge.Opts { return &bridge.Opts{ Name: ja.bridge.Name, Path: ja.bridge.Path, - Port: 8080, Lambda: true, } } From 61e19737f293b1be221419c776156ab358e19ccf Mon Sep 17 00:00:00 2001 From: Jonny Huxtable Date: Tue, 16 Apr 2019 00:21:09 +0100 Subject: [PATCH 2/4] Ignore the examples folder from coverage --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100755 index 0000000..640ffb2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "examples" \ No newline at end of file From 6ed8459b66f04e7dcd681a9b1ce4e2be8c8c5c35 Mon Sep 17 00:00:00 2001 From: Jonny Huxtable Date: Tue, 16 Apr 2019 00:50:13 +0100 Subject: [PATCH 3/4] More test coverage --- bridge/bridge.go | 2 - json/invalid.json | 1 + main.go | 9 +---- main_test.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 10 deletions(-) create mode 100755 json/invalid.json diff --git a/bridge/bridge.go b/bridge/bridge.go index b2a58b6..b42af32 100755 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -11,7 +11,6 @@ import ( "gopkg.in/guregu/null.v3" "io/ioutil" "net/http" - "net/url" "os" "time" ) @@ -302,7 +301,6 @@ type CallOpts struct { Query map[string]interface{} `json:"query"` QueryPassthrough bool `json:"queryPassthrough"` Body string `json:"body"` - PostForm url.Values `json:"postForm"` ExpectedCode int `json:"expectedCode"` } diff --git a/json/invalid.json b/json/invalid.json new file mode 100755 index 0000000..81750b9 --- /dev/null +++ b/json/invalid.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/main.go b/main.go index 9efeb43..c6d8f3b 100755 --- a/main.go +++ b/main.go @@ -68,17 +68,10 @@ func NewJSONBridges(uri string) ([]bridge.Bridge, error) { func (ja *JSON) Run(h *bridge.Helper) (interface{}, error) { r := make(map[string]interface{}) p := make(map[string]interface{}) - f := make(map[string][]string) for k, v := range ja.bridge.Opts.Query { p[k] = h.GetParam(fmt.Sprintf("%s", v)) } ja.bridge.Opts.Query = p - for k, s := range ja.bridge.Opts.PostForm { - for _, v := range s { - f[k] = append(f[k], h.GetParam(v)) - } - } - ja.bridge.Opts.PostForm = f var url string if len(ja.bridge.URL) == 0 { url = h.GetParam("url") @@ -102,7 +95,7 @@ func (ja *JSON) Opts() *bridge.Opts { // Handler is the entrypoint for GCP functions func Handler(w http.ResponseWriter, r *http.Request) { env := os.Getenv("BRIDGE") - if len(env) != 0 { + if len(env) == 0 { w.Write([]byte("No bridge set")) return } else if b, err := NewJSONBridges(env); err != nil { diff --git a/main_test.go b/main_test.go index 753d574..2bf2669 100755 --- a/main_test.go +++ b/main_test.go @@ -1,8 +1,14 @@ package main import ( + "bytes" + "encoding/json" "github.com/linkpoolio/bridges/bridge" "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" "testing" ) @@ -55,3 +61,94 @@ func TestNewJSONBridges(t *testing.T) { }) } } + +func TestNewJSONBridges_Errors(t *testing.T) { + _, err := NewJSONBridges("") + assert.Equal(t, "Empty bridge URI given", err.Error()) + + _, err = NewJSONBridges("json/invalid.json") + assert.Equal(t, "unexpected end of JSON input", err.Error()) + + _, err = NewJSONBridges("http://invalidqwerty.com") + assert.Contains(t, err.Error(), "no such host") +} + +func TestHandler(t *testing.T) { + p := map[string]interface{}{ + "jobRunId": "1234", + } + pb, err := json.Marshal(p) + assert.Nil(t, err) + + req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) + assert.Nil(t, err) + rr := httptest.NewRecorder() + + err = os.Setenv("BRIDGE", "json/cryptocompare.json") + assert.Nil(t, err) + Handler(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + err = os.Unsetenv("BRIDGE") + assert.Nil(t, err) + + body, err := ioutil.ReadAll(rr.Body) + assert.Nil(t, err) + json, err := bridge.Parse(body) + assert.Nil(t, err) + + assert.Equal(t, "1234", json.Get("jobRunId").String()) + + data := json.Get("data").Map() + _, ok := data["USD"] + assert.True(t, ok) + _, ok = data["JPY"] + assert.True(t, ok) + _, ok = data["EUR"] + assert.True(t, ok) +} + +func TestHandler_NilBridge(t *testing.T) { + p := map[string]interface{}{ + "jobRunId": "1234", + } + pb, err := json.Marshal(p) + assert.Nil(t, err) + + req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) + assert.Nil(t, err) + rr := httptest.NewRecorder() + + err = os.Setenv("BRIDGE", "") + assert.Nil(t, err) + Handler(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + err = os.Unsetenv("BRIDGE") + assert.Nil(t, err) + + body, err := ioutil.ReadAll(rr.Body) + assert.Nil(t, err) + assert.Equal(t, "No bridge set", string(body)) +} + +func TestHandler_InvalidBridge(t *testing.T) { + p := map[string]interface{}{ + "jobRunId": "1234", + } + pb, err := json.Marshal(p) + assert.Nil(t, err) + + req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(pb)) + assert.Nil(t, err) + rr := httptest.NewRecorder() + + err = os.Setenv("BRIDGE", "json/invalid.json") + assert.Nil(t, err) + Handler(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + err = os.Unsetenv("BRIDGE") + assert.Nil(t, err) + + body, err := ioutil.ReadAll(rr.Body) + assert.Nil(t, err) + assert.Equal(t, "unexpected end of JSON input", string(body)) +} \ No newline at end of file From bb3b8d3de4922a52fc4594626b32a6097b9f3605 Mon Sep 17 00:00:00 2001 From: Jonny Huxtable Date: Tue, 16 Apr 2019 01:04:55 +0100 Subject: [PATCH 4/4] Add another test and example placeholder json --- json/placeholder.json | 12 ++++++++++++ main_test.go | 3 +++ 2 files changed, 15 insertions(+) create mode 100755 json/placeholder.json diff --git a/json/placeholder.json b/json/placeholder.json new file mode 100755 index 0000000..6783051 --- /dev/null +++ b/json/placeholder.json @@ -0,0 +1,12 @@ +[ + { + "name": "Placeholder", + "method": "GET", + "url": "https://reqres.in/api/users", + "opts": { + "query": { + "page": "page" + } + } + } +] \ No newline at end of file diff --git a/main_test.go b/main_test.go index 2bf2669..7227503 100755 --- a/main_test.go +++ b/main_test.go @@ -31,6 +31,9 @@ func TestNewJSONBridges(t *testing.T) { "from_currency": "GBP", "to_currency": "EUR", }, []string{"Error Message"}}, + {"json/placeholder.json", "Placeholder", map[string]interface{}{ + "page": 1, + }, []string{"page", "per_page", "total_pages"}}, } for _, c := range cases { t.Run(c.path, func(t *testing.T) {