Skip to content

Commit

Permalink
feat: Add service and http handler for delivery attempt (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisson committed Mar 5, 2021
1 parent bfc4b9f commit d939c1c
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 6 deletions.
7 changes: 7 additions & 0 deletions cmd/postmand/main.go
Expand Up @@ -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) {
Expand All @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions error.go
Expand Up @@ -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")
)
97 changes: 97 additions & 0 deletions 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,
}
}
69 changes: 69 additions & 0 deletions 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)
})
}
5 changes: 5 additions & 0 deletions http/handler/error.go
Expand Up @@ -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 {
Expand Down
61 changes: 61 additions & 0 deletions mocks/DeliveryAttemptService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions repository/delivery_attempt.go
Expand Up @@ -2,6 +2,7 @@ package repository

import (
"context"
"database/sql"

"github.com/allisson/postmand"
"github.com/jmoiron/sqlx"
Expand All @@ -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
}

Expand Down
6 changes: 6 additions & 0 deletions service.go
Expand Up @@ -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)
}
27 changes: 27 additions & 0 deletions 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}
}
42 changes: 42 additions & 0 deletions 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)
})
}

0 comments on commit d939c1c

Please sign in to comment.