From 19e9c7560ebbdd7d8b77773a509adead82865d70 Mon Sep 17 00:00:00 2001 From: Timo Volkmann Date: Mon, 16 Aug 2021 20:51:55 +0200 Subject: [PATCH] Auth: Add user entity functions and tests #98 --- internal/entity/user.go | 77 ++++++++++++++++++++++- internal/entity/user_test.go | 118 ++++++++++++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/internal/entity/user.go b/internal/entity/user.go index 26f1fd600..ce80db31a 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -1,6 +1,7 @@ package entity import ( + "errors" "fmt" "time" @@ -137,12 +138,12 @@ func (m *User) Save() error { } // BeforeCreate creates a random UID if needed before inserting a new row to the database. -func (m *User) BeforeCreate(scope *gorm.Scope) error { +func (m *User) BeforeCreate(tx *gorm.DB) error { if rnd.IsUID(m.UserUID, 'u') { return nil } - - return scope.SetColumn("UserUID", rnd.PPID('u')) + m.UserUID = rnd.PPID('u') + return nil } // FirstOrCreateUser returns an existing row, inserts a new row or nil in case of errors. @@ -191,6 +192,21 @@ func FindUserByUID(uid string) *User { } } +// DeleteUserByName deletes an existing user or returns error if not found. +func DeleteUserByName(userName string) error { + if userName == "" { + return fmt.Errorf("can not delete user from db: empty username") + } + user := FindUserByName(userName) + if err := Db().Where("user_name = ?", userName).Delete(&User{}).Error; user == nil || err != nil { + return fmt.Errorf("user %s not found", txt.Quote(userName)) + } + if err := Db().Where("uid = ?", user.UserUID).Delete(&Password{}).Error; err != nil { + log.Debug(err) + } + return nil +} + // String returns an identifier that can be used in logs. func (m *User) String() string { if m.UserName != "" { @@ -321,3 +337,58 @@ func (m *User) Role() acl.Role { return acl.RoleDefault } + +// Validate Makes sure username and email are unique and meet requirements. Returns error if any property is invalid +func (m *User) Validate() error { + if m.UserName == "" { + return errors.New("username must not be empty") + } + if len(m.UserName) < 4 { + return errors.New("username must be at least 4 characters") + } + var result = &User{} + var err error + if err = Db().Unscoped().Where("user_name = ?", m.UserName).First(result).Error; err == nil { + return errors.New("username already exists") + } else if err != gorm.ErrRecordNotFound { + return err + } + // stop here if no email is provided + if m.PrimaryEmail == "" { + return nil + } + if err = Db().Unscoped().Where("primary_email = ?", m.PrimaryEmail).First(result).Error; err == nil { + return errors.New("email already exists") + } else if err != gorm.ErrRecordNotFound { + return err + } + return nil +} + +// CreateWithPassword Creates User with Password in db transaction. +func (m *User) CreateWithPassword(password string) error { + if len(password) < 4 { + return fmt.Errorf("new password for %s must be at least 4 characters", txt.Quote(m.UserName)) + } + return Db().Transaction(func(tx *gorm.DB) error { + if err := tx.Create(m).Error; err != nil { + return err + } + pw := NewPassword(m.UserUID, password) + if err := tx.Create(&pw).Error; err != nil { + return err + } + log.Infof("created user %v with uid %v", txt.Quote(m.UserName), txt.Quote(m.UserUID)) + return nil + }) +} + +// AllUsers Returns a list of all registered Users. +func AllUsers() []User { + var users []User + if err := Db().Find(&users).Error; err != nil { + log.Error(err) + return []User{} + } + return users +} diff --git a/internal/entity/user_test.go b/internal/entity/user_test.go index b8e3393ee..5a0e79c11 100644 --- a/internal/entity/user_test.go +++ b/internal/entity/user_test.go @@ -1,9 +1,8 @@ package entity import ( - "testing" - "github.com/photoprism/photoprism/internal/acl" + "testing" "github.com/stretchr/testify/assert" ) @@ -376,3 +375,118 @@ func TestUser_Role(t *testing.T) { assert.Equal(t, acl.Role("*"), p.Role()) }) } + +func TestDeleteUserByName(t *testing.T) { + u := FirstOrCreateUser(&User{ + ID: 877, + AddressID: 1, + UserName: "delete", + FullName: "Delete", + PrimaryEmail: "delete@example.com", + }) + + t.Run("delete empty username", func(t *testing.T) { + err := DeleteUserByName("") + assert.Error(t, err) + }) + t.Run("delete fail", func(t *testing.T) { + err := DeleteUserByName("notmatching") + assert.Error(t, err) + }) + t.Run("delete success", func(t *testing.T) { + err := DeleteUserByName(u.UserName) + assert.Nil(t, err) + }) +} + +func TestUser_Validate(t *testing.T) { + t.Run("valid", func(t *testing.T) { + u := &User{ + ID: 878, + AddressID: 1, + UserName: "validate", + FullName: "Validate", + PrimaryEmail: "validate@example.com", + } + err := u.Validate() + assert.Nil(t, err) + }) + t.Run("username empty", func(t *testing.T) { + u := &User{ + ID: 878, + AddressID: 1, + UserName: "", + FullName: "Validate", + PrimaryEmail: "validate@example.com", + } + err := u.Validate() + assert.Error(t, err) + }) + t.Run("username too short", func(t *testing.T) { + u := &User{ + ID: 878, + AddressID: 1, + UserName: "val", + FullName: "Validate", + PrimaryEmail: "validate@example.com", + } + err := u.Validate() + assert.Error(t, err) + }) + + t.Run("username not unique", func(t *testing.T) { + u := FirstOrCreateUser(&User{ + ID: 879, + AddressID: 1, + UserName: "notunique", + }) + err := u.Validate() + assert.Error(t, err) + }) + t.Run("email not unique", func(t *testing.T) { + u := FirstOrCreateUser(&User{ + ID: 880, + AddressID: 1, + PrimaryEmail: "notunique@example.com", + }) + err := u.Validate() + assert.Error(t, err) + }) + +} + +func TestCreateWithPassword(t *testing.T) { + t.Run("valid", func(t *testing.T) { + u := &User{ + ID: 881, + AddressID: 1, + UserName: "thomas", + FullName: "Thomas", + PrimaryEmail: "thomas@example.com", + } + err := u.CreateWithPassword("helloworld") + assert.Nil(t, err) + }) + t.Run("password too short", func(t *testing.T) { + u := &User{ + ID: 882, + AddressID: 1, + UserName: "thomas", + FullName: "Thomas", + PrimaryEmail: "thomas@example.com", + } + err := u.CreateWithPassword("hel") + assert.Error(t, err) + }) +} + +func TestAllUsers(t *testing.T) { + t.Run("list all", func(t *testing.T) { + users := AllUsers() + for _, user := range users { + log.Infof("user: %v, %s, %s, %s", user.ID, user.UserUID, user.UserName, user.FullName) + } + log.Infof("user count: %v", len(users)) + assert.Greater(t, len(users), 3) + }) +}