feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
+4 -10
View File
@@ -30,22 +30,16 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
seed = u.Name
}
img, err := avatar.RandomImage([]byte(seed))
if err != nil {
return fmt.Errorf("RandomImage: %w", err)
}
img := avatar.RandomImageDefaultSize([]byte(seed))
u.Avatar = avatars.HashEmail(seed)
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
_, 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
return png.Encode(w, img)
}); err != nil {
return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
}
@@ -74,7 +68,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
switch {
case u.UseCustomAvatar:
useLocalAvatar = true
case disableGravatar, setting.OfflineMode:
case disableGravatar:
useLocalAvatar = true
autoGenerateAvatar = true
}
+170 -21
View File
@@ -5,9 +5,12 @@ package user
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm/schemas"
)
// Badge represents a user badge
@@ -22,7 +25,16 @@ type Badge struct {
type UserBadge struct { //nolint:revive // export stutter
ID int64 `xorm:"pk autoincr"`
BadgeID int64
UserID int64 `xorm:"INDEX"`
UserID int64
}
// TableIndices implements xorm's TableIndices interface
func (n *UserBadge) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 1)
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
ubUnique.AddColumn("user_id", "badge_id")
indices = append(indices, ubUnique)
return indices
}
func init() {
@@ -42,32 +54,85 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
return badges, count, err
}
// CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Insert(badge)
return err
// GetBadgeUsersOptions contains options for getting users with a specific badge
type GetBadgeUsersOptions struct {
db.ListOptions
BadgeSlug string
}
// GetBadge returns a badge
// GetBadgeUsers returns the users that have a specific badge with pagination support.
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
sess := db.GetEngine(ctx).
Select("`user`.*").
Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id").
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.BadgeSlug)
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}
users := make([]*User, 0, opts.PageSize)
count, err := sess.FindAndCount(&users)
return users, count, err
}
// CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error {
exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
if err != nil {
return err
}
if exists {
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
}
if _, err := db.GetEngine(ctx).Insert(badge); err != nil {
// Handle race between existence check and insert.
exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
if existErr == nil && exists {
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
}
return err
}
return nil
}
// GetBadge returns a specific badge
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
if !has {
if err != nil {
return nil, err
}
return badge, err
if !has {
return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug)
}
return badge, nil
}
// UpdateBadge updates a badge based on its slug.
func UpdateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
return err
}
// DeleteBadge deletes a badge.
// DeleteBadge deletes a badge and all associated user_badge entries.
func DeleteBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
return err
return db.WithTx(ctx, func(ctx context.Context) error {
// First delete all user_badge entries for this badge
if _, err := db.GetEngine(ctx).
Where("badge_id = ?", badge.ID).
Delete(&UserBadge{}); err != nil {
return err
}
// Then delete the badge itself
if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil {
return err
}
return nil
})
}
// AddUserBadge adds a badge to a user.
@@ -84,12 +149,25 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
if err != nil {
return err
} else if !has {
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug)
}
exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
if err != nil {
return err
}
if exists {
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
}
if err := db.Insert(ctx, &UserBadge{
BadgeID: badge.ID,
UserID: u.ID,
}); err != nil {
exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
if existErr == nil && exists {
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
}
return err
}
}
@@ -102,16 +180,33 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
return RemoveUserBadges(ctx, u, []*Badge{badge})
}
// RemoveUserBadges removes badges from a user.
// RemoveUserBadges removes specific badges from a user.
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if len(badges) == 0 {
return nil
}
badgeSlugs := make([]string, 0, len(badges))
for _, badge := range badges {
if _, err := db.GetEngine(ctx).
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
Delete(&UserBadge{}); err != nil {
return err
}
badgeSlugs = append(badgeSlugs, badge.Slug)
}
var userBadges []UserBadge
if err := db.GetEngine(ctx).Table("user_badge").
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs).
Find(&userBadges); err != nil {
return err
}
userBadgeIDs := make([]int64, 0, len(userBadges))
for _, ub := range userBadges {
userBadgeIDs = append(userBadgeIDs, ub.ID)
}
if len(userBadgeIDs) == 0 {
return nil
}
if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil {
return err
}
return nil
})
@@ -122,3 +217,57 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err
}
// SearchBadgeOptions represents the options when finding badges
type SearchBadgeOptions struct {
db.ListOptions
Keyword string
Slug string
ID int64
OrderBy db.SearchOrderBy
}
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.Keyword != "" {
keywordCond := builder.Or(
db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword),
db.BuildCaseInsensitiveLike("badge.description", opts.Keyword),
)
cond = cond.And(keywordCond)
}
if opts.ID > 0 {
cond = cond.And(builder.Eq{"badge.id": opts.ID})
}
if len(opts.Slug) > 0 {
cond = cond.And(builder.Eq{"badge.slug": opts.Slug})
}
return cond
}
func (opts *SearchBadgeOptions) ToOrders() string {
return opts.OrderBy.String()
}
// SearchBadges returns badges based on the provided SearchBadgeOptions options
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
return db.FindAndCount[Badge](ctx, opts)
}
// GetBadgeByID returns a specific badge by ID
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).ID(id).Get(badge)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
}
return badge, nil
}
@@ -0,0 +1,185 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user_test
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
func TestBadge(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetBadgeNotExist", testGetBadgeNotExist)
t.Run("CreateBadgeAlreadyExists", testCreateBadgeAlreadyExists)
t.Run("GetBadgeUsers", testGetBadgeUsers)
t.Run("AddAndRemoveUserBadges", testAddAndRemoveUserBadges)
t.Run("SearchBadgesOrderingAndKeyword", testSearchBadgesOrderingAndKeyword)
}
func testGetBadgeNotExist(t *testing.T) {
badge, err := user_model.GetBadge(t.Context(), "does-not-exist")
assert.Nil(t, badge)
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
}
func testCreateBadgeAlreadyExists(t *testing.T) {
badge := &user_model.Badge{
Slug: "duplicate-badge-slug",
Description: "First",
}
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
err := user_model.CreateBadge(t.Context(), &user_model.Badge{
Slug: "duplicate-badge-slug",
Description: "Second",
})
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrAlreadyExist)
}
func testGetBadgeUsers(t *testing.T) {
// Create a test badge
badge := &user_model.Badge{
Slug: "test-badge-users",
Description: "Test Badge",
ImageURL: "test.png",
}
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
// Create test users and assign badges
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge))
assert.NoError(t, user_model.AddUserBadge(t.Context(), user2, badge))
defer func() {
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge))
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user2, badge))
}()
// Test getting users with pagination
opts := &user_model.GetBadgeUsersOptions{
BadgeSlug: badge.Slug,
ListOptions: db.ListOptions{
Page: 1,
PageSize: 1,
},
}
users, count, err := user_model.GetBadgeUsers(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
assert.Len(t, users, 1)
// Test second page
opts.Page = 2
users, count, err = user_model.GetBadgeUsers(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
assert.Len(t, users, 1)
// Test with non-existent badge
opts.BadgeSlug = "non-existent"
users, count, err = user_model.GetBadgeUsers(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
assert.Empty(t, users)
}
func testAddAndRemoveUserBadges(t *testing.T) {
badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1})
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
// Add a badge to user and verify that it is returned in the list
assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge1))
badges, count, err := user_model.GetUserBadges(t.Context(), user1)
assert.Equal(t, int64(1), count)
assert.Equal(t, badge1.Slug, badges[0].Slug)
assert.NoError(t, err)
// Confirm that it is impossible to duplicate the same badge
err = user_model.AddUserBadge(t.Context(), user1, badge1)
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrAlreadyExist)
// Nothing happened to the existing badge
badges, count, err = user_model.GetUserBadges(t.Context(), user1)
assert.Equal(t, int64(1), count)
assert.Equal(t, badge1.Slug, badges[0].Slug)
assert.NoError(t, err)
// Remove a badge from user and verify that it is no longer in the list
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge1))
_, count, err = user_model.GetUserBadges(t.Context(), user1)
assert.Equal(t, int64(0), count)
assert.NoError(t, err)
// Removing empty or missing badge selections should be a no-op.
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil))
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}}))
}
func testSearchBadgesOrderingAndKeyword(t *testing.T) {
createdBadges := []*user_model.Badge{
{Slug: "badge-sort-b", Description: "Badge Sort B"},
{Slug: "badge-sort-c", Description: "Badge Sort C"},
{Slug: "badge-sort-a", Description: "Badge Sort A"},
{Slug: "badge-sort-case", Description: "MiXeDCaSeKeyword"},
}
for _, badge := range createdBadges {
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
}
opts := &user_model.SearchBadgeOptions{
ListOptions: db.ListOptions{ListAll: true},
Keyword: "badge-sort-",
OrderBy: db.SearchOrderBy("`badge`.id ASC"),
}
oldestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 4, count)
assert.Equal(t, []string{"badge-sort-b", "badge-sort-c", "badge-sort-a", "badge-sort-case"}, collectBadgeSlugs(oldestFirst))
opts.OrderBy = db.SearchOrderBy("`badge`.id DESC")
newestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 4, count)
assert.Equal(t, []string{"badge-sort-case", "badge-sort-a", "badge-sort-c", "badge-sort-b"}, collectBadgeSlugs(newestFirst))
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
alpha, count, err := user_model.SearchBadges(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 4, count)
assert.Equal(t, []string{"badge-sort-a", "badge-sort-b", "badge-sort-c", "badge-sort-case"}, collectBadgeSlugs(alpha))
opts.OrderBy = db.SearchOrderBy("`badge`.slug DESC")
reverseAlpha, count, err := user_model.SearchBadges(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 4, count)
assert.Equal(t, []string{"badge-sort-case", "badge-sort-c", "badge-sort-b", "badge-sort-a"}, collectBadgeSlugs(reverseAlpha))
opts.Keyword = "mixedcasekeyword"
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
caseInsensitive, count, err := user_model.SearchBadges(t.Context(), opts)
assert.NoError(t, err)
assert.EqualValues(t, 1, count)
assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive))
}
func collectBadgeSlugs(badges []*user_model.Badge) []string {
slugs := make([]string, 0, len(badges))
for _, badge := range badges {
slugs = append(slugs, badge.Slug)
}
return slugs
}
+1 -1
View File
@@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er
return nil, err
}
if len(blocks) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
}
return blocks[0], nil
}
@@ -215,7 +215,7 @@ func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, err
if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil {
return nil, err
} else if !has {
return nil, nil
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
}
return email, nil
}
@@ -276,17 +276,22 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
return UpdateUserCols(ctx, user, "rands")
}
func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
return makeEmailPrimaryInternal(ctx, emailID, true)
func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
return makeEmailPrimaryInternal(ctx, ownerID, emailID, true)
}
func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
return makeEmailPrimaryInternal(ctx, emailID, false)
func MakeInactiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
return makeEmailPrimaryInternal(ctx, ownerID, emailID, false)
}
func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
func makeEmailPrimaryInternal(ctx context.Context, ownerID, emailID int64, isActive bool) error {
email := &EmailAddress{}
if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
if has, err := db.GetEngine(ctx).ID(emailID).
Where(builder.Eq{
"uid": ownerID,
"is_activated": isActive,
}).
Get(email); err != nil {
return err
} else if !has {
return ErrEmailAddressNotExist{}
@@ -336,7 +341,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
if err != nil {
return err
}
return MakeInactiveEmailPrimary(ctx, newEmail.ID)
return MakeInactiveEmailPrimary(ctx, uid, newEmail.ID)
})
}
@@ -46,22 +46,22 @@ func TestIsEmailUsed(t *testing.T) {
func TestMakeEmailPrimary(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
err := user_model.MakeActiveEmailPrimary(t.Context(), 9999999)
err := user_model.MakeActiveEmailPrimary(t.Context(), 1, 9999999)
assert.Error(t, err)
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
assert.Error(t, err)
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
assert.Error(t, err)
assert.True(t, user_model.IsErrUserNotExist(err))
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
assert.NoError(t, err)
user, _ := user_model.GetUserByID(t.Context(), int64(10))
+2 -2
View File
@@ -24,7 +24,7 @@ func (users UserList) GetUserIDs() []int64 {
return userIDs
}
// GetTwoFaStatus return state of 2FA enrollement
// GetTwoFaStatus return state of 2FA enrollment
func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool {
results := make(map[int64]bool, len(users))
for _, user := range users {
@@ -48,7 +48,7 @@ func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool {
func (users UserList) loadTwoFactorStatus(ctx context.Context) (map[int64]*auth.TwoFactor, error) {
if len(users) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // returns nil when there are no users
}
userIDs := users.GetUserIDs()
+29 -5
View File
@@ -6,6 +6,7 @@ package user
import (
"context"
"fmt"
"slices"
"strings"
"code.gitea.io/gitea/models/db"
@@ -17,12 +18,29 @@ import (
"xorm.io/xorm"
)
// AdminUserOrderByMap represents all possible admin user search orders
// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins.
var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{
"asc": {
"name": db.SearchOrderByAlphabetically,
"created": db.SearchOrderByOldest,
"updated": db.SearchOrderByLeastUpdated,
"id": db.SearchOrderByID,
},
"desc": {
"name": db.SearchOrderByAlphabeticallyReverse,
"created": db.SearchOrderByNewest,
"updated": db.SearchOrderByRecentUpdated,
"id": db.SearchOrderByIDReverse,
},
}
// SearchUserOptions contains the options for searching
type SearchUserOptions struct {
db.ListOptions
Keyword string
Type UserType
Types []UserType
UID int64
LoginName string // this option should be used only for admin user
SourceID int64 // this option should be used only for admin user
@@ -41,18 +59,24 @@ type SearchUserOptions struct {
IncludeReserved bool
}
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
if publicOnly {
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
}
}
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
var cond builder.Cond
cond = builder.Eq{"type": opts.Type}
cond = builder.In("type", opts.Types)
if opts.IncludeReserved {
switch opts.Type {
case UserTypeIndividual:
switch {
case slices.Contains(opts.Types, UserTypeIndividual):
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
builder.Eq{"type": UserTypeBot},
).Or(
builder.Eq{"type": UserTypeRemoteUser},
)
case UserTypeOrganization:
case slices.Contains(opts.Types, UserTypeOrganization):
cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved})
}
}
@@ -11,10 +11,12 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/json"
setting_module "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm/convert"
)
// Setting is a key value store of user settings
@@ -211,3 +213,44 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string
return err
})
}
func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) {
ret = def
str, err := GetUserSetting(ctx, userID, key)
if err != nil {
return ret, err
}
conv, ok := any(&ret).(convert.ConversionFrom)
if !ok {
conv, ok = any(ret).(convert.ConversionFrom)
}
if ok {
if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil {
return ret, err
}
} else {
if str == "" {
return ret, nil
}
err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret)
}
return ret, err
}
func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) {
conv, ok := any(&val).(convert.ConversionTo)
if !ok {
conv, ok = any(val).(convert.ConversionTo)
}
var bs []byte
if ok {
bs, err = conv.ToDB()
} else {
bs, err = json.Marshal(val)
}
if err != nil {
return err
}
return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs))
}
@@ -11,10 +11,6 @@ const (
// SettingsKeyShowOutdatedComments is the setting key whether 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
UserActivityPubPubPem = "activitypub.pub_pem"
// SignupIP is the IP address that the user signed up with
SignupIP = "signup.ip"
// SignupUserAgent is the user agent that the user signed up with
@@ -26,4 +22,6 @@ const (
SettingEmailNotificationGiteaActionsAll = "all"
SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference
SettingEmailNotificationGiteaActionsDisabled = "disabled"
SettingsKeyActionsConfig = "actions.config"
)
+151 -99
View File
@@ -7,12 +7,15 @@ package user
import (
"context"
"encoding/hex"
"errors"
"fmt"
"html/template"
"mime"
"net/mail"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -27,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -184,7 +188,7 @@ func (u *User) BeforeUpdate() {
}
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
// This trick could be removed in new releases to display the user inputed email as-is.
// This trick could be removed in new releases to display the user inputted email as-is.
u.Email = strings.ToLower(u.Email)
if !u.IsOrganization() {
if len(u.AvatarEmail) == 0 {
@@ -212,7 +216,7 @@ func (u *User) SetLastLogin() {
// GetPlaceholderEmail returns an noreply email
func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
return fmt.Sprintf("%d+%s@%s", u.ID, u.LowerName, setting.Service.NoReplyAddress)
}
// GetEmail returns a noreply email, if the user has set to keep his
@@ -249,8 +253,13 @@ func (u *User) MaxCreationLimit() int {
}
// 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
// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user.
// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised.
// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future.
func (u *User) CanCreateRepoIn(owner *User) bool {
if u.ID <= 0 || owner.ID <= 0 {
return false // fake user like Ghost or Actions user
}
if u.IsAdmin {
return true
}
@@ -298,6 +307,13 @@ func (u *User) DashboardLink() string {
return setting.AppSubURL + "/"
}
func (u *User) SettingsLink() string {
if u.IsOrganization() {
return u.OrganisationLink() + "/settings"
}
return setting.AppSubURL + "/user/settings"
}
// HomeLink returns the user or organization home page link.
func (u *User) HomeLink() string {
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
@@ -411,16 +427,6 @@ func (u *User) IsTokenAccessAllowed() bool {
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}
// DisplayName returns full name if it's not empty,
// returns username otherwise.
func (u *User) DisplayName() string {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 {
return trimmed
}
return u.Name
}
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
@@ -439,27 +445,45 @@ func (u *User) EmailTo() string {
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
}
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
// returns username otherwise.
// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
// * user.Name / user.FullName: directly used in templates
// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles
// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy
// DisplayName returns full name if it's not empty, returns username otherwise.
func (u *User) DisplayName() string {
fullName := strings.TrimSpace(u.FullName)
if fullName != "" {
return fullName
}
return u.Name
}
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username.
func (u *User) GetDisplayName() string {
if setting.UI.DefaultShowFullName {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 {
return trimmed
fullName := strings.TrimSpace(u.FullName)
if fullName != "" {
return fullName
}
}
return u.Name
}
// GetCompleteName returns the full name and username in the form of
// "Full Name (username)" if full name is not empty, otherwise it returns
// "username".
func (u *User) GetCompleteName() string {
trimmedFullName := strings.TrimSpace(u.FullName)
if len(trimmedFullName) > 0 {
return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
func (u *User) ShortName(length int) string {
return util.EllipsisDisplayString(u.GetDisplayName(), length)
}
func (u *User) GetShortDisplayNameLinkHTML() template.HTML {
fullName := strings.TrimSpace(u.FullName)
displayName, displayTooltip := u.Name, fullName
if setting.UI.DefaultShowFullName && fullName != "" {
displayName, displayTooltip = fullName, u.Name
}
return u.Name
return htmlutil.HTMLFormat(`<a class="muted" href="%s" data-tooltip-content="%s">%s</a>`, u.HomeLink(), displayTooltip, displayName)
}
func gitSafeName(name string) string {
@@ -482,18 +506,10 @@ func (u *User) GitName() string {
return fmt.Sprintf("user-%d", u.ID)
}
// ShortName ellipses username to length
func (u *User) ShortName(length int) string {
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
return util.EllipsisDisplayString(u.FullName, length)
}
return util.EllipsisDisplayString(u.Name, length)
}
// IsMailable checks if a user is eligible
// to receive emails.
// IsMailable checks if a user is eligible to receive emails.
// System users like Ghost and Gitea Actions are excluded.
func (u *User) IsMailable() bool {
return u.IsActive
return u.IsActive && !u.IsGiteaActions() && !u.IsGhost()
}
// IsUserExist checks if given username exist,
@@ -980,7 +996,7 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er
// UserPath returns the path absolute path of user repositories.
func UserPath(userName string) string { //revive:disable-line:exported
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)))
}
// GetUserByID returns the user object by given ID if exists.
@@ -1008,17 +1024,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err
}
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
if id < 0 {
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
return newFunc(), nil
u = newFunc()
}
return nil, ErrUserNotExist{UID: id}
} else if id == 0 {
return nil, ErrUserNotExist{}
}
return GetUserByID(ctx, id)
if u == nil {
u, err = GetUserByID(ctx, id)
if errors.Is(err, util.ErrNotExist) {
u = NewGhostUser()
} else if err != nil {
return 0, nil, err
}
}
return u.ID, u, nil
}
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
@@ -1039,13 +1060,13 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, nil
}
// GetUserByName returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 {
return nil, ErrUserNotExist{Name: name}
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
u := &User{}
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})
if len(types) > 0 {
sess.In("`type`", types)
}
u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
has, err := db.GetEngine(ctx).Get(u)
has, err := sess.Get(u)
if err != nil {
return nil, err
} else if !has {
@@ -1054,6 +1075,15 @@ func GetUserByName(ctx context.Context, name string) (*User, error) {
return u, nil
}
// GetUserByName returns the user object by given name, any user type.
func GetUserByName(ctx context.Context, name string) (*User, error) {
return getUserByNameWithTypes(ctx, name)
}
func GetIndividualUserByName(ctx context.Context, name string) (*User, error) {
return getUserByNameWithTypes(ctx, name, UserTypeIndividual)
}
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
// that have their email notifications set to enabled or onmention.
func GetUserEmailsByNames(ctx context.Context, names []string) []string {
@@ -1096,19 +1126,6 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]
Find(&ous)
}
// GetUserNameByID returns username for the id
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
var name string
has, err := db.GetEngine(ctx).Table("user").Where("id = ?", id).Cols("name").Get(&name)
if err != nil {
return "", err
}
if has {
return name, nil
}
return "", nil
}
// GetUserIDsByNames returns a slice of ids corresponds to names.
func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) {
ids := make([]int64, 0, len(names))
@@ -1187,19 +1204,23 @@ func (eum *EmailUserMap) GetByEmail(email string) *User {
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
if len(emails) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // return nil when there are no emails to look up
}
needCheckEmails := make(container.Set[string])
needCheckUserNames := make(container.Set[string])
needCheckUserIDs := make(container.Set[int64])
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
for _, email := range emails {
emailLower := strings.ToLower(email)
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
needCheckUserNames.Add(noReplyUserNameLower)
needCheckEmails.Add(emailLower)
} else {
needCheckEmails.Add(emailLower)
needCheckEmails.Add(emailLower)
if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
name, id := parseLocalPartToNameID(localPart)
if id != 0 {
needCheckUserIDs.Add(id)
} else if name != "" {
needCheckUserNames.Add(name)
}
}
}
@@ -1229,16 +1250,59 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
}
}
users := make(map[int64]*User, len(needCheckUserNames))
if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
return nil, err
usersByIDs := make(map[int64]*User)
if len(needCheckUserIDs) > 0 || len(needCheckUserNames) > 0 {
cond := builder.NewCond()
if len(needCheckUserIDs) > 0 {
cond = cond.Or(builder.In("id", needCheckUserIDs.Values()))
}
if len(needCheckUserNames) > 0 {
cond = cond.Or(builder.In("lower_name", needCheckUserNames.Values()))
}
if err := db.GetEngine(ctx).Where(cond).Find(&usersByIDs); err != nil {
return nil, err
}
}
for _, user := range users {
results[strings.ToLower(user.GetPlaceholderEmail())] = user
usersByName := make(map[string]*User)
for _, user := range usersByIDs {
usersByName[user.LowerName] = user
}
for _, email := range emails {
emailLower := strings.ToLower(email)
if _, ok := results[emailLower]; ok {
continue
}
localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix)
if !ok {
continue
}
name, id := parseLocalPartToNameID(localPart)
if user, ok := usersByIDs[id]; ok {
results[emailLower] = user
} else if user, ok := usersByName[name]; ok {
results[emailLower] = user
}
}
return &EmailUserMap{results}, nil
}
// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user
// returns user and id if possible
func parseLocalPartToNameID(localPart string) (string, int64) {
var id int64
idstr, name, hasPlus := strings.Cut(localPart, "+")
if hasPlus {
id, _ = strconv.ParseInt(idstr, 10, 64)
} else {
name = idstr
}
return name, id
}
// GetUserByEmail returns the user object by given e-mail if exists.
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
if len(email) == 0 {
@@ -1257,24 +1321,24 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
}
// Finally, if email address is the protected email address:
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 {
return nil, err
}
if has {
return user, nil
if localPart, ok := strings.CutSuffix(email, strings.ToLower("@"+setting.Service.NoReplyAddress)); ok {
name, id := parseLocalPartToNameID(localPart)
if id != 0 {
return GetUserByID(ctx, id)
}
return GetIndividualUserByName(ctx, name)
}
return nil, ErrUserNotExist{Name: email}
}
// GetUser checks if a user already exists
func GetUser(ctx context.Context, user *User) (bool, error) {
return db.GetEngine(ctx).Get(user)
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
has, err := db.GetEngine(ctx).Get(user)
if has && user.Type != UserTypeIndividual {
has = false
}
return has, err
}
// GetUserByOpenID returns the user object by given OpenID if exists.
@@ -1444,15 +1508,3 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
}
return &setting.Admin.UserDisabledFeatures
}
// GetUserOrOrgByName returns the user or org by name
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
var u User
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{Name: name}
}
return &u, nil
}
+34 -21
View File
@@ -4,6 +4,7 @@
package user
import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/structs"
@@ -23,10 +24,6 @@ func NewGhostUser() *User {
}
}
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 {
@@ -41,36 +38,52 @@ const (
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{
ID: ActionsUserID,
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: "Gitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeBot,
AllowCreateOrganization: true,
Visibility: structs.VisibleTypePublic,
ID: ActionsUserID,
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: "Gitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeBot,
Visibility: structs.VisibleTypePublic,
}
}
func NewActionsUserWithTaskID(id int64) *User {
u := NewActionsUser()
// LoginName is for only internal usage in this case, so it can be moved to other fields in the future
u.LoginSource = -1
u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10)
return u
}
func GetActionsUserTaskID(u *User) (int64, bool) {
if u == nil || u.ID != ActionsUserID {
return 0, false
}
prefix, payload, _ := strings.Cut(u.LoginName, "/")
if prefix != "@"+ActionsUserName {
return 0, false
} else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil {
return taskID, true
}
return 0, false
}
func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID
}
func GetSystemUserByName(name string) *User {
if IsGhostUserName(name) {
if strings.EqualFold(name, GhostUserName) {
return NewGhostUser()
}
if IsGiteaActionsUserName(name) {
if strings.EqualFold(name, ActionsUserName) {
return NewActionsUser()
}
return nil
@@ -11,20 +11,30 @@ import (
)
func TestSystemUser(t *testing.T) {
u, err := GetPossibleUserByID(t.Context(), -1)
uid, u, err := GetPossibleUserByID(t.Context(), -1)
require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
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(t.Context(), -2)
u = GetSystemUserByName("gHost")
require.NotNil(t, u)
assert.Equal(t, "Ghost", u.Name)
uid, u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err)
assert.Equal(t, int64(-2), uid)
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(t.Context(), -3)
require.Error(t, err)
u = GetSystemUserByName("Gitea-actionS")
require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName)
uid, u, err = GetPossibleUserByID(t.Context(), 999999)
require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
assert.Equal(t, "Ghost", u.Name)
}
+60 -30
View File
@@ -51,12 +51,27 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
func TestUserEmails(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
t.Run("GetUserEmailsByNames", func(t *testing.T) {
// ignore none active user email
// ignore not active user email
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"}))
assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"}))
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"}))
})
cases := []struct {
Email string
UID int64
}{
{"UseR1@example.com", 1},
{"user1-2@example.COM", 1},
{"USER2@" + setting.Service.NoReplyAddress, 2},
{"2+user2@" + setting.Service.NoReplyAddress, 2},
{"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2},
{"99999+badUser@" + setting.Service.NoReplyAddress, 0},
{"user4@example.com", 4},
{"no-such", 0},
}
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) {
@@ -70,15 +85,27 @@ func TestUserEmails(t *testing.T) {
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)
})
}
t.Run("NoReplyConflict", func(t *testing.T) {
setting.Service.NoReplyAddress = "example.com"
testGetUserByEmail(t, "user1-2@example.COM", 1)
})
})
t.Run("GetUserByEmail", func(t *testing.T) {
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
user, err := user_model.GetUserByEmail(t.Context(), email)
if uid == 0 {
require.Error(t, err)
assert.Nil(t, user)
} else {
require.NotNil(t, user)
assert.Equal(t, uid, user.ID)
}
}
for _, c := range cases {
t.Run(c.Email, func(t *testing.T) {
@@ -126,7 +153,7 @@ func TestSearchUsers(t *testing.T) {
// test orgs
testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
opts.Type = user_model.UserTypeOrganization
opts.Types = []user_model.UserType{user_model.UserTypeOrganization}
testSuccess(opts, expectedOrgIDs)
}
@@ -150,7 +177,7 @@ func TestSearchUsers(t *testing.T) {
// test users
testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
opts.Type = user_model.UserTypeIndividual
opts.Types = []user_model.UserType{user_model.UserTypeIndividual}
testSuccess(opts, expectedUserIDs)
}
@@ -648,33 +675,36 @@ func TestGetInactiveUsers(t *testing.T) {
func TestCanCreateRepo(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
const noLimit = -1
doerNormal := &user_model.User{}
doerAdmin := &user_model.User{IsAdmin: true}
doerActions := user_model.NewActionsUser()
doerNormal := &user_model.User{ID: 2}
doerAdmin := &user_model.User{ID: 1, 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.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
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}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerAdmin.CanCreateRepoIn(doerActions))
})
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, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, 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}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
})
}