diff --git a/cfgldr/config.go b/cfgldr/config.go index 7b2f3ae..b934608 100644 --- a/cfgldr/config.go +++ b/cfgldr/config.go @@ -25,6 +25,7 @@ type Jwt struct { ExpiresIn int `mapstructure:"expires_in"` RefreshTokenTTL int `mapstructure:"refresh_token_ttl"` Issuer string `mapstructure:"issuer"` + ResetTokenTTL int `mapstructure:"reset_token_ttl"` } type Redis struct { @@ -33,11 +34,23 @@ type Redis struct { Password string `mapstructure:"password"` } +type Auth struct { + ClientURL string `mapstructure:"client_url"` +} + +type Sendgrid struct { + ApiKey string `mapstructure:"api_key"` + Name string `mapstructure:"name"` + Address string `mapstructure:"address"` +} + type Config struct { App App `mapstructure:"app"` Database Database `mapstructure:"database"` Jwt Jwt `mapstructure:"jwt"` Redis Redis `mapstructure:"redis"` + Auth Auth `mapstructure:"auth"` + Sendgrid Sendgrid `mapstructure:"sendgrid"` } func LoadConfig() (config *Config, err error) { diff --git a/cmd/main.go b/cmd/main.go index bc0ad6d..9de5070 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,7 @@ import ( cacheRp "github.com/isd-sgcu/johnjud-auth/internal/repository/cache" userRp "github.com/isd-sgcu/johnjud-auth/internal/repository/user" authSvc "github.com/isd-sgcu/johnjud-auth/internal/service/auth" + emailSvc "github.com/isd-sgcu/johnjud-auth/internal/service/email" jwtSvc "github.com/isd-sgcu/johnjud-auth/internal/service/jwt" tokenSvc "github.com/isd-sgcu/johnjud-auth/internal/service/token" userSvc "github.com/isd-sgcu/johnjud-auth/internal/service/user" @@ -132,12 +133,14 @@ func main() { accessTokenCache := cacheRp.NewRepository(cacheDb) refreshTokenCache := cacheRp.NewRepository(cacheDb) + resetPasswordCache := cacheRp.NewRepository(cacheDb) jwtStrategy := strategy.NewJwtStrategy(conf.Jwt.Secret) jwtService := jwtSvc.NewService(conf.Jwt, jwtStrategy, jwtUtil) - tokenService := tokenSvc.NewService(jwtService, accessTokenCache, refreshTokenCache, uuidUtil) + tokenService := tokenSvc.NewService(jwtService, accessTokenCache, refreshTokenCache, resetPasswordCache, uuidUtil) - authService := authSvc.NewService(authRepo, userRepo, tokenService, bcryptUtil) + emailService := emailSvc.NewService(conf.Sendgrid) + authService := authSvc.NewService(authRepo, userRepo, tokenService, emailService, bcryptUtil, conf.Auth) grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer()) authPb.RegisterAuthServiceServer(grpcServer, authService) diff --git a/config/config.example.yaml b/config/config.example.yaml index 2b89cdc..b468695 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -15,8 +15,17 @@ jwt: expires_in: 3600 refresh_token_ttl: 604800 issuer: + reset_token_ttl: 900 redis: host: localhost port: 6379 password: "" + +auth: + client_url: localhost:3000 + +sendgrid: + api_key: + name: johnjud + address: johnjud@gmail.com \ No newline at end of file diff --git a/go.mod b/go.mod index 39da0da..126f022 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,8 @@ require ( github.com/rs/zerolog v1.31.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/go.sum b/go.sum index 4b6f276..74db985 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,10 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/internal/constant/email.constant.go b/internal/constant/email.constant.go new file mode 100644 index 0000000..a29f9ff --- /dev/null +++ b/internal/constant/email.constant.go @@ -0,0 +1,3 @@ +package constant + +const ResetPasswordSubject = "Reset Password Request" diff --git a/internal/domain/dto/token/token.dto.go b/internal/domain/dto/token/token.dto.go index e9450f0..523a2b9 100644 --- a/internal/domain/dto/token/token.dto.go +++ b/internal/domain/dto/token/token.dto.go @@ -29,3 +29,7 @@ type RefreshTokenCache struct { UserID string `json:"user_id"` Role constant.Role `json:"role"` } + +type ResetPasswordTokenCache struct { + UserID string `json:"user_id"` +} diff --git a/internal/service/auth/auth.service.go b/internal/service/auth/auth.service.go index 3cf3e9a..bd4d8ce 100644 --- a/internal/service/auth/auth.service.go +++ b/internal/service/auth/auth.service.go @@ -2,12 +2,14 @@ package auth import ( "context" - + "fmt" + "github.com/isd-sgcu/johnjud-auth/cfgldr" "github.com/isd-sgcu/johnjud-auth/internal/constant" "github.com/isd-sgcu/johnjud-auth/internal/domain/model" "github.com/isd-sgcu/johnjud-auth/internal/utils" "github.com/isd-sgcu/johnjud-auth/pkg/repository/auth" "github.com/isd-sgcu/johnjud-auth/pkg/repository/user" + "github.com/isd-sgcu/johnjud-auth/pkg/service/email" "github.com/isd-sgcu/johnjud-auth/pkg/service/token" "github.com/rs/zerolog/log" @@ -23,15 +25,19 @@ type serviceImpl struct { authRepo auth.Repository userRepo user.Repository tokenService token.Service + emailService email.Service bcryptUtil utils.IBcryptUtil + config cfgldr.Auth } -func NewService(authRepo auth.Repository, userRepo user.Repository, tokenService token.Service, bcryptUtil utils.IBcryptUtil) authProto.AuthServiceServer { +func NewService(authRepo auth.Repository, userRepo user.Repository, tokenService token.Service, emailService email.Service, bcryptUtil utils.IBcryptUtil, config cfgldr.Auth) authProto.AuthServiceServer { return &serviceImpl{ authRepo: authRepo, userRepo: userRepo, tokenService: tokenService, + emailService: emailService, bcryptUtil: bcryptUtil, + config: config, } } @@ -163,3 +169,31 @@ func (s *serviceImpl) SignOut(_ context.Context, request *authProto.SignOutReque return &authProto.SignOutResponse{IsSuccess: true}, nil } + +func (s *serviceImpl) ForgotPassword(_ context.Context, request *authProto.ForgotPasswordRequest) (*authProto.ForgotPasswordResponse, error) { + user := &model.User{} + err := s.userRepo.FindByEmail(request.Email, user) + if err != nil { + return nil, status.Error(codes.NotFound, constant.UserNotFoundErrorMessage) + } + + resetPasswordToken, err := s.tokenService.CreateResetPasswordToken(user.ID.String()) + if err != nil { + return nil, status.Error(codes.Internal, constant.InternalServerErrorMessage) + } + + resetPasswordURL := fmt.Sprintf("%s/reset-password/%s", s.config.ClientURL, resetPasswordToken) + emailSubject := constant.ResetPasswordSubject + emailContent := fmt.Sprintf("Please click the following url to reset password %s", resetPasswordURL) + if err := s.emailService.SendEmail(emailSubject, user.Firstname, user.Email, emailContent); err != nil { + return nil, status.Error(codes.Internal, constant.InternalServerErrorMessage) + } + + return &authProto.ForgotPasswordResponse{ + Url: resetPasswordURL, + }, nil +} + +func (s *serviceImpl) ResetPassword(_ context.Context, request *authProto.ResetPasswordRequest) (*authProto.ResetPasswordResponse, error) { + return nil, nil +} diff --git a/internal/service/auth/auth.service_test.go b/internal/service/auth/auth.service_test.go index bd9325f..6a06313 100644 --- a/internal/service/auth/auth.service_test.go +++ b/internal/service/auth/auth.service_test.go @@ -2,11 +2,14 @@ package auth import ( "context" + "fmt" + "github.com/isd-sgcu/johnjud-auth/cfgldr" "github.com/isd-sgcu/johnjud-auth/internal/constant" tokenDto "github.com/isd-sgcu/johnjud-auth/internal/domain/dto/token" "github.com/isd-sgcu/johnjud-auth/internal/domain/model" "github.com/isd-sgcu/johnjud-auth/mocks/repository/auth" "github.com/isd-sgcu/johnjud-auth/mocks/repository/user" + "github.com/isd-sgcu/johnjud-auth/mocks/service/email" "github.com/isd-sgcu/johnjud-auth/mocks/service/token" "github.com/isd-sgcu/johnjud-auth/mocks/utils" "testing" @@ -26,12 +29,14 @@ import ( type AuthServiceTest struct { suite.Suite - ctx context.Context - signupRequest *authProto.SignUpRequest - signInRequest *authProto.SignInRequest - signOutRequest *authProto.SignOutRequest - refreshTokenRequest *authProto.RefreshTokenRequest - validateRequest *authProto.ValidateRequest + ctx context.Context + signupRequest *authProto.SignUpRequest + signInRequest *authProto.SignInRequest + signOutRequest *authProto.SignOutRequest + refreshTokenRequest *authProto.RefreshTokenRequest + validateRequest *authProto.ValidateRequest + forgotPasswordRequest *authProto.ForgotPasswordRequest + authConfig cfgldr.Auth } func TestAuthService(t *testing.T) { @@ -59,6 +64,12 @@ func (t *AuthServiceTest) SetupTest() { refreshTokenRequest := &authProto.RefreshTokenRequest{ RefreshToken: faker.UUIDDigit(), } + forgotPasswordRequest := &authProto.ForgotPasswordRequest{ + Email: faker.Email(), + } + authConfig := cfgldr.Auth{ + ClientURL: "localhost", + } t.ctx = ctx t.signupRequest = signupRequest @@ -66,6 +77,8 @@ func (t *AuthServiceTest) SetupTest() { t.signOutRequest = signOutRequest t.validateRequest = validateRequest t.refreshTokenRequest = refreshTokenRequest + t.forgotPasswordRequest = forgotPasswordRequest + t.authConfig = authConfig } func (t *AuthServiceTest) TestSignupSuccess() { @@ -102,12 +115,13 @@ func (t *AuthServiceTest) TestSignupSuccess() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} bcryptUtil.On("GenerateHashedPassword", t.signupRequest.Password).Return(hashedPassword, nil) userRepo.On("Create", newUser).Return(createdUser, nil) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignUp(t.ctx, t.signupRequest) assert.Nil(t.T(), err) @@ -124,11 +138,12 @@ func (t *AuthServiceTest) TestSignupHashPasswordFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} bcryptUtil.On("GenerateHashedPassword", t.signupRequest.Password).Return("", hashPasswordErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignUp(t.ctx, t.signupRequest) status, ok := status.FromError(err) @@ -157,12 +172,13 @@ func (t *AuthServiceTest) TestSignupCreateUserDuplicateConstraint() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} bcryptUtil.On("GenerateHashedPassword", t.signupRequest.Password).Return(hashedPassword, nil) userRepo.On("Create", newUser).Return(nil, createUserErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignUp(t.ctx, t.signupRequest) status, ok := status.FromError(err) @@ -191,12 +207,13 @@ func (t *AuthServiceTest) TestSignupCreateUserInternalFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} bcryptUtil.On("GenerateHashedPassword", t.signupRequest.Password).Return(hashedPassword, nil) userRepo.On("Create", newUser).Return(nil, createUserErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignUp(t.ctx, t.signupRequest) status, ok := status.FromError(err) @@ -234,6 +251,7 @@ func (t *AuthServiceTest) TestSignInSuccess() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} userRepo.On("FindByEmail", t.signInRequest.Email, &model.User{}).Return(existUser, nil) @@ -241,7 +259,7 @@ func (t *AuthServiceTest) TestSignInSuccess() { authRepo.EXPECT().Create(newAuthSession).Return(nil) tokenService.On("CreateCredential", existUser.ID.String(), existUser.Role, newAuthSession.ID.String()).Return(credential, nil) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignIn(t.ctx, t.signInRequest) assert.Nil(t.T(), err) @@ -259,11 +277,12 @@ func (t *AuthServiceTest) TestSignInUserNotFound() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} userRepo.On("FindByEmail", t.signInRequest.Email, &model.User{}).Return(nil, findUserErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignIn(t.ctx, t.signInRequest) status, ok := status.FromError(err) @@ -293,12 +312,13 @@ func (t *AuthServiceTest) TestSignInUnmatchedPassword() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} userRepo.On("FindByEmail", t.signInRequest.Email, &model.User{}).Return(existUser, nil) bcryptUtil.On("CompareHashedPassword", existUser.Password, t.signInRequest.Password).Return(comparePwdErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignIn(t.ctx, t.signInRequest) status, ok := status.FromError(err) @@ -331,13 +351,14 @@ func (t *AuthServiceTest) TestSignInCreateAuthSessionFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} userRepo.On("FindByEmail", t.signInRequest.Email, &model.User{}).Return(existUser, nil) bcryptUtil.On("CompareHashedPassword", existUser.Password, t.signInRequest.Password).Return(nil) authRepo.EXPECT().Create(newAuthSession).Return(createAuthSessionErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignIn(t.ctx, t.signInRequest) st, ok := status.FromError(err) @@ -370,6 +391,7 @@ func (t *AuthServiceTest) TestSignInCreateCredentialFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} userRepo.On("FindByEmail", t.signInRequest.Email, &model.User{}).Return(existUser, nil) @@ -377,7 +399,7 @@ func (t *AuthServiceTest) TestSignInCreateCredentialFailed() { authRepo.EXPECT().Create(newAuthSession).Return(nil) tokenService.On("CreateCredential", existUser.ID.String(), existUser.Role, newAuthSession.ID.String()).Return(nil, createCredentialErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignIn(t.ctx, t.signInRequest) status, ok := status.FromError(err) @@ -405,11 +427,12 @@ func (t *AuthServiceTest) TestValidateSuccess() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.validateRequest.Token).Return(userCredential, nil) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.Validate(t.ctx, t.validateRequest) assert.Nil(t.T(), err) @@ -425,11 +448,12 @@ func (t *AuthServiceTest) TestValidateFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.validateRequest.Token).Return(nil, validateErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.Validate(t.ctx, t.validateRequest) st, ok := status.FromError(err) @@ -460,13 +484,14 @@ func (t *AuthServiceTest) TestRefreshTokenSuccess() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("FindRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(refreshTokenCache, nil) tokenService.On("CreateCredential", refreshTokenCache.UserID, refreshTokenCache.Role, refreshTokenCache.AuthSessionID).Return(credential, nil) tokenService.On("RemoveRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(nil) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.RefreshToken(t.ctx, t.refreshTokenRequest) assert.Nil(t.T(), err) @@ -483,11 +508,12 @@ func (t *AuthServiceTest) TestRefreshTokenInvalid() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("FindRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(nil, findTokenErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.RefreshToken(t.ctx, t.refreshTokenRequest) st, ok := status.FromError(err) @@ -507,11 +533,12 @@ func (t *AuthServiceTest) TestRefreshTokenFindTokenFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("FindRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(nil, findTokenErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.RefreshToken(t.ctx, t.refreshTokenRequest) st, ok := status.FromError(err) @@ -536,12 +563,13 @@ func (t *AuthServiceTest) TestRefreshTokenCreateCredentialFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("FindRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(refreshTokenCache, nil) tokenService.On("CreateCredential", refreshTokenCache.UserID, refreshTokenCache.Role, refreshTokenCache.AuthSessionID).Return(nil, createCredentialErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.RefreshToken(t.ctx, t.refreshTokenRequest) st, ok := status.FromError(err) @@ -571,13 +599,14 @@ func (t *AuthServiceTest) TestRefreshTokenRemoveTokenFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("FindRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(refreshTokenCache, nil) tokenService.On("CreateCredential", refreshTokenCache.UserID, refreshTokenCache.Role, refreshTokenCache.AuthSessionID).Return(credential, nil) tokenService.On("RemoveRefreshTokenCache", t.refreshTokenRequest.RefreshToken).Return(removeTokenErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.RefreshToken(t.ctx, t.refreshTokenRequest) st, ok := status.FromError(err) @@ -604,6 +633,7 @@ func (t *AuthServiceTest) TestSignOutSuccess() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.signOutRequest.Token).Return(userCredential, nil) @@ -611,7 +641,7 @@ func (t *AuthServiceTest) TestSignOutSuccess() { tokenService.On("RemoveAccessTokenCache", userCredential.AuthSessionID).Return(nil) authRepo.EXPECT().Delete(userCredential.AuthSessionID).Return(nil) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignOut(t.ctx, t.signOutRequest) assert.Nil(t.T(), err) @@ -626,11 +656,12 @@ func (t *AuthServiceTest) TestSignOutValidateFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.signOutRequest.Token).Return(nil, validateErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignOut(t.ctx, t.signOutRequest) st, ok := status.FromError(err) @@ -656,12 +687,13 @@ func (t *AuthServiceTest) TestSignOutRemoveRefreshTokenCacheFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.signOutRequest.Token).Return(userCredential, nil) tokenService.On("RemoveRefreshTokenCache", userCredential.RefreshToken).Return(removeTokenErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignOut(t.ctx, t.signOutRequest) st, ok := status.FromError(err) @@ -687,13 +719,14 @@ func (t *AuthServiceTest) TestSignOutRemoveAccessTokenCacheFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.signOutRequest.Token).Return(userCredential, nil) tokenService.On("RemoveRefreshTokenCache", userCredential.RefreshToken).Return(nil) tokenService.On("RemoveAccessTokenCache", userCredential.AuthSessionID).Return(removeTokenErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignOut(t.ctx, t.signOutRequest) st, ok := status.FromError(err) @@ -719,6 +752,7 @@ func (t *AuthServiceTest) TestSignOutDeleteAuthSessionFailed() { authRepo := mock_auth.NewMockRepository(controller) userRepo := user.UserRepositoryMock{} tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} bcryptUtil := utils.BcryptUtilMock{} tokenService.On("Validate", t.signOutRequest.Token).Return(userCredential, nil) @@ -726,7 +760,7 @@ func (t *AuthServiceTest) TestSignOutDeleteAuthSessionFailed() { tokenService.On("RemoveAccessTokenCache", userCredential.AuthSessionID).Return(nil) authRepo.EXPECT().Delete(userCredential.AuthSessionID).Return(deleteAuthErr) - authSvc := NewService(authRepo, &userRepo, &tokenService, &bcryptUtil) + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) actual, err := authSvc.SignOut(t.ctx, t.signOutRequest) st, ok := status.FromError(err) @@ -735,3 +769,131 @@ func (t *AuthServiceTest) TestSignOutDeleteAuthSessionFailed() { assert.True(t.T(), ok) assert.Equal(t.T(), expected.Error(), err.Error()) } + +func (t *AuthServiceTest) TestForgotPasswordSuccess() { + userDb := &model.User{ + Base: model.Base{ + ID: uuid.New(), + }, + Email: t.forgotPasswordRequest.Email, + Password: faker.Password(), + Firstname: faker.FirstName(), + Lastname: faker.LastName(), + Role: constant.USER, + } + resetPasswordToken := faker.Word() + resetPasswordURL := fmt.Sprintf("%s/reset-password/%s", t.authConfig.ClientURL, resetPasswordToken) + emailContent := fmt.Sprintf("Please click the following url to reset password %s", resetPasswordURL) + + expected := &authProto.ForgotPasswordResponse{Url: resetPasswordURL} + + controller := gomock.NewController(t.T()) + + authRepo := mock_auth.NewMockRepository(controller) + userRepo := user.UserRepositoryMock{} + tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} + bcryptUtil := utils.BcryptUtilMock{} + + userRepo.On("FindByEmail", t.forgotPasswordRequest.Email, &model.User{}).Return(userDb, nil) + tokenService.On("CreateResetPasswordToken", userDb.ID.String()).Return(resetPasswordToken, nil) + emailService.On("SendEmail", constant.ResetPasswordSubject, userDb.Firstname, userDb.Email, emailContent).Return(nil) + + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) + actual, err := authSvc.ForgotPassword(t.ctx, t.forgotPasswordRequest) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), expected, actual) +} + +func (t *AuthServiceTest) TestForgotPasswordUserNotFound() { + findUserErr := gorm.ErrRecordNotFound + + expected := status.Error(codes.NotFound, constant.UserNotFoundErrorMessage) + + controller := gomock.NewController(t.T()) + + authRepo := mock_auth.NewMockRepository(controller) + userRepo := user.UserRepositoryMock{} + tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} + bcryptUtil := utils.BcryptUtilMock{} + + userRepo.On("FindByEmail", t.forgotPasswordRequest.Email, &model.User{}).Return(nil, findUserErr) + + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) + actual, err := authSvc.ForgotPassword(t.ctx, t.forgotPasswordRequest) + + assert.Nil(t.T(), actual) + assert.Equal(t.T(), expected, err) +} + +func (t *AuthServiceTest) TestForgotPasswordCreateTokenFailed() { + userDb := &model.User{ + Base: model.Base{ + ID: uuid.New(), + }, + Email: t.forgotPasswordRequest.Email, + Password: faker.Password(), + Firstname: faker.FirstName(), + Lastname: faker.LastName(), + Role: constant.USER, + } + createTokenFailed := errors.New("Internal error") + + expected := status.Error(codes.Internal, constant.InternalServerErrorMessage) + + controller := gomock.NewController(t.T()) + + authRepo := mock_auth.NewMockRepository(controller) + userRepo := user.UserRepositoryMock{} + tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} + bcryptUtil := utils.BcryptUtilMock{} + + userRepo.On("FindByEmail", t.forgotPasswordRequest.Email, &model.User{}).Return(userDb, nil) + tokenService.On("CreateResetPasswordToken", userDb.ID.String()).Return("", createTokenFailed) + + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) + actual, err := authSvc.ForgotPassword(t.ctx, t.forgotPasswordRequest) + + assert.Nil(t.T(), actual) + assert.Equal(t.T(), expected, err) +} + +func (t *AuthServiceTest) TestForgotPasswordSendEmailFailed() { + userDb := &model.User{ + Base: model.Base{ + ID: uuid.New(), + }, + Email: t.forgotPasswordRequest.Email, + Password: faker.Password(), + Firstname: faker.FirstName(), + Lastname: faker.LastName(), + Role: constant.USER, + } + resetPasswordToken := faker.Word() + resetPasswordURL := fmt.Sprintf("%s/reset-password/%s", t.authConfig.ClientURL, resetPasswordToken) + emailContent := fmt.Sprintf("Please click the following url to reset password %s", resetPasswordURL) + sendEmailErr := errors.New("Internal error") + + expected := status.Error(codes.Internal, constant.InternalServerErrorMessage) + + controller := gomock.NewController(t.T()) + + authRepo := mock_auth.NewMockRepository(controller) + userRepo := user.UserRepositoryMock{} + tokenService := token.TokenServiceMock{} + emailService := email.EmailServiceMock{} + bcryptUtil := utils.BcryptUtilMock{} + + userRepo.On("FindByEmail", t.forgotPasswordRequest.Email, &model.User{}).Return(userDb, nil) + tokenService.On("CreateResetPasswordToken", userDb.ID.String()).Return(resetPasswordToken, nil) + emailService.On("SendEmail", constant.ResetPasswordSubject, userDb.Firstname, userDb.Email, emailContent).Return(sendEmailErr) + + authSvc := NewService(authRepo, &userRepo, &tokenService, &emailService, &bcryptUtil, t.authConfig) + actual, err := authSvc.ForgotPassword(t.ctx, t.forgotPasswordRequest) + + assert.Nil(t.T(), actual) + assert.Equal(t.T(), expected, err) +} diff --git a/internal/service/email/email.service.go b/internal/service/email/email.service.go new file mode 100644 index 0000000..8ebdab2 --- /dev/null +++ b/internal/service/email/email.service.go @@ -0,0 +1,30 @@ +package email + +import ( + "github.com/isd-sgcu/johnjud-auth/cfgldr" + "github.com/isd-sgcu/johnjud-auth/pkg/service/email" + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" +) + +type serviceImpl struct { + config cfgldr.Sendgrid + client *sendgrid.Client +} + +func (s *serviceImpl) SendEmail(subject string, toName string, toAddress string, content string) error { + from := mail.NewEmail(s.config.Name, s.config.Address) + to := mail.NewEmail(toName, toAddress) + message := mail.NewSingleEmail(from, subject, to, content, content) + + _, err := s.client.Send(message) + if err != nil { + return err + } + return nil +} + +func NewService(config cfgldr.Sendgrid) email.Service { + client := sendgrid.NewSendClient(config.ApiKey) + return &serviceImpl{config: config, client: client} +} diff --git a/internal/service/token/token.service.go b/internal/service/token/token.service.go index 865b78e..fc69d28 100644 --- a/internal/service/token/token.service.go +++ b/internal/service/token/token.service.go @@ -19,18 +19,20 @@ import ( ) type serviceImpl struct { - jwtService jwt.Service - accessTokenCache cache.Repository - refreshTokenCache cache.Repository - uuidUtil utils.IUuidUtil + jwtService jwt.Service + accessTokenCache cache.Repository + refreshTokenCache cache.Repository + resetPasswordTokenCache cache.Repository + uuidUtil utils.IUuidUtil } -func NewService(jwtService jwt.Service, accessTokenCache cache.Repository, refreshTokenCache cache.Repository, uuidUtil utils.IUuidUtil) token.Service { +func NewService(jwtService jwt.Service, accessTokenCache cache.Repository, refreshTokenCache cache.Repository, resetPasswordTokenCache cache.Repository, uuidUtil utils.IUuidUtil) token.Service { return &serviceImpl{ - jwtService: jwtService, - accessTokenCache: accessTokenCache, - refreshTokenCache: refreshTokenCache, - uuidUtil: uuidUtil, + jwtService: jwtService, + accessTokenCache: accessTokenCache, + refreshTokenCache: refreshTokenCache, + resetPasswordTokenCache: resetPasswordTokenCache, + uuidUtil: uuidUtil, } } @@ -167,3 +169,39 @@ func (s *serviceImpl) RemoveRefreshTokenCache(refreshToken string) error { return nil } + +func (s *serviceImpl) CreateResetPasswordToken(userId string) (string, error) { + resetPasswordToken := s.CreateRefreshToken() + tokenCache := &tokenDto.ResetPasswordTokenCache{ + UserID: userId, + } + err := s.resetPasswordTokenCache.SetValue(resetPasswordToken, tokenCache, s.jwtService.GetConfig().ResetTokenTTL) + if err != nil { + return "", err + } + return resetPasswordToken, nil +} + +func (s *serviceImpl) FindResetPasswordToken(token string) (*tokenDto.ResetPasswordTokenCache, error) { + tokenCache := &tokenDto.ResetPasswordTokenCache{} + err := s.resetPasswordTokenCache.GetValue(token, tokenCache) + if err != nil { + if err != redis.Nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + return tokenCache, nil +} + +func (s *serviceImpl) RemoveResetPasswordToken(token string) error { + err := s.resetPasswordTokenCache.DeleteValue(token) + if err != nil { + if err != redis.Nil { + return err + } + } + + return nil +} diff --git a/internal/service/token/token.service_test.go b/internal/service/token/token.service_test.go index a0e0255..bbb273e 100644 --- a/internal/service/token/token.service_test.go +++ b/internal/service/token/token.service_test.go @@ -48,6 +48,7 @@ func (t *TokenServiceTest) SetupTest() { ExpiresIn: 3600, RefreshTokenTTL: 604800, Issuer: "testIssuer", + ResetTokenTTL: 900, } validateToken := "" @@ -83,6 +84,7 @@ func (t *TokenServiceTest) TestCreateCredentialSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("SignAuth", t.userId, t.role, t.authSessionId).Return(t.accessToken, nil) @@ -91,7 +93,7 @@ func (t *TokenServiceTest) TestCreateCredentialSuccess() { accessTokenRepo.EXPECT().SetValue(t.authSessionId, accessTokenCache, t.jwtConfig.ExpiresIn).Return(nil) refreshTokenRepo.EXPECT().SetValue(t.refreshToken.String(), refreshTokenCache, t.jwtConfig.RefreshTokenTTL).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.CreateCredential(t.userId, t.role, t.authSessionId) assert.Nil(t.T(), err) @@ -109,11 +111,12 @@ func (t *TokenServiceTest) TestCreateCredentialSignAuthFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("SignAuth", t.userId, t.role, t.authSessionId).Return("", signAuthError) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.CreateCredential(t.userId, t.role, t.authSessionId) assert.Nil(t.T(), actual) @@ -134,6 +137,7 @@ func (t *TokenServiceTest) TestCreateCredentialSetAccessTokenFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("SignAuth", t.userId, t.role, t.authSessionId).Return(t.accessToken, nil) @@ -141,7 +145,7 @@ func (t *TokenServiceTest) TestCreateCredentialSetAccessTokenFailed() { uuidUtil.On("GetNewUUID").Return(t.refreshToken) accessTokenRepo.EXPECT().SetValue(t.authSessionId, accessTokenCache, t.jwtConfig.ExpiresIn).Return(setCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.CreateCredential(t.userId, t.role, t.authSessionId) assert.Nil(t.T(), actual) @@ -167,6 +171,7 @@ func (t *TokenServiceTest) TestCreateCredentialSetRefreshTokenFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("SignAuth", t.userId, t.role, t.authSessionId).Return(t.accessToken, nil) @@ -175,7 +180,7 @@ func (t *TokenServiceTest) TestCreateCredentialSetRefreshTokenFailed() { accessTokenRepo.EXPECT().SetValue(t.authSessionId, accessTokenCache, t.jwtConfig.ExpiresIn).Return(nil) refreshTokenRepo.EXPECT().SetValue(t.refreshToken.String(), refreshTokenCache, t.jwtConfig.RefreshTokenTTL).Return(setCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.CreateCredential(t.userId, t.role, t.authSessionId) assert.Nil(t.T(), actual) @@ -207,13 +212,14 @@ func (t *TokenServiceTest) TestValidateSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) accessTokenRepo.EXPECT().GetValue(payloads["auth_session_id"].(string), accessTokenCache).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), err) @@ -241,12 +247,13 @@ func (t *TokenServiceTest) TestValidateInvalidIssuer() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), actual) @@ -273,12 +280,13 @@ func (t *TokenServiceTest) TestValidateExpireToken() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), actual) @@ -293,11 +301,12 @@ func (t *TokenServiceTest) TestValidateVerifyFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(nil, expected) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), actual) @@ -325,13 +334,14 @@ func (t *TokenServiceTest) TestValidateGetCacheKeyNotFound() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) accessTokenRepo.EXPECT().GetValue(payloads["auth_session_id"].(string), accessTokenCache).Return(redis.Nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), actual) @@ -360,13 +370,14 @@ func (t *TokenServiceTest) TestValidateGetCacheInternalFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", t.validateToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) accessTokenRepo.EXPECT().GetValue(payloads["auth_session_id"].(string), accessTokenCache).Return(getCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(t.validateToken) assert.Nil(t.T(), actual) @@ -395,13 +406,14 @@ func (t *TokenServiceTest) TestValidateInvalidToken() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} jwtService.On("VerifyAuth", invalidToken).Return(jwtToken, nil) jwtService.On("GetConfig").Return(t.jwtConfig) accessTokenRepo.EXPECT().GetValue(payloads["auth_session_id"].(string), accessTokenCache).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.Validate(invalidToken) assert.Nil(t.T(), actual) @@ -416,11 +428,12 @@ func (t *TokenServiceTest) TestCreateRefreshTokenSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} uuidUtil.On("GetNewUUID").Return(t.refreshToken) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual := tokenSvc.CreateRefreshToken() assert.Equal(t.T(), expected, actual) @@ -432,11 +445,12 @@ func (t *TokenServiceTest) TestRemoveAccessTokenCacheSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} accessTokenRepo.EXPECT().DeleteValue(t.authSessionId).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) err := tokenSvc.RemoveAccessTokenCache(t.authSessionId) assert.Nil(t.T(), err) @@ -452,11 +466,12 @@ func (t *TokenServiceTest) TestRemoveAccessTokenCacheDeleteInternalFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} accessTokenRepo.EXPECT().DeleteValue(t.authSessionId).Return(deleteAccessTokenCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) err := tokenSvc.RemoveAccessTokenCache(t.authSessionId) assert.Equal(t.T(), expected, err) @@ -470,11 +485,12 @@ func (t *TokenServiceTest) TestFindRefreshTokenCacheSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} refreshTokenRepo.EXPECT().GetValue(t.refreshToken.String(), &tokenDto.RefreshTokenCache{}).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.FindRefreshTokenCache(t.refreshToken.String()) assert.Nil(t.T(), err) @@ -491,11 +507,12 @@ func (t *TokenServiceTest) TestFindRefreshTokenCacheInvalid() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} refreshTokenRepo.EXPECT().GetValue(t.refreshToken.String(), &tokenDto.RefreshTokenCache{}).Return(getCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.FindRefreshTokenCache(t.refreshToken.String()) assert.Nil(t.T(), actual) @@ -512,11 +529,12 @@ func (t *TokenServiceTest) TestFindRefreshTokenCacheInternalError() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} refreshTokenRepo.EXPECT().GetValue(t.refreshToken.String(), &tokenDto.RefreshTokenCache{}).Return(getCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) actual, err := tokenSvc.FindRefreshTokenCache(t.refreshToken.String()) assert.Nil(t.T(), actual) @@ -529,11 +547,12 @@ func (t *TokenServiceTest) TestRemoveRefreshTokenCacheSuccess() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} refreshTokenRepo.EXPECT().DeleteValue(t.refreshToken.String()).Return(nil) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) err := tokenSvc.RemoveRefreshTokenCache(t.refreshToken.String()) assert.Nil(t.T(), err) @@ -549,12 +568,170 @@ func (t *TokenServiceTest) TestRemoveRefreshTokenCacheDeleteInternalFailed() { jwtService := jwt.JwtServiceMock{} accessTokenRepo := mock_cache.NewMockRepository(controller) refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) uuidUtil := utils.UuidUtilMock{} refreshTokenRepo.EXPECT().DeleteValue(t.refreshToken.String()).Return(deleteRefreshTokenCacheErr) - tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, &uuidUtil) + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) err := tokenSvc.RemoveRefreshTokenCache(t.refreshToken.String()) assert.Equal(t.T(), expected, err) } + +func (t *TokenServiceTest) TestCreateResetPasswordTokenSuccess() { + tokenCache := &tokenDto.ResetPasswordTokenCache{ + UserID: t.userId, + } + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + uuidUtil.On("GetNewUUID").Return(t.refreshToken) + jwtService.On("GetConfig").Return(t.jwtConfig) + resetPasswordTokenRepo.EXPECT().SetValue(t.refreshToken.String(), tokenCache, t.jwtConfig.ResetTokenTTL).Return(nil) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + actual, err := tokenSvc.CreateResetPasswordToken(t.userId) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), t.refreshToken.String(), actual) +} + +func (t *TokenServiceTest) TestCreateResetPasswordTokenFailed() { + tokenCache := &tokenDto.ResetPasswordTokenCache{ + UserID: t.userId, + } + cacheErr := errors.New("Internal error") + + expected := cacheErr + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + uuidUtil.On("GetNewUUID").Return(t.refreshToken) + jwtService.On("GetConfig").Return(t.jwtConfig) + resetPasswordTokenRepo.EXPECT().SetValue(t.refreshToken.String(), tokenCache, t.jwtConfig.ResetTokenTTL).Return(cacheErr) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + actual, err := tokenSvc.CreateResetPasswordToken(t.userId) + + assert.Equal(t.T(), "", actual) + assert.Equal(t.T(), expected, err) +} + +func (t *TokenServiceTest) TestFindResetPasswordTokenSuccess() { + tokenCache := &tokenDto.ResetPasswordTokenCache{} + + expected := tokenCache + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + resetPasswordTokenRepo.EXPECT().GetValue(t.refreshToken.String(), tokenCache).Return(nil) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + actual, err := tokenSvc.FindResetPasswordToken(t.refreshToken.String()) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), expected, actual) +} + +func (t *TokenServiceTest) TestFindResetPasswordTokenNotFound() { + tokenCache := &tokenDto.ResetPasswordTokenCache{} + cacheErr := redis.Nil + + expected := status.Error(codes.InvalidArgument, cacheErr.Error()) + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + resetPasswordTokenRepo.EXPECT().GetValue(t.refreshToken.String(), tokenCache).Return(cacheErr) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + actual, err := tokenSvc.FindResetPasswordToken(t.refreshToken.String()) + + assert.Nil(t.T(), actual) + assert.Equal(t.T(), expected, err) +} + +func (t *TokenServiceTest) TestFindResetPasswordTokenInternalError() { + tokenCache := &tokenDto.ResetPasswordTokenCache{} + cacheErr := errors.New("Internal error") + + expected := status.Error(codes.Internal, cacheErr.Error()) + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + resetPasswordTokenRepo.EXPECT().GetValue(t.refreshToken.String(), tokenCache).Return(cacheErr) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + actual, err := tokenSvc.FindResetPasswordToken(t.refreshToken.String()) + + assert.Nil(t.T(), actual) + assert.Equal(t.T(), expected, err) +} + +func (t *TokenServiceTest) TestRemoveResetPasswordTokenSuccess() { + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + resetPasswordTokenRepo.EXPECT().DeleteValue(t.refreshToken.String()).Return(nil) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + err := tokenSvc.RemoveResetPasswordToken(t.refreshToken.String()) + + assert.Nil(t.T(), err) +} + +func (t *TokenServiceTest) TestRemoveResetPasswordTokenFailed() { + cacheErr := errors.New("Internal error") + + expected := cacheErr + + controller := gomock.NewController(t.T()) + + jwtService := jwt.JwtServiceMock{} + accessTokenRepo := mock_cache.NewMockRepository(controller) + refreshTokenRepo := mock_cache.NewMockRepository(controller) + resetPasswordTokenRepo := mock_cache.NewMockRepository(controller) + uuidUtil := utils.UuidUtilMock{} + + resetPasswordTokenRepo.EXPECT().DeleteValue(t.refreshToken.String()).Return(cacheErr) + + tokenSvc := NewService(&jwtService, accessTokenRepo, refreshTokenRepo, resetPasswordTokenRepo, &uuidUtil) + err := tokenSvc.RemoveResetPasswordToken(t.refreshToken.String()) + + assert.Equal(t.T(), expected, err) +} diff --git a/mocks/service/email/email.mock.go b/mocks/service/email/email.mock.go new file mode 100644 index 0000000..3910f63 --- /dev/null +++ b/mocks/service/email/email.mock.go @@ -0,0 +1,12 @@ +package email + +import "github.com/stretchr/testify/mock" + +type EmailServiceMock struct { + mock.Mock +} + +func (m *EmailServiceMock) SendEmail(subject string, toName string, toAddress string, content string) error { + args := m.Called(subject, toName, toAddress, content) + return args.Error(0) +} diff --git a/mocks/service/token/token.mock.go b/mocks/service/token/token.mock.go index df64d75..b294964 100644 --- a/mocks/service/token/token.mock.go +++ b/mocks/service/token/token.mock.go @@ -52,3 +52,26 @@ func (m *TokenServiceMock) RemoveRefreshTokenCache(refreshToken string) error { args := m.Called(refreshToken) return args.Error(0) } + +func (m *TokenServiceMock) CreateResetPasswordToken(userId string) (string, error) { + args := m.Called(userId) + if args.Get(0) != "" { + return args.Get(0).(string), nil + } + + return "", args.Error(1) +} + +func (m *TokenServiceMock) FindResetPasswordToken(token string) (*tokenDto.ResetPasswordTokenCache, error) { + args := m.Called(token) + if args.Get(0) != nil { + return args.Get(0).(*tokenDto.ResetPasswordTokenCache), nil + } + + return nil, args.Error(1) +} + +func (m *TokenServiceMock) RemoveResetPasswordToken(token string) error { + args := m.Called(token) + return args.Error(0) +} diff --git a/pkg/service/email/email.service.go b/pkg/service/email/email.service.go new file mode 100644 index 0000000..f09e612 --- /dev/null +++ b/pkg/service/email/email.service.go @@ -0,0 +1,5 @@ +package email + +type Service interface { + SendEmail(subject string, toName string, toAddress string, content string) error +} diff --git a/pkg/service/token/token.service.go b/pkg/service/token/token.service.go index 7ba74b3..2948dae 100644 --- a/pkg/service/token/token.service.go +++ b/pkg/service/token/token.service.go @@ -13,4 +13,7 @@ type Service interface { RemoveAccessTokenCache(authSessionId string) error FindRefreshTokenCache(refreshToken string) (*tokenDto.RefreshTokenCache, error) RemoveRefreshTokenCache(refreshToken string) error + CreateResetPasswordToken(userId string) (string, error) + FindResetPasswordToken(token string) (*tokenDto.ResetPasswordTokenCache, error) + RemoveResetPasswordToken(token string) error }