From 5ea06268d0536a8f572667d89d077c6609b8376f Mon Sep 17 00:00:00 2001 From: anthdm Date: Thu, 11 Jan 2024 16:00:05 +0100 Subject: [PATCH] added API basic tests --- Makefile | 2 +- cmd/cli/main.go | 7 +- examples/go/main.go | 2 +- internal/api/server.go | 38 ++++++----- internal/api/server_test.go | 107 ++++++++++++++++++++++++++++++- internal/client/client.go | 2 +- internal/shared/shared.go | 10 ++- internal/storage/memory_store.go | 82 +++++++++++++++++++++++ internal/storage/storage.go | 1 - 9 files changed, 224 insertions(+), 27 deletions(-) create mode 100644 internal/storage/memory_store.go diff --git a/Makefile b/Makefile index cd2cf3b..3a9866c 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ api: build test: @./internal/_testdata/build.sh - @go test ./internal/* -v + @go test ./internal/* proto: protoc --go_out=. --go_opt=paths=source_relative --proto_path=$(PROTO_PATH) --proto_path=. proto/types.proto diff --git a/cmd/cli/main.go b/cmd/cli/main.go index c92857d..b3e1f9b 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -13,22 +13,23 @@ import ( "github.com/anthdm/raptor/internal/client" "github.com/anthdm/raptor/internal/config" "github.com/anthdm/raptor/internal/types" + "github.com/anthdm/raptor/internal/version" "github.com/google/uuid" ) func printUsage() { fmt.Printf(` -Raptor cli v0.0.1 +Raptor cli v%s Usage: raptor COMMAND Commands: endpoint Create a new endpoint - publish Publish a specific deployment to your applications endpoint + publish Publish a deployment to an endpoint deploy Create a new deployment help Show usage -`) +`, version.Version) os.Exit(0) } diff --git a/examples/go/main.go b/examples/go/main.go index 66d8276..08b62cd 100644 --- a/examples/go/main.go +++ b/examples/go/main.go @@ -21,5 +21,5 @@ func main() { router := chi.NewMux() router.Get("/dashboard", handleDashboard) router.Get("/login", handleLogin) - raptor.Handle(http.HandlerFunc(handleLogin)) + raptor.Handle(router) } diff --git a/internal/api/server.go b/internal/api/server.go index 41165ec..7b90eee 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -48,7 +48,7 @@ func (s *Server) initRouter() { s.router.Get("/endpoint/{id}/metrics", makeAPIHandler(s.handleGetEndpointMetrics)) s.router.Post("/endpoint", makeAPIHandler(s.handleCreateEndpoint)) s.router.Post("/endpoint/{id}/deployment", makeAPIHandler(s.handleCreateDeployment)) - s.router.Post("/publish/{id}", makeAPIHandler(s.handlePublish)) + s.router.Post("/publish", makeAPIHandler(s.handlePublish)) } func handleStatus(w http.ResponseWriter, r *http.Request) { @@ -115,9 +115,16 @@ func (s *Server) handleCreateDeployment(w http.ResponseWriter, r *http.Request) return writeJSON(w, http.StatusNotFound, ErrorResponse(err)) } + // TODO: + // 1. validate the contents of the blob. + // 2. make sure we have a limit on the maximum blob size. b, err := io.ReadAll(r.Body) if err != nil { - return writeJSON(w, http.StatusNotFound, ErrorResponse(err)) + return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) + } + if len(b) == 0 { + err := fmt.Errorf("no blob") + return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) } deploy := types.NewDeployment(endpoint, b) if err := s.store.CreateDeployment(deploy); err != nil { @@ -139,11 +146,12 @@ func (s *Server) handleGetEndpoint(w http.ResponseWriter, r *http.Request) error } func (s *Server) handleGetEndpoints(w http.ResponseWriter, r *http.Request) error { - endpoints, err := s.store.GetEndpoints() - if err != nil { - return writeJSON(w, http.StatusNotFound, ErrorResponse(err)) - } - return writeJSON(w, http.StatusOK, endpoints) + return nil + // endpoints, err := s.store.GetEndpoints() + // if err != nil { + // return writeJSON(w, http.StatusNotFound, ErrorResponse(err)) + // } + // return writeJSON(w, http.StatusOK, endpoints) } // PublishParams holds all the necessary fields to publish a specific @@ -158,11 +166,12 @@ type PublishResponse struct { } func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request) error { - deployID, err := uuid.Parse(chi.URLParam(r, "id")) - if err != nil { + var params PublishParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + err := fmt.Errorf("failed to parse the response body: %s", err) return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) } - deploy, err := s.store.GetDeployment(deployID) + deploy, err := s.store.GetDeployment(params.DeploymentID) if err != nil { return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) } @@ -173,13 +182,8 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request) error { currentDeploymentID := endpoint.ActiveDeploymentID - var params PublishParams - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) - } - - if currentDeploymentID.String() == params.DeploymentID.String() { - err := fmt.Errorf("deploy %s already active", params.DeploymentID) + if currentDeploymentID.String() == deploy.ID.String() { + err := fmt.Errorf("deploy %s already active", deploy.ID) return writeJSON(w, http.StatusBadRequest, ErrorResponse(err)) } diff --git a/internal/api/server_test.go b/internal/api/server_test.go index cad4c42..3398574 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -1,19 +1,122 @@ package api import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" "testing" + "time" + + "github.com/anthdm/raptor/internal/shared" + "github.com/anthdm/raptor/internal/storage" + "github.com/anthdm/raptor/internal/types" + "github.com/stretchr/testify/require" ) func TestCreateEndpoint(t *testing.T) { + s := createServer() + + name := "My endpoint" + runtime := "go" + environment := map[string]string{"FOO": "BAR"} + + params := CreateEndpointParams{ + Name: name, + Runtime: runtime, + Environment: environment, + } + b, err := json.Marshal(params) + require.Nil(t, err) + + req := httptest.NewRequest("POST", "/endpoint", bytes.NewReader(b)) + resp := httptest.NewRecorder() + s.router.ServeHTTP(resp, req) + + var endpoint types.Endpoint + err = json.NewDecoder(resp.Body).Decode(&endpoint) + require.Nil(t, err) + + require.Equal(t, http.StatusOK, resp.Result().StatusCode) + require.Equal(t, name, endpoint.Name) + require.Equal(t, runtime, endpoint.Runtime) + require.Equal(t, environment, endpoint.Environment) + require.True(t, shared.IsZeroUUID(endpoint.ActiveDeploymentID)) } func TestGetEndpoint(t *testing.T) { + s := createServer() + endpoint := seedEndpoint(t, s) + + req := httptest.NewRequest("GET", "/endpoint/"+endpoint.ID.String(), nil) + resp := httptest.NewRecorder() + s.router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Result().StatusCode) + + var other types.Endpoint + err := json.NewDecoder(resp.Body).Decode(&other) + require.Nil(t, err) + endpoint.CreatedAT = time.Time{} + other.CreatedAT = time.Time{} + require.Equal(t, *endpoint, other) } func TestCreateDeploy(t *testing.T) { + s := createServer() + endpoint := seedEndpoint(t, s) + + req := httptest.NewRequest("POST", "/endpoint/"+endpoint.ID.String()+"/deployment", bytes.NewReader([]byte("a"))) + req.Header.Set("content-type", "application/octet-stream") + resp := httptest.NewRecorder() + s.router.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Result().StatusCode) + + var deploy types.Deployment + require.Nil(t, json.NewDecoder(resp.Body).Decode(&deploy)) + + require.Equal(t, endpoint.ID, deploy.EndpointID) + require.Equal(t, 32, len(deploy.Hash)) +} + +func TestPublish(t *testing.T) { + s := createServer() + endpoint := seedEndpoint(t, s) + deployment := types.NewDeployment(endpoint, []byte("somefakeblob")) + + require.Nil(t, s.store.CreateDeployment(deployment)) + require.True(t, shared.IsZeroUUID(endpoint.ActiveDeploymentID)) + + params := PublishParams{ + DeploymentID: deployment.ID, + } + b, err := json.Marshal(params) + require.Nil(t, err) + + req := httptest.NewRequest("POST", "/publish", bytes.NewReader(b)) + resp := httptest.NewRecorder() + s.router.ServeHTTP(resp, req) + + var publishResp PublishResponse + require.Nil(t, json.NewDecoder(resp.Body).Decode(&publishResp)) + + require.Equal(t, http.StatusOK, resp.Result().StatusCode) + require.Equal(t, deployment.ID, endpoint.ActiveDeploymentID) + require.Equal(t, deployment.ID, publishResp.DeploymentID) + require.Equal(t, "http://0.0.0.0:80/live/"+endpoint.ID.String(), publishResp.URL) } -func TestGetDeploy(t *testing.T) { +func seedEndpoint(t *testing.T, s *Server) *types.Endpoint { + e := types.NewEndpoint("My endpoint", "go", map[string]string{"FOO": "BAR"}) + require.Nil(t, s.store.CreateEndpoint(e)) + return e } -func TestRollback(t *testing.T) {} +func createServer() *Server { + cache := storage.NewDefaultModCache() + store := storage.NewMemoryStore() + s := NewServer(store, store, cache) + s.initRouter() + return s +} diff --git a/internal/client/client.go b/internal/client/client.go index aa59f32..5c5228b 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -45,7 +45,7 @@ func (c *Client) Publish(params api.PublishParams) (*api.PublishResponse, error) if err != nil { return nil, err } - url := fmt.Sprintf("%s/publish/%s", c.config.url, params.DeploymentID) + url := fmt.Sprintf("%s/publish", c.config.url) req, err := http.NewRequest("POST", url, bytes.NewReader(b)) if err != nil { return nil, err diff --git a/internal/shared/shared.go b/internal/shared/shared.go index 0079d4e..628627d 100644 --- a/internal/shared/shared.go +++ b/internal/shared/shared.go @@ -11,9 +11,13 @@ import ( "strings" "github.com/anthdm/raptor/proto" + "github.com/google/uuid" ) -const magicLen = 8 +const ( + magicLen = 8 + UUIDZERO = "00000000-0000-0000-0000-000000000000" +) var errInvalidHTTPResponse = errors.New("invalid HTTP response") @@ -99,3 +103,7 @@ func GetDecodedStdout(runtime string, stdout io.Reader) io.Reader { return stdout } } + +func IsZeroUUID(id uuid.UUID) bool { + return id.String() == UUIDZERO +} diff --git a/internal/storage/memory_store.go b/internal/storage/memory_store.go new file mode 100644 index 0000000..0c379fa --- /dev/null +++ b/internal/storage/memory_store.go @@ -0,0 +1,82 @@ +package storage + +import ( + "fmt" + "sync" + + "github.com/anthdm/raptor/internal/types" + "github.com/google/uuid" +) + +type MemoryStore struct { + mu sync.RWMutex + endpoints map[uuid.UUID]*types.Endpoint + deploys map[uuid.UUID]*types.Deployment +} + +func NewMemoryStore() *MemoryStore { + return &MemoryStore{ + endpoints: make(map[uuid.UUID]*types.Endpoint), + deploys: make(map[uuid.UUID]*types.Deployment), + } +} + +func (s *MemoryStore) CreateEndpoint(e *types.Endpoint) error { + s.mu.Lock() + defer s.mu.Unlock() + s.endpoints[e.ID] = e + return nil +} + +func (s *MemoryStore) GetEndpoint(id uuid.UUID) (*types.Endpoint, error) { + s.mu.RLock() + defer s.mu.RUnlock() + e, ok := s.endpoints[id] + if !ok { + return nil, fmt.Errorf("could not find endpoint with id (%s)", id) + } + return e, nil +} + +func (s *MemoryStore) UpdateEndpoint(id uuid.UUID, params UpdateEndpointParams) error { + endpoint, err := s.GetEndpoint(id) + if err != nil { + return err + } + s.mu.Lock() + defer s.mu.Unlock() + if params.ActiveDeployID.String() != "00000000-0000-0000-0000-000000000000" { + endpoint.ActiveDeploymentID = params.ActiveDeployID + } + if params.Environment != nil { + for key, val := range params.Environment { + endpoint.Environment[key] = val + } + } + return nil +} + +func (s *MemoryStore) CreateDeployment(deploy *types.Deployment) error { + s.mu.Lock() + defer s.mu.Unlock() + s.deploys[deploy.ID] = deploy + return nil +} + +func (s *MemoryStore) GetDeployment(id uuid.UUID) (*types.Deployment, error) { + s.mu.RLock() + defer s.mu.RUnlock() + deploy, ok := s.deploys[id] + if !ok { + return nil, fmt.Errorf("could not find deployment with id (%s)", id) + } + return deploy, nil +} + +func (s *MemoryStore) CreateRuntimeMetric(_ *types.RuntimeMetric) error { + return nil +} + +func (s *MemoryStore) GetRuntimeMetrics(_ uuid.UUID) ([]types.RuntimeMetric, error) { + return nil, nil +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index a57892c..e48fc56 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -9,7 +9,6 @@ type Store interface { CreateEndpoint(*types.Endpoint) error UpdateEndpoint(uuid.UUID, UpdateEndpointParams) error GetEndpoint(uuid.UUID) (*types.Endpoint, error) - GetEndpoints() ([]types.Endpoint, error) CreateDeployment(*types.Deployment) error GetDeployment(uuid.UUID) (*types.Deployment, error) }