Skip to content

Commit

Permalink
feat: Add http handler for deliveries (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisson committed Mar 5, 2021
1 parent d31501c commit bfc4b9f
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 20 deletions.
9 changes: 9 additions & 0 deletions cmd/postmand/main.go
Expand Up @@ -78,8 +78,11 @@ func main() {
Usage: "executes http server",
Action: func(c *cli.Context) error {
webhookRepository := repository.NewWebhook(db)
deliveryRepository := repository.NewDelivery(db)
webhookService := service.NewWebhook(webhookRepository)
deliveryService := service.NewDelivery(deliveryRepository)
webhookHandler := handler.NewWebhook(webhookService, logger)
deliveryHandler := handler.NewDelivery(deliveryService, logger)

mux := http.NewRouter(logger)
mux.Route("/v1/webhooks", func(r chi.Router) {
Expand All @@ -89,6 +92,12 @@ func main() {
r.Put("/{webhook_id}", webhookHandler.Update)
r.Delete("/{webhook_id}", webhookHandler.Delete)
})
mux.Route("/v1/deliveries", func(r chi.Router) {
r.Get("/", deliveryHandler.List)
r.Post("/", deliveryHandler.Create)
r.Get("/{delivery_id}", deliveryHandler.Get)
r.Delete("/{delivery_id}", deliveryHandler.Delete)
})

server := http.NewServer(mux, env.GetInt("POSTMAND_HTTP_PORT", 8000), logger)
server.Run()
Expand Down
2 changes: 2 additions & 0 deletions error.go
Expand Up @@ -5,4 +5,6 @@ import "errors"
var (
// ErrWebhookNotFound is returned by any operation that can't load a webhook.
ErrWebhookNotFound = errors.New("webhook_not_found")
// ErrDeliveryNotFound is returned by any operation that can't load a delivery.
ErrDeliveryNotFound = errors.New("delivery_not_found")
)
147 changes: 147 additions & 0 deletions http/handler/delivery.go
@@ -0,0 +1,147 @@
package handler

import (
"net/http"

"github.com/allisson/postmand"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"go.uber.org/zap"
)

type deliveryList struct {
Deliveries []*postmand.Delivery `json:"deliveries"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}

// Delivery implements rest interface for delivery.
type Delivery struct {
deliveryService postmand.DeliveryService
logger *zap.Logger
}

// List deliveries.
func (d Delivery) List(w http.ResponseWriter, r *http.Request) {
listOptions, err := makeListOptions(r, []string{})
if err != nil {
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
return
}
listOptions.OrderBy = "created_at"
listOptions.Order = "desc"

// Call service
deliveries, err := d.deliveryService.List(r.Context(), listOptions)
if err != nil {
d.logger.Error(
"service-error",
zap.String("name", "DeliveryService"),
zap.String("method", "List"),
zap.Error(err),
)
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
return
}

// Return response
dl := deliveryList{
Deliveries: deliveries,
Limit: listOptions.Limit,
Offset: listOptions.Offset,
}
makeJSONResponse(w, http.StatusOK, dl, d.logger)
}

// Get delivery.
func (d Delivery) Get(w http.ResponseWriter, r *http.Request) {
deliveryID, err := uuid.Parse(chi.URLParam(r, "delivery_id"))
if err != nil {
er := errorResponses["invalid_id"]
makeErrorResponse(w, &er, d.logger)
return
}

// Call service
getOptions := postmand.RepositoryGetOptions{Filters: map[string]interface{}{"id": deliveryID}}
delivery, err := d.deliveryService.Get(r.Context(), getOptions)
if err != nil {
if err == postmand.ErrDeliveryNotFound {
er := errorResponses["delivery_not_found"]
makeErrorResponse(w, &er, d.logger)
return
}
d.logger.Error(
"service-error",
zap.String("name", "DeliveryService"),
zap.String("method", "Get"),
zap.Error(err),
)
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
return
}

// Return response
makeJSONResponse(w, http.StatusOK, delivery, d.logger)
}

// Create delivery.
func (d Delivery) Create(w http.ResponseWriter, r *http.Request) {
// Parse request
delivery := postmand.Delivery{}
if er := readBodyJSON(r, &delivery, d.logger); er != nil {
makeErrorResponse(w, er, d.logger)
return
}

// Call service
if err := d.deliveryService.Create(r.Context(), &delivery); err != nil {
d.logger.Error(
"service-error",
zap.String("name", "DeliveryService"),
zap.String("method", "Create"),
zap.Error(err),
)
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
}

// Return response
makeJSONResponse(w, http.StatusCreated, delivery, d.logger)
}

// Delete delivery.
func (d Delivery) Delete(w http.ResponseWriter, r *http.Request) {
deliveryID, err := uuid.Parse(chi.URLParam(r, "delivery_id"))
if err != nil {
er := errorResponses["invalid_id"]
makeErrorResponse(w, &er, d.logger)
return
}

// Call service
if err := d.deliveryService.Delete(r.Context(), deliveryID); err != nil {
d.logger.Error(
"service-error",
zap.String("name", "DeliveryService"),
zap.String("method", "Delete"),
zap.Error(err),
)
er := errorResponses["internal_server_error"]
makeErrorResponse(w, &er, d.logger)
}

// Return response
makeResponse(w, []byte(""), http.StatusNoContent, "application/json", d.logger)
}

// NewDelivery creates a new Delivery.
func NewDelivery(deliveryService postmand.DeliveryService, logger *zap.Logger) *Delivery {
return &Delivery{
deliveryService: deliveryService,
logger: logger,
}
}
126 changes: 126 additions & 0 deletions http/handler/delivery_test.go
@@ -0,0 +1,126 @@
package handler

import (
"encoding/json"
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 makeDelivery() postmand.Delivery {
deliveryID, _ := uuid.Parse("b919ca2c-6b0f-4a22-a61f-8c882ee69323")
webhookID, _ := uuid.Parse("cd9b7318-36c6-4534-be84-fe78042aeaf2")

return postmand.Delivery{
ID: deliveryID,
WebhookID: webhookID,
Payload: `{}`,
}
}

func TestDelivery(t *testing.T) {
logger, _ := zap.NewDevelopment()

t.Run("List", func(t *testing.T) {
deliveryService := &mocks.DeliveryService{}
listOptions := postmand.RepositoryListOptions{Filters: map[string]interface{}{}, Limit: 50, Offset: 0, OrderBy: "created_at", Order: "desc"}
deliveryHandler := NewDelivery(deliveryService, logger)
router := http.NewRouter(logger)
router.Get("/v1/deliveries", deliveryHandler.List)

deliveryService.On("List", mock.Anything, listOptions).Return([]*postmand.Delivery{{}}, nil)
apitest.New().
Handler(router).
Get("/v1/deliveries").
Expect(t).
Body(`{"deliveries":[{"id":"00000000-0000-0000-0000-000000000000","webhook_id":"00000000-0000-0000-0000-000000000000","payload":"","scheduled_at":"0001-01-01T00:00:00Z","delivery_attempts":0,"status":"","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}],"limit":50,"offset":0}`).
Status(nethttp.StatusOK).
End()

deliveryService.AssertExpectations(t)
})

t.Run("Get", func(t *testing.T) {
deliveryService := &mocks.DeliveryService{}
delivery := makeDelivery()
getOptions := postmand.RepositoryGetOptions{Filters: map[string]interface{}{"id": delivery.ID}}
deliveryHandler := NewDelivery(deliveryService, logger)
router := http.NewRouter(logger)
router.Get("/v1/deliveries/{delivery_id}", deliveryHandler.Get)

deliveryService.On("Get", mock.Anything, getOptions).Return(&delivery, nil)
apitest.New().
Handler(router).
Get("/v1/deliveries/b919ca2c-6b0f-4a22-a61f-8c882ee69323").
Expect(t).
Body(`{"created_at":"0001-01-01T00:00:00Z", "delivery_attempts":0, "id":"b919ca2c-6b0f-4a22-a61f-8c882ee69323", "payload":"{}", "scheduled_at":"0001-01-01T00:00:00Z", "status":"", "updated_at":"0001-01-01T00:00:00Z", "webhook_id":"cd9b7318-36c6-4534-be84-fe78042aeaf2"}`).
Status(nethttp.StatusOK).
End()

deliveryService.AssertExpectations(t)
})

t.Run("Create with malformed request body", func(t *testing.T) {
deliveryService := &mocks.DeliveryService{}
deliveryHandler := NewDelivery(deliveryService, logger)
router := http.NewRouter(logger)
router.Post("/v1/deliveries", deliveryHandler.Create)

apitest.New().
Handler(router).
Post("/v1/deliveries").
JSON(`{`).
Expect(t).
Body(`{"code":3, "message":"malformed request body"}`).
Status(nethttp.StatusBadRequest).
End()

deliveryService.AssertExpectations(t)
})

t.Run("Create with valid body", func(t *testing.T) {
deliveryService := &mocks.DeliveryService{}
deliveryHandler := NewDelivery(deliveryService, logger)
delivery := makeDelivery()
jsonDelivery, _ := json.Marshal(&delivery)
router := http.NewRouter(logger)
router.Post("/v1/deliveries", deliveryHandler.Create)

deliveryService.On("Create", mock.Anything, &delivery).Return(nil)
apitest.New().
Handler(router).
Post("/v1/deliveries").
JSON(jsonDelivery).
Expect(t).
Body(`{"created_at":"0001-01-01T00:00:00Z", "delivery_attempts":0, "id":"b919ca2c-6b0f-4a22-a61f-8c882ee69323", "payload":"{}", "scheduled_at":"0001-01-01T00:00:00Z", "status":"", "updated_at":"0001-01-01T00:00:00Z", "webhook_id":"cd9b7318-36c6-4534-be84-fe78042aeaf2"}`).
Status(nethttp.StatusCreated).
End()

deliveryService.AssertExpectations(t)
})

t.Run("Delete", func(t *testing.T) {
deliveryService := &mocks.DeliveryService{}
deliveryHandler := NewDelivery(deliveryService, logger)
delivery := makeDelivery()
router := http.NewRouter(logger)
router.Delete("/v1/deliveries/{delivery_id}", deliveryHandler.Delete)

deliveryService.On("Delete", mock.Anything, delivery.ID).Return(nil)
apitest.New().
Handler(router).
Delete("/v1/deliveries/b919ca2c-6b0f-4a22-a61f-8c882ee69323").
Expect(t).
Status(nethttp.StatusNoContent).
End()

deliveryService.AssertExpectations(t)
})
}
5 changes: 5 additions & 0 deletions http/handler/error.go
Expand Up @@ -28,6 +28,11 @@ var errorResponses = map[string]errorResponse{
Message: "webhook not found",
StatusCode: http.StatusNotFound,
},
"delivery_not_found": {
Code: 6,
Message: "delivery not found",
StatusCode: http.StatusNotFound,
},
}

type errorResponse struct {
Expand Down
2 changes: 0 additions & 2 deletions http/handler/webhook_test.go
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/zap"
)
Expand All @@ -34,7 +33,6 @@ func makeWebhook() postmand.Webhook {
}

func TestWebhook(t *testing.T) {
assert.True(t, true)
logger, _ := zap.NewDevelopment()

t.Run("List", func(t *testing.T) {
Expand Down

0 comments on commit bfc4b9f

Please sign in to comment.