aboutsummaryrefslogtreecommitdiffstats
path: root/models/user
diff options
context:
space:
mode:
Diffstat (limited to 'models/user')
-rw-r--r--models/user/avatar.go26
-rw-r--r--models/user/avatar_test.go39
-rw-r--r--models/user/badge.go2
-rw-r--r--models/user/email_address.go15
-rw-r--r--models/user/email_address_test.go10
-rw-r--r--models/user/must_change_password.go2
-rw-r--r--models/user/openid.go5
-rw-r--r--models/user/search.go9
-rw-r--r--models/user/setting.go5
-rw-r--r--models/user/setting_keys.go3
-rw-r--r--models/user/setting_test.go8
-rw-r--r--models/user/user.go271
-rw-r--r--models/user/user_list.go9
-rw-r--r--models/user/user_system.go49
-rw-r--r--models/user/user_system_test.go32
-rw-r--r--models/user/user_test.go167
16 files changed, 454 insertions, 198 deletions
diff --git a/models/user/avatar.go b/models/user/avatar.go
index 5453c78fc6..542bd93b98 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -5,7 +5,6 @@ package user
import (
"context"
- "crypto/md5"
"fmt"
"image/png"
"io"
@@ -38,27 +37,32 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
u.Avatar = avatars.HashEmail(seed)
- // Don't share the images so that we can delete them easily
- if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
- if err := png.Encode(w, img); err != nil {
- log.Error("Encode: %v", err)
+ _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+ if err != nil {
+ // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
+ // Don't share the images so that we can delete them easily
+ if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
+ if err := png.Encode(w, img); err != nil {
+ log.Error("Encode: %v", err)
+ }
+ return nil
+ }); err != nil {
+ return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
}
- return err
- }); err != nil {
- return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
}
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
return err
}
- log.Info("New random avatar created: %d", u.ID)
return nil
}
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
- if u.IsGhost() {
+ // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
+ // which comes from git configure information
+ if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
return avatars.DefaultAvatarLink()
}
@@ -101,7 +105,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool {
if !u.UseCustomAvatar || len(u.Avatar) == 0 {
return true
}
- avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
+ avatarID := avatar.HashAvatar(u.ID, data)
return u.Avatar != avatarID
}
diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go
index 1078875ee1..941068957c 100644
--- a/models/user/avatar_test.go
+++ b/models/user/avatar_test.go
@@ -4,13 +4,18 @@
package user
import (
+ "io"
+ "strings"
"testing"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestUserAvatarLink(t *testing.T) {
@@ -26,3 +31,37 @@ func TestUserAvatarLink(t *testing.T) {
link = u.AvatarLink(db.DefaultContext)
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
}
+
+func TestUserAvatarGenerate(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ var err error
+ tmpDir := t.TempDir()
+ storage.Avatars, err = storage.NewLocalStorage(t.Context(), &setting.Storage{Path: tmpDir})
+ require.NoError(t, err)
+
+ u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
+
+ // there was no avatar, generate a new one
+ assert.Empty(t, u.Avatar)
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.NotEmpty(t, u.Avatar)
+
+ // make sure the generated one exists
+ oldAvatarPath := u.CustomAvatarRelativePath()
+ _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ // and try to change its content
+ _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
+ require.NoError(t, err)
+
+ // try to generate again
+ err = GenerateRandomAvatar(db.DefaultContext, u)
+ require.NoError(t, err)
+ assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
+ f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
+ require.NoError(t, err)
+ defer f.Close()
+ content, _ := io.ReadAll(f)
+ assert.Equal(t, "abcd", string(content))
+}
diff --git a/models/user/badge.go b/models/user/badge.go
index 3ff3530a36..e475ceb748 100644
--- a/models/user/badge.go
+++ b/models/user/badge.go
@@ -19,7 +19,7 @@ type Badge struct {
}
// UserBadge represents a user badge
-type UserBadge struct { //nolint:revive
+type UserBadge struct { //nolint:revive // export stutter
ID int64 `xorm:"pk autoincr"`
BadgeID int64
UserID int64 `xorm:"INDEX"`
diff --git a/models/user/email_address.go b/models/user/email_address.go
index 74ba5f617a..2ba6a56450 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -8,7 +8,6 @@ import (
"context"
"fmt"
"net/mail"
- "regexp"
"strings"
"time"
@@ -153,8 +152,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
return err
}
-var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
-
// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
@@ -514,7 +511,7 @@ func validateEmailBasic(email string) error {
return ErrEmailInvalid{email}
}
- if !emailRegexp.MatchString(email) {
+ if !globalVars().emailRegexp.MatchString(email) {
return ErrEmailCharIsNotSupported{email}
}
@@ -545,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool {
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}
+
+func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) {
+ emails := make([]string, 0, 2)
+ if err := db.GetEngine(ctx).Table("email_address").Select("email").
+ Where("uid=? AND is_activated=?", uid, true).Asc("id").
+ Find(&emails); err != nil {
+ return nil, err
+ }
+ return emails, nil
+}
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index d72d873de2..c0666246b0 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -4,6 +4,7 @@
package user_test
import (
+ "slices"
"testing"
"code.gitea.io/gitea/models/db"
@@ -100,12 +101,7 @@ func TestListEmails(t *testing.T) {
assert.Greater(t, count, int64(5))
contains := func(match func(s *user_model.SearchEmailResult) bool) bool {
- for _, v := range emails {
- if match(v) {
- return true
- }
- }
- return false
+ return slices.ContainsFunc(emails, match)
}
assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 18 }))
@@ -205,7 +201,7 @@ func TestEmailAddressValidate(t *testing.T) {
}
for kase, err := range kases {
t.Run(kase, func(t *testing.T) {
- assert.EqualValues(t, err, user_model.ValidateEmail(kase))
+ assert.Equal(t, err, user_model.ValidateEmail(kase))
})
}
}
diff --git a/models/user/must_change_password.go b/models/user/must_change_password.go
index 7eab08de89..686847c7d7 100644
--- a/models/user/must_change_password.go
+++ b/models/user/must_change_password.go
@@ -34,7 +34,7 @@ func SetMustChangePassword(ctx context.Context, all, mustChangePassword bool, in
if !all {
include = sliceTrimSpaceDropEmpty(include)
if len(include) == 0 {
- return 0, util.NewSilentWrapErrorf(util.ErrInvalidArgument, "no users to include provided")
+ return 0, util.ErrorWrap(util.ErrInvalidArgument, "no users to include provided")
}
cond = cond.And(builder.In("lower_name", include))
diff --git a/models/user/openid.go b/models/user/openid.go
index ee4ecabae0..420c67ca18 100644
--- a/models/user/openid.go
+++ b/models/user/openid.go
@@ -11,9 +11,6 @@ import (
"code.gitea.io/gitea/modules/util"
)
-// ErrOpenIDNotExist openid is not known
-var ErrOpenIDNotExist = util.NewNotExistErrorf("OpenID is unknown")
-
// UserOpenID is the list of all OpenID identities of a user.
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
type UserOpenID struct { //revive:disable-line:exported
@@ -99,7 +96,7 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
if err != nil {
return err
} else if deleted != 1 {
- return ErrOpenIDNotExist
+ return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}
diff --git a/models/user/search.go b/models/user/search.go
index 85915f4020..cfd0d011bc 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -45,13 +45,14 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
var cond builder.Cond
cond = builder.Eq{"type": opts.Type}
if opts.IncludeReserved {
- if opts.Type == UserTypeIndividual {
+ switch opts.Type {
+ case UserTypeIndividual:
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
builder.Eq{"type": UserTypeBot},
).Or(
builder.Eq{"type": UserTypeRemoteUser},
)
- } else if opts.Type == UserTypeOrganization {
+ case UserTypeOrganization:
cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved})
}
}
@@ -136,7 +137,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
// SearchUsers takes options i.e. keyword and part of user name to search,
// it returns results in given range and number of total results.
-func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ int64, _ error) {
+func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _ int64, _ error) {
sessCount := opts.toSearchQueryBase(ctx)
defer sessCount.Close()
count, err := sessCount.Count(new(User))
@@ -151,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close()
if opts.Page > 0 {
- sessQuery = db.SetSessionPagination(sessQuery, opts)
+ sessQuery = db.SetSessionPagination(sessQuery, &opts)
}
// the sql may contain JOIN, so we must only select User related columns
diff --git a/models/user/setting.go b/models/user/setting.go
index b4af0e5ccd..c65afae76c 100644
--- a/models/user/setting.go
+++ b/models/user/setting.go
@@ -5,6 +5,7 @@ package user
import (
"context"
+ "errors"
"fmt"
"strings"
@@ -114,10 +115,10 @@ func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, er
func validateUserSettingKey(key string) error {
if len(key) == 0 {
- return fmt.Errorf("setting key must be set")
+ return errors.New("setting key must be set")
}
if strings.ToLower(key) != key {
- return fmt.Errorf("setting key should be lowercase")
+ return errors.New("setting key should be lowercase")
}
return nil
}
diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go
index 3149aae18b..2c2ed078be 100644
--- a/models/user/setting_keys.go
+++ b/models/user/setting_keys.go
@@ -10,6 +10,7 @@ const (
SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour"
// SettingsKeyShowOutdatedComments is the setting key wether or not to show outdated comments in PRs
SettingsKeyShowOutdatedComments = "comment_code.show_outdated"
+
// UserActivityPubPrivPem is user's private key
UserActivityPubPrivPem = "activitypub.priv_pem"
// UserActivityPubPubPem is user's public key
@@ -18,4 +19,6 @@ const (
SignupIP = "signup.ip"
// SignupUserAgent is the user agent that the user signed up with
SignupUserAgent = "signup.user_agent"
+
+ SettingsKeyCodeViewShowFileTree = "code_view.show_file_tree"
)
diff --git a/models/user/setting_test.go b/models/user/setting_test.go
index c607d9fd00..3c199013f3 100644
--- a/models/user/setting_test.go
+++ b/models/user/setting_test.go
@@ -30,15 +30,15 @@ func TestSettings(t *testing.T) {
settings, err := user_model.GetSettings(db.DefaultContext, 99, []string{keyName})
assert.NoError(t, err)
assert.Len(t, settings, 1)
- assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
+ assert.Equal(t, newSetting.SettingValue, settings[keyName].SettingValue)
settingValue, err := user_model.GetUserSetting(db.DefaultContext, 99, keyName)
assert.NoError(t, err)
- assert.EqualValues(t, newSetting.SettingValue, settingValue)
+ assert.Equal(t, newSetting.SettingValue, settingValue)
settingValue, err = user_model.GetUserSetting(db.DefaultContext, 99, "no_such")
assert.NoError(t, err)
- assert.EqualValues(t, "", settingValue)
+ assert.Empty(t, settingValue)
// updated setting
updatedSetting := &user_model.Setting{UserID: 99, SettingKey: keyName, SettingValue: "Updated"}
@@ -49,7 +49,7 @@ func TestSettings(t *testing.T) {
settings, err = user_model.GetUserAllSettings(db.DefaultContext, 99)
assert.NoError(t, err)
assert.Len(t, settings, 1)
- assert.EqualValues(t, updatedSetting.SettingValue, settings[updatedSetting.SettingKey].SettingValue)
+ assert.Equal(t, updatedSetting.SettingValue, settings[updatedSetting.SettingKey].SettingValue)
// delete setting
err = user_model.DeleteUserSetting(db.DefaultContext, 99, keyName)
diff --git a/models/user/user.go b/models/user/user.go
index 19879fbcc7..7c871bf575 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strings"
+ "sync"
"time"
"unicode"
@@ -213,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
}
-// GetEmail returns an noreply email, if the user has set to keep his
+// GetEmail returns a noreply email, if the user has set to keep his
// email address private, otherwise the primary email address.
func (u *User) GetEmail() string {
if u.KeepEmailPrivate {
@@ -246,19 +247,20 @@ func (u *User) MaxCreationLimit() int {
return u.MaxRepoCreation
}
-// CanCreateRepo returns if user login can create a repository
-// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
-func (u *User) CanCreateRepo() bool {
+// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
+// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
+func (u *User) CanCreateRepoIn(owner *User) bool {
if u.IsAdmin {
return true
}
- if u.MaxRepoCreation <= -1 {
- if setting.Repository.MaxCreationLimit <= -1 {
+ const noLimit = -1
+ if owner.MaxRepoCreation == noLimit {
+ if setting.Repository.MaxCreationLimit == noLimit {
return true
}
- return u.NumRepos < setting.Repository.MaxCreationLimit
+ return owner.NumRepos < setting.Repository.MaxCreationLimit
}
- return u.NumRepos < u.MaxRepoCreation
+ return owner.NumRepos < owner.MaxRepoCreation
}
// CanCreateOrganization returns true if user can create organisation.
@@ -271,13 +273,12 @@ func (u *User) CanEditGitHook() bool {
return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)
}
-// CanForkRepo returns if user login can fork a repository
-// It checks especially that the user can create repos, and potentially more
-func (u *User) CanForkRepo() bool {
+// CanForkRepoIn ONLY checks repository count limit
+func (u *User) CanForkRepoIn(owner *User) bool {
if setting.Repository.AllowForkWithoutMaximumLimit {
return true
}
- return u.CanCreateRepo()
+ return u.CanCreateRepoIn(owner)
}
// CanImportLocal returns true if user can migrate repository by local path.
@@ -384,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
}
// IsPasswordSet checks if the password is set or left empty
+// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
func (u *User) IsPasswordSet() bool {
- return len(u.Passwd) != 0
+ return u.Passwd != ""
}
-// IsOrganization returns true if user is actually a organization.
+// IsOrganization returns true if user is actually an organization.
func (u *User) IsOrganization() bool {
return u.Type == UserTypeOrganization
}
@@ -398,13 +400,14 @@ func (u *User) IsIndividual() bool {
return u.Type == UserTypeIndividual
}
-func (u *User) IsUser() bool {
- return u.Type == UserTypeIndividual || u.Type == UserTypeBot
+// IsTypeBot returns whether the user is of type bot
+func (u *User) IsTypeBot() bool {
+ return u.Type == UserTypeBot
}
-// IsBot returns whether or not the user is of type bot
-func (u *User) IsBot() bool {
- return u.Type == UserTypeBot
+// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
+func (u *User) IsTokenAccessAllowed() bool {
+ return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}
// DisplayName returns full name if it's not empty,
@@ -417,19 +420,9 @@ func (u *User) DisplayName() string {
return u.Name
}
-var emailToReplacer = strings.NewReplacer(
- "\n", "",
- "\r", "",
- "<", "",
- ">", "",
- ",", "",
- ":", "",
- ";", "",
-)
-
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
- sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
+ sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
// should be an edge case but nice to have
if sanitizedDisplayName == u.Email {
@@ -502,10 +495,10 @@ func (u *User) IsMailable() bool {
return u.IsActive
}
-// IsUserExist checks if given user name exist,
-// the user name should be noncased unique.
+// IsUserExist checks if given username exist,
+// the username should be non-cased unique.
// If uid is presented, then check will rule out that one,
-// it is used when update a user name in settings page.
+// it is used when update a username in settings page.
func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) {
if len(name) == 0 {
return false, nil
@@ -515,7 +508,7 @@ func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) {
Get(&User{LowerName: strings.ToLower(name)})
}
-// Note: As of the beginning of 2022, it is recommended to use at least
+// SaltByteLength as of the beginning of 2022, it is recommended to use at least
// 64 bits of salt, but NIST is already recommending to use to 128 bits.
// (16 bytes = 16 * 8 = 128 bits)
const SaltByteLength = 16
@@ -526,28 +519,58 @@ func GetUserSalt() (string, error) {
if err != nil {
return "", err
}
- // Returns a 32 bytes long string.
+ // Returns a 32-byte long string.
return hex.EncodeToString(rBytes), nil
}
-// Note: The set of characters here can safely expand without a breaking change,
-// but characters removed from this set can cause user account linking to break
-var (
- customCharsReplacement = strings.NewReplacer("Æ", "AE")
- removeCharsRE = regexp.MustCompile("['`´]")
- transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
- replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
-)
+type globalVarsStruct struct {
+ customCharsReplacement *strings.Replacer
+ removeCharsRE *regexp.Regexp
+ transformDiacritics transform.Transformer
+ replaceCharsHyphenRE *regexp.Regexp
+ emailToReplacer *strings.Replacer
+ emailRegexp *regexp.Regexp
+ systemUserNewFuncs map[int64]func() *User
+}
+
+var globalVars = sync.OnceValue(func() *globalVarsStruct {
+ return &globalVarsStruct{
+ // Note: The set of characters here can safely expand without a breaking change,
+ // but characters removed from this set can cause user account linking to break
+ customCharsReplacement: strings.NewReplacer("Æ", "AE"),
+
+ removeCharsRE: regexp.MustCompile("['`´]"),
+ transformDiacritics: transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC),
+ replaceCharsHyphenRE: regexp.MustCompile(`[\s~+]`),
+
+ emailToReplacer: strings.NewReplacer(
+ "\n", "",
+ "\r", "",
+ "<", "",
+ ">", "",
+ ",", "",
+ ":", "",
+ ";", "",
+ ),
+ emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
+
+ systemUserNewFuncs: map[int64]func() *User{
+ GhostUserID: NewGhostUser,
+ ActionsUserID: NewActionsUser,
+ },
+ }
+})
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
func NormalizeUserName(s string) (string, error) {
+ vars := globalVars()
s, _, _ = strings.Cut(s, "@")
- strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
+ strDiacriticsRemoved, n, err := transform.String(vars.transformDiacritics, vars.customCharsReplacement.Replace(s))
if err != nil {
return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
}
- return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
+ return vars.replaceCharsHyphenRE.ReplaceAllLiteralString(vars.removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
}
var (
@@ -805,6 +828,21 @@ func IsLastAdminUser(ctx context.Context, user *User) bool {
type CountUserFilter struct {
LastLoginSince *int64
IsAdmin optional.Option[bool]
+ IsActive optional.Option[bool]
+}
+
+// HasUsers checks whether there are any users in the database, or only one user exists.
+func HasUsers(ctx context.Context) (ret struct {
+ HasAnyUser, HasOnlyOneUser bool
+}, err error,
+) {
+ res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query()
+ if err != nil {
+ return ret, fmt.Errorf("error checking user existence: %w", err)
+ }
+ ret.HasAnyUser = len(res) != 0
+ ret.HasOnlyOneUser = len(res) == 1
+ return ret, nil
}
// CountUsers returns number of users.
@@ -825,6 +863,10 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
if opts.IsAdmin.Has() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
}
+
+ if opts.IsActive.Has() {
+ cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
+ }
}
count, err := sess.Where(cond).Count(new(User))
@@ -963,30 +1005,28 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err
}
-// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0
+// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
- switch id {
- case GhostUserID:
- return NewGhostUser(), nil
- case ActionsUserID:
- return NewActionsUser(), nil
- case 0:
+ if id < 0 {
+ if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
+ return newFunc(), nil
+ }
+ return nil, ErrUserNotExist{UID: id}
+ } else if id == 0 {
return nil, ErrUserNotExist{}
- default:
- return GetUserByID(ctx, id)
}
+ return GetUserByID(ctx, id)
}
-// GetPossibleUserByIDs returns the users if id > 0 or return system users if id < 0
+// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
uniqueIDs := container.SetOf(ids...)
users := make([]*User, 0, len(ids))
_ = uniqueIDs.Remove(0)
- if uniqueIDs.Remove(GhostUserID) {
- users = append(users, NewGhostUser())
- }
- if uniqueIDs.Remove(ActionsUserID) {
- users = append(users, NewActionsUser())
+ for systemUID, newFunc := range globalVars().systemUserNewFuncs {
+ if uniqueIDs.Remove(systemUID) {
+ users = append(users, newFunc())
+ }
}
res, err := GetUserByIDs(ctx, uniqueIDs.Values())
if err != nil {
@@ -996,7 +1036,7 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, nil
}
-// GetUserByNameCtx returns user by given name.
+// GetUserByName returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 {
return nil, ErrUserNotExist{Name: name}
@@ -1027,8 +1067,8 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string {
return mails
}
-// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails
-func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
+// GetMailableUsersByIDs gets users from ids, but only if they can receive mails
+func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
if len(ids) == 0 {
return nil, nil
}
@@ -1053,17 +1093,6 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([
Find(&ous)
}
-// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
-func GetUserNamesByIDs(ctx context.Context, ids []int64) ([]string, error) {
- unames := make([]string, 0, len(ids))
- err := db.GetEngine(ctx).In("id", ids).
- Table("user").
- Asc("name").
- Cols("name").
- Find(&unames)
- return unames, err
-}
-
// GetUserNameByID returns username for the id
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
var name string
@@ -1119,28 +1148,96 @@ func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User {
}
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
-func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []*UserCommit {
+func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) {
var (
- emails = make(map[string]*User)
newCommits = make([]*UserCommit, 0, len(oldCommits))
+ emailSet = make(container.Set[string])
)
for _, c := range oldCommits {
- var u *User
if c.Author != nil {
- if v, ok := emails[c.Author.Email]; !ok {
- u, _ = GetUserByEmail(ctx, c.Author.Email)
- emails[c.Author.Email] = u
- } else {
- u = v
- }
+ emailSet.Add(c.Author.Email)
}
+ }
+
+ emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values())
+ if err != nil {
+ return nil, err
+ }
+ for _, c := range oldCommits {
+ user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
+ if user == nil {
+ user = &User{
+ Name: c.Author.Name,
+ Email: c.Author.Email,
+ }
+ }
newCommits = append(newCommits, &UserCommit{
- User: u,
+ User: user,
Commit: c,
})
}
- return newCommits
+ return newCommits, nil
+}
+
+type EmailUserMap struct {
+ m map[string]*User
+}
+
+func (eum *EmailUserMap) GetByEmail(email string) *User {
+ return eum.m[strings.ToLower(email)]
+}
+
+func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
+ if len(emails) == 0 {
+ return nil, nil
+ }
+
+ needCheckEmails := make(container.Set[string])
+ needCheckUserNames := make(container.Set[string])
+ for _, email := range emails {
+ if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
+ username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
+ needCheckUserNames.Add(strings.ToLower(username))
+ } else {
+ needCheckEmails.Add(strings.ToLower(email))
+ }
+ }
+
+ emailAddresses := make([]*EmailAddress, 0, len(needCheckEmails))
+ if err := db.GetEngine(ctx).In("lower_email", needCheckEmails.Values()).
+ And("is_activated=?", true).
+ Find(&emailAddresses); err != nil {
+ return nil, err
+ }
+ userIDs := make(container.Set[int64])
+ for _, email := range emailAddresses {
+ userIDs.Add(email.UID)
+ }
+ results := make(map[string]*User, len(emails))
+
+ if len(userIDs) > 0 {
+ users, err := GetUsersMapByIDs(ctx, userIDs.Values())
+ if err != nil {
+ return nil, err
+ }
+
+ for _, email := range emailAddresses {
+ user := users[email.UID]
+ if user != nil {
+ results[email.LowerEmail] = user
+ }
+ }
+ }
+
+ users := make(map[int64]*User, len(needCheckUserNames))
+ if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
+ return nil, err
+ }
+ for _, user := range users {
+ results[strings.ToLower(user.GetPlaceholderEmail())] = user
+ }
+ return &EmailUserMap{results}, nil
}
// GetUserByEmail returns the user object by given e-mail if exists.
@@ -1161,8 +1258,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
}
// Finally, if email address is the protected email address:
- if strings.HasSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) {
- username := strings.TrimSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress))
+ if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
+ username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
user := &User{}
has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user)
if err != nil {
diff --git a/models/user/user_list.go b/models/user/user_list.go
index c66d59f0d9..1b6a27dd86 100644
--- a/models/user/user_list.go
+++ b/models/user/user_list.go
@@ -11,12 +11,13 @@ import (
func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, error) {
userMaps := make(map[int64]*User, len(userIDs))
+ if len(userIDs) == 0 {
+ return userMaps, nil
+ }
+
left := len(userIDs)
for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
+ limit := min(left, db.DefaultMaxInSize)
err := db.GetEngine(ctx).
In("id", userIDs[:limit]).
Find(&userMaps)
diff --git a/models/user/user_system.go b/models/user/user_system.go
index 612cdb2cae..e07274d291 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -10,9 +10,8 @@ import (
)
const (
- GhostUserID = -1
- GhostUserName = "Ghost"
- GhostUserLowerName = "ghost"
+ GhostUserID int64 = -1
+ GhostUserName = "Ghost"
)
// NewGhostUser creates and returns a fake user for someone has deleted their account.
@@ -20,10 +19,14 @@ func NewGhostUser() *User {
return &User{
ID: GhostUserID,
Name: GhostUserName,
- LowerName: GhostUserLowerName,
+ LowerName: strings.ToLower(GhostUserName),
}
}
+func IsGhostUserName(name string) bool {
+ return strings.EqualFold(name, GhostUserName)
+}
+
// IsGhost check if user is fake user for a deleted account
func (u *User) IsGhost() bool {
if u == nil {
@@ -32,22 +35,16 @@ func (u *User) IsGhost() bool {
return u.ID == GhostUserID && u.Name == GhostUserName
}
-// NewReplaceUser creates and returns a fake user for external user
-func NewReplaceUser(name string) *User {
- return &User{
- ID: 0,
- Name: name,
- LowerName: strings.ToLower(name),
- }
-}
-
const (
- ActionsUserID = -2
- ActionsUserName = "gitea-actions"
- ActionsFullName = "Gitea Actions"
- ActionsEmail = "teabot@gitea.io"
+ ActionsUserID int64 = -2
+ ActionsUserName = "gitea-actions"
+ ActionsUserEmail = "teabot@gitea.io"
)
+func IsGiteaActionsUserName(name string) bool {
+ return strings.EqualFold(name, ActionsUserName)
+}
+
// NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User {
return &User{
@@ -55,16 +52,26 @@ func NewActionsUser() *User {
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
- FullName: ActionsFullName,
- Email: ActionsEmail,
+ FullName: "Gitea Actions",
+ Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
- Type: UserTypeIndividual,
+ Type: UserTypeBot,
AllowCreateOrganization: true,
Visibility: structs.VisibleTypePublic,
}
}
-func (u *User) IsActions() bool {
+func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID
}
+
+func GetSystemUserByName(name string) *User {
+ if IsGhostUserName(name) {
+ return NewGhostUser()
+ }
+ if IsGiteaActionsUserName(name) {
+ return NewActionsUser()
+ }
+ return nil
+}
diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go
new file mode 100644
index 0000000000..97768b509b
--- /dev/null
+++ b/models/user/user_system_test.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSystemUser(t *testing.T) {
+ u, err := GetPossibleUserByID(db.DefaultContext, -1)
+ require.NoError(t, err)
+ assert.Equal(t, "Ghost", u.Name)
+ assert.Equal(t, "ghost", u.LowerName)
+ assert.True(t, u.IsGhost())
+ assert.True(t, IsGhostUserName("gHost"))
+
+ u, err = GetPossibleUserByID(db.DefaultContext, -2)
+ require.NoError(t, err)
+ assert.Equal(t, "gitea-actions", u.Name)
+ assert.Equal(t, "gitea-actions", u.LowerName)
+ assert.True(t, u.IsGiteaActions())
+ assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
+
+ _, err = GetPossibleUserByID(db.DefaultContext, -3)
+ require.Error(t, err)
+}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 7ebc64f69e..a2597ba3f5 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -4,7 +4,6 @@
package user_test
import (
- "context"
"crypto/rand"
"fmt"
"strings"
@@ -20,11 +19,28 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
+func TestIsUsableUsername(t *testing.T) {
+ assert.NoError(t, user_model.IsUsableUsername("a"))
+ assert.NoError(t, user_model.IsUsableUsername("foo.wiki"))
+ assert.NoError(t, user_model.IsUsableUsername("foo.git"))
+
+ assert.Error(t, user_model.IsUsableUsername("a--b"))
+ assert.Error(t, user_model.IsUsableUsername("-1_."))
+ assert.Error(t, user_model.IsUsableUsername(".profile"))
+ assert.Error(t, user_model.IsUsableUsername("-"))
+ assert.Error(t, user_model.IsUsableUsername("🌞"))
+ assert.Error(t, user_model.IsUsableUsername("the..repo"))
+ assert.Error(t, user_model.IsUsableUsername("foo.RSS"))
+ assert.Error(t, user_model.IsUsableUsername("foo.PnG"))
+}
+
func TestOAuth2Application_LoadUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: 1})
@@ -33,14 +49,43 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
assert.NotNil(t, user)
}
-func TestGetUserEmailsByNames(t *testing.T) {
+func TestUserEmails(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
-
- // ignore none active user email
- assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
- assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
-
- assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
+ t.Run("GetUserEmailsByNames", func(t *testing.T) {
+ // ignore none active user email
+ assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
+ assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
+ assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
+ })
+ t.Run("GetUsersByEmails", func(t *testing.T) {
+ defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
+ testGetUserByEmail := func(t *testing.T, email string, uid int64) {
+ m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{email})
+ require.NoError(t, err)
+ user := m.GetByEmail(email)
+ if uid == 0 {
+ require.Nil(t, user)
+ return
+ }
+ require.NotNil(t, user)
+ assert.Equal(t, uid, user.ID)
+ }
+ cases := []struct {
+ Email string
+ UID int64
+ }{
+ {"UseR1@example.com", 1},
+ {"user1-2@example.COM", 1},
+ {"USER2@" + setting.Service.NoReplyAddress, 2},
+ {"user4@example.com", 4},
+ {"no-such", 0},
+ }
+ for _, c := range cases {
+ t.Run(c.Email, func(t *testing.T) {
+ testGetUserByEmail(t, c.Email, c.UID)
+ })
+ }
+ })
}
func TestCanCreateOrganization(t *testing.T) {
@@ -63,73 +108,73 @@ func TestCanCreateOrganization(t *testing.T) {
func TestSearchUsers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) {
+ testSuccess := func(opts user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) {
users, _, err := user_model.SearchUsers(db.DefaultContext, opts)
assert.NoError(t, err)
cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts)
if assert.Len(t, users, len(expectedUserOrOrgIDs), "case: %s", cassText) {
for i, expectedID := range expectedUserOrOrgIDs {
- assert.EqualValues(t, expectedID, users[i].ID, "case: %s", cassText)
+ assert.Equal(t, expectedID, users[i].ID, "case: %s", cassText)
}
}
}
// test orgs
- testOrgSuccess := func(opts *user_model.SearchUserOptions, expectedOrgIDs []int64) {
+ testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
opts.Type = user_model.UserTypeOrganization
testSuccess(opts, expectedOrgIDs)
}
- testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}},
[]int64{3, 6})
- testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}},
[]int64{7, 17})
- testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}},
[]int64{19, 25})
- testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
[]int64{26, 41})
- testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
[]int64{42})
- testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}},
+ testOrgSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}},
[]int64{})
// test users
- testUserSuccess := func(opts *user_model.SearchUserOptions, expectedUserIDs []int64) {
+ testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
opts.Type = user_model.UserTypeIndividual
testSuccess(opts, expectedUserIDs)
}
- testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
+ testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
- testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
+ testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
[]int64{9})
- testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
- testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
// order by name asc default
- testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
- testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)},
[]int64{1})
- testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)},
[]int64{29})
- testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
[]int64{37})
- testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
+ testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
[]int64{24})
}
@@ -159,9 +204,9 @@ func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16)
u := &user_model.User{}
algos := hash.RecommendedHashAlgorithms
- for j := 0; j < len(algos); j++ {
+ for j := range algos {
u.PasswdHashAlgo = algos[j]
- for i := 0; i < 50; i++ {
+ for range 50 {
// generate a random password
rand.Read(b)
pass := string(b)
@@ -186,7 +231,7 @@ func BenchmarkHashPassword(b *testing.B) {
pass := "password1337"
u := &user_model.User{Passwd: pass}
b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
u.SetPassword(pass)
}
}
@@ -264,7 +309,7 @@ func TestCreateUserCustomTimestamps(t *testing.T) {
err := user_model.CreateUser(db.DefaultContext, user, &user_model.Meta{})
assert.NoError(t, err)
- fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+ fetched, err := user_model.GetUserByID(t.Context(), user.ID)
assert.NoError(t, err)
assert.Equal(t, creationTimestamp, fetched.CreatedUnix)
assert.Equal(t, creationTimestamp, fetched.UpdatedUnix)
@@ -291,7 +336,7 @@ func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
timestampEnd := time.Now().Unix()
- fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+ fetched, err := user_model.GetUserByID(t.Context(), user.ID)
assert.NoError(t, err)
assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
@@ -318,14 +363,14 @@ func TestGetUserIDsByNames(t *testing.T) {
func TestGetMaileableUsersByIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- results, err := user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
+ results, err := user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
assert.NoError(t, err)
assert.Len(t, results, 1)
if len(results) > 1 {
assert.Equal(t, 1, results[0].ID)
}
- results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
+ results, err = user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
assert.NoError(t, err)
assert.Len(t, results, 2)
if len(results) > 2 {
@@ -488,18 +533,15 @@ func TestIsUserVisibleToViewer(t *testing.T) {
}
func Test_ValidateUser(t *testing.T) {
- oldSetting := setting.Service.AllowedUserVisibilityModesSlice
- defer func() {
- setting.Service.AllowedUserVisibilityModesSlice = oldSetting
- }()
- setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true}
+ defer test.MockVariableValue(&setting.Service.AllowedUserVisibilityModesSlice, []bool{true, false, true})()
+
kases := map[*user_model.User]bool{
{ID: 1, Visibility: structs.VisibleTypePublic}: true,
{ID: 2, Visibility: structs.VisibleTypeLimited}: false,
{ID: 2, Visibility: structs.VisibleTypePrivate}: true,
}
for kase, expected := range kases {
- assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase)
+ assert.Equal(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase)
}
}
@@ -523,7 +565,7 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
for _, testCase := range testCases {
normalizedName, err := user_model.NormalizeUserName(testCase.Input)
assert.NoError(t, err)
- assert.EqualValues(t, testCase.Expected, normalizedName)
+ assert.Equal(t, testCase.Expected, normalizedName)
if testCase.IsNormalizedValid {
assert.NoError(t, user_model.IsUsableUsername(normalizedName))
} else {
@@ -550,7 +592,7 @@ func TestEmailTo(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.result, func(t *testing.T) {
testUser := &user_model.User{FullName: testCase.fullName, Email: testCase.mail}
- assert.EqualValues(t, testCase.result, testUser.EmailTo())
+ assert.Equal(t, testCase.result, testUser.EmailTo())
})
}
}
@@ -561,12 +603,7 @@ func TestDisabledUserFeatures(t *testing.T) {
testValues := container.SetOf(setting.UserFeatureDeletion,
setting.UserFeatureManageSSHKeys,
setting.UserFeatureManageGPGKeys)
-
- oldSetting := setting.Admin.ExternalUserDisableFeatures
- defer func() {
- setting.Admin.ExternalUserDisableFeatures = oldSetting
- }()
- setting.Admin.ExternalUserDisableFeatures = testValues
+ defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, testValues)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
@@ -602,3 +639,37 @@ func TestGetInactiveUsers(t *testing.T) {
assert.NoError(t, err)
assert.Empty(t, users)
}
+
+func TestCanCreateRepo(t *testing.T) {
+ defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
+ const noLimit = -1
+ doerNormal := &user_model.User{}
+ doerAdmin := &user_model.User{IsAdmin: true}
+ t.Run("NoGlobalLimit", func(t *testing.T) {
+ setting.Repository.MaxCreationLimit = noLimit
+
+ assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
+ assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
+ assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
+
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
+ })
+
+ t.Run("GlobalLimit50", func(t *testing.T) {
+ setting.Repository.MaxCreationLimit = 50
+
+ assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
+ assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
+ assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
+ assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
+ assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
+
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
+ assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
+ })
+}