mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
747 lines
27 KiB
Go
747 lines
27 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package user_test
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/hanzoai/git/models/auth"
|
||
"github.com/hanzoai/git/models/db"
|
||
"github.com/hanzoai/git/models/unittest"
|
||
user_model "github.com/hanzoai/git/models/user"
|
||
"github.com/hanzoai/git/modules/auth/password/hash"
|
||
"github.com/hanzoai/git/modules/container"
|
||
"github.com/hanzoai/git/modules/optional"
|
||
"github.com/hanzoai/git/modules/setting"
|
||
"github.com/hanzoai/git/modules/structs"
|
||
"github.com/hanzoai/git/modules/test"
|
||
"github.com/hanzoai/git/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})
|
||
user, err := user_model.GetUserByID(t.Context(), app.UID)
|
||
assert.NoError(t, err)
|
||
assert.NotNil(t, user)
|
||
}
|
||
|
||
func TestUserEmails(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.forge.internal")()
|
||
t.Run("GetUserEmailsByNames", func(t *testing.T) {
|
||
// ignore not active user email
|
||
assert.ElementsMatch(t, []string{"[email protected]"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"}))
|
||
assert.ElementsMatch(t, []string{"[email protected]", "[email protected]"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"}))
|
||
assert.ElementsMatch(t, []string{"[email protected]"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"}))
|
||
})
|
||
|
||
cases := []struct {
|
||
Email string
|
||
UID int64
|
||
}{
|
||
{"[email protected]", 1},
|
||
{"[email protected]", 1},
|
||
{"USER2@" + setting.Service.NoReplyAddress, 2},
|
||
{"2+user2@" + setting.Service.NoReplyAddress, 2},
|
||
{"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2},
|
||
{"99999+badUser@" + setting.Service.NoReplyAddress, 0},
|
||
{"[email protected]", 4},
|
||
{"no-such", 0},
|
||
}
|
||
t.Run("GetUsersByEmails", func(t *testing.T) {
|
||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.forge.internal")()
|
||
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||
m, err := user_model.GetUsersByEmails(t.Context(), []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)
|
||
}
|
||
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, "[email protected]", 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) {
|
||
testGetUserByEmail(t, c.Email, c.UID)
|
||
})
|
||
}
|
||
|
||
t.Run("NoReplyConflict", func(t *testing.T) {
|
||
setting.Service.NoReplyAddress = "example.com"
|
||
testGetUserByEmail(t, "[email protected]", 1)
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestCanCreateOrganization(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||
assert.True(t, admin.CanCreateOrganization())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
assert.True(t, user.CanCreateOrganization())
|
||
// Disable user create organization permission.
|
||
user.AllowCreateOrganization = false
|
||
assert.False(t, user.CanCreateOrganization())
|
||
|
||
setting.Admin.DisableRegularOrgCreation = true
|
||
user.AllowCreateOrganization = true
|
||
assert.True(t, admin.CanCreateOrganization())
|
||
assert.False(t, user.CanCreateOrganization())
|
||
}
|
||
|
||
func TestSearchUsers(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
testSuccess := func(opts user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) {
|
||
users, _, err := user_model.SearchUsers(t.Context(), 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.Equal(t, expectedID, users[i].ID, "case: %s", cassText)
|
||
}
|
||
}
|
||
}
|
||
|
||
// test orgs
|
||
testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
|
||
opts.Types = []user_model.UserType{user_model.UserTypeOrganization}
|
||
testSuccess(opts, expectedOrgIDs)
|
||
}
|
||
|
||
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}},
|
||
[]int64{7, 17})
|
||
|
||
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}},
|
||
[]int64{26, 41})
|
||
|
||
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}},
|
||
[]int64{})
|
||
|
||
// test users
|
||
testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
|
||
opts.Types = []user_model.UserType{user_model.UserTypeIndividual}
|
||
testSuccess(opts, expectedUserIDs)
|
||
}
|
||
|
||
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)},
|
||
[]int64{9})
|
||
|
||
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)},
|
||
[]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)},
|
||
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
||
|
||
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)},
|
||
[]int64{29})
|
||
|
||
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)},
|
||
[]int64{24})
|
||
}
|
||
|
||
func TestEmailNotificationPreferences(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
for _, test := range []struct {
|
||
expected string
|
||
userID int64
|
||
}{
|
||
{user_model.EmailNotificationsEnabled, 1},
|
||
{user_model.EmailNotificationsEnabled, 2},
|
||
{user_model.EmailNotificationsOnMention, 3},
|
||
{user_model.EmailNotificationsOnMention, 4},
|
||
{user_model.EmailNotificationsEnabled, 5},
|
||
{user_model.EmailNotificationsEnabled, 6},
|
||
{user_model.EmailNotificationsDisabled, 7},
|
||
{user_model.EmailNotificationsEnabled, 8},
|
||
{user_model.EmailNotificationsOnMention, 9},
|
||
} {
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID})
|
||
assert.Equal(t, test.expected, user.EmailNotificationsPreference)
|
||
}
|
||
}
|
||
|
||
func TestHashPasswordDeterministic(t *testing.T) {
|
||
b := make([]byte, 16)
|
||
u := &user_model.User{}
|
||
algos := hash.RecommendedHashAlgorithms
|
||
for j := range algos {
|
||
u.PasswdHashAlgo = algos[j]
|
||
for range 50 {
|
||
// generate a random password
|
||
rand.Read(b)
|
||
pass := string(b)
|
||
|
||
// save the current password in the user - hash it and store the result
|
||
u.SetPassword(pass)
|
||
r1 := u.Passwd
|
||
|
||
// run again
|
||
u.SetPassword(pass)
|
||
r2 := u.Passwd
|
||
|
||
assert.NotEqual(t, r1, r2)
|
||
assert.True(t, u.ValidatePassword(pass))
|
||
}
|
||
}
|
||
}
|
||
|
||
func BenchmarkHashPassword(b *testing.B) {
|
||
// BenchmarkHashPassword ensures that it takes a reasonable amount of time
|
||
// to hash a password - in order to protect from brute-force attacks.
|
||
pass := "password1337"
|
||
u := &user_model.User{Passwd: pass}
|
||
b.ResetTimer()
|
||
for b.Loop() {
|
||
u.SetPassword(pass)
|
||
}
|
||
}
|
||
|
||
func TestNewGitSig(t *testing.T) {
|
||
users := make([]*user_model.User, 0, 20)
|
||
err := db.GetEngine(t.Context()).Find(&users)
|
||
assert.NoError(t, err)
|
||
|
||
for _, user := range users {
|
||
sig := user.NewGitSig()
|
||
assert.NotContains(t, sig.Name, "<")
|
||
assert.NotContains(t, sig.Name, ">")
|
||
assert.NotContains(t, sig.Name, "\n")
|
||
assert.NotEmpty(t, strings.TrimSpace(sig.Name))
|
||
}
|
||
}
|
||
|
||
func TestDisplayName(t *testing.T) {
|
||
users := make([]*user_model.User, 0, 20)
|
||
err := db.GetEngine(t.Context()).Find(&users)
|
||
assert.NoError(t, err)
|
||
|
||
for _, user := range users {
|
||
displayName := user.DisplayName()
|
||
assert.Equal(t, strings.TrimSpace(displayName), displayName)
|
||
if len(strings.TrimSpace(user.FullName)) == 0 {
|
||
assert.Equal(t, user.Name, displayName)
|
||
}
|
||
assert.NotEmpty(t, strings.TrimSpace(displayName))
|
||
}
|
||
}
|
||
|
||
func TestCreateUserInvalidEmail(t *testing.T) {
|
||
user := &user_model.User{
|
||
Name: "GitBot",
|
||
Email: "[email protected]\r\n",
|
||
Passwd: ";p['////..-++']",
|
||
IsAdmin: false,
|
||
Theme: setting.UI.DefaultTheme,
|
||
MustChangePassword: false,
|
||
}
|
||
|
||
err := user_model.CreateUser(t.Context(), user, &user_model.Meta{})
|
||
assert.Error(t, err)
|
||
assert.True(t, user_model.IsErrEmailCharIsNotSupported(err))
|
||
}
|
||
|
||
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
|
||
// add new user with user2's email
|
||
user.Name = "testuser"
|
||
user.LowerName = strings.ToLower(user.Name)
|
||
user.ID = 0
|
||
err := user_model.CreateUser(t.Context(), user, &user_model.Meta{})
|
||
assert.Error(t, err)
|
||
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
|
||
}
|
||
|
||
func TestCreateUserCustomTimestamps(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
|
||
// Add new user with a custom creation timestamp.
|
||
var creationTimestamp timeutil.TimeStamp = 12345
|
||
user.Name = "testuser"
|
||
user.LowerName = strings.ToLower(user.Name)
|
||
user.ID = 0
|
||
user.Email = "[email protected]"
|
||
user.CreatedUnix = creationTimestamp
|
||
err := user_model.CreateUser(t.Context(), user, &user_model.Meta{})
|
||
assert.NoError(t, err)
|
||
|
||
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)
|
||
}
|
||
|
||
func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
|
||
// There is no way to use a mocked time for the XORM auto-time functionality,
|
||
// so use the real clock to approximate the expected timestamp.
|
||
timestampStart := time.Now().Unix()
|
||
|
||
// Add new user without a custom creation timestamp.
|
||
user.Name = "Testuser"
|
||
user.LowerName = strings.ToLower(user.Name)
|
||
user.ID = 0
|
||
user.Email = "[email protected]"
|
||
user.CreatedUnix = 0
|
||
user.UpdatedUnix = 0
|
||
err := user_model.CreateUser(t.Context(), user, &user_model.Meta{})
|
||
assert.NoError(t, err)
|
||
|
||
timestampEnd := time.Now().Unix()
|
||
|
||
fetched, err := user_model.GetUserByID(t.Context(), user.ID)
|
||
assert.NoError(t, err)
|
||
|
||
assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
|
||
assert.LessOrEqual(t, fetched.CreatedUnix, timestampEnd)
|
||
|
||
assert.LessOrEqual(t, timestampStart, fetched.UpdatedUnix)
|
||
assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
|
||
}
|
||
|
||
func TestGetUserIDsByNames(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
// ignore non existing
|
||
IDs, err := user_model.GetUserIDsByNames(t.Context(), []string{"user1", "user2", "none_existing_user"}, true)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, []int64{1, 2}, IDs)
|
||
|
||
// ignore non existing
|
||
IDs, err = user_model.GetUserIDsByNames(t.Context(), []string{"user1", "do_not_exist"}, false)
|
||
assert.Error(t, err)
|
||
assert.Equal(t, []int64(nil), IDs)
|
||
}
|
||
|
||
func TestGetMaileableUsersByIDs(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
results, err := user_model.GetMailableUsersByIDs(t.Context(), []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.GetMailableUsersByIDs(t.Context(), []int64{1, 4}, true)
|
||
assert.NoError(t, err)
|
||
assert.Len(t, results, 2)
|
||
if len(results) > 2 {
|
||
assert.Equal(t, 1, results[0].ID)
|
||
assert.Equal(t, 4, results[1].ID)
|
||
}
|
||
}
|
||
|
||
func TestNewUserRedirect(t *testing.T) {
|
||
// redirect to a completely new name
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||
assert.NoError(t, user_model.NewUserRedirect(t.Context(), user.ID, user.Name, "newusername"))
|
||
|
||
unittest.AssertExistsAndLoadBean(t, &user_model.Redirect{
|
||
LowerName: user.LowerName,
|
||
RedirectUserID: user.ID,
|
||
})
|
||
unittest.AssertExistsAndLoadBean(t, &user_model.Redirect{
|
||
LowerName: "olduser1",
|
||
RedirectUserID: user.ID,
|
||
})
|
||
}
|
||
|
||
func TestNewUserRedirect2(t *testing.T) {
|
||
// redirect to previously used name
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||
assert.NoError(t, user_model.NewUserRedirect(t.Context(), user.ID, user.Name, "olduser1"))
|
||
|
||
unittest.AssertExistsAndLoadBean(t, &user_model.Redirect{
|
||
LowerName: user.LowerName,
|
||
RedirectUserID: user.ID,
|
||
})
|
||
unittest.AssertNotExistsBean(t, &user_model.Redirect{
|
||
LowerName: "olduser1",
|
||
RedirectUserID: user.ID,
|
||
})
|
||
}
|
||
|
||
func TestNewUserRedirect3(t *testing.T) {
|
||
// redirect for a previously-unredirected user
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
assert.NoError(t, user_model.NewUserRedirect(t.Context(), user.ID, user.Name, "newusername"))
|
||
|
||
unittest.AssertExistsAndLoadBean(t, &user_model.Redirect{
|
||
LowerName: user.LowerName,
|
||
RedirectUserID: user.ID,
|
||
})
|
||
}
|
||
|
||
func TestGetUserByOpenID(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
_, err := user_model.GetUserByOpenID(t.Context(), "https://unknown")
|
||
if assert.Error(t, err) {
|
||
assert.True(t, user_model.IsErrUserNotExist(err))
|
||
}
|
||
|
||
user, err := user_model.GetUserByOpenID(t.Context(), "https://user1.domain1.tld")
|
||
if assert.NoError(t, err) {
|
||
assert.Equal(t, int64(1), user.ID)
|
||
}
|
||
|
||
user, err = user_model.GetUserByOpenID(t.Context(), "https://domain1.tld/user2/")
|
||
if assert.NoError(t, err) {
|
||
assert.Equal(t, int64(2), user.ID)
|
||
}
|
||
}
|
||
|
||
func TestFollowUser(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
testSuccess := func(follower, followed *user_model.User) {
|
||
assert.NoError(t, user_model.FollowUser(t.Context(), follower, followed))
|
||
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
|
||
}
|
||
|
||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||
|
||
testSuccess(user4, user2)
|
||
testSuccess(user5, user2)
|
||
|
||
assert.NoError(t, user_model.FollowUser(t.Context(), user2, user2))
|
||
|
||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||
}
|
||
|
||
func TestUnfollowUser(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
testSuccess := func(followerID, followedID int64) {
|
||
assert.NoError(t, user_model.UnfollowUser(t.Context(), followerID, followedID))
|
||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
|
||
}
|
||
testSuccess(4, 2)
|
||
testSuccess(5, 2)
|
||
testSuccess(2, 2)
|
||
|
||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||
}
|
||
|
||
func TestIsUserVisibleToViewer(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // admin, public
|
||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // normal, public
|
||
user20 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) // public, same team as user31
|
||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) // public, is restricted
|
||
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31}) // private, same team as user20
|
||
user33 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 33}) // limited, follows 31
|
||
|
||
test := func(u, viewer *user_model.User, expected bool) {
|
||
name := func(u *user_model.User) string {
|
||
if u == nil {
|
||
return "<nil>"
|
||
}
|
||
return u.Name
|
||
}
|
||
assert.Equal(t, expected, user_model.IsUserVisibleToViewer(t.Context(), u, viewer), "user %v should be visible to viewer %v: %v", name(u), name(viewer), expected)
|
||
}
|
||
|
||
// admin viewer
|
||
test(user1, user1, true)
|
||
test(user20, user1, true)
|
||
test(user31, user1, true)
|
||
test(user33, user1, true)
|
||
|
||
// non admin viewer
|
||
test(user4, user4, true)
|
||
test(user20, user4, true)
|
||
test(user31, user4, false)
|
||
test(user33, user4, true)
|
||
test(user4, nil, true)
|
||
|
||
// public user
|
||
test(user4, user20, true)
|
||
test(user4, user31, true)
|
||
test(user4, user33, true)
|
||
|
||
// limited user
|
||
test(user33, user33, true)
|
||
test(user33, user4, true)
|
||
test(user33, user29, false)
|
||
test(user33, nil, false)
|
||
|
||
// private user
|
||
test(user31, user31, true)
|
||
test(user31, user4, false)
|
||
test(user31, user20, true)
|
||
test(user31, user29, false)
|
||
test(user31, user33, true)
|
||
test(user31, nil, false)
|
||
}
|
||
|
||
func Test_ValidateUser(t *testing.T) {
|
||
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.Equal(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase)
|
||
}
|
||
}
|
||
|
||
func Test_NormalizeUserFromEmail(t *testing.T) {
|
||
testCases := []struct {
|
||
Input string
|
||
Expected string
|
||
IsNormalizedValid bool
|
||
}{
|
||
{"[email protected]", "name", true},
|
||
{"test'`´name", "testname", true},
|
||
{"Sinéad.O'Connor", "Sinead.OConnor", true},
|
||
{"Æsir", "AEsir", true},
|
||
{"éé", "ee", true}, // \u00e9\u0065\u0301
|
||
{"Awareness Hub", "Awareness-Hub", true},
|
||
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
|
||
{".bad.", ".bad.", false},
|
||
{"new😀user", "new😀user", false}, // No plans to support
|
||
{`"quoted"`, `"quoted"`, false}, // No plans to support
|
||
}
|
||
for _, testCase := range testCases {
|
||
normalizedName, err := user_model.NormalizeUserName(testCase.Input)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, testCase.Expected, normalizedName)
|
||
if testCase.IsNormalizedValid {
|
||
assert.NoError(t, user_model.IsUsableUsername(normalizedName))
|
||
} else {
|
||
assert.Error(t, user_model.IsUsableUsername(normalizedName))
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestEmailTo(t *testing.T) {
|
||
testCases := []struct {
|
||
fullName string
|
||
mail string
|
||
result string
|
||
}{
|
||
{"Awareness Hub", "[email protected]", "Awareness Hub <[email protected]>"},
|
||
{"[email protected]", "[email protected]", "[email protected]"},
|
||
{"Hi Its <Mee>", "[email protected]", "Hi Its Mee <[email protected]>"},
|
||
{"Sinéad.O'Connor", "[email protected]", "=?utf-8?q?Sin=C3=A9ad.O'Connor?= <[email protected]>"},
|
||
{"Æsir", "[email protected]", "=?utf-8?q?=C3=86sir?= <[email protected]>"},
|
||
{"new😀user", "[email protected]", "=?utf-8?q?new=F0=9F=98=80user?= <[email protected]>"},
|
||
{`"quoted"`, "[email protected]", "quoted <[email protected]>"},
|
||
}
|
||
|
||
for _, testCase := range testCases {
|
||
t.Run(testCase.result, func(t *testing.T) {
|
||
testUser := &user_model.User{FullName: testCase.fullName, Email: testCase.mail}
|
||
assert.Equal(t, testCase.result, testUser.EmailTo())
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDisabledUserFeatures(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
testValues := container.SetOf(setting.UserFeatureDeletion,
|
||
setting.UserFeatureManageSSHKeys,
|
||
setting.UserFeatureManageGPGKeys)
|
||
defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, testValues)()
|
||
|
||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||
|
||
assert.Empty(t, setting.Admin.UserDisabledFeatures.Values())
|
||
|
||
// no features should be disabled with a plain login type
|
||
assert.LessOrEqual(t, user.LoginType, auth.Plain)
|
||
assert.Empty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
|
||
for _, f := range testValues.Values() {
|
||
assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f))
|
||
}
|
||
|
||
// check disabled features with external login type
|
||
user.LoginType = auth.OAuth2
|
||
|
||
// all features should be disabled
|
||
assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
|
||
for _, f := range testValues.Values() {
|
||
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
|
||
}
|
||
}
|
||
|
||
func TestGetInactiveUsers(t *testing.T) {
|
||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||
|
||
// all inactive users
|
||
// user1's createdunix is 1730468968
|
||
users, err := user_model.GetInactiveUsers(t.Context(), 0)
|
||
assert.NoError(t, err)
|
||
assert.Len(t, users, 1)
|
||
interval := time.Now().Unix() - 1730468968 + 3600*24
|
||
users, err = user_model.GetInactiveUsers(t.Context(), time.Duration(interval*int64(time.Second)))
|
||
assert.NoError(t, err)
|
||
assert.Empty(t, users)
|
||
}
|
||
|
||
func TestCanCreateRepo(t *testing.T) {
|
||
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
|
||
defer test.MockVariableValue(&setting.Repository.UserMaxCreationLimit)()
|
||
defer test.MockVariableValue(&setting.Repository.OrgMaxCreationLimit)()
|
||
const noLimit = -1
|
||
doerActions := user_model.NewActionsUser()
|
||
doerNormal := &user_model.User{ID: 2}
|
||
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
|
||
orgOwner := func(numRepos, maxRepoCreation int) *user_model.User {
|
||
return &user_model.User{ID: 3, Type: user_model.UserTypeOrganization, NumRepos: numRepos, MaxRepoCreation: maxRepoCreation}
|
||
}
|
||
t.Run("NoGlobalLimit", func(t *testing.T) {
|
||
setting.Repository.UserMaxCreationLimit = noLimit
|
||
setting.Repository.OrgMaxCreationLimit = noLimit
|
||
|
||
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{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.UserMaxCreationLimit = 50
|
||
setting.Repository.OrgMaxCreationLimit = 50
|
||
|
||
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{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}))
|
||
})
|
||
|
||
t.Run("UserBlockedOrgsUnlimited", func(t *testing.T) {
|
||
// User and org limits are independent: a deployment can block personal repos while leaving orgs unrestricted.
|
||
setting.Repository.UserMaxCreationLimit = 0
|
||
setting.Repository.OrgMaxCreationLimit = noLimit
|
||
|
||
// regular user is blocked
|
||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 0, MaxRepoCreation: noLimit}))
|
||
// per-user override grants individual exceptions even when the global user limit is 0
|
||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 3, MaxRepoCreation: 5}))
|
||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 5, MaxRepoCreation: 5}))
|
||
|
||
// organization can create unlimited repos
|
||
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit)))
|
||
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(999, noLimit)))
|
||
// per-org override still wins over the global org limit
|
||
assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(5, 5)))
|
||
})
|
||
|
||
t.Run("OrgGlobalLimitWithPerOrgOverride", func(t *testing.T) {
|
||
setting.Repository.UserMaxCreationLimit = noLimit
|
||
setting.Repository.OrgMaxCreationLimit = 10
|
||
|
||
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(5, noLimit)))
|
||
assert.False(t, doerNormal.CanCreateRepoIn(orgOwner(10, noLimit)))
|
||
|
||
// per-org override bypasses the global org limit
|
||
assert.True(t, doerNormal.CanCreateRepoIn(orgOwner(10, 100)))
|
||
})
|
||
}
|