Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add service and http handler for delivery attempt #15

Merged
merged 1 commit into from Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
})
}