feat(user): add CreateBotUser
This commit is contained in:
parent
506bfa2549
commit
1637ecd0c7
|
|
@ -121,12 +121,81 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
|
||||||
return newUserOut, err
|
return newUserOut, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateBotUser creates a bot user owned by the given owner.
|
||||||
|
// Bots have no email or password and cannot authenticate interactively.
|
||||||
|
// It intentionally bypasses checkIfUserIsValid / checkIfUserExists because
|
||||||
|
// those enforce email+password and would flag duplicate empty emails.
|
||||||
|
func CreateBotUser(s *xorm.Session, bot *User, owner *User) (*User, error) {
|
||||||
|
if owner == nil || owner.ID == 0 {
|
||||||
|
return nil, ErrNoUsernamePassword{}
|
||||||
|
}
|
||||||
|
if owner.IsBot() {
|
||||||
|
return nil, &ErrBotNotOwned{UserID: owner.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse the same username format rules as regular user creation
|
||||||
|
if err := checkUsernameFormat(bot.Username); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(bot.Username, "bot-") {
|
||||||
|
return nil, &ErrBotUsernameMustHavePrefix{Username: bot.Username}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := GetUserByUsername(s, bot.Username); err == nil {
|
||||||
|
return nil, ErrUsernameExists{Username: bot.Username}
|
||||||
|
} else if !IsErrUserDoesNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.ID = 0
|
||||||
|
bot.BotOwnerID = owner.ID
|
||||||
|
bot.Status = StatusActive
|
||||||
|
bot.Issuer = IssuerLocal
|
||||||
|
bot.Password = ""
|
||||||
|
bot.Email = ""
|
||||||
|
|
||||||
|
if _, err := s.Insert(bot); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newBot, err := GetUserByID(s, bot.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
events.DispatchOnCommit(s, &CreatedEvent{User: newBot})
|
||||||
|
return newBot, nil
|
||||||
|
}
|
||||||
|
|
||||||
// HashPassword hashes a password
|
// HashPassword hashes a password
|
||||||
func HashPassword(password string) (string, error) {
|
func HashPassword(password string) (string, error) {
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), config.ServiceBcryptRounds.GetInt())
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), config.ServiceBcryptRounds.GetInt())
|
||||||
return string(bytes), err
|
return string(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkUsernameFormat validates username format rules shared by regular and bot users.
|
||||||
|
func checkUsernameFormat(username string) error {
|
||||||
|
if username == "" {
|
||||||
|
return ErrNoUsernamePassword{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(username, " ") {
|
||||||
|
return &ErrUsernameMustNotContainSpaces{
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if username matches the reserved link-share pattern
|
||||||
|
linkSharePattern := regexp.MustCompile(`^link-share-\d+$`)
|
||||||
|
if linkSharePattern.MatchString(username) {
|
||||||
|
return ErrUsernameReserved{
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkIfUserIsValid(user *User) error {
|
func checkIfUserIsValid(user *User) error {
|
||||||
if user.Email == "" ||
|
if user.Email == "" ||
|
||||||
(user.Issuer != IssuerLocal && user.Subject == "") ||
|
(user.Issuer != IssuerLocal && user.Subject == "") ||
|
||||||
|
|
@ -135,18 +204,8 @@ func checkIfUserIsValid(user *User) error {
|
||||||
return ErrNoUsernamePassword{}
|
return ErrNoUsernamePassword{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(user.Username, " ") {
|
if err := checkUsernameFormat(user.Username); err != nil {
|
||||||
return &ErrUsernameMustNotContainSpaces{
|
return err
|
||||||
Username: user.Username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if username matches the reserved link-share pattern
|
|
||||||
linkSharePattern := regexp.MustCompile(`^link-share-\d+$`)
|
|
||||||
if linkSharePattern.MatchString(user.Username) {
|
|
||||||
return ErrUsernameReserved{
|
|
||||||
Username: user.Username,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve the bot- prefix for bot users (created via CreateBotUser)
|
// Reserve the bot- prefix for bot users (created via CreateBotUser)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,75 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCreateBotUser(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
owner, err := GetUserByID(s, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bot, err := CreateBotUser(s, &User{Username: "bot-reviewer"}, owner)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, bot.IsBot())
|
||||||
|
assert.Equal(t, owner.ID, bot.BotOwnerID)
|
||||||
|
assert.Equal(t, StatusActive, bot.Status)
|
||||||
|
assert.Empty(t, bot.Email)
|
||||||
|
assert.Empty(t, bot.Password)
|
||||||
|
})
|
||||||
|
t.Run("empty username", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
owner, err := GetUserByID(s, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateBotUser(s, &User{Username: ""}, owner)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("username with spaces", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
owner, err := GetUserByID(s, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateBotUser(s, &User{Username: "bot- name"}, owner)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.True(t, IsErrUsernameMustNotContainSpaces(err))
|
||||||
|
})
|
||||||
|
t.Run("missing bot- prefix", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
owner, err := GetUserByID(s, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateBotUser(s, &User{Username: "reviewer"}, owner)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.True(t, IsErrBotUsernameMustHavePrefix(err))
|
||||||
|
})
|
||||||
|
t.Run("duplicate username", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
owner, err := GetUserByID(s, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateBotUser(s, &User{Username: "bot-dup"}, owner)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = CreateBotUser(s, &User{Username: "bot-dup"}, owner)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.True(t, IsErrUsernameExists(err))
|
||||||
|
})
|
||||||
|
t.Run("bot cannot create bot", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
botOwner := &User{ID: 999, BotOwnerID: 1}
|
||||||
|
_, err := CreateBotUser(s, &User{Username: "bot-child"}, botOwner)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.True(t, IsErrBotNotOwned(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateUser_RejectsBotPrefix(t *testing.T) {
|
func TestCreateUser_RejectsBotPrefix(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue