Skip to content

Commit

Permalink
Merge pull request #26 from isd-sgcu/feature/JOH-51
Browse files Browse the repository at this point in the history
[JOH-51] Implement forgot password endpoint
  • Loading branch information
Nitiwat-owen authored Jan 16, 2024
2 parents df69d94 + 13320ac commit b388de5
Show file tree
Hide file tree
Showing 15 changed files with 1,261 additions and 150 deletions.
8 changes: 8 additions & 0 deletions src/app/dto/auth.dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ type SignOutResponse struct {
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}

type ForgotPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}

type ForgotPasswordResponse struct {
IsSuccess bool `json:"is_success"`
}
46 changes: 46 additions & 0 deletions src/app/handler/auth/auth.handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,49 @@ func (h *Handler) RefreshToken(c router.IContext) {

c.JSON(http.StatusOK, response)
}

// ForgotPassword is a function to send email to reset password when you forgot password
// @Summary Forgot Password
// @Description Return isSuccess
// @Param request body dto.ForgotPasswordRequest true "forgotPassword request dto"
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} dto.ForgotPasswordResponse
// @Failure 400 {object} dto.ResponseBadRequestErr "Invalid email"
// @Failure 500 {object} dto.ResponseInternalErr "Internal service error"
// @Failure 503 {object} dto.ResponseServiceDownErr "Service is down"
// @Router /v1/auth/forgot-password [post]
func (h *Handler) ForgotPassword(c router.IContext) {
request := &dto.ForgotPasswordRequest{}
err := c.Bind(request)
if err != nil {
c.JSON(http.StatusBadRequest, dto.ResponseErr{
StatusCode: http.StatusBadRequest,
Message: constant.BindingRequestErrorMessage + err.Error(),
Data: nil,
})
return
}

if err := h.validate.Validate(request); err != nil {
var errorMessage []string
for _, reqErr := range err {
errorMessage = append(errorMessage, reqErr.Message)
}
c.JSON(http.StatusBadRequest, dto.ResponseErr{
StatusCode: http.StatusBadRequest,
Message: constant.InvalidRequestBodyMessage + strings.Join(errorMessage, ", "),
Data: nil,
})
return
}

response, respErr := h.service.ForgotPassword(request)
if respErr != nil {
c.JSON(respErr.StatusCode, respErr)
return
}

c.JSON(http.StatusOK, response)
}
103 changes: 98 additions & 5 deletions src/app/handler/auth/auth.handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import (

type AuthHandlerTest struct {
suite.Suite
signupRequest *dto.SignupRequest
signInRequest *dto.SignInRequest
refreshTokenRequest *dto.RefreshTokenRequest
bindErr error
validateErr []*dto.BadReqErrResponse
signupRequest *dto.SignupRequest
signInRequest *dto.SignInRequest
refreshTokenRequest *dto.RefreshTokenRequest
forgotPasswordRequest *dto.ForgotPasswordRequest
bindErr error
validateErr []*dto.BadReqErrResponse
}

func TestAuthHandler(t *testing.T) {
Expand All @@ -33,6 +34,7 @@ func (t *AuthHandlerTest) SetupTest() {
signupRequest := &dto.SignupRequest{}
signInRequest := &dto.SignInRequest{}
refreshTokenRequest := &dto.RefreshTokenRequest{}
forgotPasswordRequest := &dto.ForgotPasswordRequest{}
bindErr := errors.New("Binding request failed")
validateErr := []*dto.BadReqErrResponse{
{
Expand All @@ -50,6 +52,7 @@ func (t *AuthHandlerTest) SetupTest() {
t.signupRequest = signupRequest
t.signInRequest = signInRequest
t.refreshTokenRequest = refreshTokenRequest
t.forgotPasswordRequest = forgotPasswordRequest
t.bindErr = bindErr
t.validateErr = validateErr
}
Expand Down Expand Up @@ -378,3 +381,93 @@ func (t *AuthHandlerTest) TestRefreshTokenServiceError() {

handler.RefreshToken(context)
}

func (t *AuthHandlerTest) TestForgotPasswordSuccess() {
forgotPasswordResponse := &dto.ForgotPasswordResponse{
IsSuccess: true,
}

controller := gomock.NewController(t.T())

authSvc := authMock.NewMockService(controller)
userSvc := userMock.NewMockService(controller)
validator := validatorMock.NewMockIDtoValidator(controller)
context := routerMock.NewMockIContext(controller)

handler := NewHandler(authSvc, userSvc, validator)

context.EXPECT().Bind(t.forgotPasswordRequest).Return(nil)
validator.EXPECT().Validate(t.forgotPasswordRequest).Return(nil)
authSvc.EXPECT().ForgotPassword(t.forgotPasswordRequest).Return(forgotPasswordResponse, nil)
context.EXPECT().JSON(http.StatusOK, forgotPasswordResponse)

handler.ForgotPassword(context)
}

func (t *AuthHandlerTest) TestForgotPasswordBindFailed() {
errResponse := dto.ResponseErr{
StatusCode: http.StatusBadRequest,
Message: constant.BindingRequestErrorMessage + t.bindErr.Error(),
Data: nil,
}

controller := gomock.NewController(t.T())

authSvc := authMock.NewMockService(controller)
userSvc := userMock.NewMockService(controller)
validator := validatorMock.NewMockIDtoValidator(controller)
context := routerMock.NewMockIContext(controller)

handler := NewHandler(authSvc, userSvc, validator)

context.EXPECT().Bind(t.forgotPasswordRequest).Return(t.bindErr)
context.EXPECT().JSON(http.StatusBadRequest, errResponse)
handler.ForgotPassword(context)
}

func (t *AuthHandlerTest) TestForgotPasswordValidateFailed() {
errResponse := dto.ResponseErr{
StatusCode: http.StatusBadRequest,
Message: constant.InvalidRequestBodyMessage + "BadRequestError1, BadRequestError2",
Data: nil,
}

controller := gomock.NewController(t.T())

authSvc := authMock.NewMockService(controller)
userSvc := userMock.NewMockService(controller)
validator := validatorMock.NewMockIDtoValidator(controller)
context := routerMock.NewMockIContext(controller)

handler := NewHandler(authSvc, userSvc, validator)

context.EXPECT().Bind(t.forgotPasswordRequest).Return(nil)
validator.EXPECT().Validate(t.forgotPasswordRequest).Return(t.validateErr)
context.EXPECT().JSON(http.StatusBadRequest, errResponse)

handler.ForgotPassword(context)
}

func (t *AuthHandlerTest) TestForgotPasswordServiceError() {
forgotPasswordErr := &dto.ResponseErr{
StatusCode: http.StatusInternalServerError,
Message: constant.InternalErrorMessage,
Data: nil,
}

controller := gomock.NewController(t.T())

authSvc := authMock.NewMockService(controller)
userSvc := userMock.NewMockService(controller)
validator := validatorMock.NewMockIDtoValidator(controller)
context := routerMock.NewMockIContext(controller)

handler := NewHandler(authSvc, userSvc, validator)

context.EXPECT().Bind(t.forgotPasswordRequest).Return(nil)
validator.EXPECT().Validate(t.forgotPasswordRequest).Return(nil)
authSvc.EXPECT().ForgotPassword(t.forgotPasswordRequest).Return(nil, forgotPasswordErr)
context.EXPECT().JSON(http.StatusInternalServerError, forgotPasswordErr)

handler.ForgotPassword(context)
}
53 changes: 53 additions & 0 deletions src/app/service/auth/auth.service.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,56 @@ func (s *Service) RefreshToken(request *dto.RefreshTokenRequest) (*dto.Credentia
ExpiresIn: int(response.Credential.ExpiresIn),
}, nil
}

func (s *Service) ForgotPassword(request *dto.ForgotPasswordRequest) (*dto.ForgotPasswordResponse, *dto.ResponseErr) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_, err := s.client.ForgotPassword(ctx, &authProto.ForgotPasswordRequest{
Email: request.Email,
})
if err != nil {
st, ok := status.FromError(err)
log.Error().
Str("service", "auth").
Str("action", "ForgotPassword").
Str("email", request.Email).
Msg(st.Message())
if !ok {
return nil, &dto.ResponseErr{
StatusCode: http.StatusInternalServerError,
Message: constant.InternalErrorMessage,
Data: nil,
}
}
switch st.Code() {
case codes.NotFound:
return nil, &dto.ResponseErr{
StatusCode: http.StatusNotFound,
Message: constant.UserNotFoundMessage,
Data: nil,
}
case codes.Internal:
return nil, &dto.ResponseErr{
StatusCode: http.StatusInternalServerError,
Message: constant.InternalErrorMessage,
Data: nil,
}
default:
return nil, &dto.ResponseErr{
StatusCode: http.StatusServiceUnavailable,
Message: constant.UnavailableServiceMessage,
Data: nil,
}
}
}

log.Info().
Str("service", "auth").
Str("action", "ForgotPassword").
Str("email", request.Email).
Msg("Forgot password successfully")
return &dto.ForgotPasswordResponse{
IsSuccess: true,
}, nil
}
122 changes: 118 additions & 4 deletions src/app/service/auth/auth.service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import (

type AuthServiceTest struct {
suite.Suite
signupRequestDto *dto.SignupRequest
signInDto *dto.SignInRequest
token string
refreshTokenRequest *dto.RefreshTokenRequest
signupRequestDto *dto.SignupRequest
signInDto *dto.SignInRequest
token string
refreshTokenRequest *dto.RefreshTokenRequest
forgotPasswordRequest *dto.ForgotPasswordRequest
}

func TestAuthService(t *testing.T) {
Expand All @@ -43,11 +44,15 @@ func (t *AuthServiceTest) SetupTest() {
refreshTokenRequest := &dto.RefreshTokenRequest{
RefreshToken: faker.UUIDDigit(),
}
forgotPasswordRequest := &dto.ForgotPasswordRequest{
Email: faker.Email(),
}

t.signupRequestDto = signupRequestDto
t.signInDto = signInDto
t.token = token
t.refreshTokenRequest = refreshTokenRequest
t.forgotPasswordRequest = forgotPasswordRequest
}

func (t *AuthServiceTest) TestSignupSuccess() {
Expand Down Expand Up @@ -601,3 +606,112 @@ func (t *AuthServiceTest) TestRefreshTokenUnknownError() {
assert.Nil(t.T(), actual)
assert.Equal(t.T(), expected, err)
}

func (t *AuthServiceTest) TestForgotPasswordSuccess() {
protoReq := &authProto.ForgotPasswordRequest{
Email: t.forgotPasswordRequest.Email,
}
protoResp := &authProto.ForgotPasswordResponse{
Url: faker.URL(),
}
expected := &dto.ForgotPasswordResponse{
IsSuccess: true,
}

client := auth.AuthClientMock{}
client.On("ForgotPassword", protoReq).Return(protoResp, nil)

svc := NewService(&client)
actual, err := svc.ForgotPassword(t.forgotPasswordRequest)

assert.Nil(t.T(), err)
assert.Equal(t.T(), expected, actual)
}

func (t *AuthServiceTest) TestForgotPasswordNotFound() {
protoReq := &authProto.ForgotPasswordRequest{
Email: t.forgotPasswordRequest.Email,
}
protoErr := status.Error(codes.NotFound, "Not found")

expected := &dto.ResponseErr{
StatusCode: http.StatusNotFound,
Message: constant.UserNotFoundMessage,
Data: nil,
}

client := auth.AuthClientMock{}
client.On("ForgotPassword", protoReq).Return(nil, protoErr)

svc := NewService(&client)
actual, err := svc.ForgotPassword(t.forgotPasswordRequest)

assert.Nil(t.T(), actual)
assert.Equal(t.T(), expected, err)
}

func (t *AuthServiceTest) TestForgotPasswordInternalErr() {
protoReq := &authProto.ForgotPasswordRequest{
Email: t.forgotPasswordRequest.Email,
}
protoErr := status.Error(codes.Internal, "Internal error")

expected := &dto.ResponseErr{
StatusCode: http.StatusInternalServerError,
Message: constant.InternalErrorMessage,
Data: nil,
}

client := auth.AuthClientMock{}
client.On("ForgotPassword", protoReq).Return(nil, protoErr)

svc := NewService(&client)
actual, err := svc.ForgotPassword(t.forgotPasswordRequest)

assert.Nil(t.T(), actual)
assert.Equal(t.T(), expected, err)
}

func (t *AuthServiceTest) TestForgotPasswordUnavailableService() {
protoReq := &authProto.ForgotPasswordRequest{
Email: t.forgotPasswordRequest.Email,
}
protoErr := status.Error(codes.Unavailable, "Connection lost")

expected := &dto.ResponseErr{
StatusCode: http.StatusServiceUnavailable,
Message: constant.UnavailableServiceMessage,
Data: nil,
}

client := auth.AuthClientMock{}
client.On("ForgotPassword", protoReq).Return(nil, protoErr)

svc := NewService(&client)
actual, err := svc.ForgotPassword(t.forgotPasswordRequest)

assert.Nil(t.T(), actual)
assert.Equal(t.T(), expected, err)
}

func (t *AuthServiceTest) TestForgotPasswordUnknownErr() {
protoReq := &authProto.ForgotPasswordRequest{
Email: t.forgotPasswordRequest.Email,
}
protoErr := errors.New("Unknown Error")

expected := &dto.ResponseErr{
StatusCode: http.StatusInternalServerError,
Message: constant.InternalErrorMessage,
Data: nil,
}

client := auth.AuthClientMock{}
client.On("ForgotPassword", protoReq).Return(nil, protoErr)

svc := NewService(&client)
actual, err := svc.ForgotPassword(t.forgotPasswordRequest)

assert.Nil(t.T(), actual)
assert.Equal(t.T(), expected, err)
}
Loading

0 comments on commit b388de5

Please sign in to comment.