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/server.go b/http/server.go index afee4b9217..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{})) @@ -82,6 +83,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}", ccip_handler.ExecCCIP) + ccip.Post("/", ccip_handler.ExecCCIP) + }) api.Route("/p2p", func(p2p chi.Router) { p2p.Route("/replicators", func(p2p_replicators chi.Router) { p2p_replicators.Get("/", store_handler.GetAllReplicators)