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

[JOH-51] Implement forgot password endpoint #26

Merged
merged 6 commits into from
Jan 16, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading