-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
57aa5b4
commit 8dafed3
Showing
6 changed files
with
505 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
}) | ||
} |
Oops, something went wrong.