From d939c1c3cefbd72b22af251a460f2a994b954005 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Fri, 5 Mar 2021 17:30:25 -0300 Subject: [PATCH] feat: Add service and http handler for delivery attempt (#15) --- cmd/postmand/main.go | 7 ++ error.go | 2 + http/handler/delivery_attempt.go | 97 +++++++++++++++++++++++++++ http/handler/delivery_attempt_test.go | 69 +++++++++++++++++++ http/handler/error.go | 5 ++ mocks/DeliveryAttemptService.go | 61 +++++++++++++++++ repository/delivery_attempt.go | 16 +++-- service.go | 6 ++ service/delivery_attempt.go | 27 ++++++++ service/delivery_attempt_test.go | 42 ++++++++++++ 10 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 http/handler/delivery_attempt.go create mode 100644 http/handler/delivery_attempt_test.go create mode 100644 mocks/DeliveryAttemptService.go create mode 100644 service/delivery_attempt.go create mode 100644 service/delivery_attempt_test.go diff --git a/cmd/postmand/main.go b/cmd/postmand/main.go index 79d8c46..a789de3 100644 --- a/cmd/postmand/main.go +++ b/cmd/postmand/main.go @@ -79,10 +79,13 @@ func main() { Action: func(c *cli.Context) error { webhookRepository := repository.NewWebhook(db) deliveryRepository := repository.NewDelivery(db) + deliveryAttemptRepository := repository.NewDeliveryAttempt(db) webhookService := service.NewWebhook(webhookRepository) deliveryService := service.NewDelivery(deliveryRepository) + deliveryAttemptService := service.NewDeliveryAttempt(deliveryAttemptRepository) webhookHandler := handler.NewWebhook(webhookService, logger) deliveryHandler := handler.NewDelivery(deliveryService, logger) + deliveryAttemptHandler := handler.NewDeliveryAttempt(deliveryAttemptService, logger) mux := http.NewRouter(logger) mux.Route("/v1/webhooks", func(r chi.Router) { @@ -98,6 +101,10 @@ func main() { r.Get("/{delivery_id}", deliveryHandler.Get) r.Delete("/{delivery_id}", deliveryHandler.Delete) }) + mux.Route("/v1/delivery-attempts", func(r chi.Router) { + r.Get("/", deliveryAttemptHandler.List) + r.Get("/{delivery_attempt_id}", deliveryAttemptHandler.Get) + }) server := http.NewServer(mux, env.GetInt("POSTMAND_HTTP_PORT", 8000), logger) server.Run() diff --git a/error.go b/error.go index 9c238b2..0f5de62 100644 --- a/error.go +++ b/error.go @@ -7,4 +7,6 @@ var ( ErrWebhookNotFound = errors.New("webhook_not_found") // ErrDeliveryNotFound is returned by any operation that can't load a delivery. ErrDeliveryNotFound = errors.New("delivery_not_found") + // ErrDeliveryAttemptNotFound is returned by any operation that can't load a delivery attempt. + ErrDeliveryAttemptNotFound = errors.New("delivery_attempt_not_found") ) diff --git a/http/handler/delivery_attempt.go b/http/handler/delivery_attempt.go new file mode 100644 index 0000000..45b8a50 --- /dev/null +++ b/http/handler/delivery_attempt.go @@ -0,0 +1,97 @@ +package handler + +import ( + "net/http" + + "github.com/allisson/postmand" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "go.uber.org/zap" +) + +type deliveryAttemptList struct { + DeliveryAttempts []*postmand.DeliveryAttempt `json:"delivery_attempts"` + Limit int `json:"limit"` + Offset int `json:"offset"` +} + +// DeliveryAttempt implements rest interface for delivery attempt. +type DeliveryAttempt struct { + deliveryAttemptService postmand.DeliveryAttemptService + logger *zap.Logger +} + +// List delivery attempts. +func (d DeliveryAttempt) List(w http.ResponseWriter, r *http.Request) { + listOptions, err := makeListOptions(r, []string{"webhook_id", "delivery_id", "success"}) + if err != nil { + er := errorResponses["internal_server_error"] + makeErrorResponse(w, &er, d.logger) + return + } + listOptions.OrderBy = "created_at" + listOptions.Order = "desc" + + // Call service + deliveryAttempts, err := d.deliveryAttemptService.List(r.Context(), listOptions) + if err != nil { + d.logger.Error( + "service-error", + zap.String("name", "DeliveryAttemptService"), + zap.String("method", "List"), + zap.Error(err), + ) + er := errorResponses["internal_server_error"] + makeErrorResponse(w, &er, d.logger) + return + } + + // Return response + dl := deliveryAttemptList{ + DeliveryAttempts: deliveryAttempts, + Limit: listOptions.Limit, + Offset: listOptions.Offset, + } + makeJSONResponse(w, http.StatusOK, dl, d.logger) +} + +// Get delivery attempt. +func (d DeliveryAttempt) Get(w http.ResponseWriter, r *http.Request) { + deliveryAttemptID, err := uuid.Parse(chi.URLParam(r, "delivery_attempt_id")) + if err != nil { + er := errorResponses["invalid_id"] + makeErrorResponse(w, &er, d.logger) + return + } + + // Call service + getOptions := postmand.RepositoryGetOptions{Filters: map[string]interface{}{"id": deliveryAttemptID}} + deliveryAttempt, err := d.deliveryAttemptService.Get(r.Context(), getOptions) + if err != nil { + if err == postmand.ErrDeliveryAttemptNotFound { + er := errorResponses["delivery_attempt_not_found"] + makeErrorResponse(w, &er, d.logger) + return + } + d.logger.Error( + "service-error", + zap.String("name", "DeliveryAttemptService"), + zap.String("method", "Get"), + zap.Error(err), + ) + er := errorResponses["internal_server_error"] + makeErrorResponse(w, &er, d.logger) + return + } + + // Return response + makeJSONResponse(w, http.StatusOK, deliveryAttempt, d.logger) +} + +// NewDeliveryAttempt creates a new DeliveryAttempt. +func NewDeliveryAttempt(deliveryAttemptService postmand.DeliveryAttemptService, logger *zap.Logger) *DeliveryAttempt { + return &DeliveryAttempt{ + deliveryAttemptService: deliveryAttemptService, + logger: logger, + } +} diff --git a/http/handler/delivery_attempt_test.go b/http/handler/delivery_attempt_test.go new file mode 100644 index 0000000..465293f --- /dev/null +++ b/http/handler/delivery_attempt_test.go @@ -0,0 +1,69 @@ +package handler + +import ( + nethttp "net/http" + "testing" + + "github.com/allisson/postmand" + "github.com/allisson/postmand/http" + "github.com/allisson/postmand/mocks" + "github.com/google/uuid" + "github.com/steinfletcher/apitest" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func makeDeliveryAttempt() postmand.DeliveryAttempt { + deliveryAttemptID, _ := uuid.Parse("97087247-d89d-410e-b915-740b4c6d9d99") + deliveryID, _ := uuid.Parse("b919ca2c-6b0f-4a22-a61f-8c882ee69323") + webhookID, _ := uuid.Parse("cd9b7318-36c6-4534-be84-fe78042aeaf2") + + return postmand.DeliveryAttempt{ + ID: deliveryAttemptID, + DeliveryID: deliveryID, + WebhookID: webhookID, + } +} + +func TestDeliveryAttempt(t *testing.T) { + logger, _ := zap.NewDevelopment() + + t.Run("List", func(t *testing.T) { + deliveryAttemptService := &mocks.DeliveryAttemptService{} + listOptions := postmand.RepositoryListOptions{Filters: map[string]interface{}{}, Limit: 50, Offset: 0, OrderBy: "created_at", Order: "desc"} + deliveryAttemptHandler := NewDeliveryAttempt(deliveryAttemptService, logger) + router := http.NewRouter(logger) + router.Get("/v1/delivery-attempts", deliveryAttemptHandler.List) + + deliveryAttemptService.On("List", mock.Anything, listOptions).Return([]*postmand.DeliveryAttempt{{}}, nil) + apitest.New(). + Handler(router). + Get("/v1/delivery-attempts"). + Expect(t). + Body(`{"delivery_attempts":[{"id":"00000000-0000-0000-0000-000000000000","webhook_id":"00000000-0000-0000-0000-000000000000","delivery_id":"00000000-0000-0000-0000-000000000000","raw_response":"","response_status_code":0,"execution_duration":0,"success":false,"error":"","created_at":"0001-01-01T00:00:00Z"}],"limit":50,"offset":0}`). + Status(nethttp.StatusOK). + End() + + deliveryAttemptService.AssertExpectations(t) + }) + + t.Run("Get", func(t *testing.T) { + deliveryAttemptService := &mocks.DeliveryAttemptService{} + deliveryAttempt := makeDeliveryAttempt() + getOptions := postmand.RepositoryGetOptions{Filters: map[string]interface{}{"id": deliveryAttempt.ID}} + deliveryAttemptHandler := NewDeliveryAttempt(deliveryAttemptService, logger) + router := http.NewRouter(logger) + router.Get("/v1/delivery-attempts/{delivery_attempt_id}", deliveryAttemptHandler.Get) + + deliveryAttemptService.On("Get", mock.Anything, getOptions).Return(&deliveryAttempt, nil) + apitest.New(). + Handler(router). + Get("/v1/delivery-attempts/97087247-d89d-410e-b915-740b4c6d9d99"). + Expect(t). + Body(`{"id":"97087247-d89d-410e-b915-740b4c6d9d99","webhook_id":"cd9b7318-36c6-4534-be84-fe78042aeaf2","delivery_id":"b919ca2c-6b0f-4a22-a61f-8c882ee69323","raw_response":"","response_status_code":0,"execution_duration":0,"success":false,"error":"","created_at":"0001-01-01T00:00:00Z"}`). + Status(nethttp.StatusOK). + End() + + deliveryAttemptService.AssertExpectations(t) + }) +} diff --git a/http/handler/error.go b/http/handler/error.go index 8b84465..9028591 100644 --- a/http/handler/error.go +++ b/http/handler/error.go @@ -33,6 +33,11 @@ var errorResponses = map[string]errorResponse{ Message: "delivery not found", StatusCode: http.StatusNotFound, }, + "delivery_attempt_not_found": { + Code: 7, + Message: "delivery attempt not found", + StatusCode: http.StatusNotFound, + }, } type errorResponse struct { diff --git a/mocks/DeliveryAttemptService.go b/mocks/DeliveryAttemptService.go new file mode 100644 index 0000000..50b0475 --- /dev/null +++ b/mocks/DeliveryAttemptService.go @@ -0,0 +1,61 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + postmand "github.com/allisson/postmand" + mock "github.com/stretchr/testify/mock" +) + +// DeliveryAttemptService is an autogenerated mock type for the DeliveryAttemptService type +type DeliveryAttemptService struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, getOptions +func (_m *DeliveryAttemptService) Get(ctx context.Context, getOptions postmand.RepositoryGetOptions) (*postmand.DeliveryAttempt, error) { + ret := _m.Called(ctx, getOptions) + + var r0 *postmand.DeliveryAttempt + if rf, ok := ret.Get(0).(func(context.Context, postmand.RepositoryGetOptions) *postmand.DeliveryAttempt); ok { + r0 = rf(ctx, getOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*postmand.DeliveryAttempt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, postmand.RepositoryGetOptions) error); ok { + r1 = rf(ctx, getOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, listOptions +func (_m *DeliveryAttemptService) List(ctx context.Context, listOptions postmand.RepositoryListOptions) ([]*postmand.DeliveryAttempt, error) { + ret := _m.Called(ctx, listOptions) + + var r0 []*postmand.DeliveryAttempt + if rf, ok := ret.Get(0).(func(context.Context, postmand.RepositoryListOptions) []*postmand.DeliveryAttempt); ok { + r0 = rf(ctx, listOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*postmand.DeliveryAttempt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, postmand.RepositoryListOptions) error); ok { + r1 = rf(ctx, listOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/repository/delivery_attempt.go b/repository/delivery_attempt.go index c2d0759..29cfb77 100644 --- a/repository/delivery_attempt.go +++ b/repository/delivery_attempt.go @@ -2,6 +2,7 @@ package repository import ( "context" + "database/sql" "github.com/allisson/postmand" "github.com/jmoiron/sqlx" @@ -15,23 +16,26 @@ type DeliveryAttempt struct { // Get returns postmand.DeliveryAttempt by options filter. func (d DeliveryAttempt) Get(ctx context.Context, getOptions postmand.RepositoryGetOptions) (*postmand.DeliveryAttempt, error) { deliveryAttempt := postmand.DeliveryAttempt{} - sql, args := getQuery("delivery_attempts", getOptions) - err := d.db.GetContext(ctx, &deliveryAttempt, sql, args...) + query, args := getQuery("delivery_attempts", getOptions) + err := d.db.GetContext(ctx, &deliveryAttempt, query, args...) + if err == sql.ErrNoRows { + return &deliveryAttempt, postmand.ErrDeliveryAttemptNotFound + } return &deliveryAttempt, err } // List returns a slice of postmand.DeliveryAttempt by options filter. func (d DeliveryAttempt) List(ctx context.Context, listOptions postmand.RepositoryListOptions) ([]*postmand.DeliveryAttempt, error) { deliveryAttempts := []*postmand.DeliveryAttempt{} - sql, args := listQuery("delivery_attempts", listOptions) - err := d.db.SelectContext(ctx, &deliveryAttempts, sql, args...) + query, args := listQuery("delivery_attempts", listOptions) + err := d.db.SelectContext(ctx, &deliveryAttempts, query, args...) return deliveryAttempts, err } // Create postmand.DeliveryAttempt on database. func (d DeliveryAttempt) Create(ctx context.Context, deliveryAttempt *postmand.DeliveryAttempt) error { - sql, args := insertQuery("delivery_attempts", deliveryAttempt) - _, err := d.db.ExecContext(ctx, sql, args...) + query, args := insertQuery("delivery_attempts", deliveryAttempt) + _, err := d.db.ExecContext(ctx, query, args...) return err } diff --git a/service.go b/service.go index 5442df5..db7e1a0 100644 --- a/service.go +++ b/service.go @@ -30,3 +30,9 @@ type DeliveryService interface { Update(ctx context.Context, delivery *Delivery) error Delete(ctx context.Context, id ID) error } + +// DeliveryAttemptService is the interface that will be used to perform operations with delivery attempt. +type DeliveryAttemptService interface { + Get(ctx context.Context, getOptions RepositoryGetOptions) (*DeliveryAttempt, error) + List(ctx context.Context, listOptions RepositoryListOptions) ([]*DeliveryAttempt, error) +} diff --git a/service/delivery_attempt.go b/service/delivery_attempt.go new file mode 100644 index 0000000..cd9bf77 --- /dev/null +++ b/service/delivery_attempt.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + + "github.com/allisson/postmand" +) + +// DeliveryAttempt implements postmand.DeliveryAttemptService interface. +type DeliveryAttempt struct { + deliveryAttemptRepository postmand.DeliveryAttemptRepository +} + +// Get returns postmand.DeliveryAttempt by options filter. +func (d DeliveryAttempt) Get(ctx context.Context, getOptions postmand.RepositoryGetOptions) (*postmand.DeliveryAttempt, error) { + return d.deliveryAttemptRepository.Get(ctx, getOptions) +} + +// List returns a slice of postmand.DeliveryAttempt by options filter. +func (d DeliveryAttempt) List(ctx context.Context, listOptions postmand.RepositoryListOptions) ([]*postmand.DeliveryAttempt, error) { + return d.deliveryAttemptRepository.List(ctx, listOptions) +} + +// NewDeliveryAttempt will create an implementation of postmand.DeliveryAttemptService. +func NewDeliveryAttempt(deliveryAttemptRepository postmand.DeliveryAttemptRepository) *DeliveryAttempt { + return &DeliveryAttempt{deliveryAttemptRepository: deliveryAttemptRepository} +} diff --git a/service/delivery_attempt_test.go b/service/delivery_attempt_test.go new file mode 100644 index 0000000..761e84c --- /dev/null +++ b/service/delivery_attempt_test.go @@ -0,0 +1,42 @@ +package service + +import ( + "context" + "testing" + + "github.com/allisson/postmand" + "github.com/allisson/postmand/mocks" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeliveryAttempt(t *testing.T) { + ctx := context.Background() + + t.Run("Get", func(t *testing.T) { + deliveryAttemptRepository := &mocks.DeliveryAttemptRepository{} + deliveryAttemptService := NewDeliveryAttempt(deliveryAttemptRepository) + expectedDeliveryAttempt := &postmand.DeliveryAttempt{ID: uuid.New()} + getOptions := postmand.RepositoryGetOptions{Filters: map[string]interface{}{"id": expectedDeliveryAttempt.ID}} + + deliveryAttemptRepository.On("Get", mock.Anything, getOptions).Return(expectedDeliveryAttempt, nil) + delivery, err := deliveryAttemptService.Get(ctx, getOptions) + assert.Nil(t, err) + assert.Equal(t, expectedDeliveryAttempt, delivery) + deliveryAttemptRepository.AssertExpectations(t) + }) + + t.Run("List", func(t *testing.T) { + deliveryAttemptRepository := &mocks.DeliveryAttemptRepository{} + deliveryAttemptService := NewDeliveryAttempt(deliveryAttemptRepository) + expectedDeliveryAttempt := &postmand.DeliveryAttempt{ID: uuid.New()} + listOptions := postmand.RepositoryListOptions{Filters: map[string]interface{}{"id": expectedDeliveryAttempt.ID}, Limit: 1, Offset: 0} + + deliveryAttemptRepository.On("List", mock.Anything, listOptions).Return([]*postmand.DeliveryAttempt{expectedDeliveryAttempt}, nil) + webhooks, err := deliveryAttemptService.List(ctx, listOptions) + assert.Nil(t, err) + assert.Equal(t, expectedDeliveryAttempt, webhooks[0]) + deliveryAttemptRepository.AssertExpectations(t) + }) +}