0

I am performing unit tests for a service where request dto's are validated and user passwords are hashed using Go's Bcrypt package before being passed to a repository for insertion into the DB.

I don't know how my mock functions should return a dummy response which should match with the service's hashing.

func Test_should_create_new_account(t *testing.T) {
    // Arrange
    teardown := setup(t)
    defer teardown()

    // ** Focus here **
    hashedpassword, err := AppCrypto.HashAndSalt([]byte("securepassword"))

    request := dto.RegisterRequest{
        Email:    "test@test.com",
        Password: "securepassword",
        RoleID:   1,
    }

    account := realDomain.Account{
        Email:    request.Email,
        Password: hashedpassword,
        RoleID:   request.RoleID,
    }

    accountWithID := account
    accountWithID.AccountID = 1

    mockRepo.EXPECT().Create(account).Return(&accountWithID, nil)
    // Act
    res, err := service.RegisterAccount(request)

    // Assert
    if err != nil {
        t.Error("Failed while creating account")
    }
    if !res.Created {
        t.Error("Failed while creating account")
    }
}

HashAndSalt simply hashed a given string.

// HashAndSalt Hashes a given string
func HashAndSalt(pwd []byte) (string, *errs.AppError) {

    // Use GenerateFromPassword to hash & salt pwd.
    // MinCost is just an integer constant provided by the bcrypt
    // package along with DefaultCost & MaxCost.
    // The cost can be any value you want provided it isn't lower
    // than the MinCost (4)
    hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
    if err != nil {
        return "", errs.NewUnexpectedError("An unexpected error ocurred while hashing the password" + err.Error())
    } // GenerateFromPassword returns a byte slice so we need to
    // convert the bytes to a string and return it
    return string(hash), nil
}

This is the service's RegisterAccount

func (d DefaultAccountService) RegisterAccount(request dto.RegisterRequest) (*dto.RegisterResponse, *errs.AppError) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }
    // Hash the request's password
    hashedpassword, err := AppCrypto.HashAndSalt([]byte(request.Password))

    if err != nil {
        return nil, err
    }
    // Assign the hashed password to the request obj
    request.Password = hashedpassword
    a := request.ToDomainObject()

    _, err = d.repo.Create(a)
    if err != nil {
        return nil, err
    }
    response := dto.RegisterResponse{
        Created: true,
    }
    return &response, nil
}

This is the thrown error, notice the want and got blocks where the mock request does not match with the given request.

accountService.go:34: Unexpected call to *domain.MockAccountRepository.Create([{0 test@test.com 1 $2a$04$.ORGMDZNk3.ySMpKwJYYcONdpAbgMJh79UDApzwRnzkCe.qeiECUG false}]) at /home/dio/Documents/Code/go-beex-backend/auth-server/mocks/domain/accountRepositoryDB.go:40 because: 
        expected call at /home/dio/Documents/Code/go-beex-backend/auth-server/service/accountService_test.go:52 doesn't match the argument at index 0.
        Got: {0 test@test.com 1 $2a$04$.ORGMDZNk3.ySMpKwJYYcONdpAbgMJh79UDApzwRnzkCe.qeiECUG false}
        Want: is equal to {0 test@test.com 1 $2a$04$Bah8tCOzf7Z9Suw55DfyHOvnsBbXLyJEWV8QZ.owCBUOxxomAuEM2 false}

Hope my explanation makes sense, unit testing was not discussed in the article I am basing my code from.

Jeremy
  • 1,447
  • 20
  • 40
  • Testing the hashes is [glass box testing](https://en.wikipedia.org/wiki/White-box_testing). Testing that their password works is [black box testing](https://en.wikipedia.org/wiki/Black-box_testing). – Schwern Feb 07 '21 at 23:25

1 Answers1

0

I managed to mock my crypto package, and then mock in my test, the solution is quite lengthy, but I hope it helps someone else in the future. I am still learning Go so some terms may not be correct (heh)

First create a package where your hashing logic will be created, more info here, I am using mockgen to create my mocks, so I added the annotation in my interface

//go:generate mockgen -destination=../mocks/crypto/mockCrypto.go -package=crypto auth-server/crypto AppCrypto
type AppCrypto interface {
    HashAndSalt(pwd []byte) (string, *errs.AppError)
    ComparePasswords(hashedPwd string, plainPwd []byte) bool
}


type DefaultAppCrypto struct {
}

// HashAndSalt Hashes a given string
func (d DefaultAppCrypto) HashAndSalt(pwd []byte) (string, *errs.AppError) {

    hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
    if err != nil {
        return "", errs.NewUnexpectedError("An unexpected error ocurred while hashing the password" + err.Error())
    }
    return string(hash), nil
}

func (d DefaultAppCrypto) ComparePasswords(hashedPwd string, plainPwd []byte) bool { 
    byteHash := []byte(hashedPwd)
    err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)
    if err != nil {
        return false
    }

    return true
}

I am following the hexagonal architecture which y learned in this Udemy course here (Highly recommended), so I am creating an account service for registering new users which expects a repo for accessing the data layer in its struct, so if we were to inject the crypto package we would then be able to mock its implementation in the test.

Some code will be omitted here, mostly the implementation:

type AccountService interface {
    RegisterAccount(dto.RegisterRequest) (*dto.RegisterResponse, *errs.AppError)
}

type DefaultAccountService struct {
    repo   domain.AccountRepository
    crypto crypto.AppCrypto
}

// More code here

func NewAccountService(repo domain.AccountRepository, crypto crypto.AppCrypto) DefaultAccountService {
    return DefaultAccountService{repo, crypto}
}

NOW we should be able to mock this package, so something like this would work.

var mockRepo *domain.MockAccountRepository
var mockCrypto *crypto.MockAppCrypto
var ctrl gomock.Controller
var service AccountService

func setup(t *testing.T) func() {
    ctrl := gomock.NewController(t)
    mockRepo = domain.NewMockAccountRepository(ctrl)
    mockCrypto = crypto.NewMockAppCrypto(ctrl)
    service = NewAccountService(mockRepo, mockCrypto)

    return func() {
        service = nil
        defer ctrl.Finish()
    }
}

func Test_should_create_new_account(t *testing.T) {
    // Arrange
    teardown := setup(t)
    defer teardown()

    cryptoService := realCrypto.DefaultAppCrypto{}

    password := "securepassword"
    hashedpassword, err := cryptoService.HashAndSalt([]byte(password))

    request := dto.RegisterRequest{
        Email:    "test@test.com",
        Password: password,
        RoleID:   1,
    }

    account := realDomain.Account{
        Email:    request.Email,
        Password: hashedpassword,
        RoleID:   request.RoleID,
    }

    accountWithID := account
    accountWithID.AccountID = 1

    // ** MOCK THE DATA LAYER IMPLEMENTATION
    mockRepo.EXPECT().Create(account).Return(&accountWithID, nil)
    // ** MOCK THE CRYPTO PACKAGE's HashAndSalt
    mockCrypto.EXPECT().HashAndSalt([]byte(password)).Return(hashedpassword, nil)

    // Act
    res, err := service.RegisterAccount(request)

    // Assert

    if err != nil {
        t.Error("Failed while creating account")
    }

    if !res.Created {
        t.Error("Failed while creating account")
    }

}

Originally my crypto package only had the function needed for hashing, but I was forced to include them in a struct, so if someone with more experience knows a way to mock the hashing functions in a way where the account service would use the mock implementation would be great because the code would be much simpler.

Jeremy
  • 1,447
  • 20
  • 40