diff --git a/trigger/http/approvals_endpoint.go b/trigger/http/approvals_endpoint.go index 35edb60e..723716f5 100644 --- a/trigger/http/approvals_endpoint.go +++ b/trigger/http/approvals_endpoint.go @@ -5,9 +5,15 @@ import ( "fmt" "net/http" + "github.com/keel-hq/keel/cache" "github.com/keel-hq/keel/types" ) +type approveRequest struct { + Identifier string `json:"identifier"` + Voter string `json:"voter"` +} + func (s *TriggerServer) approvalsHandler(resp http.ResponseWriter, req *http.Request) { // unknown lists all approvals, err := s.approvalsManager.List() @@ -31,6 +37,46 @@ func (s *TriggerServer) approvalsHandler(resp http.ResponseWriter, req *http.Req resp.Write(bts) } +func (s *TriggerServer) approvalApproveHandler(resp http.ResponseWriter, req *http.Request) { + + var ar approveRequest + dec := json.NewDecoder(req.Body) + defer req.Body.Close() + + err := dec.Decode(&ar) + if err != nil { + fmt.Fprintf(resp, "%s", err) + resp.WriteHeader(http.StatusBadRequest) + return + } + + if ar.Identifier == "" { + http.Error(resp, "identifier not supplied", http.StatusBadRequest) + return + } + + approval, err := s.approvalsManager.Approve(ar.Identifier, ar.Voter) + if err != nil { + if err == cache.ErrNotFound { + http.Error(resp, fmt.Sprintf("approval '%s' not found", ar.Identifier), http.StatusNotFound) + return + } + + fmt.Fprintf(resp, "%s", err) + resp.WriteHeader(http.StatusInternalServerError) + return + } + + bts, err := json.Marshal(&approval) + if err != nil { + fmt.Fprintf(resp, "%s", err) + resp.WriteHeader(http.StatusInternalServerError) + return + } + + resp.Write(bts) +} + func (s *TriggerServer) approvalDeleteHandler(resp http.ResponseWriter, req *http.Request) { identifier := getID(req) diff --git a/trigger/http/approvals_endpoint_test.go b/trigger/http/approvals_endpoint_test.go index b0b74378..898e74bf 100644 --- a/trigger/http/approvals_endpoint_test.go +++ b/trigger/http/approvals_endpoint_test.go @@ -1,6 +1,7 @@ package http import ( + "bytes" "encoding/json" "net/http" "net/http/httptest" @@ -109,5 +110,202 @@ func TestDeleteApproval(t *testing.T) { if err == nil { t.Errorf("expected approval to be deleted") } - +} + +func TestApprove(t *testing.T) { + fp := &fakeProvider{} + mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second) + am := approvals.New(mem, codecs.DefaultSerializer()) + providers := provider.New([]provider.Provider{fp}, am) + srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am}) + srv.registerRoutes(srv.router) + + err := am.Create(&types.Approval{ + Identifier: "12345", + VotesRequired: 5, + NewVersion: "2.0.0", + CurrentVersion: "1.0.0", + }) + + if err != nil { + t.Fatalf("failed to create approval: %s", err) + } + + // listing + req, err := http.NewRequest("POST", "/v1/approvals", bytes.NewBufferString(`{"identifier":"12345", "voter": "foo"}`)) + if err != nil { + t.Fatalf("failed to create req: %s", err) + } + + rec := httptest.NewRecorder() + + srv.router.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Errorf("unexpected status code: %d", rec.Code) + + t.Log(rec.Body.String()) + } + + approved, err := am.Get("12345") + if err != nil { + t.Fatalf("failed to get approval: %s", err) + } + + if approved.VotesReceived != 1 { + t.Errorf("expected to find one voter") + } + + if approved.Voters[0] != "foo" { + t.Errorf("unexpected voter: %s", approved.Voters[0]) + } +} + +func TestApproveNotFound(t *testing.T) { + fp := &fakeProvider{} + mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second) + am := approvals.New(mem, codecs.DefaultSerializer()) + providers := provider.New([]provider.Provider{fp}, am) + srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am}) + srv.registerRoutes(srv.router) + + // listing + req, err := http.NewRequest("POST", "/v1/approvals", bytes.NewBufferString(`{"identifier":"12345", "voter": "foo"}`)) + if err != nil { + t.Fatalf("failed to create req: %s", err) + } + + rec := httptest.NewRecorder() + + srv.router.ServeHTTP(rec, req) + if rec.Code != 404 { + t.Errorf("unexpected status code: %d", rec.Code) + + t.Log(rec.Body.String()) + } +} + +func TestApproveGarbageRequest(t *testing.T) { + fp := &fakeProvider{} + mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second) + am := approvals.New(mem, codecs.DefaultSerializer()) + providers := provider.New([]provider.Provider{fp}, am) + srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am}) + srv.registerRoutes(srv.router) + + // listing + req, err := http.NewRequest("POST", "/v1/approvals", bytes.NewBufferString(`{"foo":"bar"}`)) + if err != nil { + t.Fatalf("failed to create req: %s", err) + } + + rec := httptest.NewRecorder() + + srv.router.ServeHTTP(rec, req) + if rec.Code != 400 { + t.Errorf("unexpected status code: %d", rec.Code) + + t.Log(rec.Body.String()) + } +} + +func TestSameVoter(t *testing.T) { + fp := &fakeProvider{} + mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second) + am := approvals.New(mem, codecs.DefaultSerializer()) + providers := provider.New([]provider.Provider{fp}, am) + srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am}) + srv.registerRoutes(srv.router) + + err := am.Create(&types.Approval{ + Identifier: "12345", + VotesRequired: 5, + NewVersion: "2.0.0", + CurrentVersion: "1.0.0", + VotesReceived: 1, + Voters: []string{"foo"}, + }) + + if err != nil { + t.Fatalf("failed to create approval: %s", err) + } + + // listing + req, err := http.NewRequest("POST", "/v1/approvals", bytes.NewBufferString(`{"identifier":"12345", "voter": "foo"}`)) + if err != nil { + t.Fatalf("failed to create req: %s", err) + } + + rec := httptest.NewRecorder() + + srv.router.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Errorf("unexpected status code: %d", rec.Code) + + t.Log(rec.Body.String()) + } + + approved, err := am.Get("12345") + if err != nil { + t.Fatalf("failed to get approval: %s", err) + } + + if approved.VotesReceived != 1 { + t.Errorf("expected to find one voter") + } + + if approved.Voters[0] != "foo" { + t.Errorf("unexpected voter: %s", approved.Voters[0]) + } +} +func TestDifferentVoter(t *testing.T) { + fp := &fakeProvider{} + mem := memory.NewMemoryCache(100*time.Second, 100*time.Second, 10*time.Second) + am := approvals.New(mem, codecs.DefaultSerializer()) + providers := provider.New([]provider.Provider{fp}, am) + srv := NewTriggerServer(&Opts{Providers: providers, ApprovalManager: am}) + srv.registerRoutes(srv.router) + + err := am.Create(&types.Approval{ + Identifier: "12345", + VotesRequired: 5, + NewVersion: "2.0.0", + CurrentVersion: "1.0.0", + VotesReceived: 1, + Voters: []string{"bar"}, + }) + + if err != nil { + t.Fatalf("failed to create approval: %s", err) + } + + // listing + req, err := http.NewRequest("POST", "/v1/approvals", bytes.NewBufferString(`{"identifier":"12345", "voter": "foo"}`)) + if err != nil { + t.Fatalf("failed to create req: %s", err) + } + + rec := httptest.NewRecorder() + + srv.router.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Errorf("unexpected status code: %d", rec.Code) + + t.Log(rec.Body.String()) + } + + approved, err := am.Get("12345") + if err != nil { + t.Fatalf("failed to get approval: %s", err) + } + + if approved.VotesReceived != 2 { + t.Errorf("expected to find 2 voters") + } + + if approved.Voters[0] != "bar" { + t.Errorf("unexpected voter: %s", approved.Voters[0]) + } + if approved.Voters[1] != "foo" { + t.Errorf("unexpected voter: %s", approved.Voters[0]) + } } diff --git a/trigger/http/http.go b/trigger/http/http.go index a8b52d94..73bae394 100644 --- a/trigger/http/http.go +++ b/trigger/http/http.go @@ -85,6 +85,8 @@ func (s *TriggerServer) registerRoutes(mux *mux.Router) { // approvals mux.HandleFunc("/v1/approvals", s.approvalsHandler).Methods("GET", "OPTIONS") + // approving + mux.HandleFunc("/v1/approvals", s.approvalApproveHandler).Methods("POST", "OPTIONS") mux.HandleFunc("/v1/approvals/{id}", s.approvalDeleteHandler).Methods("DELETE", "OPTIONS") // native webhooks handler