package pwreset import ( "errors" "fmt" "strings" "testing" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" "src.dualinventive.com/go/authentication-service/internal/domain" ) const ( userName string = "some-username" email string = "some-email@some-domain.com" invalidEmail string = "some$in!@valid-email@some-domain.com.tar.gz" password string = "Some-password1" passwordVerify string = "Some-password1" passwordVerifyInvalid string = "Some-other-password2" someGormError string = "some-gorm-error" someSMTPError string = "some-smtp-error" someResetCode string = "some-reset-code" someOtherResetCode string = "some-other-reset-code" ) var ( testCredentialRepo *TestCredentialRepository //nolint:gochecknoglobals testCodeRepo *TestCodeRepository //nolint:gochecknoglobals testTemplateRepo *TestTemplateRepository //nolint:gochecknoglobals testEmailSender *TestEmailSender //nolint:gochecknoglobals ) func before() { testCredentialRepo = &TestCredentialRepository{returnErr: []error{nil, nil, nil}} testCodeRepo = &TestCodeRepository{0, make(map[string]string), []error{nil, nil, nil}} testTemplateRepo = &TestTemplateRepository{} testEmailSender = &TestEmailSender{nil, []*sent{}} } func TestRequestPasswordResetWhenFailedFetch(t *testing.T) { before() //given testCredentialRepo.returnMe = nil testCredentialRepo.returnErr = []error{errors.New(someGormError)} //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.Equal(t, &ErrUserFetchFailed{userName, errors.New(someGormError)}, err) } func TestRequestPasswordResetWithUnknownUsername(t *testing.T) { before() //given testCredentialRepo.returnMe = nil testCredentialRepo.returnErr = []error{nil} //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.Equal(t, &ErrUserNotFound{userName}, err) } func TestRequestPasswordResetWithEmptyEmail(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: invalidEmail} //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.Equal(t, &ErrUserEmailInvalid{userName, invalidEmail}, err) } func TestRequestPasswordResetWithInvalidEmail(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: ""} //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.Equal(t, &ErrUserEmailAddressEmpty{userName}, err) } func TestRequestPasswordResetWhenFailedCodeCreation(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.returnErr = []error{errors.New("some-error")} //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.True(t, strings.HasPrefix(err.Error(), "failed to create password reset code for user 'some-username'")) require.Equal(t, 0, len(testEmailSender.sent)) } func TestRequestPasswordResetWhenFailedSendMail(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testEmailSender.returnErr = errors.New(someSMTPError) //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.NotNil(t, err) require.Equal( t, "failed to send password reset code to email 'some-email@some-domain.com' "+ "for user 'some-username': some-smtp-error", err.Error()) } func TestRequestPasswordReset(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testTemplateRepo.returnMe = []byte("test template") //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) //then require.Nil(t, err) require.Equal(t, 1, len(testCodeRepo.codes)) require.NotNil(t, testCodeRepo.codes[userName]) require.Equal(t, 1, len(testEmailSender.sent)) sent := testEmailSender.sent[0] require.NotNil(t, sent) require.Equal(t, sent.body, []byte("test template")) require.Equal(t, sent.to[0], email) } func TestRequestPasswordResetWhenPreviousCodeExistsThenOverwriteRestCode(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testTemplateRepo.returnMe = []byte("test template") //when err := RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) resetCode1 := testCodeRepo.codes[userName] //then require.Nil(t, err) require.Equal(t, len(testCodeRepo.codes), 1) require.NotNil(t, resetCode1) //when err = RequestPasswordReset(testCredentialRepo, testEmailSender, testTemplateRepo, testCodeRepo, userName) resetCode2 := testCodeRepo.codes[userName] //then require.Nil(t, err) require.Equal(t, len(testCodeRepo.codes), 1) require.NotNil(t, resetCode2) require.NotEqual(t, resetCode1, resetCode2) require.Equal(t, len(testEmailSender.sent), 2) } func TestRedeemPasswordResetWhenUserFetchFailure(t *testing.T) { before() //given testCredentialRepo.returnErr[0] = errors.New("some-database-error") //when err := RedeemPasswordReset(testCredentialRepo, testCodeRepo, userName, "", password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "failed to retrieve user 'some-username': some-database-error", err.Error()) } func TestRedeemPasswordResetWithUnknownUsername(t *testing.T) { before() //given testCredentialRepo.returnMe = nil testCredentialRepo.returnErr = []error{nil} //when err := RedeemPasswordReset(testCredentialRepo, testCodeRepo, userName, "", password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "user 'some-username' not found", err.Error()) } func TestRedeemPasswordResetWithResetCodeFetchFailure(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.returnErr = []error{errors.New("some-redis-error")} //when err := RedeemPasswordReset(testCredentialRepo, testCodeRepo, userName, "", password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "failed to get password reset code for user 'some-username': some-redis-error", err.Error()) } func TestRedeemPasswordResetWithUnknownResetCode(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = "" //when err := RedeemPasswordReset(testCredentialRepo, testCodeRepo, userName, "", password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "no password reset code found for user 'some-username'", err.Error()) } func TestRedeemPasswordResetWithInvalidResetCode(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = someResetCode //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someOtherResetCode, password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "invalid password reset code 'some-other-reset-code' for user 'some-username'", err.Error()) } func TestRedeemPasswordResetWithInvalidPasswordVerify(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = someResetCode //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someResetCode, password, passwordVerifyInvalid) //then require.NotNil(t, err) require.Equal(t, "password verify failed", err.Error()) } func TestRedeemPasswordResetWhenPasswordChangeFailure(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCredentialRepo.returnErr = []error{nil, errors.New("some-database-error")} testCodeRepo.codes[userName] = someResetCode //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someResetCode, password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "failed to change password for user 'some-username': some-database-error", err.Error()) } func TestRedeemPasswordResetWhenDeleteResetCodeFailed(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = someResetCode testCodeRepo.returnErr = []error{nil, errors.New("some-redis-error")} //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someResetCode, password, passwordVerify) //then require.NotNil(t, err) require.Equal(t, "failed to delete password reset code for user 'some-username': some-redis-error", err.Error()) } func TestRedeemPasswordReset(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = someResetCode //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someResetCode, password, passwordVerify) //then require.Nil(t, err) setPassword := testCredentialRepo.password require.Nil(t, bcrypt.CompareHashAndPassword(setPassword, []byte(password))) require.Len(t, testCodeRepo.codes, 0) } func TestRedeemPasswordResetWhenInvalidPassword(t *testing.T) { passwords := map[string]bool{ "aB1": true, "Some-password1": true, "#@!!%Ac1": true, "aB": false, "!@#@!#!@#!@%&&&$%$!": false, "too-simple": false, "too-simple1": false, } for pass, valid := range passwords { t.Run(fmt.Sprintf("with password %s", pass), func(t *testing.T) { before() //given testCredentialRepo.returnMe = &domain.User{Name: userName, Email: email} testCodeRepo.codes[userName] = someResetCode //when err := RedeemPasswordReset( testCredentialRepo, testCodeRepo, userName, someResetCode, pass, //nolint:scopelint pass) //nolint:scopelint //then if valid { //nolint:scopelint require.Nil(t, err) } else { require.NotNil(t, err) require.Equal(t, "invalid password", err.Error()) } }) } } type TestCredentialRepository struct { callCount int returnMe *domain.User returnErr []error password []byte } func (r *TestCredentialRepository) GetUserByUserName(userName string, companyCode string) (*domain.User, error) { err := r.returnErr[r.callCount] r.callCount++ return r.returnMe, err } func (r *TestCredentialRepository) SetPassword(userName *domain.User, passwordHash []byte) error { r.password = passwordHash err := r.returnErr[r.callCount] r.callCount++ return err } type TestCodeRepository struct { callCount int codes map[string]string returnErr []error } func (r *TestCodeRepository) CreateCode(userName string, uuid string) error { r.codes[userName] = uuid err := r.returnErr[r.callCount] r.callCount++ return err } func (r *TestCodeRepository) DeleteCode(userName string) error { delete(r.codes, userName) err := r.returnErr[r.callCount] r.callCount++ return err } func (r *TestCodeRepository) GetCode(userName string) (string, error) { err := r.returnErr[r.callCount] r.callCount++ return r.codes[userName], err } type TestTemplateRepository struct { returnMe []byte returnErr error } func (t *TestTemplateRepository) Template(username, fromEmail, toEmail, code string) ([]byte, error) { return t.returnMe, t.returnErr } type TestEmailSender struct { returnErr error sent []*sent } func (s *TestEmailSender) From() string { return "" } func (s *TestEmailSender) Send(to []string, body []byte) error { s.sent = append(s.sent, &sent{to, body}) return s.returnErr } type sent struct { to []string body []byte }