diff options
| author | mihna123 <mihailonvojinovic@gmail.com> | 2026-08-29 23:28:28 +0200 |
|---|---|---|
| committer | mihna123 <mihailonvojinovic@gmail.com> | 2026-08-29 23:28:28 +0200 |
| commit | 41d4965db78414ba53237f791919ae17c0b99e14 (patch) | |
| tree | bd3e25202e8cb66cff14b7c119c7afdbbc7b1ff6 /internal | |
| parent | 7ba6bc53be1546b44610840c46996dd6e2147405 (diff) | |
| download | openstore-41d4965db78414ba53237f791919ae17c0b99e14.tar.gz openstore-41d4965db78414ba53237f791919ae17c0b99e14.zip | |
user: add a service stub with validated register method
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/user/input.go | 65 | ||||
| -rw-r--r-- | internal/user/mock_test.go | 150 | ||||
| -rw-r--r-- | internal/user/service.go | 56 | ||||
| -rw-r--r-- | internal/user/service_test.go | 134 |
4 files changed, 405 insertions, 0 deletions
diff --git a/internal/user/input.go b/internal/user/input.go new file mode 100644 index 0000000..38db678 --- /dev/null +++ b/internal/user/input.go @@ -0,0 +1,65 @@ +package user + +import ( + "errors" + "fmt" + "regexp" +) + +type RegisterInput struct { + Username string + Email string + Phone string + Password string +} + +type CreateInput struct { + Username string + Email string + Phone string + PwHash string +} + +var ErrInvalidInput = errors.New("invalid input") +var ErrUserExists = errors.New("user already exists") +var ErrUserNotFound = errors.New("user not found") + +var usrnameRegExp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`) +var mailRegExp = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`) +var phoneRegExp = regexp.MustCompile(`^\+?[0-9][0-9\s-]*$`) + +func (in *RegisterInput) validate() error { + if len(in.Password) < 8 { + return fmt.Errorf("%w: password too short - min is 8", ErrInvalidInput) + } + + if len(in.Password) > 72 { + return fmt.Errorf("%w: password too long - max is 72", ErrInvalidInput) + } + + if in.Username == "" { + return fmt.Errorf("%w: username is mandatory", ErrInvalidInput) + } + + if !usrnameRegExp.MatchString(in.Username) { + return fmt.Errorf("%w: bad username - must start with a letter", ErrInvalidInput) + } + + if len(in.Username) < 3 { + return fmt.Errorf("%w: username too short - min is 3 chars", ErrInvalidInput) + } + + if in.Email == "" { + return fmt.Errorf("%w: email is mandatory", ErrInvalidInput) + } + + if !mailRegExp.MatchString(in.Email) { + return fmt.Errorf("%w: bad email format", ErrInvalidInput) + } + + if in.Phone != "" && !phoneRegExp.MatchString(in.Phone) { + return fmt.Errorf("%w: bad phone format", ErrInvalidInput) + } + + return nil +} diff --git a/internal/user/mock_test.go b/internal/user/mock_test.go new file mode 100644 index 0000000..0f0f672 --- /dev/null +++ b/internal/user/mock_test.go @@ -0,0 +1,150 @@ +package user + +type RepositoryMock struct { + users []User + err error +} + +func getEmptyRepository() *RepositoryMock { + return &RepositoryMock{users: []User{}, err: nil} +} + +func (r *RepositoryMock) Create(in *CreateInput) (*User, error) { + if r.err != nil { + return nil, r.err + } + + user := User{ + Username: in.Username, + Phone: in.Phone, + Email: in.Email, + passwordHash: in.PwHash, + } + + r.users = append(r.users, user) + return &user, nil +} + +func (r *RepositoryMock) GetByUsernameOrEmail(username string, email string) (*User, error) { + if r.err != nil { + return nil, r.err + } + + for _, user := range r.users { + if user.Username == username || user.Email == email { + return &user, nil + } + } + + return nil, ErrUserNotFound +} + +func getInvalidRegisterInputs() map[string]RegisterInput { + baseInput := RegisterInput{ + Username: "testusername", + Email: "testemail@mail.com", + Phone: "+232442223", + Password: "1233213344234", + } + + badInputs := map[string]RegisterInput{} + + input := baseInput + input.Username = "" + badInputs["no username"] = input + + input = baseInput + input.Username = " " + badInputs["username is only whitespace"] = input + + input = baseInput + input.Username = "m ike" + badInputs["username contains a space"] = input + + input = baseInput + input.Username = "1mike" + badInputs["username starts with a number"] = input + + input = baseInput + input.Username = "ab" + badInputs["username is less than 3 chars"] = input + + input = baseInput + input.Email = "" + badInputs["no email"] = input + + input = baseInput + input.Email = " " + badInputs["email is just whitespace"] = input + + input = baseInput + input.Email = "some weird@email.com" + badInputs["email contains inner whitespace"] = input + + input = baseInput + input.Email = "someweird@em ail.com" + badInputs["email contains whitespace after @ and before dot"] = input + + input = baseInput + input.Email = "someweird@email.c om" + badInputs["email contains whitespace after dot"] = input + + input = baseInput + input.Email = "emailnoat.com" + badInputs["email has no @"] = input + + input = baseInput + input.Email = "emailnodot@somcom" + badInputs["email has no dot"] = input + + input = baseInput + input.Email = "@." + badInputs["email is only at dot"] = input + + input = baseInput + input.Phone = "070e833" + badInputs["phone number has letters"] = input + + input = baseInput + input.Phone = " " + badInputs["phone number is only empty space"] = input + + input = baseInput + input.Password = "" + badInputs["no password"] = input + + input = baseInput + input.Password = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + badInputs["password is over 72 chars"] = input + + input = baseInput + input.Password = "1234" + badInputs["password is less than 8 chars"] = input + + return badInputs +} + +func (r *RepositoryMock) fill() { + users := []User{ + { + Username: "johnsmith", + Email: "jognsmith@email.com", + Phone: "+332200334", + passwordHash: "213412", + }, + { + Username: "janedoe", + Email: "janedoe@email.com", + Phone: "+332200334", + passwordHash: "213412", + }, + { + Username: "mrtest", + Email: "mrtest@email.com", + Phone: "+332200334", + passwordHash: "213412", + }, + } + + r.users = users +} diff --git a/internal/user/service.go b/internal/user/service.go new file mode 100644 index 0000000..a8a72a6 --- /dev/null +++ b/internal/user/service.go @@ -0,0 +1,56 @@ +package user + +import ( + "errors" + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +type Repository interface { + Create(in *CreateInput) (*User, error) + GetByUsernameOrEmail(username string, email string) (*User, error) +} + +type Service struct { + repository Repository + bcryptCost int +} + +func New(r Repository) *Service { + return &Service{repository: r, bcryptCost: bcrypt.DefaultCost} +} + +func (s *Service) Register(in *RegisterInput) (*User, error) { + if err := in.validate(); err != nil { + return nil, fmt.Errorf("register: %w", err) + } + + _, err := s.repository.GetByUsernameOrEmail(in.Username, in.Email) + if err == nil { + return nil, fmt.Errorf("register: %w", ErrUserExists) + } + + if !errors.Is(err, ErrUserNotFound) { + return nil, fmt.Errorf("register: %w", err) + } + + pwHash, err := bcrypt.GenerateFromPassword([]byte(in.Password), s.bcryptCost) + if err != nil { + return nil, fmt.Errorf("register: %w", err) + } + + ci := &CreateInput{ + Username: in.Username, + Email: in.Email, + Phone: in.Phone, + PwHash: string(pwHash), + } + + user, err := s.repository.Create(ci) + if err != nil { + return nil, fmt.Errorf("register: %w", err) + } + + return user, nil +} diff --git a/internal/user/service_test.go b/internal/user/service_test.go new file mode 100644 index 0000000..938d2c3 --- /dev/null +++ b/internal/user/service_test.go @@ -0,0 +1,134 @@ +package user + +import ( + "errors" + "log/slog" + "os" + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestMain(m *testing.M) { + slog.SetDefault(slog.New(slog.DiscardHandler)) + os.Exit(m.Run()) +} + +func NewTestUserService(t *testing.T) *Service { + t.Helper() + return &Service{repository: getEmptyRepository(), bcryptCost: bcrypt.MinCost} +} + +func TestRegisterInputValidateFaultyData(t *testing.T) { + badInputs := getInvalidRegisterInputs() + + for name, in := range badInputs { + t.Run(name, func(t *testing.T) { + err := in.validate() + if err == nil { + t.Errorf("bad input got through: %+v", in) + } + if !errors.Is(err, ErrInvalidInput) { + t.Errorf("unexpected error: got %v, want ErrInvalidInput", err) + } + }) + } +} + +func TestRegisterBadInput(t *testing.T) { + in := &RegisterInput{} + s := NewTestUserService(t) + _, err := s.Register(in) + if err == nil { + t.Error("no error when registering with faulty input") + } +} + +func TestRegisterExistingUser(t *testing.T) { + r := getEmptyRepository() + r.fill() + + badInputs := map[string]RegisterInput{ + "existing username": { + Username: r.users[0].Username, + Email: "thisemaildoesnt@exist.com", + Phone: "123321", + Password: "22332233", + }, + "existing email": { + Username: "thisusernamedoesntexist", + Email: r.users[0].Email, + Phone: "123321", + Password: "22332233", + }, + } + + for name, in := range badInputs { + t.Run(name, func(t *testing.T) { + s := &Service{repository: r, bcryptCost: bcrypt.MinCost} + _, err := s.Register(&in) + if err == nil { + t.Error("existing user registered") + } + + if !errors.Is(err, ErrUserExists) { + t.Errorf("unexpected error: got %v, want ErrUserExists", err) + } + }) + } +} + +func TestRegisterRepositoryError(t *testing.T) { + s := &Service{repository: &RepositoryMock{err: errors.New("db down")}} + in := &RegisterInput{Username: "bob", Email: "b@b.com", Password: "thisissecure"} + _, err := s.Register(in) + + if err == nil { + t.Error("register swallowed repository error") + } + + if errors.Is(err, ErrUserExists) { + t.Error("repository failure reported as ErrUserExists") + } +} + +func TestSuccessfulRegister(t *testing.T) { + s := NewTestUserService(t) + in := &RegisterInput{ + Username: "happyuser123", + Email: "happyuser@email.com", + Phone: "+381233232", + Password: "supersecretpassword1234", + } + usr, err := s.Register(in) + if err != nil { + t.Errorf("register with good input failed: %+v", in) + } + + err = bcrypt.CompareHashAndPassword([]byte(usr.passwordHash), []byte(in.Password)) + if err != nil { + t.Errorf("hash and password compare error: %v", err) + } +} + +func TestRegisterSameUserTwice(t *testing.T) { + s := NewTestUserService(t) + in := &RegisterInput{ + Username: "happyuser123", + Email: "happyuser@email.com", + Phone: "+381233232", + Password: "supersecretpassword1234", + } + + s.Register(in) + + _, err := s.Register(in) + if err == nil { + t.Errorf("registered same user twice: %+v", in) + } + + if !errors.Is(err, ErrUserExists) { + t.Errorf("unexpected error: got %v, want ErrUserExists", err) + } + +} |
