79763572

Date: 2025-09-13 07:30:19
Score: 1
Natty:
Report link

after @zahra-keshtkar gave the solution i come up the solution with abstract the usersService's depedency

so i did this :

this is the usersService's sruct :

type UsersServices struct {
    Repository  repository.Repository
    Token       utils.Token
    Transaction db.Transactioner
}

i created transactioner interfaces like this :

type Transactioner interface {
    Begin() (*sql.Tx, error)
    Rollback() error
    Commit() error
    WithTx(ctx context.Context, fn func(tx *sql.Tx) error) error
}

then in testing i did like this :

type tokenInvitation struct {
    token string
}

func (t tokenInvitation) Generate() string {

    return t.token
}

type statusTransaction int

func (s *statusTransaction) string(stat statusTransaction) string {

    return []string{"init", "begin", "open", "failed"}[stat]
}

const (
    initial statusTransaction = iota
    begin
    open
    failed
)

type transactionMock struct {
    status statusTransaction
}

func (t *transactionMock) Begin() (*sql.Tx, error) {
    t.status = begin

    if t.status.string(begin) != "begin" {
        return nil, errors.New("transaction not begin")
    }

    return nil, nil
}

func (t *transactionMock) Rollback() error {
    t.status = failed
    if t.status.string(failed) != "failed" {
        return errors.New("transaction rollback errors")
    }

    return nil
}

func (t *transactionMock) Commit() error {
    t.status = open
    if t.status.string(open) != "open" {
        return errors.New("transaction errors")
    }

    return nil
}

func (t *transactionMock) WithTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
    _, err := t.Begin()
    if err != nil {
        return err
    }

    err = fn(nil)

    if err != nil {
        if err := t.Rollback(); err != nil {
            return err
        }
        return err
    }

    return t.Commit()
}

func TestRegisterAccount(t *testing.T) {

    tkn := tokenInvitation{token: "this is test token"}
    transMock := transactionMock{status: initial}
    usersSrv := setupUsersServiceTest(tkn, &transMock)

    request := RegisterRequest{
        Username: "username",
        Email:    "[email protected]",
        Password: "HelloWorld$123",
    }

    want := &RegisterResponse{Token: tkn.token}
    got, err := usersSrv.RegisterAccount(context.Background(), request)
    if err != nil {
        t.Errorf("got error %q but want none", err)
    }

    if !reflect.DeepEqual(want, got) {
        t.Errorf("want to equal %v, but got: %v", want, got)
    }
}

func setupUsersServiceTest(token tokenInvitation, transx db.Transactioner) UsersServices {

    return UsersServices{
        Repository: repository.Repository{
            Users:      &mock.UsersRepositoryMock{},
            Invitation: &mock.InvitationRepositoryMock{},
        },
        Transaction: transx,
        Token:       token,
    }

}

then in the implementation i just change the transaction function like this :

func (us *UsersServices) RegisterAccount(ctx context.Context, req RegisterRequest) (*RegisterResponse, error) {

    var response = new(RegisterResponse)

    err := utils.IsPasswordValid(req.Password)
    if err != nil {
        //Todo: handle error client
        return nil, errorService.New(err, err)
    }

    err = us.Transaction.WithTx(ctx, func(tx *sql.Tx) error {

        var newAccount repository.UserModel
        newAccount.Email = req.Email
        newAccount.Username = req.Username

        if err = newAccount.Password.ParseFromPassword(req.Password); err != nil {
            //Todo: handle error client
            return errorService.New(err, err)
        }

        usrId, err := us.Repository.Users.Insert(ctx, tx, newAccount)
        if err != nil {
            switch {
            case strings.Contains(err.Error(), CONFLICT_CODE):
                return errorService.New(ErrUserAlreadyExist, err)
            default:
                //Todo: handle error client
                return errorService.New(err, err)
            }

        }

        tokenIvt := us.Token.Generate()

        invt := repository.InvitationModel{
            UserId:   usrId,
            Token:    tokenIvt,
            ExpireAt: time.Hour * 24,
        }

        err = us.Repository.Invitation.Insert(ctx, tx, invt)
        if err != nil {
            //Todo: handle error client
            return errorService.New(err, err)
        }

        // register and invite success, send to response
        response.Token = tokenIvt

        return nil
    })

    if err != nil {
        return nil, err
    }

    return response, nil
}

but my concern is i still need to pass the *sql.Tx in the intefaces then ignore it in the test, im not sure it is good to just leave it like that

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @zahra-keshtkar
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: lizzyy