I am having some issues when it comes to model design, specifically handling Model specific actions vs Database actions. A good example would be my User model.
When creating a user in my DB, I want to:
- Verify the password meets criteria (Model action)
- Create the digest (Model action)
- Set timestamps (model action)
- Save the Email, Digest, and Timestamps to the DB (DB Action)
When testing, I obviously want to have a set of unit test for all 4, however #4 has calls to the other 3, something I don’t want to retest, or risk having #4 test fail if any of those 3 do.
I have come up with creating a separate Interface for ModelActions vs StoreActions, and sending the UserAction interface to a store action when needed, however as I write it out, I am already sensing some serious code smell.
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Password string `json:"password"`
ConfirmationPassword string `json:"confirmationPassword"`
passwordDigest string `json:"-"`
CreatedAt time.Time `json:"createdAt,omitempty"`
ModifiedAt time.Time `json:"modifiedAt,omitempty"`
}
//UserStore is the interface for all User functions that interact with the database
type UserStore interface {
GetUserByEmailAndPassword(email, password string) (User, error)
UpdatePassword(u UserAction, previousPassword, password, confirmationPassword string) error
UserExists(email string) (bool, error)
CreateUser(u UserAction) error
}
// I am going against design Principles by having GetID, GetEmail, since JSON unmarshalling needs the struct fields to be capitalized, which is already a warning sign for me
type UserAction interface {
GetID() int
GetEmail() string
Timestamps() (time.Time, time.Time)
SetID(id int)
SetTimestamps()
SetPassword(password, confirmation string)
SetDigest(digest string)
CreateDigest() (string, error)
VerifyPassword() error
ComparePassword(password string) error
}
// Example of UserActions
func (u *User) CreateDigest() (string, error) {
var digest string
if err := u.VerifyPassword(); err != nil {
return digest, err
}
passwordByte, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return digest, err
}
digest = string(passwordByte)
return digest, nil
}
func (u *User) VerifyPassword() error {
if len(u.Password) < 6 {
return &modelError{"Password", "must be at least 6 characters long"}
}
if u.Password != u.ConfirmationPassword {
return &modelError{"ConfirmationPassword", "does not match Password"}
}
return nil
}
// Example of DB Action
func (db *DB) CreateUser(ua UserAction) error {
if exists, err := db.UserExists(ua.GetEmail()); err != nil {
return err
} else if exists {
return &modelError{"Email", "already exists in the system"}
}
// set password
digest, err := ua.CreateDigest()
if err != nil {
return err
}
ua.SetDigest(digest)
ua.SetTimestamps()
createdAt, modifiedAt := ua.Timestamps()
rows, err := db.Query(`
INSERT INTO users (email, password_digest, created_at, modified_at)
VALUES ($1, $2, $3, $4)
RETURNING id
`, ua.GetEmail(), digest, createdAt, modifiedAt)
if err != nil {
return err
}
defer rows.Close()
var id int
for rows.Next() {
if err := rows.Scan(&id); err != nil {
return err
}
}
ua.SetID(id)
return nil
}
Is there a better way to model these separate actions so the UserActions can be mocked when testing the DB/Store functions? I tried storing the User
struct as part of the interface, something such as:
type UserAction {
SetTimestamps()
CreateDigest() (string, error)
VerifyPassword() error
ComparePassword(password string) error
User() *User
}
This however causes cyclical imports when creating mocks, and also opens up all fields, which are already arguably available since the model's fields are exportable