Skip to content

Commit

Permalink
feat(http): add http port
Browse files Browse the repository at this point in the history
  • Loading branch information
rafael-piovesan committed Dec 24, 2021
1 parent 57aa5b4 commit 8dafed3
Show file tree
Hide file tree
Showing 6 changed files with 505 additions and 0 deletions.
51 changes: 51 additions & 0 deletions api/http/handler/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package handler

import (
"net/http"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/rafael-piovesan/go-rocket-ride/entity"
)

type Handler struct {
binder *echo.DefaultBinder
validate *validator.Validate
}

func New() *Handler {
return &Handler{
binder: &echo.DefaultBinder{},
validate: validator.New(),
}
}

// BindAndValidate binds and validates the data struct pointed to by 'i'.
// It expects a stuct pointer as parameter, since it needs to populate its
// fields, otherwise it'll panic.
func (h *Handler) BindAndValidate(c echo.Context, i interface{}) error {
if i == nil {
return nil
}

if err := h.binder.BindHeaders(c, i); err != nil {
return err
}

if err := h.binder.BindBody(c, i); err != nil {
return err
}

if err := h.validate.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}

func (h *Handler) GetUserID(c echo.Context) (uid int64, err error) {
uid, ok := c.Get("user-id").(int64)
if !ok {
err = echo.NewHTTPError(http.StatusUnauthorized, entity.ErrPermissionDenied.Error())
}
return
}
80 changes: 80 additions & 0 deletions api/http/handler/ride.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package handler

import (
"encoding/json"
"errors"
"net/http"
"time"

"github.com/labstack/echo/v4"
rocketride "github.com/rafael-piovesan/go-rocket-ride"
"github.com/rafael-piovesan/go-rocket-ride/entity"
)

type createRequest struct {
IdemKey string `json:"idem_key" header:"idempotency-key" validate:"required,max=100"`
OrigLat float64 `json:"origin_lat" validate:"min=-90,max=90"`
OrigLon float64 `json:"origin_lon" validate:"min=-180,max=180"`
TgtLat float64 `json:"target_lat" validate:"min=-90,max=90"`
TgtLon float64 `json:"target_lon" validate:"min=-180,max=180"`
}

type RideHanlder struct {
*Handler
uc rocketride.RideUseCase
}

func NewRideHandler(uc rocketride.RideUseCase) *RideHanlder {
return &RideHanlder{
Handler: New(),
uc: uc,
}
}

func (r *RideHanlder) Create(c echo.Context) error {
userID, err := r.GetUserID(c)
if err != nil {
return err
}

cr := createRequest{}
if err := r.BindAndValidate(c, &cr); err != nil {
return err
}

rd := &entity.Ride{
OriginLat: cr.OrigLat,
OriginLon: cr.OrigLon,
TargetLat: cr.TgtLat,
TargetLon: cr.TgtLon,
}

rp, _ := json.Marshal(rd)

ik := &entity.IdempotencyKey{
CreatedAt: time.Now().UTC(),
IdempotencyKey: cr.IdemKey,
UserID: userID,
RequestMethod: c.Request().Method,
RequestPath: c.Request().RequestURI,
RequestParams: rp,
}

ik, err = r.uc.Create(c.Request().Context(), ik, rd)

if errors.Is(err, entity.ErrIdemKeyParamsMismatch) || errors.Is(err, entity.ErrIdemKeyRequestInProgress) {
return c.String(http.StatusConflict, err.Error())
}

if err != nil || ik.ResponseCode == nil || ik.ResponseBody == nil {
return c.String(http.StatusInternalServerError, "internal error")
}

rCode := int(*ik.ResponseCode)
rBody, err := json.Marshal(*ik.ResponseBody)
if err != nil {
return c.String(http.StatusInternalServerError, "internal error")
}

return c.JSONBlob(rCode, rBody)
}
252 changes: 252 additions & 0 deletions api/http/handler/ride_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
//go:build unit
// +build unit

package handler

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/brianvoe/gofakeit/v6"
"github.com/labstack/echo/v4"
"github.com/rafael-piovesan/go-rocket-ride/entity"
"github.com/rafael-piovesan/go-rocket-ride/entity/idempotency"
"github.com/rafael-piovesan/go-rocket-ride/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestCreate(t *testing.T) {
e := echo.New()
uc := &mocks.RideUseCase{}
handler := NewRideHandler(uc)

callArgs := []interface{}{
mock.Anything,
mock.AnythingOfType("*entity.IdempotencyKey"),
mock.AnythingOfType("*entity.Ride"),
}

t.Run("User info not found", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()

// context is missing the "user-id" key
c := e.NewContext(req, rec)

err := handler.Create(c)

if assert.Error(t, err) && assert.IsType(t, (&echo.HTTPError{}), err) {
he := (err).(*echo.HTTPError)
assert.Equal(t, http.StatusUnauthorized, he.Code)
assert.Equal(t, entity.ErrPermissionDenied.Error(), he.Message)
}
})

t.Run("Header input validation", func(t *testing.T) {
payload := `{"origin_lat": 0.0, "origin_lon": 0.0, "target_lat": 0.0, "target_lon": 0.0}`
tests := []struct {
wantErr bool
header string
value string
}{
{wantErr: true, header: gofakeit.AnimalType(), value: gofakeit.LetterN(uint(gofakeit.Number(101, 1000)))},
{wantErr: true, header: gofakeit.AnimalType(), value: gofakeit.LetterN(uint(gofakeit.Number(0, 100)))},
{wantErr: true, header: "idempotency-key", value: gofakeit.LetterN(uint(gofakeit.Number(101, 1000)))},
{wantErr: false, header: "idempotency-key", value: gofakeit.LetterN(uint(gofakeit.Number(0, 100)))},
}
for _, tc := range tests {
if !tc.wantErr {
rCode := idempotency.ResponseCodeOK
rBody := idempotency.ResponseBody{Message: "ok"}

ik := &entity.IdempotencyKey{
ResponseCode: &rCode,
ResponseBody: &rBody,
}
uc.On("Create", callArgs...).Once().Return(ik, nil)
}

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(payload))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

// testing different header values
req.Header.Set(tc.header, tc.value)

rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user-id", int64(0))

err := handler.Create(c)

if tc.wantErr {
if assert.Error(t, err) && assert.IsType(t, (&echo.HTTPError{}), err) {
he := (err).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code)
}
} else {
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "{\"message\":\"ok\"}", rec.Body.String())
}
}
}
})

t.Run("Body input validation", func(t *testing.T) {
tpl := `{"origin_lat": %v, "origin_lon": %v, "target_lat": %v, "target_lon": %v}`
tests := []struct {
wantErr bool
payload string
}{
{wantErr: true, payload: fmt.Sprintf(tpl, -90.0000000001, 0.0, 0.0, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 90.0000000001, 0.0, 0.0, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, -180.0000000001, 0.0, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, 180.0000000001, 0.0, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, 0.0, -90.0000000001, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, 0.0, 90.0000000001, 0.0)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, 0.0, 0.0, -180.0000000001)},
{wantErr: true, payload: fmt.Sprintf(tpl, 0.0, 0.0, 0.0, 180.0000000001)},

{wantErr: false, payload: fmt.Sprintf(tpl, -90.0, 0.0, 0.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 90.0, 0.0, 0.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, -180.0, 0.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, 180.0, 0.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, 0.0, -90.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, 0.0, 90.0, 0.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, 0.0, 0.0, -180.0)},
{wantErr: false, payload: fmt.Sprintf(tpl, 0.0, 0.0, 0.0, 180.0)},
}
for _, tc := range tests {
if !tc.wantErr {
rCode := idempotency.ResponseCodeOK
rBody := idempotency.ResponseBody{Message: "ok"}

ik := &entity.IdempotencyKey{
ResponseCode: &rCode,
ResponseBody: &rBody,
}
uc.On("Create", callArgs...).Once().Return(ik, nil)
}

// testing different payload values
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.payload))

req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("idempotency-key", gofakeit.UUID())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user-id", int64(0))

err := handler.Create(c)

if tc.wantErr {
if assert.Error(t, err) && assert.IsType(t, (&echo.HTTPError{}), err) {
he := (err).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code)
}
} else {
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "{\"message\":\"ok\"}", rec.Body.String())
}
}
}
})

t.Run("Error on create ride", func(t *testing.T) {
payload := `{"origin_lat": 0.0, "origin_lon": 0.0, "target_lat": 0.0, "target_lon": 0.0}`
rCode := idempotency.ResponseCodeOK
rBody := idempotency.ResponseBody{Message: "filled"}
tests := []struct {
desc string
retArgs []interface{}
retCode int
retMsg string
}{
{
desc: "error on idempotency key params mismatch",
retArgs: []interface{}{nil, entity.ErrIdemKeyParamsMismatch},
retCode: http.StatusConflict,
retMsg: entity.ErrIdemKeyParamsMismatch.Error(),
},
{
desc: "error on idempotency key request in progress",
retArgs: []interface{}{nil, entity.ErrIdemKeyRequestInProgress},
retCode: http.StatusConflict,
retMsg: entity.ErrIdemKeyRequestInProgress.Error(),
},
{
desc: "generic error on create ride uc",
retArgs: []interface{}{nil, errors.New("error CreateRide")},
retCode: http.StatusInternalServerError,
retMsg: "internal error",
},
{
desc: "empty idempotency key response code",
retArgs: []interface{}{&entity.IdempotencyKey{ResponseBody: &rBody}, nil},
retCode: http.StatusInternalServerError,
retMsg: "internal error",
},
{
desc: "empty idempotency key response body",
retArgs: []interface{}{&entity.IdempotencyKey{ResponseCode: &rCode}, nil},
retCode: http.StatusInternalServerError,
retMsg: "internal error",
},
}

for _, tc := range tests {
uc.On("Create", callArgs...).Once().Return(tc.retArgs...)

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(payload))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("idempotency-key", gofakeit.UUID())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user-id", int64(0))

err := handler.Create(c)

if assert.NoError(t, err, tc.desc) {
assert.Equal(t, tc.retCode, rec.Code, tc.desc)
assert.Equal(t, tc.retMsg, rec.Body.String(), tc.desc)
}
}
})

t.Run("Success on create ride", func(t *testing.T) {
payload := `{"origin_lat": 0.0, "origin_lon": 0.0, "target_lat": 0.0, "target_lon": 0.0}`
rCode := idempotency.ResponseCodeOK
rBody := idempotency.ResponseBody{Message: gofakeit.UUID()}
body, err := json.Marshal(rBody)
require.NoError(t, err)

ik := &entity.IdempotencyKey{
ResponseCode: &rCode,
ResponseBody: &rBody,
}
uc.On("Create", callArgs...).Once().Return(ik, nil)

req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(payload))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set("idempotency-key", gofakeit.UUID())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user-id", int64(0))

err = handler.Create(c)

if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, body, rec.Body.Bytes())
}
})
}
Loading

0 comments on commit 8dafed3

Please sign in to comment.