diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 709c95a..4e0caf4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,8 @@ jobs: - name: Build run: go build -v ./... - name: Test + env: + RESGATE_TEST_EXTENDED: 1 run: go test -v -covermode=atomic -coverprofile=cover.out -coverpkg=./server/... ./... - name: Install goveralls run: go install github.com/mattn/goveralls@latest diff --git a/scripts/cover.sh b/scripts/cover.sh index ad1bd82..8e08477 100755 --- a/scripts/cover.sh +++ b/scripts/cover.sh @@ -1,5 +1,5 @@ #!/bin/bash -e # Run from directory above via ./scripts/cover.sh -go test -v -covermode=atomic -coverprofile=./cover.out -coverpkg=./server/... ./... +env RESGATE_TEST_EXTENDED=1 go test -v -covermode=atomic -coverprofile=./cover.out -coverpkg=./server/... ./... go tool cover -html=cover.out diff --git a/server/apiHandler.go b/server/apiHandler.go index 0505035..bb4caac 100644 --- a/server/apiHandler.go +++ b/server/apiHandler.go @@ -78,7 +78,7 @@ func (s *Service) apiHandler(w http.ResponseWriter, r *http.Request) { apiPath := s.cfg.APIPath - // NotFound on oaths with trailing slash (unless it is only the APIPath) + // NotFound on paths with trailing slash (unless it is only the APIPath) if len(path) > len(apiPath) && path[len(path)-1] == '/' { notFoundHandler(w, s.enc) return diff --git a/server/httpServer.go b/server/httpServer.go index 725a796..0a0d5e2 100644 --- a/server/httpServer.go +++ b/server/httpServer.go @@ -10,8 +10,8 @@ import ( func (s *Service) initHTTPServer() { } -// startHTTPServer initializes the server and starts a goroutine with a http server -// Service.mu is held when called +// startHTTPServer initializes the server and starts a goroutine with a http +// server Service.mu is held when called. func (s *Service) startHTTPServer() { if s.cfg.NoHTTP { return @@ -60,6 +60,7 @@ func (s *Service) stopHTTPServer() { } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Global OPTIONS handling taken from http.ServeMux if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") diff --git a/server/rescache/legacy_test.go b/server/rescache/legacy_test.go deleted file mode 100644 index 36c3079..0000000 --- a/server/rescache/legacy_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package rescache_test - -import ( - "encoding/json" - "fmt" - "reflect" - "testing" - - "github.com/resgateio/resgate/server/codec" - "github.com/resgateio/resgate/server/rescache" -) - -func TestLegacy120Model_MarshalJSON_ReturnsSoftReferenceAsString(t *testing.T) { - var v map[string]codec.Value - dta := []byte(`{"name":"softparent","child":{"rid":"test.model","soft":true}}`) - expected := []byte(`{"name":"softparent","child":"test.model"}`) - err := json.Unmarshal(dta, &v) - if err != nil { - t.Fatal(err) - } - m := &rescache.Model{Values: v} - lm := (*rescache.Legacy120Model)(m) - - out, err := lm.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - AssertEqualJSON(t, "Legacy120Model.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) -} - -func TestLegacy120Model_MarshalJSON_ReturnsDataValuePlaceholder(t *testing.T) { - var v map[string]codec.Value - dta := []byte(`{"name":"data","primitive":{"data":12},"object":{"data":{"foo":["bar"]}},"array":{"data":[{"foo":"bar"}]}}`) - expected := []byte(`{"name":"data","primitive":12,"object":"[Data]","array":"[Data]"}`) - err := json.Unmarshal(dta, &v) - if err != nil { - t.Fatal(err) - } - m := &rescache.Model{Values: v} - lm := (*rescache.Legacy120Model)(m) - - out, err := lm.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - AssertEqualJSON(t, "Legacy120Model.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) -} - -func TestLegacy120Collection_MarshalJSON_ReturnsSoftReferenceAsString(t *testing.T) { - var v []codec.Value - dta := []byte(`["softparent",{"rid":"test.model","soft":true}]`) - expected := []byte(`["softparent","test.model"]`) - err := json.Unmarshal(dta, &v) - if err != nil { - t.Fatal(err) - } - m := &rescache.Collection{Values: v} - lm := (*rescache.Legacy120Collection)(m) - - out, err := lm.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - AssertEqualJSON(t, "Legacy120Collection.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) -} - -func TestLegacy120Collection_MarshalJSON_ReturnsDataValuePlaceholder(t *testing.T) { - var v []codec.Value - dta := []byte(`["data",{"data":12},{"data":{"foo":["bar"]}},{"data":[{"foo":"bar"}]}]`) - expected := []byte(`["data",12,"[Data]","[Data]"]`) - err := json.Unmarshal(dta, &v) - if err != nil { - t.Fatal(err) - } - m := &rescache.Collection{Values: v} - lm := (*rescache.Legacy120Collection)(m) - - out, err := lm.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - AssertEqualJSON(t, "Legacy120Collection.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) -} - -func TestLegacy120Value_MarshalJSON_ReturnsSoftReferenceAsString(t *testing.T) { - var v codec.Value - dta := []byte(`{"rid":"test.model","soft":true}`) - expected := []byte(`"test.model"`) - err := json.Unmarshal(dta, &v) - if err != nil { - t.Fatal(err) - } - lv := rescache.Legacy120Value(v) - - out, err := lv.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - AssertEqualJSON(t, "Legacy120Value.MarshalJSON", json.RawMessage(out), json.RawMessage(expected)) -} - -// AssertEqualJSON expects that a and b json marshals into equal values, and -// returns true if they do, otherwise logs a fatal error and returns false. -func AssertEqualJSON(t *testing.T, name string, result, expected interface{}, ctx ...interface{}) bool { - aa, aj := jsonMap(t, result) - bb, bj := jsonMap(t, expected) - - if !reflect.DeepEqual(aa, bb) { - t.Fatalf("expected %s to be:\n\t%s\nbut got:\n\t%s%s", name, bj, aj, ctxString(ctx)) - return false - } - - return true -} - -func ctxString(ctx []interface{}) string { - if len(ctx) == 0 { - return "" - } - return "\nin " + fmt.Sprint(ctx...) -} - -func jsonMap(t *testing.T, v interface{}) (interface{}, []byte) { - var err error - j, err := json.Marshal(v) - if err != nil { - panic(fmt.Sprintf("test: error marshaling value:\n\t%+v\nerror:\n\t%s", v, err)) - } - - var m interface{} - err = json.Unmarshal(j, &m) - if err != nil { - panic(fmt.Sprintf("test: error unmarshaling value:\n\t%s\nerror:\n\t%s", j, err)) - } - - return m, j -} diff --git a/server/rescache/resourcePattern.go b/server/rescache/resourcePattern.go index 79d1967..bc4bb2b 100644 --- a/server/rescache/resourcePattern.go +++ b/server/rescache/resourcePattern.go @@ -6,63 +6,45 @@ type ResourcePattern struct { hasWild bool } -const ( - pwc = '*' - fwc = '>' - btsep = '.' -) - -// ParseResourcePattern parses a string as a resource pattern. -// It uses the same wildcard matching as used in NATS -func ParseResourcePattern(pattern string) ResourcePattern { - p := ResourcePattern{ - pattern: pattern, - } - - plen := len(pattern) - if plen == 0 { +// ParseResourcePattern parses a string as a resource pattern p. It uses the +// same wildcard matching as used in NATS. +func ParseResourcePattern(p string) ResourcePattern { + l := len(p) + if l == 0 || p[l-1] == '.' { return ResourcePattern{} } - - var c byte - tcount := 0 - offset := 0 + start := true + alone := false hasWild := false - for i := 0; i <= plen; i++ { - if i == plen { - c = btsep + for i, c := range p { + if c == '.' { + if start { + return ResourcePattern{} + } + alone = false + start = true } else { - c = pattern[i] - } - - switch c { - case btsep: - // Empty tokens are invalid - if offset == i { + if alone || c < 33 || c > 126 || c == '?' { return ResourcePattern{} } - if hasWild { - if i-offset > 1 { + switch c { + case '>': + if !start || i < l-1 { return ResourcePattern{} } - hasWild = false - } - offset = i + 1 - tcount++ - case pwc: - p.hasWild = true - hasWild = true - case fwc: - // If wildcard isn't the last char - if i < plen-1 { - return ResourcePattern{} + hasWild = true + case '*': + if !start { + return ResourcePattern{} + } + hasWild = true + alone = true } - p.hasWild = true - hasWild = true + start = false } } - return p + return ResourcePattern{pattern: p, hasWild: hasWild} } // IsValid reports whether the resource pattern is valid @@ -70,9 +52,10 @@ func (p ResourcePattern) IsValid() bool { return len(p.pattern) > 0 } -// Match reports whether a resource name, s, matches the resource pattern +// Match reports whether a resource name, s, matches the resource pattern. func (p ResourcePattern) Match(s string) bool { - if len(p.pattern) == 0 { + plen := len(p.pattern) + if plen == 0 { return false } @@ -81,7 +64,6 @@ func (p ResourcePattern) Match(s string) bool { } slen := len(s) - plen := len(p.pattern) if plen > slen { return false @@ -91,12 +73,12 @@ func (p ResourcePattern) Match(s string) bool { pi := 0 for { switch p.pattern[pi] { - case fwc: + case '>': return true - case pwc: + case '*': pi++ for { - if s[si] == btsep { + if s[si] == '.' { break } si++ diff --git a/server/wsConn.go b/server/wsConn.go index d0c3f63..04bda25 100644 --- a/server/wsConn.go +++ b/server/wsConn.go @@ -290,33 +290,6 @@ func (c *wsConn) SetVersion(protocol string) (string, error) { return ProtocolVersion, nil } -func (c *wsConn) GetSubscription(rid string, cb func(sub *Subscription, err error)) { - sub, err := c.Subscribe(rid, true, nil) - if err != nil { - cb(nil, err) - return - } - - sub.CanGet(func(err error) { - if err != nil { - cb(nil, err) - c.Unsubscribe(sub, true, false, 1, true) - return - } - - sub.OnReady(func() { - err := sub.Error() - if err != nil { - cb(nil, err) - return - } - cb(sub, nil) - sub.ReleaseRPCResources() - c.Unsubscribe(sub, true, false, 1, true) - }) - }) -} - // GetHTTPSubscription is called from apiHandler on a HTTP GET request. It // differs from GetSubscription by making an access call separately, and not // within the subscription, in order to call access with isHTTP set to true. diff --git a/test/00connect_test.go b/test/00connect_test.go index 73fae5b..cb60e2c 100644 --- a/test/00connect_test.go +++ b/test/00connect_test.go @@ -3,6 +3,7 @@ package test import ( "fmt" "net/http" + "os" "testing" "github.com/resgateio/resgate/server" @@ -65,3 +66,26 @@ func TestConnect_AllowOrigin_Connects(t *testing.T) { }) } } + +// Test that the server responds with 400 on request-target being *. +func TestStart_WithAsteriskAsRequestURI_BadRequestStatusResponse(t *testing.T) { + runTest(t, func(s *Session) { + hreq := s.HTTPRequest("GET", "", nil, func(r *http.Request) { + r.RequestURI = "*" + }) + hreq.GetResponse(t).AssertStatusCode(t, http.StatusBadRequest) + }) +} + +// Test that the server starts and stops without error when enabling the HTTP server +func TestStart_WithHTTPServer_NoErrors(t *testing.T) { + ref := os.Getenv("RESGATE_TEST_EXTENDED") + if ref == "" { + t.Skip("no RESGATE_TEST_EXTENDED environment value") + } + runTest(t, func(s *Session) {}, func(cfg *server.Config) { + cfg.NoHTTP = false + cfg.Port = 58080 + cfg.MetricsPort = 58090 + }) +} diff --git a/test/14http_get_test.go b/test/14http_get_test.go index 5ad0423..d2d81b0 100644 --- a/test/14http_get_test.go +++ b/test/14http_get_test.go @@ -26,23 +26,29 @@ func TestHTTPGetInvalidURLs(t *testing.T) { {"/api/test/mådel/action", http.StatusNotFound, reserr.ErrNotFound}, } - for i, l := range tbl { - runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { - hreq := s.HTTPRequest("GET", l.URL, nil) - hresp := hreq. - GetResponse(t). - AssertStatusCode(t, l.ExpectedCode) - - if l.Expected != nil { - if err, ok := l.Expected.(*reserr.Error); ok { - hresp.AssertError(t, err) - } else if code, ok := l.Expected.(string); ok { - hresp.AssertErrorCode(t, code) - } else { - hresp.AssertBody(t, l.Expected) + encodings := []string{"json", "jsonFlat"} + + for _, enc := range encodings { + for i, l := range tbl { + runNamedTest(t, fmt.Sprintf("#%d (%s)", i+1, enc), func(s *Session) { + hreq := s.HTTPRequest("GET", l.URL, nil) + hresp := hreq. + GetResponse(t). + AssertStatusCode(t, l.ExpectedCode) + + if l.Expected != nil { + if err, ok := l.Expected.(*reserr.Error); ok { + hresp.AssertError(t, err) + } else if code, ok := l.Expected.(string); ok { + hresp.AssertErrorCode(t, code) + } else { + hresp.AssertBody(t, l.Expected) + } } - } - }) + }, func(c *server.Config) { + c.APIEncoding = enc + }) + } } } diff --git a/test/15http_post_test.go b/test/15http_post_test.go index bd3f642..7bc4d81 100644 --- a/test/15http_post_test.go +++ b/test/15http_post_test.go @@ -13,30 +13,36 @@ import ( // Test response to a HTTP POST request to a primitive query model method func TestHTTPPostOnPrimitiveQueryModel(t *testing.T) { - runTest(t, func(s *Session) { - successResponse := json.RawMessage(`{"foo":"bar"}`) + encodings := []string{"json", "jsonFlat"} - hreq := s.HTTPRequest("POST", "/api/test/model/method?q=foo&f=bar", nil) + for _, enc := range encodings { + runNamedTest(t, enc, func(s *Session) { + successResponse := json.RawMessage(`{"foo":"bar"}`) - // Handle query model access request - s. - GetRequest(t). - AssertSubject(t, "access.test.model"). - AssertPathPayload(t, "token", nil). - AssertPathPayload(t, "query", "q=foo&f=bar"). - AssertPathPayload(t, "isHttp", true). - RespondSuccess(json.RawMessage(`{"call":"method"}`)) - // Handle query model call request - s. - GetRequest(t). - AssertSubject(t, "call.test.model.method"). - AssertPathPayload(t, "query", "q=foo&f=bar"). - AssertPathPayload(t, "isHttp", true). - RespondSuccess(successResponse) + hreq := s.HTTPRequest("POST", "/api/test/model/method?q=foo&f=bar", nil) - // Validate http response - hreq.GetResponse(t).Equals(t, http.StatusOK, successResponse) - }) + // Handle query model access request + s. + GetRequest(t). + AssertSubject(t, "access.test.model"). + AssertPathPayload(t, "token", nil). + AssertPathPayload(t, "query", "q=foo&f=bar"). + AssertPathPayload(t, "isHttp", true). + RespondSuccess(json.RawMessage(`{"call":"method"}`)) + // Handle query model call request + s. + GetRequest(t). + AssertSubject(t, "call.test.model.method"). + AssertPathPayload(t, "query", "q=foo&f=bar"). + AssertPathPayload(t, "isHttp", true). + RespondSuccess(successResponse) + + // Validate http response + hreq.GetResponse(t).Equals(t, http.StatusOK, successResponse) + }, func(c *server.Config) { + c.APIEncoding = enc + }) + } } // Test responses to HTTP post requests @@ -86,52 +92,59 @@ func TestHTTPPostResponses(t *testing.T) { {nil, fullCallAccess, []byte(`{"resource":{"rid":"test..model"}}`), http.StatusInternalServerError, nil, reserr.CodeInternalError}, } - for i, l := range tbl { - runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) { - // Send HTTP post request - hreq := s.HTTPRequest("POST", "/api/test/model/method", l.Params) + encodings := []string{"json", "jsonFlat"} - req := s.GetRequest(t) - req.AssertSubject(t, "access.test.model") - req.AssertPathPayload(t, "isHttp", true) - if l.AccessResponse == nil { - req.Timeout() - } else if err, ok := l.AccessResponse.(*reserr.Error); ok { - req.RespondError(err) - } else { - req.RespondSuccess(l.AccessResponse) - } + for _, enc := range encodings { + for i, l := range tbl { + runNamedTest(t, fmt.Sprintf("#%d (%s)", i+1, enc), func(s *Session) { + // Send HTTP post request + hreq := s.HTTPRequest("POST", "/api/test/model/method", l.Params) - if l.CallResponse != noRequest { - // Get call request - req = s.GetRequest(t) - req.AssertSubject(t, "call.test.model.method") - req.AssertPathPayload(t, "params", json.RawMessage(l.Params)) - if l.CallResponse == requestTimeout { + req := s.GetRequest(t) + req.AssertSubject(t, "access.test.model") + req.AssertPathPayload(t, "isHttp", true) + if l.AccessResponse == nil { req.Timeout() - } else if err, ok := l.CallResponse.(*reserr.Error); ok { + } else if err, ok := l.AccessResponse.(*reserr.Error); ok { req.RespondError(err) - } else if raw, ok := l.CallResponse.([]byte); ok { - req.RespondRaw(raw) } else { - req.RespondSuccess(l.CallResponse) + req.RespondSuccess(l.AccessResponse) } - } - // Validate client response - hresp := hreq.GetResponse(t) - hresp.AssertStatusCode(t, l.ExpectedCode) - if err, ok := l.Expected.(*reserr.Error); ok { - hresp.AssertError(t, err) - } else if code, ok := l.Expected.(string); ok { - hresp.AssertErrorCode(t, code) - } else { - hresp.AssertBody(t, l.Expected) - } + if l.CallResponse != noRequest { + // Get call request + req = s.GetRequest(t) + req.AssertSubject(t, "call.test.model.method") + req.AssertPathPayload(t, "params", json.RawMessage(l.Params)) + if l.CallResponse == requestTimeout { + req.Timeout() + } else if err, ok := l.CallResponse.(*reserr.Error); ok { + req.RespondError(err) + } else if raw, ok := l.CallResponse.([]byte); ok { + req.RespondRaw(raw) + } else { + req.RespondSuccess(l.CallResponse) + } + } - // Validate headers - hresp.AssertHeaders(t, l.ExpectedHeaders) - }) + // Validate client response + hresp := hreq.GetResponse(t) + hresp.AssertStatusCode(t, l.ExpectedCode) + if err, ok := l.Expected.(*reserr.Error); ok { + hresp.AssertError(t, err) + } else if code, ok := l.Expected.(string); ok { + hresp.AssertErrorCode(t, code) + } else { + hresp.AssertBody(t, l.Expected) + } + + // Validate headers + hresp.AssertHeaders(t, l.ExpectedHeaders) + + }, func(c *server.Config) { + c.APIEncoding = enc + }) + } } } diff --git a/test/33metrics_test.go b/test/33metrics_test.go index a20cdd8..fc71b35 100644 --- a/test/33metrics_test.go +++ b/test/33metrics_test.go @@ -371,6 +371,7 @@ func TestMetrics_CacheResources_ExpectedGaugeValues(t *testing.T) { Actions: func(t *testing.T, s *Session, c *Conn) { subscribeToTestModel(t, s, c) c.Request("unsubscribe.test.model", nil).GetResponse(t) + c.AssertNoEvent(t, "test.model") }, ExpectedResources: 1, ExpectedSubscriptions: 0, @@ -380,6 +381,7 @@ func TestMetrics_CacheResources_ExpectedGaugeValues(t *testing.T) { Actions: func(t *testing.T, s *Session, c *Conn) { subscribeToTestModelParent(t, s, c, false) c.Request("unsubscribe.test.model.parent", nil).GetResponse(t) + c.AssertNoEvent(t, "test.model.parent") }, ExpectedResources: 2, ExpectedSubscriptions: 0, @@ -390,6 +392,7 @@ func TestMetrics_CacheResources_ExpectedGaugeValues(t *testing.T) { subscribeToTestModel(t, s, c) subscribeToTestModelParent(t, s, c, true) c.Request("unsubscribe.test.model", nil).GetResponse(t) + c.AssertNoEvent(t, "test.model") }, ExpectedResources: 2, ExpectedSubscriptions: 2, @@ -400,6 +403,7 @@ func TestMetrics_CacheResources_ExpectedGaugeValues(t *testing.T) { subscribeToTestModel(t, s, c) subscribeToTestModelParent(t, s, c, true) c.Request("unsubscribe.test.model.parent", nil).GetResponse(t) + c.AssertNoEvent(t, "test.model.parent") }, ExpectedResources: 2, ExpectedSubscriptions: 1,