src.dualinventive.com/go/authentication-service/internal/pwreset/pwreset_test.go

446 lines
12 KiB
Go

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
}