From c873a0747a9055ab1983ca450a624ee85132b1a2 Mon Sep 17 00:00:00 2001 From: Keenan Nemetz Date: Mon, 18 Sep 2023 11:02:13 -0700 Subject: [PATCH 1/2] add ccip http handler --- http/handler_store.go | 52 +++++++++++++++++++++++++++++++++++++++++++ http/server.go | 4 ++++ 2 files changed, 56 insertions(+) diff --git a/http/handler_store.go b/http/handler_store.go index d0cbdf42d2..b8a8107a4b 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -12,10 +12,12 @@ package http import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "io" "net/http" + "strings" "github.com/go-chi/chi/v5" @@ -330,3 +332,53 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { } } } + +type CCIPRequest struct { + Sender string `json:"sender"` + Data string `json:"data"` +} + +type CCIPResponse struct { + Data string `json:"data"` +} + +// ExecCCIP handles GraphQL over Cross Chain Interoperability Protocol requests. +func (s *storeHandler) ExecCCIP(rw http.ResponseWriter, req *http.Request) { + store := req.Context().Value(storeContextKey).(client.Store) + + var ccipReq CCIPRequest + switch req.Method { + case http.MethodGet: + ccipReq.Sender = chi.URLParam(req, "sender") + ccipReq.Data = chi.URLParam(req, "data") + case http.MethodPost: + if err := requestJSON(req, &ccipReq); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + } + + data, err := hex.DecodeString(strings.TrimPrefix(ccipReq.Data, "0x")) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + var request GraphQLRequest + if err := json.Unmarshal(data, &request); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + result := store.ExecRequest(req.Context(), request.Query) + if result.Pub != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{ErrStreamingNotSupported}) + return + } + resultJSON, err := json.Marshal(result.GQL) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + resultHex := "0x" + hex.EncodeToString(resultJSON) + responseJSON(rw, http.StatusOK, CCIPResponse{Data: resultHex}) +} diff --git a/http/server.go b/http/server.go index afee4b9217..ccc343fe48 100644 --- a/http/server.go +++ b/http/server.go @@ -82,6 +82,10 @@ func NewServer(db client.DB) *Server { graphQL.Get("/", store_handler.ExecRequest) graphQL.Post("/", store_handler.ExecRequest) }) + api.Route("/ccip", func(ccip chi.Router) { + ccip.Get("/{sender}/{data}", store_handler.ExecCCIP) + ccip.Post("/", store_handler.ExecCCIP) + }) api.Route("/p2p", func(p2p chi.Router) { p2p.Route("/replicators", func(p2p_replicators chi.Router) { p2p_replicators.Get("/", store_handler.GetAllReplicators) From 7ff71779e66ed27c7ac0cd32f59dcb9c63f85acf Mon Sep 17 00:00:00 2001 From: Keenan Nemetz Date: Mon, 18 Sep 2023 16:27:05 -0700 Subject: [PATCH 2/2] move ccip handler to handler_ccip.go. add ccip handler tests --- http/handler_ccip.go | 74 ++++++++++++++ http/handler_ccip_test.go | 207 ++++++++++++++++++++++++++++++++++++++ http/handler_store.go | 52 ---------- http/server.go | 5 +- 4 files changed, 284 insertions(+), 54 deletions(-) create mode 100644 http/handler_ccip.go create mode 100644 http/handler_ccip_test.go diff --git a/http/handler_ccip.go b/http/handler_ccip.go new file mode 100644 index 0000000000..a0d1af7823 --- /dev/null +++ b/http/handler_ccip.go @@ -0,0 +1,74 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package http + +import ( + "encoding/hex" + "encoding/json" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + + "github.com/sourcenetwork/defradb/client" +) + +type ccipHandler struct{} + +type CCIPRequest struct { + Sender string `json:"sender"` + Data string `json:"data"` +} + +type CCIPResponse struct { + Data string `json:"data"` +} + +// ExecCCIP handles GraphQL over Cross Chain Interoperability Protocol requests. +func (c *ccipHandler) ExecCCIP(rw http.ResponseWriter, req *http.Request) { + store := req.Context().Value(storeContextKey).(client.Store) + + var ccipReq CCIPRequest + switch req.Method { + case http.MethodGet: + ccipReq.Sender = chi.URLParam(req, "sender") + ccipReq.Data = chi.URLParam(req, "data") + case http.MethodPost: + if err := requestJSON(req, &ccipReq); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + } + + data, err := hex.DecodeString(strings.TrimPrefix(ccipReq.Data, "0x")) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + var request GraphQLRequest + if err := json.Unmarshal(data, &request); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + result := store.ExecRequest(req.Context(), request.Query) + if result.Pub != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{ErrStreamingNotSupported}) + return + } + resultJSON, err := json.Marshal(result.GQL) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + resultHex := "0x" + hex.EncodeToString(resultJSON) + responseJSON(rw, http.StatusOK, CCIPResponse{Data: resultHex}) +} diff --git a/http/handler_ccip_test.go b/http/handler_ccip_test.go new file mode 100644 index 0000000000..7884e16df7 --- /dev/null +++ b/http/handler_ccip_test.go @@ -0,0 +1,207 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package http + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/datastore/memory" + "github.com/sourcenetwork/defradb/db" +) + +func TestCCIPGet_WithValidData(t *testing.T) { + cdb := setupDatabase(t) + + gqlData, err := json.Marshal(&GraphQLRequest{ + Query: `query { + User { + name + } + }`, + }) + require.NoError(t, err) + + data := "0x" + hex.EncodeToString([]byte(gqlData)) + sender := "0x0000000000000000000000000000000000000000" + url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data) + + req := httptest.NewRequest(http.MethodGet, url, nil) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var ccipRes CCIPResponse + err = json.Unmarshal(resData, &ccipRes) + require.NoError(t, err) + + resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) + require.NoError(t, err) + + assert.JSONEq(t, `{"data": [{"name": "bob"}]}`, string(resHex)) +} + +func TestCCIPGet_WithSubscription(t *testing.T) { + cdb := setupDatabase(t) + + gqlData, err := json.Marshal(&GraphQLRequest{ + Query: `subscription { + User { + name + } + }`, + }) + require.NoError(t, err) + + data := "0x" + hex.EncodeToString([]byte(gqlData)) + sender := "0x0000000000000000000000000000000000000000" + url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data) + + req := httptest.NewRequest(http.MethodGet, url, nil) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, 400, res.StatusCode) +} + +func TestCCIPGet_WithInvalidData(t *testing.T) { + cdb := setupDatabase(t) + + data := "invalid_hex_data" + sender := "0x0000000000000000000000000000000000000000" + url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data) + + req := httptest.NewRequest(http.MethodGet, url, nil) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, 400, res.StatusCode) +} + +func TestCCIPPost_WithValidData(t *testing.T) { + cdb := setupDatabase(t) + + gqlJSON, err := json.Marshal(&GraphQLRequest{ + Query: `query { + User { + name + } + }`, + }) + require.NoError(t, err) + + body, err := json.Marshal(&CCIPRequest{ + Data: "0x" + hex.EncodeToString([]byte(gqlJSON)), + Sender: "0x0000000000000000000000000000000000000000", + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body)) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var ccipRes CCIPResponse + err = json.Unmarshal(resData, &ccipRes) + require.NoError(t, err) + + resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) + require.NoError(t, err) + + assert.JSONEq(t, `{"data": [{"name": "bob"}]}`, string(resHex)) +} + +func TestCCIPPost_WithInvalidGraphQLRequest(t *testing.T) { + cdb := setupDatabase(t) + + body, err := json.Marshal(&CCIPRequest{ + Data: "0x" + hex.EncodeToString([]byte("invalid_graphql_request")), + Sender: "0x0000000000000000000000000000000000000000", + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body)) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, 400, res.StatusCode) +} + +func TestCCIPPost_WithInvalidBody(t *testing.T) { + cdb := setupDatabase(t) + + req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", nil) + rec := httptest.NewRecorder() + + handler := NewServer(cdb) + handler.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, 400, res.StatusCode) +} + +func setupDatabase(t *testing.T) client.DB { + ctx := context.Background() + + cdb, err := db.NewDB(ctx, memory.NewDatastore(ctx), db.WithUpdateEvents()) + require.NoError(t, err) + + _, err = cdb.AddSchema(ctx, `type User { + name: String + }`) + require.NoError(t, err) + + col, err := cdb.GetCollectionByName(ctx, "User") + require.NoError(t, err) + + doc, err := client.NewDocFromJSON([]byte(`{"name": "bob"}`)) + require.NoError(t, err) + + err = col.Create(ctx, doc) + require.NoError(t, err) + + return cdb +} diff --git a/http/handler_store.go b/http/handler_store.go index b8a8107a4b..d0cbdf42d2 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -12,12 +12,10 @@ package http import ( "bytes" - "encoding/hex" "encoding/json" "fmt" "io" "net/http" - "strings" "github.com/go-chi/chi/v5" @@ -332,53 +330,3 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { } } } - -type CCIPRequest struct { - Sender string `json:"sender"` - Data string `json:"data"` -} - -type CCIPResponse struct { - Data string `json:"data"` -} - -// ExecCCIP handles GraphQL over Cross Chain Interoperability Protocol requests. -func (s *storeHandler) ExecCCIP(rw http.ResponseWriter, req *http.Request) { - store := req.Context().Value(storeContextKey).(client.Store) - - var ccipReq CCIPRequest - switch req.Method { - case http.MethodGet: - ccipReq.Sender = chi.URLParam(req, "sender") - ccipReq.Data = chi.URLParam(req, "data") - case http.MethodPost: - if err := requestJSON(req, &ccipReq); err != nil { - responseJSON(rw, http.StatusBadRequest, errorResponse{err}) - return - } - } - - data, err := hex.DecodeString(strings.TrimPrefix(ccipReq.Data, "0x")) - if err != nil { - responseJSON(rw, http.StatusBadRequest, errorResponse{err}) - return - } - var request GraphQLRequest - if err := json.Unmarshal(data, &request); err != nil { - responseJSON(rw, http.StatusBadRequest, errorResponse{err}) - return - } - - result := store.ExecRequest(req.Context(), request.Query) - if result.Pub != nil { - responseJSON(rw, http.StatusBadRequest, errorResponse{ErrStreamingNotSupported}) - return - } - resultJSON, err := json.Marshal(result.GQL) - if err != nil { - responseJSON(rw, http.StatusBadRequest, errorResponse{err}) - return - } - resultHex := "0x" + hex.EncodeToString(resultJSON) - responseJSON(rw, http.StatusOK, CCIPResponse{Data: resultHex}) -} diff --git a/http/server.go b/http/server.go index ccc343fe48..92da350aa1 100644 --- a/http/server.go +++ b/http/server.go @@ -33,6 +33,7 @@ func NewServer(db client.DB) *Server { store_handler := &storeHandler{} collection_handler := &collectionHandler{} lens_handler := &lensHandler{} + ccip_handler := &ccipHandler{} router := chi.NewRouter() router.Use(middleware.RequestLogger(&logFormatter{})) @@ -83,8 +84,8 @@ func NewServer(db client.DB) *Server { graphQL.Post("/", store_handler.ExecRequest) }) api.Route("/ccip", func(ccip chi.Router) { - ccip.Get("/{sender}/{data}", store_handler.ExecCCIP) - ccip.Post("/", store_handler.ExecCCIP) + ccip.Get("/{sender}/{data}", ccip_handler.ExecCCIP) + ccip.Post("/", ccip_handler.ExecCCIP) }) api.Route("/p2p", func(p2p chi.Router) { p2p.Route("/replicators", func(p2p_replicators chi.Router) {